diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca1bc79510..26979df834 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -234,16 +234,14 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: docker exec -t builder sh -eu {0} run: | - scripts/desktop/build-lib-linux.sh - cd apps/multiplatform - ./gradlew packageDeb + scripts/desktop/make-deb-linux.sh - name: Prepare Desktop id: linux_desktop_build if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_*_amd64.deb ) + path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/deb/simplex_amd64.deb ) echo "package_path=$path" >> $GITHUB_OUTPUT echo "package_hash=$(echo SHA2-256\(${{ matrix.desktop_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT diff --git a/Dockerfile.build b/Dockerfile.build index 76bb1127f2..9b7c38fa87 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -16,6 +16,7 @@ RUN apt-get update && \ apt-get install -y curl \ libpq-dev \ git \ + strip-nondeterminism \ sqlite3 \ libsqlite3-dev \ build-essential \ diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 2ad8d546f2..180af685e5 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -45,21 +45,10 @@ struct ContentView: View { @State private var showChooseLAMode = false @State private var showSetPasscode = false @State private var waitingForOrPassedAuth = true - @State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListUserPickerSheet: UserPickerSheet? = nil private let callTopPadding: CGFloat = 40 - private enum ChatListActionSheet: Identifiable { - case planAndConnectSheet(sheet: PlanAndConnectActionSheet) - - var id: String { - switch self { - case let .planAndConnectSheet(sheet): return sheet.id - } - } - } - private var accessAuthenticated: Bool { chatModel.contentViewAccessAuthenticated || contentAccessAuthenticationExtended } @@ -181,11 +170,6 @@ struct ContentView: View { if case .onboardingComplete = step, chatModel.currentUser != nil { mainView() - .actionSheet(item: $chatListActionSheet) { sheet in - switch sheet { - case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) - } - } } else { OnboardingView(onboarding: step) } @@ -453,10 +437,7 @@ struct ContentView: View { let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( link, - showAlert: showPlanAndConnectAlert, - showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) }, - dismiss: false, - incognito: nil + dismiss: false ) } else { AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) @@ -479,10 +460,6 @@ struct ContentView: View { } } } - - private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) { - AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false)) - } } final class AlertManager: ObservableObject { diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 3bf4cb7b56..6c47d654e0 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -39,9 +39,9 @@ enum ChatCommand: ChatCmdProtocol { case apiGetSettings(settings: AppSettings) case apiGetChatTags(userId: Int64) case apiGetChats(userId: Int64) - case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String) - case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) - case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String) + case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) + case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) case apiCreateChatTag(tag: ChatTagData) case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) case apiDeleteChatTag(tagId: Int64) @@ -49,15 +49,15 @@ enum ChatCommand: ChatCmdProtocol { case apiReorderChatTags(tagIds: [Int64]) case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String) - case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool) + case apiDeleteChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode) case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) case apiArchiveReceivedReports(groupId: Int64) case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode) - case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) + case apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction) - case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64]) - case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) + case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) + case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -68,6 +68,8 @@ enum ChatCommand: ChatCmdProtocol { case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile) case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) + case apiAcceptMember(groupId: Int64, groupMemberId: Int64, memberRole: GroupMemberRole) + case apiDeleteMemberSupportChat(groupId: Int64, groupMemberId: Int64) case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole) case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool) case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool) @@ -78,6 +80,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiDeleteGroupLink(groupId: Int64) case apiGetGroupLink(groupId: Int64) + case apiAddGroupShortLink(groupId: Int64) case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) case apiTestProtoServer(userId: Int64, server: String) @@ -132,6 +135,7 @@ enum ChatCommand: ChatCmdProtocol { case apiCreateMyAddress(userId: Int64, short: Bool) case apiDeleteMyAddress(userId: Int64) case apiShowMyAddress(userId: Int64) + case apiAddMyAddressShortLink(userId: Int64) case apiSetProfileAddress(userId: Int64, on: Bool) case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) case apiAcceptContact(incognito: Bool, contactReqId: Int64) @@ -147,8 +151,8 @@ enum ChatCommand: ChatCmdProtocol { case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) // WebRTC calls / case apiGetNetworkStatuses - case apiChatRead(type: ChatType, id: Int64) - case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) + case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) + case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) @@ -209,15 +213,16 @@ enum ChatCommand: ChatCmdProtocol { case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))" case let .apiGetChatTags(userId): return "/_get tags \(userId)" case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" - case let .apiGetChat(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" + - (search == "" ? "" : " search=\(search)") - case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" - case let .apiSendMessages(type, id, live, ttl, composedMessages): + case let .apiGetChat(chatId, scope, contentTag, pagination, search): + let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : "" + return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") + case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)" + case let .apiSendMessages(type, id, scope, live, ttl, composedMessages): let msgs = encodeJSON(composedMessages) let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))" - case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)" case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))" case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))" @@ -226,17 +231,17 @@ enum ChatCommand: ChatCmdProtocol { return "/_create *\(noteFolderId) json \(msgs)" case let .apiReportMessage(groupId, chatItemId, reportReason, reportText): return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)" - case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)" - case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiUpdateChatItem(type, id, scope, itemId, um, live): return "/_update item \(ref(type, id, scope: scope)) \(itemId) live=\(onOff(live)) \(um.cmdString)" + case let .apiDeleteChatItem(type, id, scope, itemIds, mode): return "/_delete item \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)" case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" - case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" + case let .apiChatItemReaction(type, id, scope, itemId, add, reaction): return "/_reaction \(ref(type, id, scope: scope)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))" - case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" - case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl): + case let .apiPlanForwardChatItems(type, id, scope, itemIds): return "/_forward plan \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ","))" + case let .apiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl): let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" + return "/_forward \(ref(toChatType, toChatId, scope: toScope)) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -247,6 +252,8 @@ enum ChatCommand: ChatCmdProtocol { case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))" case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" + case let .apiAcceptMember(groupId, groupMemberId, memberRole): return "/_accept member #\(groupId) \(groupMemberId) \(memberRole.rawValue)" + case let .apiDeleteMemberSupportChat(groupId, groupMemberId): return "/_delete member chat #\(groupId) \(groupMemberId)" case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)" case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))" case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))" @@ -257,6 +264,7 @@ enum ChatCommand: ChatCmdProtocol { case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" + case let .apiAddGroupShortLink(groupId): return "/_short link #\(groupId)" case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" @@ -270,13 +278,13 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))" case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" - case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))" + case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id, scope: nil)) \(chatItemTTLStr(seconds: seconds))" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" case .reconnectAllServers: return "/reconnect" case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)" - case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" + case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id, scope: nil)) \(encodeJSON(chatSettings))" case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" @@ -308,8 +316,8 @@ enum ChatCommand: ChatCmdProtocol { case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")" case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" - case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)" - case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" + case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id, scope: nil)) \(chatDeleteMode.cmdString)" + case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id, scope: nil))" case let .apiListContacts(userId): return "/_contacts \(userId)" case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))" @@ -321,6 +329,7 @@ enum ChatCommand: ChatCmdProtocol { case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))" case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" case let .apiShowMyAddress(userId): return "/_show_address \(userId)" + case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" @@ -334,9 +343,9 @@ enum ChatCommand: ChatCmdProtocol { case .apiGetCallInvitations: return "/_call get" case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" case .apiGetNetworkStatuses: return "/_network_statuses" - case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))" - case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))" - case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" + case let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))" + case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))" + case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(onOff(unreadChat))" case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" case let .cancelFile(fileId): return "/fcancel \(fileId)" @@ -421,6 +430,8 @@ enum ChatCommand: ChatCmdProtocol { case .apiNewGroup: return "apiNewGroup" case .apiAddMember: return "apiAddMember" case .apiJoinGroup: return "apiJoinGroup" + case .apiAcceptMember: return "apiAcceptMember" + case .apiDeleteMemberSupportChat: return "apiDeleteMemberSupportChat" case .apiMembersRole: return "apiMembersRole" case .apiBlockMembersForAll: return "apiBlockMembersForAll" case .apiRemoveMembers: return "apiRemoveMembers" @@ -431,6 +442,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" case .apiDeleteGroupLink: return "apiDeleteGroupLink" case .apiGetGroupLink: return "apiGetGroupLink" + case .apiAddGroupShortLink: return "apiAddGroupShortLink" case .apiCreateMemberContact: return "apiCreateMemberContact" case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" case .apiTestProtoServer: return "apiTestProtoServer" @@ -484,6 +496,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiCreateMyAddress: return "apiCreateMyAddress" case .apiDeleteMyAddress: return "apiDeleteMyAddress" case .apiShowMyAddress: return "apiShowMyAddress" + case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink" case .apiSetProfileAddress: return "apiSetProfileAddress" case .apiAddressAutoAccept: return "apiAddressAutoAccept" case .apiAcceptContact: return "apiAcceptContact" @@ -523,8 +536,20 @@ enum ChatCommand: ChatCmdProtocol { } } - func ref(_ type: ChatType, _ id: Int64) -> String { - "\(type.rawValue)\(id)" + func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String { + "\(type.rawValue)\(id)\(scopeRef(scope: scope))" + } + + func scopeRef(scope: GroupChatScope?) -> String { + switch (scope) { + case .none: "" + case let .memberSupport(groupMemberId_): + if let groupMemberId = groupMemberId_ { + "(_support:\(groupMemberId))" + } else { + "(_support)" + } + } } func joinedIds(_ ids: [Int64]) -> String { @@ -711,6 +736,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case contactDeleted(user: UserRef, contact: Contact) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) + case itemsReadForChat(user: UserRef, chatInfo: ChatInfo) case chatCleared(user: UserRef, chatInfo: ChatInfo) case userProfileNoChange(user: User) case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary) @@ -749,6 +775,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .contactDeleted: "contactDeleted" case .contactConnectionDeleted: "contactConnectionDeleted" case .groupDeletedUser: "groupDeletedUser" + case .itemsReadForChat: "itemsReadForChat" case .chatCleared: "chatCleared" case .userProfileNoChange: "userProfileNoChange" case .userProfileUpdated: "userProfileUpdated" @@ -781,6 +808,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .itemsReadForChat(u, chatInfo): return withUser(u, String(describing: chatInfo)) case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) case .userProfileNoChange: return noDetails case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) @@ -831,6 +859,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool) case leftMemberUser(user: UserRef, groupInfo: GroupInfo) case groupMembers(user: UserRef, group: SimpleXChat.Group) + case memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case memberSupportChatDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) case groupUpdated(user: UserRef, toGroup: GroupInfo) @@ -879,6 +909,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case .userDeletedMembers: "userDeletedMembers" case .leftMemberUser: "leftMemberUser" case .groupMembers: "groupMembers" + case .memberAccepted: "memberAccepted" + case .memberSupportChatDeleted: "memberSupportChatDeleted" case .membersRoleUser: "membersRoleUser" case .membersBlockedForAllUser: "membersBlockedForAllUser" case .groupUpdated: "groupUpdated" @@ -923,6 +955,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)") case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupMembers(u, group): return withUser(u, String(describing: group)) + case let .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") + case let .memberSupportChatDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) @@ -988,6 +1022,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember) case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact) case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) + case memberAcceptedByOther(user: UserRef, groupInfo: GroupInfo, acceptingMember: GroupMember, member: GroupMember) case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, withMessages: Bool) @@ -1064,6 +1099,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case .groupLinkConnecting: "groupLinkConnecting" case .businessLinkConnecting: "businessLinkConnecting" case .joinedGroupMemberConnecting: "joinedGroupMemberConnecting" + case .memberAcceptedByOther: "memberAcceptedByOther" case .memberRole: "memberRole" case .memberBlockedForAll: "memberBlockedForAll" case .deletedMemberUser: "deletedMemberUser" @@ -1144,6 +1180,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))") case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))") case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)") + case let .memberAcceptedByOther(u, groupInfo, acceptingMember, member): return withUser(u, "groupInfo: \(groupInfo)\nacceptingMember: \(acceptingMember)\nmember: \(member)") case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)") case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)") case let .deletedMemberUser(u, groupInfo, member, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nwithMessages: \(withMessages)") diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 9b9fda0397..f8cb022095 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -52,8 +52,26 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { items.append(item) } +// analogue for SecondaryContextFilter in Kotlin +enum SecondaryItemsModelFilter { + case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo) + case msgContentTagContext(contentTag: MsgContentTag) + + func descr() -> String { + switch self { + case let .groupChatScopeContext(groupScopeInfo): + return "groupChatScopeContext \(groupScopeInfo.toChatScope())" + case let .msgContentTagContext(contentTag): + return "msgContentTagContext \(contentTag.rawValue)" + } + } +} + +// analogue for ChatsContext in Kotlin class ItemsModel: ObservableObject { - static let shared = ItemsModel() + static let shared = ItemsModel(secondaryIMFilter: nil) + public var secondaryIMFilter: SecondaryItemsModelFilter? + public var preloadState = PreloadState() private let publisher = ObservableObjectPublisher() private var bag = Set() var reversedChatItems: [ChatItem] = [] { @@ -77,13 +95,20 @@ class ItemsModel: ObservableObject { chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id } - init() { + init(secondaryIMFilter: SecondaryItemsModelFilter? = nil) { + self.secondaryIMFilter = secondaryIMFilter publisher .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { self.objectWillChange.send() } .store(in: &bag) } + static func loadSecondaryChat(_ chatId: ChatId, chatFilter: SecondaryItemsModelFilter, willNavigate: @escaping () -> Void = {}) { + let im = ItemsModel(secondaryIMFilter: chatFilter) + ChatModel.shared.secondaryIM = im + im.loadOpenChat(chatId, willNavigate: willNavigate) + } + func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) { navigationTimeoutTask?.cancel() loadChatTask?.cancel() @@ -99,7 +124,7 @@ class ItemsModel: ObservableObject { loadChatTask = Task { await MainActor.run { self.isLoading = true } // try? await Task.sleep(nanoseconds: 1000_000000) - await loadChat(chatId: chatId) + await loadChat(chatId: chatId, im: self) if !Task.isCancelled { await MainActor.run { self.isLoading = false @@ -114,7 +139,7 @@ class ItemsModel: ObservableObject { loadChatTask?.cancel() loadChatTask = Task { // try? await Task.sleep(nanoseconds: 1000_000000) - await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil) + await loadChat(chatId: chatId, im: self, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil) if !Task.isCancelled { await MainActor.run { if openAroundItemId == nil { @@ -124,6 +149,34 @@ class ItemsModel: ObservableObject { } } } + + public var contentTag: MsgContentTag? { + switch secondaryIMFilter { + case nil: nil + case .groupChatScopeContext: nil + case let .msgContentTagContext(contentTag): contentTag + } + } + + public var groupScopeInfo: GroupChatScopeInfo? { + switch secondaryIMFilter { + case nil: nil + case let .groupChatScopeContext(scopeInfo): scopeInfo + case .msgContentTagContext: nil + } + } +} + +class PreloadState { + var prevFirstVisible: Int64 = Int64.min + var prevItemsCount: Int = 0 + var preloading: Bool = false + + func clear() { + prevFirstVisible = Int64.min + prevItemsCount = 0 + preloading = false + } } class ChatTagsModel: ObservableObject { @@ -287,7 +340,6 @@ final class ChatModel: ObservableObject { // current chat @Published var chatId: String? @Published var openAroundItemId: ChatItem.ID? = nil - var chatItemStatuses: Dictionary = [:] @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list @@ -336,6 +388,10 @@ final class ChatModel: ObservableObject { let im = ItemsModel.shared + // ItemsModel for secondary chat view (such as support scope chat), as opposed to ItemsModel.shared used for primary chat + @Published var secondaryIM: ItemsModel? = nil + @Published var secondaryPendingInviteeChatOpened = false + static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } let ntfEnableLocal = true @@ -393,7 +449,7 @@ final class ChatModel: ObservableObject { func getGroupChat(_ groupId: Int64) -> Chat? { chats.first { chat in - if case let .group(groupInfo) = chat.chatInfo { + if case let .group(groupInfo, _) = chat.chatInfo { return groupInfo.groupId == groupId } else { return false @@ -446,7 +502,11 @@ final class ChatModel: ObservableObject { func updateChatInfo(_ cInfo: ChatInfo) { if let i = getChatIndex(cInfo.id) { - chats[i].chatInfo = cInfo + if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil { + chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil) + } else { + chats[i].chatInfo = cInfo + } chats[i].created = Date.now } } @@ -468,7 +528,7 @@ final class ChatModel: ObservableObject { } func updateGroup(_ groupInfo: GroupInfo) { - updateChat(.group(groupInfo: groupInfo)) + updateChat(.group(groupInfo: groupInfo, groupChatScope: nil)) } private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) { @@ -511,77 +571,112 @@ final class ChatModel: ObservableObject { // } func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { + // updates membersRequireAttention + updateChatInfo(cInfo) // mark chat non deleted if case let .direct(contact) = cInfo, contact.chatDeleted { var updatedContact = contact updatedContact.chatDeleted = false updateContact(updatedContact) } - // update previews + // update chat list if let i = getChatIndex(cInfo.id) { - chats[i].chatItems = switch cInfo { - case .group: - if let currentPreviewItem = chats[i].chatItems.first { - if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs { - [cItem] + // update preview + if cInfo.groupChatScope() == nil || cInfo.groupInfo?.membership.memberPending ?? false { + chats[i].chatItems = switch cInfo { + case .group: + if let currentPreviewItem = chats[i].chatItems.first { + if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs { + [cItem] + } else { + [currentPreviewItem] + } } else { - [currentPreviewItem] + [cItem] } - } else { + default: [cItem] } - default: - [cItem] - } - if case .rcvNew = cItem.meta.itemStatus { - unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0) + if case .rcvNew = cItem.meta.itemStatus { + unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0) + } } + // pop chat popChatCollector.throttlePopChat(cInfo.id, currentPosition: i) } else { - addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + if cInfo.groupChatScope() == nil { + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + } else { + addChat(Chat(chatInfo: cInfo, chatItems: [])) + } } - // add to current chat - if chatId == cInfo.id { - _ = _upsertChatItem(cInfo, cItem) + // add to current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + _ = _upsertChatItem(ciIM, cInfo, cItem) + } + } + + func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? { + let cInfoScope = cInfo.groupChatScope() + if let cInfoScope = cInfoScope { + switch cInfoScope { + case .memberSupport: + switch secondaryIM?.secondaryIMFilter { + case .none: + return nil + case let .groupChatScopeContext(groupScopeInfo): + return (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil + case let .msgContentTagContext(contentTag): + return (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil + } + } + } else { + return cInfo.id == chatId ? im : nil } } func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { - // update previews - var res: Bool - if let chat = getChat(cInfo.id) { - if let pItem = chat.chatItems.last { - if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + // update chat list + var itemAdded: Bool = false + if cInfo.groupChatScope() == nil { + if let chat = getChat(cInfo.id) { + if let pItem = chat.chatItems.last { + if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + chat.chatItems = [cItem] + } + } else { chat.chatItems = [cItem] } } else { - chat.chatItems = [cItem] + addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) + itemAdded = true + } + if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { + VoiceItemState.stopVoiceInChatView(cInfo, cItem) } - res = false - } else { - addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) - res = true } - if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { - VoiceItemState.stopVoiceInChatView(cInfo, cItem) + // update current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + itemAdded = _upsertChatItem(ciIM, cInfo, cItem) } - // update current chat - return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res + return itemAdded } - private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { - if let i = getChatItemIndex(cItem) { - _updateChatItem(at: i, with: cItem) - ChatItemDummyModel.shared.sendUpdate() + private func _upsertChatItem(_ ciIM: ItemsModel, _ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { + if let i = getChatItemIndex(ciIM, cItem) { + let oldStatus = ciIM.reversedChatItems[i].meta.itemStatus + let newStatus = cItem.meta.itemStatus + var ci = cItem + if shouldKeepOldSndCIStatus(oldStatus: oldStatus, newStatus: newStatus) { + ci.meta.itemStatus = oldStatus + } + _updateChatItem(ciIM: ciIM, at: i, with: ci) + ChatItemDummyModel.shared.sendUpdate() // TODO [knocking] review what's this return false } else { - var ci = cItem - if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { - ci.meta.itemStatus = status - } - im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) - im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0) - im.itemAdded = true + ciIM.reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0) + ciIM.chatState.itemAdded((cItem.id, cItem.isRcvNew), hasLiveDummy ? 1 : 0) + ciIM.itemAdded = true ChatItemDummyModel.shared.sendUpdate() return true } @@ -595,40 +690,42 @@ final class ChatModel: ObservableObject { } func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { + if let ciIM = getCIItemsModel(cInfo, cItem), + let i = getChatItemIndex(ciIM, cItem) { withConditionalAnimation { - _updateChatItem(at: i, with: cItem) + _updateChatItem(ciIM: ciIM, at: i, with: cItem) } - } else if let status = status { - chatItemStatuses.updateValue(status, forKey: cItem.id) } } - private func _updateChatItem(at i: Int, with cItem: ChatItem) { - im.reversedChatItems[i] = cItem - im.reversedChatItems[i].viewTimestamp = .now + private func _updateChatItem(ciIM: ItemsModel, at i: Int, with cItem: ChatItem) { + ciIM.reversedChatItems[i] = cItem + ciIM.reversedChatItems[i].viewTimestamp = .now } - func getChatItemIndex(_ cItem: ChatItem) -> Int? { - im.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) + func getChatItemIndex(_ ciIM: ItemsModel, _ cItem: ChatItem) -> Int? { + ciIM.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) } func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { - if cItem.isRcvNew { - unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0) - } - // update previews - if let chat = getChat(cInfo.id) { - if let pItem = chat.chatItems.last, pItem.id == cItem.id { - chat.chatItems = [ChatItem.deletedItemDummy()] + // update chat list + if cInfo.groupChatScope() == nil { + if cItem.isRcvNew { + unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0) + } + // update previews + if let chat = getChat(cInfo.id) { + if let pItem = chat.chatItems.last, pItem.id == cItem.id { + chat.chatItems = [ChatItem.deletedItemDummy()] + } } } - // remove from current chat - if chatId == cInfo.id { - if let i = getChatItemIndex(cItem) { + // remove from current scope + if let ciIM = getCIItemsModel(cInfo, cItem) { + if let i = getChatItemIndex(ciIM, cItem) { withAnimation { - let item = im.reversedChatItems.remove(at: i) - im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) + let item = ciIM.reversedChatItems.remove(at: i) + ciIM.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed()) } } } @@ -644,7 +741,7 @@ final class ChatModel: ObservableObject { if chatId == groupInfo.id { for i in 0.. = [] var i = 0 var ids = Set(itemIds) - while i < im.reversedChatItems.count && !ids.isEmpty { - let item = im.reversedChatItems[i] + while i < chatIM.reversedChatItems.count && !ids.isEmpty { + let item = chatIM.reversedChatItems[i] if ids.contains(item.id) && item.isRcvNew { - markChatItemRead_(i) + markChatItemRead_(chatIM, i) unreadItemIds.insert(item.id) ids.remove(item.id) } i += 1 } - im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed()) + chatIM.chatState.itemsRead(unreadItemIds, chatIM.reversedChatItems.reversed()) } self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead) } @@ -888,13 +984,13 @@ final class ChatModel: ObservableObject { } } - private func markChatItemRead_(_ i: Int) { - let meta = im.reversedChatItems[i].meta + private func markChatItemRead_(_ chatIM: ItemsModel, _ i: Int) { + let meta = chatIM.reversedChatItems[i].meta if case .rcvNew = meta.itemStatus { - im.reversedChatItems[i].meta.itemStatus = .rcvRead - im.reversedChatItems[i].viewTimestamp = .now + chatIM.reversedChatItems[i].meta.itemStatus = .rcvRead + chatIM.reversedChatItems[i].viewTimestamp = .now if meta.itemLive != true, let ttl = meta.itemTimed?.ttl { - im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) + chatIM.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) } } } @@ -973,7 +1069,7 @@ final class ChatModel: ObservableObject { var count = 0 var ns: [String] = [] if let ciCategory = chatItem.mergeCategory, - var i = getChatItemIndex(chatItem) { + var i = getChatItemIndex(im, chatItem) { // TODO [knocking] review: use getCIItemsModel? while i < im.reversedChatItems.count { let ci = im.reversedChatItems[i] if ci.mergeCategory != ciCategory { break } @@ -989,7 +1085,7 @@ final class ChatModel: ObservableObject { // returns the index of the passed item and the next item (it has smaller index) func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) { - if let i = getChatItemIndex(ci) { + if let i = getChatItemIndex(im, ci) { // TODO [knocking] review: use getCIItemsModel? (i, i > 0 ? im.reversedChatItems[i - 1] : nil) } else { (nil, nil) @@ -1100,7 +1196,7 @@ final class ChatModel: ObservableObject { func removeWallpaperFilesFromChat(_ chat: Chat) { if case let .direct(contact) = chat.chatInfo { removeWallpaperFilesFromTheme(contact.uiThemes) - } else if case let .group(groupInfo) = chat.chatInfo { + } else if case let .group(groupInfo, _) = chat.chatInfo { removeWallpaperFilesFromTheme(groupInfo.uiThemes) } } @@ -1164,6 +1260,18 @@ final class Chat: ObservableObject, Identifiable, ChatLike { var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } + var supportUnreadCount: Int { + switch chatInfo { + case let .group(groupInfo, _): + if groupInfo.canModerate { + return groupInfo.membersRequireAttention + } else { + return groupInfo.membership.supportChat?.unread ?? 0 + } + default: return 0 + } + } + 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 d92411decd..4f9d75bada 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -344,43 +344,54 @@ func apiGetChatTagsAsync() async throws -> [ChatTag] { let loadItemsPerPage = 50 -func apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { - let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search)) +func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search)) if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) } throw r.unexpected } -func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { - await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems) +func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async { + await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems) } -func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { - let m = ChatModel.shared - let im = ItemsModel.shared +func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async { await MainActor.run { - m.chatItemStatuses = [:] if clearItems { im.reversedChatItems = [] - ItemsModel.shared.chatState.clear() + im.chatState.clear() } } - await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 }) + await apiLoadMessages( + chatId, + im, + ( // pagination + openAroundItemId != nil + ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) + : ( + search == "" + ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage) + ) + ), + search, + openAroundItemId, + { 0...0 } + ) } -func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo { - let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId)) +func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> ChatItemInfo { + let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, scope: scope, itemId: itemId)) if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo } throw r.unexpected } -func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { - let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds)) +func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) { + let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(fromChatType: type, fromChatId: id, fromScope: scope, itemIds: itemIds)) if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) } throw r.unexpected } -func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { - let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl) +func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { + let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) } @@ -412,8 +423,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws { try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds)) } -func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { - let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages) +func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, live: live, ttl: ttl, composedMessages: composedMessages) return await processSendMessageCmd(toChatType: type, cmd: cmd) } @@ -490,8 +501,8 @@ private func createChatItemsErrorAlert(_ r: ChatError) { ) } -func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem { - let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay) +func apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem { + let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, scope: scope, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay) switch r { case let .chatItemUpdated(_, aChatItem): return aChatItem.chatItem case let .chatItemNotChanged(_, aChatItem): return aChatItem.chatItem @@ -499,8 +510,8 @@ func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: } } -func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem { - let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) +func apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem { + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, scope: scope, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay) if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem } throw r.unexpected } @@ -512,8 +523,8 @@ func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) throw r.unexpected } -func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { - let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) +func apiDeleteChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, scope: scope, itemIds: itemIds, mode: mode), bgDelay: msgDelay) if case let .chatItemsDeleted(_, items, _) = r { return items } throw r.unexpected } @@ -1199,6 +1210,13 @@ private func userAddressResponse(_ r: APIResult) throws -> UserCo } } +func apiAddMyAddressShortLink() async throws -> UserContactLink { + let userId = try currentUserId("apiAddMyAddressShortLink") + let r: ChatResponse1 = try await chatSendCmd(.apiAddMyAddressShortLink(userId: userId)) + if case let .userContactLink(_, contactLink) = r { return contactLink } + throw r.unexpected +} + func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? { let userId = try currentUserId("userAddressAutoAccept") let r: APIResult = await chatApiSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept)) @@ -1237,12 +1255,14 @@ func apiRejectContactRequest(contactReqId: Int64) async throws { throw r.unexpected } -func apiChatRead(type: ChatType, id: Int64) async throws { - try await sendCommandOkResp(.apiChatRead(type: type, id: id)) +func apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) async throws { + try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: scope)) } -func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws { - try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds)) +func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ChatInfo { + let r: ChatResponse1 = try await chatSendCmd(.apiChatItemsRead(type: type, id: id, scope: scope, itemIds: itemIds)) + if case let .itemsReadForChat(_, updatedChatInfo) = r { return updatedChatInfo } + throw r.unexpected } func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { @@ -1545,13 +1565,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] { throw r.unexpected } -func markChatRead(_ chat: Chat) async { +func markChatRead(_ im: ItemsModel, _ chat: Chat) async { do { if chat.chatStats.unreadCount > 0 { let cInfo = chat.chatInfo - try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId) + try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope()) await MainActor.run { - withAnimation { ChatModel.shared.markAllChatItemsRead(cInfo) } + withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) } } } if chat.chatStats.unreadChat { @@ -1574,11 +1594,12 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async { } } -func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async { +func apiMarkChatItemsRead(_ im: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async { do { - try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds) - DispatchQueue.main.async { - ChatModel.shared.markChatItemsRead(cInfo, itemIds, mentionsRead) + let updatedChatInfo = try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemIds: itemIds) + await MainActor.run { + ChatModel.shared.updateChatInfo(updatedChatInfo) + ChatModel.shared.markChatItemsRead(im, cInfo, itemIds, mentionsRead) } } catch { logger.error("apiChatItemsRead error: \(responseError(error))") @@ -1626,9 +1647,21 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { } } -func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] { +func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiAcceptMember(groupId: groupId, groupMemberId: groupMemberId, memberRole: memberRole)) + if case let .memberAccepted(_, groupInfo, member) = r { return (groupInfo, member) } + throw r.unexpected +} + +func apiDeleteMemberSupportChat(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupInfo, GroupMember) { + let r: ChatResponse2 = try await chatSendCmd(.apiDeleteMemberSupportChat(groupId: groupId, groupMemberId: groupMemberId)) + if case let .memberSupportChatDeleted(_, groupInfo, member) = r { return (groupInfo, member) } + throw r.unexpected +} + +func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> (GroupInfo, [GroupMember]) { let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false) - if case let .userDeletedMembers(_, _, members, withMessages) = r { return members } + if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) } throw r.unexpected } @@ -1669,8 +1702,8 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] { func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats - .compactMap{ $0.chatInfo.contact } - .filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } + .compactMap{ c in c.chatInfo.sendMsgEnabled ? c.chatInfo.contact : nil } + .filter{ c in !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } @@ -1710,6 +1743,12 @@ func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRo } } +func apiAddGroupShortLink(_ groupId: Int64) async throws -> (CreatedConnLink, GroupMemberRole) { + let r: ChatResponse2 = try await chatSendCmd(.apiAddGroupShortLink(groupId: groupId)) + if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) } + throw r.unexpected +} + func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { let r: ChatResponse2 = try await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) if case let .newMemberContact(_, contact, _, _) = r { return contact } @@ -2132,7 +2171,7 @@ func processReceivedMsg(_ res: ChatEvent) async { let cInfo = chatItem.chatInfo let cItem = chatItem.chatItem if !cItem.isDeletedContent && active(user) { - await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } + _ = await MainActor.run { m.upsertChatItem(cInfo, cItem) } } if let endTask = m.messageDelivery[cItem.id] { switch cItem.meta.itemStatus { @@ -2180,6 +2219,9 @@ func processReceivedMsg(_ res: ChatEvent) async { m.decreaseGroupReportsCounter(item.deletedChatItem.chatInfo.id) } } + if let updatedChatInfo = items.last?.deletedChatItem.chatInfo { + m.updateChatInfo(updatedChatInfo) + } } case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member_): await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member_) @@ -2228,6 +2270,13 @@ func processReceivedMsg(_ res: ChatEvent) async { _ = m.upsertGroupMember(groupInfo, member) } } + case let .memberAcceptedByOther(user, groupInfo, _, member): + if active(user) { + await MainActor.run { + _ = m.upsertGroupMember(groupInfo, member) + m.updateGroup(groupInfo) + } + } case let .deletedMemberUser(user, groupInfo, member, withMessages): // TODO update user member if active(user) { await MainActor.run { @@ -2240,6 +2289,7 @@ func processReceivedMsg(_ res: ChatEvent) async { case let .deletedMember(user, groupInfo, byMember, deletedMember, withMessages): if active(user) { await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, deletedMember) if withMessages { m.removeMemberItems(deletedMember, byMember: byMember, groupInfo) @@ -2249,6 +2299,7 @@ func processReceivedMsg(_ res: ChatEvent) async { case let .leftMember(user, groupInfo, member): if active(user) { await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, member) } } @@ -2263,6 +2314,12 @@ func processReceivedMsg(_ res: ChatEvent) async { await MainActor.run { m.updateGroup(groupInfo) } + if m.chatId == groupInfo.id, + case .memberSupport(nil) = m.secondaryIM?.groupScopeInfo { + await MainActor.run { + m.secondaryPendingInviteeChatOpened = false + } + } } case let .joinedGroupMember(user, groupInfo, member): if active(user) { @@ -2549,7 +2606,7 @@ func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemID return } let im = ItemsModel.shared - let cInfo = ChatInfo.group(groupInfo: groupInfo) + let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil) await MainActor.run { m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count) } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index f8d69c5fc8..47c0f61c79 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -159,7 +159,7 @@ struct SimpleXApp: App { if let id = chatModel.chatId, let chat = chatModel.getChat(id), !NtfManager.shared.navigatingToChat { - Task { await loadChat(chat: chat, clearItems: false) } + Task { await loadChat(chat: chat, im: ItemsModel.shared, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { await MainActor.run { chatModel.ntfContactRequest = nil } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 62a41c504a..b60842a4a0 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -22,11 +22,28 @@ struct ChatInfoToolbar: View { Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo) Spacer().frame(width: 16) } - ChatInfoImage( - chat: chat, - size: imageSize, - color: Color(uiColor: .tertiaryLabel) - ) + ZStack(alignment: .bottomTrailing) { + ChatInfoImage( + chat: chat, + size: imageSize, + color: Color(uiColor: .tertiaryLabel) + ) + if chat.chatStats.reportsCount > 0 { + Image(systemName: "flag.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .red) + } else if chat.supportUnreadCount > 0 { + Image(systemName: "flag.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, theme.colors.primary) + } + } .padding(.trailing, 4) let t = Text(cInfo.displayName).font(.headline) (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 8194c8fe6f..0498dc5d70 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -687,7 +687,7 @@ struct ChatTTLOption: View { let m = ChatModel.shared do { try await setChatTTL(chatType: chat.chatInfo.chatType, id: chat.chatInfo.apiId, ttl) - await loadChat(chat: chat, clearItems: true) + await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true) await MainActor.run { progressIndicator = false currentChatItemTTL = chatItemTTL @@ -700,7 +700,7 @@ struct ChatTTLOption: View { } catch let error { logger.error("setChatTTL error \(responseError(error))") - await loadChat(chat: chat, clearItems: true) + await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true) await MainActor.run { chatItemTTL = currentChatItemTTL progressIndicator = false @@ -938,7 +938,7 @@ struct ChatWallpaperEditorSheet: View { self.chat = chat self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { uiThemes - } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { uiThemes } else { ThemeModeOverrides() @@ -974,7 +974,7 @@ struct ChatWallpaperEditorSheet: View { private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides { if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { uiThemes - } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { uiThemes } else { ThemeModeOverrides() @@ -1052,12 +1052,12 @@ struct ChatWallpaperEditorSheet: View { chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact)) themes = themesFromChat(chat.wrappedValue) } - } else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo { + } else if case var ChatInfo.group(groupInfo, _) = chat.wrappedValue.chatInfo { groupInfo.uiThemes = changedThemesConstant await MainActor.run { - ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo)) - chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo)) + ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)) themes = themesFromChat(chat.wrappedValue) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index 02be8af73b..b2b4441646 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -12,8 +12,8 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel @Environment(\.revealed) var revealed: Bool - @ObservedObject var im = ItemsModel.shared @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var feature: Feature @@ -53,7 +53,7 @@ struct CIChatFeatureView: View { private func mergedFeatures() -> [FeatureInfo]? { var fs: [FeatureInfo] = [] var icons: Set = [] - if var i = m.getChatItemIndex(chatItem) { + if var i = m.getChatItemIndex(im, chatItem) { while i < im.reversedChatItems.count, let f = featureInfo(im.reversedChatItems[i]) { if !icons.contains(f.icon) { @@ -108,6 +108,7 @@ struct CIChatFeatureView_Previews: PreviewProvider { let enabled = FeatureEnabled(forUser: false, forContact: false) CIChatFeatureView( chat: Chat.sampleData, + im: ItemsModel.shared, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary) ).environment(\.revealed, true) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index b0b404d8b5..1b9376b5db 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -278,6 +278,7 @@ func showFileErrorAlert(_ err: FileError, temporary: Bool = false) { struct CIFileView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentFile: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -293,16 +294,16 @@ struct CIFileView_Previews: PreviewProvider { file: nil ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentFile, scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: fileChatItemWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 360)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index d30369339d..d1f49f635a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -12,7 +12,7 @@ import SimpleXChat struct CIImageView: View { @EnvironmentObject var m: ChatModel let chatItem: ChatItem - var scrollToItemId: ((ChatItem.ID) -> Void)? = nil + var scrollToItem: ((ChatItem.ID) -> Void)? = nil var preview: UIImage? let maxWidth: CGFloat var imgWidth: CGFloat? @@ -26,7 +26,7 @@ struct CIImageView: View { if let uiImage = getLoadedImage(file) { Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { - FullScreenMediaView(chatItem: chatItem, scrollToItemId: scrollToItemId, image: uiImage, showView: $showFullScreenImage) + FullScreenMediaView(chatItem: chatItem, scrollToItem: scrollToItem, image: uiImage, showView: $showFullScreenImage) } .if(!smallView) { view in view.modifier(PrivacyBlur(blurred: $blurred)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 4e5713c263..3201332c1e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -45,7 +45,7 @@ struct CIRcvDecryptionError: View { viewBody() .onAppear { // for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear - if case let .group(groupInfo) = chat.chatInfo, + if case let .group(groupInfo, _) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir { do { let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId) @@ -83,7 +83,7 @@ struct CIRcvDecryptionError: View { } else { basicDecryptionErrorItem() } - } else if case let .group(groupInfo) = chat.chatInfo, + } else if case let .group(groupInfo, _) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir, let mem = m.getGroupMember(groupMember.groupMemberId), let memberStats = mem.wrapped.activeConn?.connectionStats { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 715e606a74..47aee2a586 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -435,6 +435,7 @@ class VoiceItemState { struct CIVoiceView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -457,10 +458,10 @@ struct CIVoiceView_Previews: PreviewProvider { duration: 30, allowMenu: Binding.constant(true) ) - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }, allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItemId: { _ in }, allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }, allowMenu: .constant(true)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, scrollToItemId: { _ in }, allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index f4e2a4135a..0b6f249b9c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -77,6 +77,7 @@ struct FramedCIVoiceView: View { struct FramedCIVoiceView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), @@ -92,11 +93,11 @@ struct FramedCIVoiceView_Previews: PreviewProvider { file: CIFile.getSample(fileStatus: .sndComplete) ) Group { - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWithQuote, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 360)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index b27d266d8a..c9c9952688 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -13,8 +13,10 @@ struct FramedItemView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem - var scrollToItemId: (ChatItem.ID) -> Void + var scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? var preview: UIImage? var maxWidth: CGFloat = .infinity @State var msgWidth: CGFloat = 0 @@ -56,12 +58,16 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) .simultaneousGesture(TapGesture().onEnded { - if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { + if let ci = im.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { - scrollToItemId(ci.id) + scrollToItem(ci.id) } } else if let id = qi.itemId { - scrollToItemId(id) + if (chatItem.isReport && im.secondaryIMFilter != nil) { + scrollToItemId = id + } else { + scrollToItem(id) + } } else { showQuotedItemDoesNotExistAlert() } @@ -70,7 +76,7 @@ struct FramedItemView: View { framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text(itemForwarded.text(chat.chatInfo.chatType)).italic(), pad: true) } - ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: framedMsgContentView) + ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: framedMsgContentView) .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) } @@ -119,7 +125,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, _): - CIImageView(chatItem: chatItem, scrollToItemId: scrollToItemId, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) + CIImageView(chatItem: chatItem, scrollToItem: scrollToItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -290,7 +296,7 @@ struct FramedItemView: View { private func membership() -> GroupMember? { switch chat.chatInfo { - case let .group(groupInfo: groupInfo): return groupInfo.membership + case let .group(groupInfo: groupInfo, _): return groupInfo.membership default: return nil } } @@ -386,15 +392,16 @@ func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { struct FramedItemView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group{ - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -402,17 +409,18 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 200)) @@ -421,17 +429,18 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true)) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 200)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index 10e5efa298..f243a83142 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -14,7 +14,7 @@ import AVKit struct FullScreenMediaView: View { @EnvironmentObject var m: ChatModel @State var chatItem: ChatItem - var scrollToItemId: ((ChatItem.ID) -> Void)? + var scrollToItem: ((ChatItem.ID) -> Void)? @State var image: UIImage? @State var player: AVPlayer? = nil @State var url: URL? = nil @@ -71,7 +71,7 @@ struct FullScreenMediaView: View { let w = abs(t.width) if t.height > 60 && t.height > w * 2 { showView = false - scrollToItemId?(chatItem.id) + scrollToItem?(chatItem.id) } else if w > 60 && w > abs(t.height) * 2 && !scrolling { let previous = t.width > 0 scrolling = true diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 87a9b2ce61..c6a5d0353c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -14,6 +14,7 @@ struct MarkedDeletedItemView: View { @EnvironmentObject var theme: AppTheme @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem var body: some View { @@ -29,14 +30,14 @@ struct MarkedDeletedItemView: View { var mergedMarkedDeletedText: LocalizedStringKey { if !revealed, let ciCategory = chatItem.mergeCategory, - var i = m.getChatItemIndex(chatItem) { + var i = m.getChatItemIndex(im, chatItem) { var moderated = 0 var blocked = 0 var blockedByAdmin = 0 var deleted = 0 var moderatedBy: Set = [] - while i < ItemsModel.shared.reversedChatItems.count, - let ci = .some(ItemsModel.shared.reversedChatItems[i]), + while i < im.reversedChatItems.count, + let ci = .some(im.reversedChatItems[i]), ci.mergeCategory == ciCategory, let itemDeleted = ci.meta.itemDeleted { switch itemDeleted { @@ -85,6 +86,7 @@ struct MarkedDeletedItemView_Previews: PreviewProvider { Group { MarkedDeletedItemView( chat: Chat.sampleData, + im: ItemsModel.shared, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)) ).environment(\.revealed, true) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index f5558bcd93..a412bf4452 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -40,25 +40,31 @@ extension EnvironmentValues { struct ChatItemView: View { @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel @EnvironmentObject var theme: AppTheme @Environment(\.showTimestamp) var showTimestamp: Bool @Environment(\.revealed) var revealed: Bool var chatItem: ChatItem - var scrollToItemId: (ChatItem.ID) -> Void + var scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? var maxWidth: CGFloat = .infinity @Binding var allowMenu: Bool init( chat: Chat, + im: ItemsModel, chatItem: ChatItem, - scrollToItemId: @escaping (ChatItem.ID) -> Void, + scrollToItem: @escaping (ChatItem.ID) -> Void, + scrollToItemId: Binding = .constant(nil), showMember: Bool = false, maxWidth: CGFloat = .infinity, allowMenu: Binding = .constant(false) ) { self.chat = chat + self.im = im self.chatItem = chatItem - self.scrollToItemId = scrollToItemId + self.scrollToItem = scrollToItem + _scrollToItemId = scrollToItemId self.maxWidth = maxWidth _allowMenu = allowMenu } @@ -66,14 +72,14 @@ struct ChatItemView: View { var body: some View { let ci = chatItem if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) { - MarkedDeletedItemView(chat: chat, chatItem: chatItem) + MarkedDeletedItemView(chat: chat, im: im, chatItem: chatItem) } else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chat: chat, chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { - ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case + ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { framedItemView() } @@ -101,8 +107,10 @@ struct ChatItemView: View { }() return FramedItemView( chat: chat, + im: im, chatItem: chatItem, - scrollToItemId: scrollToItemId, + scrollToItem: scrollToItem, + scrollToItemId: $scrollToItemId, preview: preview, maxWidth: maxWidth, imgWidth: adjustedMaxWidth, @@ -117,6 +125,7 @@ struct ChatItemContentView: View { @EnvironmentObject var theme: AppTheme @Environment(\.revealed) var revealed: Bool @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel var chatItem: ChatItem var msgContentView: () -> Content @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -140,7 +149,9 @@ struct ChatItemContentView: View { case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case .rcvDirectEvent: eventItemView() case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem) + case .rcvGroupEvent(.newMemberPendingReview): CIEventView(eventText: pendingReviewEventItemText()) case .rcvGroupEvent: eventItemView() + case .sndGroupEvent(.userPendingReview): CIEventView(eventText: pendingReviewEventItemText()) case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() case .sndConnEvent: eventItemView() @@ -149,7 +160,7 @@ struct ChatItemContentView: View { case let .rcvChatPreference(feature, allowed, param): CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) case let .sndChatPreference(feature, _, _): - CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) + CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) @@ -181,6 +192,13 @@ struct ChatItemContentView: View { CIEventView(eventText: eventItemViewText(theme.colors.secondary)) } + private func pendingReviewEventItemText() -> Text { + Text(chatItem.content.text) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .fontWeight(.bold) + } + private func eventItemViewText(_ secondaryColor: Color) -> Text { if !revealed, let t = mergedGroupEventText { return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor) @@ -196,7 +214,7 @@ struct ChatItemContentView: View { } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { - CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor) + CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor) } private var mergedGroupEventText: Text? { @@ -256,16 +274,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { struct ChatItemView_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared Group{ - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getDeletedContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true) + ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true) } .environment(\.revealed, false) .previewLayout(.fixed(width: 360, height: 70)) @@ -275,10 +294,12 @@ struct ChatItemView_Previews: PreviewProvider { struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { static var previews: some View { + let im = ItemsModel.shared let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil) Group{ ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), @@ -286,10 +307,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - scrollToItemId: { _ in } + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead), @@ -297,10 +320,11 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - scrollToItemId: { _ in } + scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), @@ -308,10 +332,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - scrollToItemId: { _ in } + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), @@ -319,10 +345,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - scrollToItemId: { _ in } + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) ChatItemView( chat: Chat.sampleData, + im: im, chatItem: ChatItem( chatDir: .directRcv, meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)), @@ -330,7 +358,8 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { quotedItem: nil, file: nil ), - scrollToItemId: { _ in } + scrollToItem: { _ in }, + scrollToItemId: Binding.constant(nil) ) } .environment(\.revealed, true) diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift index 07034cf8ec..93ecf870eb 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift @@ -13,8 +13,8 @@ let TRIM_KEEP_COUNT = 200 func apiLoadMessages( _ chatId: ChatId, + _ im: ItemsModel, _ pagination: ChatPagination, - _ chatState: ActiveChatState, _ search: String = "", _ openAroundItemId: ChatItem.ID? = nil, _ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange = { 0 ... 0 } @@ -22,7 +22,7 @@ func apiLoadMessages( let chat: Chat let navInfo: NavigationInfo do { - (chat, navInfo) = try await apiGetChat(chatId: chatId, pagination: pagination, search: search) + (chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search) } catch let error { logger.error("apiLoadMessages error: \(responseError(error))") return @@ -38,30 +38,31 @@ func apiLoadMessages( return } - let unreadAfterItemId = chatState.unreadAfterItemId + let unreadAfterItemId = im.chatState.unreadAfterItemId - let oldItems = Array(ItemsModel.shared.reversedChatItems.reversed()) + let oldItems = Array(im.reversedChatItems.reversed()) var newItems: [ChatItem] = [] switch pagination { case .initial: let newSplits: [Int64] = if !chat.chatItems.isEmpty && navInfo.afterTotal > 0 { [chat.chatItems.last!.id] } else { [] } - if chatModel.getChat(chat.id) == nil { + if im.secondaryIMFilter == nil && chatModel.getChat(chat.id) == nil { chatModel.addChat(chat) } await MainActor.run { - chatModel.chatItemStatuses.removeAll() - ItemsModel.shared.reversedChatItems = chat.chatItems.reversed() - chatModel.updateChatInfo(chat.chatInfo) - chatState.splits = newSplits - if !chat.chatItems.isEmpty { - chatState.unreadAfterItemId = chat.chatItems.last!.id + im.reversedChatItems = chat.chatItems.reversed() + if im.secondaryIMFilter == nil { + chatModel.updateChatInfo(chat.chatInfo) } - chatState.totalAfter = navInfo.afterTotal - chatState.unreadTotal = chat.chatStats.unreadCount - chatState.unreadAfter = navInfo.afterUnread - chatState.unreadAfterNewestLoaded = navInfo.afterUnread + im.chatState.splits = newSplits + if !chat.chatItems.isEmpty { + im.chatState.unreadAfterItemId = chat.chatItems.last!.id + } + im.chatState.totalAfter = navInfo.afterTotal + im.chatState.unreadTotal = chat.chatStats.unreadCount + im.chatState.unreadAfter = navInfo.afterUnread + im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread - PreloadState.shared.clear() + im.preloadState.clear() } case let .before(paginationChatItemId, _): newItems.append(contentsOf: oldItems) @@ -71,15 +72,15 @@ func apiLoadMessages( let wasSize = newItems.count let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() } let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination( - unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexes + unreadAfterItemId, &newItems, newIds, im.chatState.splits, visibleItemIndexes ) let insertAt = max((indexInCurrentItems - (wasSize - newItems.count) + modifiedSplits.trimmedIds.count), 0) newItems.insert(contentsOf: chat.chatItems, at: insertAt) let newReversed: [ChatItem] = newItems.reversed() await MainActor.run { - ItemsModel.shared.reversedChatItems = newReversed - chatState.splits = modifiedSplits.newSplits - chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems) + im.reversedChatItems = newReversed + im.chatState.splits = modifiedSplits.newSplits + im.chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems) } case let .after(paginationChatItemId, _): newItems.append(contentsOf: oldItems) @@ -89,7 +90,7 @@ func apiLoadMessages( let mappedItems = mapItemsToIds(chat.chatItems) let newIds = mappedItems.0 let (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination( - mappedItems.1, paginationChatItemId, &newItems, newIds, chat, chatState.splits + mappedItems.1, paginationChatItemId, &newItems, newIds, chat, im.chatState.splits ) let indexToAdd = min(indexInCurrentItems + 1, newItems.count) let indexToAddIsLast = indexToAdd == newItems.count @@ -97,19 +98,19 @@ func apiLoadMessages( let new: [ChatItem] = newItems let newReversed: [ChatItem] = newItems.reversed() await MainActor.run { - ItemsModel.shared.reversedChatItems = newReversed - chatState.splits = newSplits - chatState.moveUnreadAfterItem(chatState.splits.first ?? new.last!.id, new) + im.reversedChatItems = newReversed + im.chatState.splits = newSplits + im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new) // loading clear bottom area, updating number of unread items after the newest loaded item if indexToAddIsLast { - chatState.unreadAfterNewestLoaded -= unreadInLoaded + im.chatState.unreadAfterNewestLoaded -= unreadInLoaded } } case .around: var newSplits: [Int64] if openAroundItemId == nil { newItems.append(contentsOf: oldItems) - newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed) + newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, im.chatState.splits, visibleItemIndexesNonReversed) } else { newSplits = [] } @@ -120,33 +121,37 @@ func apiLoadMessages( let newReversed: [ChatItem] = newItems.reversed() let orderedSplits = newSplits await MainActor.run { - ItemsModel.shared.reversedChatItems = newReversed - chatState.splits = orderedSplits - chatState.unreadAfterItemId = chat.chatItems.last!.id - chatState.totalAfter = navInfo.afterTotal - chatState.unreadTotal = chat.chatStats.unreadCount - chatState.unreadAfter = navInfo.afterUnread + im.reversedChatItems = newReversed + im.chatState.splits = orderedSplits + im.chatState.unreadAfterItemId = chat.chatItems.last!.id + im.chatState.totalAfter = navInfo.afterTotal + im.chatState.unreadTotal = chat.chatStats.unreadCount + im.chatState.unreadAfter = navInfo.afterUnread if let openAroundItemId { - chatState.unreadAfterNewestLoaded = navInfo.afterUnread - ChatModel.shared.openAroundItemId = openAroundItemId - ChatModel.shared.chatId = chatId + im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread + if im.secondaryIMFilter == nil { + ChatModel.shared.openAroundItemId = openAroundItemId // TODO [knocking] move openAroundItemId from ChatModel to ItemsModel? + ChatModel.shared.chatId = chat.id + } } else { // no need to set it, count will be wrong // chatState.unreadAfterNewestLoaded = navInfo.afterUnread } - PreloadState.shared.clear() + im.preloadState.clear() } case .last: newItems.append(contentsOf: oldItems) - let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits) + let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, im.chatState.splits) newItems.append(contentsOf: chat.chatItems) let items = newItems await MainActor.run { - ItemsModel.shared.reversedChatItems = items.reversed() - chatState.splits = newSplits - chatModel.updateChatInfo(chat.chatInfo) - chatState.unreadAfterNewestLoaded = 0 + im.reversedChatItems = items.reversed() + im.chatState.splits = newSplits + if im.secondaryIMFilter == nil { + chatModel.updateChatInfo(chat.chatInfo) + } + im.chatState.unreadAfterNewestLoaded = 0 } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift index 0a55ed48cc..5f2102b8bc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct MergedItems: Hashable, Equatable { + let im: ItemsModel let items: [MergedItem] let splits: [SplitRange] // chat item id, index in list @@ -23,15 +24,15 @@ struct MergedItems: Hashable, Equatable { hasher.combine("\(items.hashValue)") } - static func create(_ items: [ChatItem], _ revealedItems: Set, _ chatState: ActiveChatState) -> MergedItems { - if items.isEmpty { - return MergedItems(items: [], splits: [], indexInParentItems: [:]) + static func create(_ im: ItemsModel, _ revealedItems: Set) -> MergedItems { + if im.reversedChatItems.isEmpty { + return MergedItems(im: im, items: [], splits: [], indexInParentItems: [:]) } - let unreadCount = chatState.unreadTotal + let unreadCount = im.chatState.unreadTotal - let unreadAfterItemId = chatState.unreadAfterItemId - let itemSplits = chatState.splits + let unreadAfterItemId = im.chatState.unreadAfterItemId + let itemSplits = im.chatState.splits var mergedItems: [MergedItem] = [] // Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems var splitRanges: [SplitRange] = [] @@ -40,19 +41,19 @@ struct MergedItems: Hashable, Equatable { var unclosedSplitIndex: Int? = nil var unclosedSplitIndexInParent: Int? = nil var visibleItemIndexInParent = -1 - var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded + var unreadBefore = unreadCount - im.chatState.unreadAfterNewestLoaded var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil var lastRangeInReversedForMergedItems: BoxedValue>? = nil var recent: MergedItem? = nil - while index < items.count { - let item = items[index] - let prev = index >= 1 ? items[index - 1] : nil - let next = index + 1 < items.count ? items[index + 1] : nil + while index < im.reversedChatItems.count { + let item = im.reversedChatItems[index] + let prev = index >= 1 ? im.reversedChatItems[index - 1] : nil + let next = index + 1 < im.reversedChatItems.count ? im.reversedChatItems[index + 1] : nil let category = item.mergeCategory let itemIsSplit = itemSplits.contains(item.id) if item.id == unreadAfterItemId { - unreadBefore = unreadCount - chatState.unreadAfter + unreadBefore = unreadCount - im.chatState.unreadAfter } if item.isRcvNew { unreadBefore -= 1 @@ -106,18 +107,19 @@ struct MergedItems: Hashable, Equatable { // found item that is considered as a split if let unclosedSplitIndex, let unclosedSplitIndexInParent { // it was at least second split in the list - splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1)) + splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1)) } unclosedSplitIndex = index unclosedSplitIndexInParent = visibleItemIndexInParent - } else if index + 1 == items.count, let unclosedSplitIndex, let unclosedSplitIndexInParent { + } else if index + 1 == im.reversedChatItems.count, let unclosedSplitIndex, let unclosedSplitIndexInParent { // just one split for the whole list, there will be no more, it's the end - splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent)) + splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent)) } indexInParentItems[item.id] = visibleItemIndexInParent index += 1 } return MergedItems( + im: im, items: mergedItems, splits: splitRanges, indexInParentItems: indexInParentItems @@ -127,7 +129,6 @@ struct MergedItems: Hashable, Equatable { // Use this check to ensure that mergedItems state based on currently actual state of global // splits and reversedChatItems func isActualState() -> Bool { - let im = ItemsModel.shared // do not load anything if global splits state is different than in merged items because it // will produce undefined results in terms of loading and placement of items. // Same applies to reversedChatItems @@ -434,7 +435,7 @@ class BoxedValue: Equatable, Hashable { } @MainActor -func visibleItemIndexesNonReversed(_ listState: EndlessScrollView.ListState, _ mergedItems: MergedItems) -> ClosedRange { +func visibleItemIndexesNonReversed(_ im: ItemsModel, _ listState: EndlessScrollView.ListState, _ mergedItems: MergedItems) -> ClosedRange { let zero = 0 ... 0 let items = mergedItems.items if items.isEmpty { @@ -445,12 +446,12 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView.Li guard let newest, let oldest else { return zero } - let size = ItemsModel.shared.reversedChatItems.count + let size = im.reversedChatItems.count let range = size - oldest ... size - newest if range.lowerBound < 0 || range.upperBound < 0 { return zero } - // visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed() + // visible items mapped to their underlying data structure which is im.reversedChatItems.reversed() return range } diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift index c1a1eec7d2..2fb1c3fb35 100644 --- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift +++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift @@ -9,7 +9,7 @@ import SwiftUI import SimpleXChat -func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat) async { +func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat, _ im: ItemsModel) async { await MainActor.run { loadingMoreItems.wrappedValue = true loadingBottomItems.wrappedValue = true @@ -22,27 +22,15 @@ func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Bindin } return } - await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState) + await apiLoadMessages(chat.chatInfo.id, im, ChatPagination.last(count: 50)) await MainActor.run { loadingMoreItems.wrappedValue = false loadingBottomItems.wrappedValue = false } } -class PreloadState { - static let shared = PreloadState() - var prevFirstVisible: Int64 = Int64.min - var prevItemsCount: Int = 0 - var preloading: Bool = false - - func clear() { - prevFirstVisible = Int64.min - prevItemsCount = 0 - preloading = false - } -} - func preloadIfNeeded( + _ im: ItemsModel, _ allowLoadMoreItems: Binding, _ ignoreLoadingRequests: Binding, _ listState: EndlessScrollView.ListState, @@ -50,7 +38,7 @@ func preloadIfNeeded( loadItems: @escaping (Bool, ChatPagination) async -> Bool, loadLastItems: @escaping () async -> Void ) { - let state = PreloadState.shared + let state = im.preloadState guard !listState.isScrolling && !listState.isAnimatedScrolling, !state.preloading, listState.totalItemsCount > 0 @@ -63,7 +51,7 @@ func preloadIfNeeded( Task { defer { state.preloading = false } var triedToLoad = true - await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in + await preloadItems(im, mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in triedToLoad = await loadItems(false, pagination) return triedToLoad } @@ -73,11 +61,11 @@ func preloadIfNeeded( } // it's important to ask last items when the view is fully covered with items. Otherwise, visible items from one // split will be merged with last items and position of scroll will change unexpectedly. - if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + if listState.itemsCanCoverScreen && !im.lastItemsLoaded { await loadLastItems() } } - } else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded { + } else if listState.itemsCanCoverScreen && !im.lastItemsLoaded { state.preloading = true Task { defer { state.preloading = false } @@ -87,6 +75,7 @@ func preloadIfNeeded( } func preloadItems( + _ im: ItemsModel, _ mergedItems: MergedItems, _ allowLoadMoreItems: Bool, _ listState: EndlessScrollView.ListState, @@ -105,7 +94,7 @@ async { let splits = mergedItems.splits let lastVisibleIndex = listState.lastVisibleItemIndex var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) - let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed() + let items: [ChatItem] = im.reversedChatItems.reversed() if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining { lastIndexToLoadFrom = items.count - 1 } @@ -122,7 +111,7 @@ async { let sizeWas = items.count let firstItemIdWas = items.first?.id let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT)) - if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id { + if triedToLoad && sizeWas == im.reversedChatItems.count && firstItemIdWas == im.reversedChatItems.last?.id { ignoreLoadingRequests.wrappedValue = loadFromItemId return false } @@ -133,7 +122,7 @@ async { let splits = mergedItems.splits let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) }) // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) - let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems + let reversedItems: [ChatItem] = im.reversedChatItems if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex { let index = split.indexRangeInReversed.lowerBound if index >= 0 { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index c136ebc01b..8ce0c50849 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -15,8 +15,6 @@ private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @ObservedObject var im = ItemsModel.shared - @State var mergedItems: BoxedValue = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState)) @State var revealedItems: Set = Set() @State var theme: AppTheme = buildTheme() @Environment(\.dismiss) var dismiss @@ -24,6 +22,10 @@ struct ChatView: View { @Environment(\.presentationMode) var presentationMode @Environment(\.scenePhase) var scenePhase @State @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel + @State var mergedItems: BoxedValue + @State var floatingButtonModel: FloatingButtonModel + @Binding var scrollToItemId: ChatItem.ID? @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() @@ -55,12 +57,14 @@ struct ChatView: View { @State private var allowLoadMoreItems: Bool = false @State private var ignoreLoadingRequests: Int64? = nil @State private var animatedScrollingInProgress: Bool = false - @State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel() + @State private var showUserSupportChatSheet = false @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + let userSupportScopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil) + var body: some View { if #available(iOS 16.0, *) { viewBody @@ -73,25 +77,39 @@ struct ChatView: View { private var viewBody: some View { let cInfo = chat.chatInfo + let memberSupportChat: (groupInfo: GroupInfo, member: GroupMember?)? = + if case let .group(groupInfo, .memberSupport(member)) = cInfo { + (groupInfo, member) + } else { + nil + } + let userMemberKnockingChat = memberSupportChat?.groupInfo.membership.memberPending == true return ZStack { let wallpaperImage = theme.wallpaper.type.image let wallpaperType = theme.wallpaper.type let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background) let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base) Color.clear.ignoresSafeArea(.all) - .if(wallpaperImage != nil) { view in + .if(wallpaperImage != nil && im.secondaryIMFilter == nil) { view in view.modifier( ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor) ) } VStack(spacing: 0) { ZStack(alignment: .bottomTrailing) { - chatItemsList() - if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { - GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) + if userMemberKnockingChat { + ZStack(alignment: .top) { + chatItemsList() + userMemberKnockingTitleBar() + } + } else { + chatItemsList() } - FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { + GroupMentionsView(im: im, groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) + } + FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { + mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) } ) @@ -101,6 +119,7 @@ struct ChatView: View { let reason = chat.chatInfo.userCantSendReason ComposeView( chat: chat, + im: im, composeState: $composeState, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, @@ -118,7 +137,7 @@ struct ChatView: View { } } else { SelectedItemsBottomToolbar( - chatItems: ItemsModel.shared.reversedChatItems, + im: im, selectedChatItems: $selectedChatItems, chatInfo: chat.chatInfo, deleteItems: { forAll in @@ -129,7 +148,7 @@ struct ChatView: View { showArchiveSelectedReports = true }, moderateItems: { - if case let .group(groupInfo) = chat.chatInfo { + if case let .group(groupInfo, _) = chat.chatInfo { showModerateSelectedMessagesAlert(groupInfo) } }, @@ -148,7 +167,11 @@ struct ChatView: View { } .background(ToolbarMaterial.material(toolbarMaterial)) } - .navigationTitle(cInfo.chatViewName) + .navigationTitle( + memberSupportChat == nil + ? cInfo.chatViewName + : memberSupportChat?.member?.chatViewName ?? NSLocalizedString("Chat with admins", comment: "chat toolbar") + ) .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) .environmentObject(theme) @@ -172,7 +195,7 @@ struct ChatView: View { archiveReports(chat.chatInfo, selected.sorted(), false, deletedSelectedMessages) } } - if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive { + if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive { Button("For all moderators", role: .destructive) { if let selected = selectedChatItems { archiveReports(chat.chatInfo, selected.sorted(), true, deletedSelectedMessages) @@ -181,23 +204,20 @@ struct ChatView: View { } } .appSheet(item: $selectedMember) { member in - Group { - if case let .group(groupInfo) = chat.chatInfo { - GroupMemberInfoView( - groupInfo: groupInfo, - chat: chat, - groupMember: member, - navigation: true - ) - } + if case let .group(groupInfo, _) = chat.chatInfo { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: member, + scrollToItemId: $scrollToItemId, + navigation: true + ) } } // it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field .appSheet(isPresented: $showAddMembersSheet) { - Group { - if case let .group(groupInfo) = cInfo { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) - } + if case let .group(groupInfo, _) = cInfo { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) } } .sheet(isPresented: Binding( @@ -216,6 +236,21 @@ struct ChatView: View { ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState) } } + .appSheet( + isPresented: $showUserSupportChatSheet, + onDismiss: { + if chat.chatInfo.groupInfo?.membership.memberPending ?? false { + chatModel.chatId = nil + } + } + ) { + if let groupInfo = cInfo.groupInfo { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: userSupportScopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } + } .onAppear { scrollView.listState.onUpdateListener = onChatItemsUpdated selectedChatItems = nil @@ -231,6 +266,14 @@ struct ChatView: View { } } } + // if this is the main chat of the group with the pending member (knocking) + if case let .group(groupInfo, nil) = chat.chatInfo, + groupInfo.membership.memberPending { + ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) { + showUserSupportChatSheet = true + chatModel.secondaryPendingInviteeChatOpened = true + } + } } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false @@ -245,7 +288,7 @@ struct ChatView: View { initChatView() theme = buildTheme() closeSearch() - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] { @@ -262,10 +305,15 @@ struct ChatView: View { dismiss() } } + .onChange(of: chatModel.secondaryPendingInviteeChatOpened) { opened in + if im.secondaryIMFilter != nil && !opened { + dismiss() + } + } .onChange(of: chatModel.openAroundItemId) { openAround in if let openAround { closeSearch() - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) chatModel.openAroundItemId = nil @@ -288,9 +336,8 @@ struct ChatView: View { if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { - chatModel.chatItemStatuses = [:] - ItemsModel.shared.reversedChatItems = [] - ItemsModel.shared.chatState.clear() + im.reversedChatItems = [] + im.chatState.clear() chatModel.groupMembers = [] chatModel.groupMembersIndexes.removeAll() chatModel.membersLoaded = false @@ -303,124 +350,220 @@ struct ChatView: View { } .toolbar { ToolbarItem(placement: .principal) { - if selectedChatItems != nil { - SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) - } else if case let .direct(contact) = cInfo { - Button { - Task { - showChatInfoSheet = true - } - } label: { - ChatInfoToolbar(chat: chat) - } - .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { - ChatInfoView( - chat: chat, - contact: contact, - localAlias: chat.chatInfo.localAlias, - featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), - currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), - onSearch: { focusSearch() } - ) - } - } else if case let .group(groupInfo) = cInfo { - Button { - Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } - } label: { - ChatInfoToolbar(chat: chat) - .tint(theme.colors.primary) - } - .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { - GroupChatInfoView( - chat: chat, - groupInfo: Binding( - get: { groupInfo }, - set: { gInfo in - chat.chatInfo = .group(groupInfo: gInfo) - chat.created = Date.now - } - ), - onSearch: { focusSearch() }, - localAlias: groupInfo.localAlias - ) - } - } else if case .local = cInfo { - ChatInfoToolbar(chat: chat) + if im.secondaryIMFilter == nil { + primaryPrincipalToolbarContent() + } else if !userMemberKnockingChat { // no toolbar while knocking chat, it's unstable on sheet + secondaryPrincipalToolbarContent() } } ToolbarItem(placement: .navigationBarTrailing) { - if selectedChatItems != nil { - Button { - withAnimation { - selectedChatItems = nil - } - } 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) - } - } - 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") - } - } - 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() - } - } - Menu { - searchButton() - ToggleNtfsButton(chat: chat) - } label: { - Image(systemName: "ellipsis") - } - } - case .local: - searchButton() - default: - EmptyView() + if im.secondaryIMFilter == nil { + primaryTrailingToolbarContent() + } else if !userMemberKnockingChat { + secondaryTrailingToolbarContent() + } + } + } + .if(im.secondaryIMFilter == nil) { v in + v.onChange(of: scrollToItemId) { itemId in + if let itemId = itemId { + dismissAllSheets(animated: false) { + scrollToItem(itemId) + scrollToItemId = nil } } } } } + @inline(__always) + @ViewBuilder private func primaryPrincipalToolbarContent() -> some View { + let cInfo = chat.chatInfo + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else if case let .direct(contact) = cInfo { + Button { + Task { + showChatInfoSheet = true + } + } label: { + ChatInfoToolbar(chat: chat) + } + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + ChatInfoView( + chat: chat, + contact: contact, + localAlias: chat.chatInfo.localAlias, + featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), + currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences), + onSearch: { focusSearch() } + ) + } + } else if case let .group(groupInfo, _) = cInfo { + Button { + Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } + } label: { + ChatInfoToolbar(chat: chat) + .tint(theme.colors.primary) + } + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + GroupChatInfoView( + chat: chat, + groupInfo: Binding( + get: { groupInfo }, + set: { gInfo in + chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil) + chat.created = Date.now + } + ), + scrollToItemId: $scrollToItemId, + onSearch: { focusSearch() }, + localAlias: groupInfo.localAlias + ) + } + } else if case .local = cInfo { + ChatInfoToolbar(chat: chat) + } + } + + @inline(__always) + @ViewBuilder private func primaryTrailingToolbarContent() -> some View { + let cInfo = chat.chatInfo + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil + } + } 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) + } + } + 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") + } + } + 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() + } + } + Menu { + searchButton() + ToggleNtfsButton(chat: chat) + } label: { + Image(systemName: "ellipsis") + } + } + case .local: + searchButton() + default: + EmptyView() + } + } + } + + @inline(__always) + @ViewBuilder private func secondaryPrincipalToolbarContent() -> some View { + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else { + switch im.secondaryIMFilter { + case let .groupChatScopeContext(groupScopeInfo): + switch groupScopeInfo { + case let .memberSupport(groupMember_): + if let groupMember = groupMember_ { + MemberSupportChatToolbar(groupMember: groupMember) + } else { + textChatToolbar("Chat with admins") + } + } + case let .msgContentTagContext(contentTag): + switch contentTag { + case .report: + textChatToolbar("Member reports") + default: + EmptyView() + } + case .none: + EmptyView() + } + } + } + + @inline(__always) + @ViewBuilder private func secondaryTrailingToolbarContent() -> some View { + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil + } + } label: { + Text("Cancel") + } + } else { + searchButton() + } + } + + @inline(__always) + private func userMemberKnockingTitleBar() -> some View { + VStack(spacing: 0) { + Text("Chat with admins") + .font(.headline) + .foregroundColor(theme.colors.onBackground) + .padding(.top, 8) + .padding(.bottom, 14) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + Divider() + } + } + + func textChatToolbar(_ text: LocalizedStringKey) -> some View { + Text(text) + .font(.headline) + .lineLimit(1) + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } + private func initChatView() { let cInfo = chat.chatInfo // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. @@ -451,19 +594,19 @@ struct ChatView: View { floatingButtonModel.updateOnListChange(scrollView.listState) } - private func scrollToItemId(_ itemId: ChatItem.ID) { + private func scrollToItem(_ itemId: ChatItem.ID) { Task { do { var index = mergedItems.boxedValue.indexInParentItems[itemId] if index == nil { let pagination = ChatPagination.around(chatItemId: itemId, count: ChatPagination.PRELOAD_COUNT * 2) - let oldSize = ItemsModel.shared.reversedChatItems.count + let oldSize = im.reversedChatItems.count let triedToLoad = await loadChatItems(chat, pagination) if !triedToLoad { return } var repeatsLeft = 50 - while oldSize == ItemsModel.shared.reversedChatItems.count && repeatsLeft > 0 { + while oldSize == im.reversedChatItems.count && repeatsLeft > 0 { try await Task.sleep(nanoseconds: 20_000000) repeatsLeft -= 1 } @@ -473,7 +616,7 @@ struct ChatView: View { closeKeyboardAndRun { Task { await MainActor.run { animatedScrollingInProgress = true } - await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index)) + await scrollView.scrollToItemAnimated(min(im.reversedChatItems.count - 1, index)) await MainActor.run { animatedScrollingInProgress = false } } } @@ -548,11 +691,13 @@ struct ChatView: View { ? (g.size.width - 32) : (g.size.width - 32) * 0.84 return ChatItemWithMenu( + im: im, chat: $chat, index: index, isLastItem: index == mergedItems.boxedValue.items.count - 1, chatItem: ci, - scrollToItemId: scrollToItemId, + scrollToItem: scrollToItem, + scrollToItemId: $scrollToItemId, merged: mergedItem, maxWidth: maxWidth, composeState: $composeState, @@ -580,7 +725,7 @@ struct ChatView: View { } } .onChange(of: im.reversedChatItems) { items in - mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) if im.itemAdded { im.itemAdded = false @@ -592,7 +737,7 @@ struct ChatView: View { } } .onChange(of: revealedItems) { revealed in - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState) + mergedItems.boxedValue = MergedItems.create(im, revealed) scrollView.updateItems(mergedItems.boxedValue.items) } .onChange(of: chat.id) { _ in @@ -627,7 +772,7 @@ struct ChatView: View { private func updateWithInitiallyLoadedItems() { if mergedItems.boxedValue.items.isEmpty { - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, ItemsModel.shared.chatState) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) } let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() }) let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil } @@ -647,8 +792,8 @@ struct ChatView: View { private func searchTextChanged(_ s: String) { Task { - await loadChat(chat: chat, search: s) - mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState) + await loadChat(chat: chat, im: im, search: s) + mergedItems.boxedValue = MergedItems.create(im, revealedItems) await MainActor.run { scrollView.updateItems(mergedItems.boxedValue.items) } @@ -663,79 +808,8 @@ struct ChatView: View { } } - class FloatingButtonModel: ObservableObject { - @Published var unreadAbove: Int = 0 - @Published var unreadBelow: Int = 0 - @Published var isNearBottom: Bool = true - @Published var date: Date? = nil - @Published var isDateVisible: Bool = false - var hideDateWorkItem: DispatchWorkItem? = nil - - func updateOnListChange(_ listState: EndlessScrollView.ListState) { - let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState) - let unreadBelow = if let lastVisibleItem { - max(0, ItemsModel.shared.chatState.unreadTotal - lastVisibleItem.unreadBefore) - } else { - 0 - } - let unreadAbove = ItemsModel.shared.chatState.unreadTotal - unreadBelow - let date: Date? = - if let lastVisible = listState.visibleItems.last { - Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs) - } else { - nil - } - - // set the counters and date indicator - DispatchQueue.main.async { [weak self] in - guard let it = self else { return } - it.setDate(visibility: true) - it.unreadAbove = unreadAbove - it.unreadBelow = unreadBelow - it.date = date - } - - // set floating button indication mode - let nearBottom = listState.firstVisibleItemIndex < 1 - if nearBottom != self.isNearBottom { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in - self?.isNearBottom = nearBottom - } - } - - // hide Date indicator after 1 second of no scrolling - hideDateWorkItem?.cancel() - let workItem = DispatchWorkItem { [weak self] in - guard let it = self else { return } - it.setDate(visibility: false) - it.hideDateWorkItem = nil - } - DispatchQueue.main.async { [weak self] in - self?.hideDateWorkItem = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem) - } - } - - func resetDate() { - date = nil - isDateVisible = false - } - - private func setDate(visibility isVisible: Bool) { - if isVisible { - if !isNearBottom, - !isDateVisible, - let date, !Calendar.current.isDateInToday(date) { - withAnimation { self.isDateVisible = true } - } - } else if isDateVisible { - withAnimation { self.isDateVisible = false } - } - } - - } - private struct FloatingButtons: View { + @ObservedObject var im: ItemsModel let theme: AppTheme let scrollView: EndlessScrollView let chat: Chat @@ -780,7 +854,7 @@ struct ChatView: View { .contextMenu { Button { Task { - await markChatRead(chat) + await markChatRead(im, chat) } } label: { Label("Mark read", systemImage: "checkmark") @@ -805,7 +879,7 @@ struct ChatView: View { } } .onTapGesture { - if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded { + if loadingBottomItems || !im.lastItemsLoaded { requestedTopScroll = false requestedBottomScroll = true } else { @@ -825,7 +899,7 @@ struct ChatView: View { } } .onChange(of: loadingBottomItems) { loading in - if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded { + if !loading && requestedBottomScroll && im.lastItemsLoaded { requestedBottomScroll = false scrollToBottom() } @@ -835,9 +909,9 @@ struct ChatView: View { private func scrollToTopUnread() { Task { - if !ItemsModel.shared.chatState.splits.isEmpty { + if !im.chatState.splits.isEmpty { await MainActor.run { loadingMoreItems = true } - await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false) + await loadChat(chatId: chat.id, im: im, openAroundItemId: nil, clearItems: false) await MainActor.run { reloadItems() } if let index = listState.items.lastIndex(where: { $0.hasUnread() }) { await MainActor.run { animatedScrollingInProgress = true } @@ -947,7 +1021,7 @@ struct ChatView: View { private func addMembersButton() -> some View { Button { - if case let .group(gInfo) = chat.chatInfo { + if case let .group(gInfo, _) = chat.chatInfo { Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } } } } label: { @@ -957,7 +1031,7 @@ struct ChatView: View { private func groupLinkButton() -> some View { Button { - if case let .group(gInfo) = chat.chatInfo { + if case let .group(gInfo, _) = chat.chatInfo { Task { do { if let link = try apiGetGroupLink(gInfo.groupId) { @@ -1008,6 +1082,7 @@ struct ChatView: View { let (validItems, confirmation) = try await apiPlanForwardChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemIds: Array(selectedChatItems) ) if let confirmation { @@ -1097,7 +1172,6 @@ struct ChatView: View { } func openForwardingSheet(_ items: [Int64]) async { - let im = ItemsModel.shared var items = Set(items) var fci = [ChatItem]() for reversedChatItem in im.reversedChatItems { @@ -1136,11 +1210,11 @@ struct ChatView: View { private func loadChatItemsUnchecked(_ chat: Chat, _ pagination: ChatPagination) async -> Bool { await apiLoadMessages( chat.chatInfo.id, + im, pagination, - im.chatState, searchText, nil, - { visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) } + { visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) } ) return true } @@ -1152,11 +1226,12 @@ struct ChatView: View { func onChatItemsUpdated() { if !mergedItems.boxedValue.isActualState() { - //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(ItemsModel.shared.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(ItemsModel.shared.reversedChatItems.count)") + //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(im.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(im.reversedChatItems.count)") return } floatingButtonModel.updateOnListChange(scrollView.listState) preloadIfNeeded( + im, $allowLoadMoreItems, $ignoreLoadingRequests, scrollView.listState, @@ -1170,13 +1245,14 @@ struct ChatView: View { }, loadLastItems: { if !loadingMoreItems { - await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat) + await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat, im) } } ) } private struct ChatItemWithMenu: View { + @ObservedObject var im: ItemsModel @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner @@ -1185,7 +1261,8 @@ struct ChatView: View { let index: Int let isLastItem: Bool let chatItem: ChatItem - let scrollToItemId: (ChatItem.ID) -> Void + let scrollToItem: (ChatItem.ID) -> Void + @Binding var scrollToItemId: ChatItem.ID? let merged: MergedItem let maxWidth: CGFloat @Binding var composeState: ComposeState @@ -1261,8 +1338,6 @@ struct ChatView: View { } var body: some View { - let im = ItemsModel.shared - let last = isLastItem ? im.reversedChatItems.last : nil let listItem = merged.newest() let item = listItem.item @@ -1306,12 +1381,12 @@ struct ChatView: View { let (itemIds, unreadMentions) = unreadItemIds(range) if !itemIds.isEmpty { waitToMarkRead { - await apiMarkChatItemsRead(chat.chatInfo, itemIds, mentionsRead: unreadMentions) + await apiMarkChatItemsRead(im, chat.chatInfo, itemIds, mentionsRead: unreadMentions) } } } else if chatItem.isRcvNew { waitToMarkRead { - await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0) + await apiMarkChatItemsRead(im, chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0) } } } @@ -1333,7 +1408,6 @@ struct ChatView: View { } private func unreadItemIds(_ range: ClosedRange) -> ([ChatItem.ID], Int) { - let im = ItemsModel.shared var unreadItems: [ChatItem.ID] = [] var unreadMentions: Int = 0 @@ -1546,8 +1620,10 @@ struct ChatView: View { } ChatItemView( chat: chat, + im: im, chatItem: ci, - scrollToItemId: scrollToItemId, + scrollToItem: scrollToItem, + scrollToItemId: $scrollToItemId, maxWidth: maxWidth, allowMenu: $allowMenu ) @@ -1587,7 +1663,7 @@ struct ChatView: View { self.archivingReports = [] } } - if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive { + if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive { Button("For all moderators", role: .destructive) { if let reports = self.archivingReports { archiveReports(chat.chatInfo, reports.sorted(), true) @@ -1636,7 +1712,7 @@ struct ChatView: View { }) } switch chat.chatInfo { - case let .group(groupInfo): + case let .group(groupInfo, _): v.contextMenu { ReactionContextMenu( groupInfo: groupInfo, @@ -1659,7 +1735,7 @@ struct ChatView: View { @ViewBuilder private func menu(_ ci: ChatItem, _ range: ClosedRange?, live: Bool) -> some View { - if case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil { + if case let .group(gInfo, _) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil { if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator { archiveReportButton(ci) } @@ -1718,7 +1794,7 @@ struct ChatView: View { if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { moderateButton(ci, groupInfo) } else if ci.meta.itemDeleted == nil && chat.groupFeatureEnabled(.reports), - case let .group(gInfo) = chat.chatInfo, + case let .group(gInfo, _) = chat.chatInfo, gInfo.membership.memberRole == .member && !live && composeState.voiceMessageRecordingState == .noRecording { @@ -1829,6 +1905,7 @@ struct ChatView: View { let chatItem = try await apiChatItemReaction( type: cInfo.chatType, id: cInfo.apiId, + scope: cInfo.groupChatScope(), itemId: ci.id, add: add, reaction: reaction @@ -1942,11 +2019,11 @@ struct ChatView: View { Task { do { let cInfo = chat.chatInfo - let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, itemId: ci.id) + let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id) await MainActor.run { chatItemInfo = ciInfo } - if case let .group(gInfo) = chat.chatInfo { + if case let .group(gInfo, _) = chat.chatInfo { await m.loadGroupMembers(gInfo) } } catch let error { @@ -2000,13 +2077,13 @@ struct ChatView: View { private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button { Button(role: .destructive) { if !revealed, - let currIndex = m.getChatItemIndex(ci), + let currIndex = m.getChatItemIndex(im, ci), let ciCategory = ci.mergeCategory { let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) if let range = itemsRange(currIndex, prevHidden) { var itemIds: [Int64] = [] for i in range { - itemIds.append(ItemsModel.shared.reversedChatItems[i].id) + itemIds.append(im.reversedChatItems[i].id) } showDeleteMessages = true deletingItems = itemIds @@ -2144,12 +2221,12 @@ struct ChatView: View { selectedChatItems = selectedChatItems ?? [] var itemIds: [Int64] = [] if !revealed, - let currIndex = m.getChatItemIndex(ci), + let currIndex = m.getChatItemIndex(im, 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) + itemIds.append(im.reversedChatItems[i].id) } } else { itemIds.append(ci.id) @@ -2183,6 +2260,7 @@ struct ChatView: View { try await apiDeleteChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemIds: [di.id], mode: mode ) @@ -2199,6 +2277,7 @@ struct ChatView: View { if deletedItem.isActiveReport { m.decreaseGroupReportsCounter(chat.chatInfo.id) } + m.updateChatInfo(itemDeletion.deletedChatItem.chatInfo) } } } @@ -2237,14 +2316,14 @@ struct ChatView: View { if searchIsNotBlank { goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) { closeKeyboardAndRun { - ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id) + im.loadOpenChatNoWait(chat.id, chatItem.id) } } } else if let chatTypeApiIdMsgId { goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) { closeKeyboardAndRun { let (chatType, apiId, msgId) = chatTypeApiIdMsgId - ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId) + im.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId) } } } @@ -2271,6 +2350,84 @@ struct ChatView: View { } } +class FloatingButtonModel: ObservableObject { + @ObservedObject var im: ItemsModel + + public init(im: ItemsModel) { + self.im = im + } + + @Published var unreadAbove: Int = 0 + @Published var unreadBelow: Int = 0 + @Published var isNearBottom: Bool = true + @Published var date: Date? = nil + @Published var isDateVisible: Bool = false + var hideDateWorkItem: DispatchWorkItem? = nil + + func updateOnListChange(_ listState: EndlessScrollView.ListState) { + let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState) + let unreadBelow = if let lastVisibleItem { + max(0, im.chatState.unreadTotal - lastVisibleItem.unreadBefore) + } else { + 0 + } + let unreadAbove = im.chatState.unreadTotal - unreadBelow + let date: Date? = + if let lastVisible = listState.visibleItems.last { + Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs) + } else { + nil + } + + // set the counters and date indicator + DispatchQueue.main.async { [weak self] in + guard let it = self else { return } + it.setDate(visibility: true) + it.unreadAbove = unreadAbove + it.unreadBelow = unreadBelow + it.date = date + } + + // set floating button indication mode + let nearBottom = listState.firstVisibleItemIndex < 1 + if nearBottom != self.isNearBottom { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in + self?.isNearBottom = nearBottom + } + } + + // hide Date indicator after 1 second of no scrolling + hideDateWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self] in + guard let it = self else { return } + it.setDate(visibility: false) + it.hideDateWorkItem = nil + } + DispatchQueue.main.async { [weak self] in + self?.hideDateWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem) + } + } + + func resetDate() { + date = nil + isDateVisible = false + } + + private func setDate(visibility isVisible: Bool) { + if isVisible { + if !isNearBottom, + !isDateVisible, + let date, !Calendar.current.isDateInToday(date) { + withAnimation { self.isDateVisible = true } + } + } else if isDateVisible { + withAnimation { self.isDateVisible = false } + } + } + +} + private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" } @@ -2292,6 +2449,7 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe try await apiDeleteChatItems( type: chatInfo.chatType, id: chatInfo.apiId, + scope: chatInfo.groupChatScope(), itemIds: itemIds, mode: mode ) @@ -2309,6 +2467,9 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe ChatModel.shared.decreaseGroupReportsCounter(chat.chatInfo.id) } } + if let updatedChatInfo = deletedItems.last?.deletedChatItem.chatInfo { + ChatModel.shared.updateChatInfo(updatedChatInfo) + } } await onSuccess() } catch { @@ -2340,6 +2501,9 @@ func archiveReports(_ chatInfo: ChatInfo, _ itemIds: [Int64], _ forAll: Bool, _ ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id) } } + if let updatedChatInfo = deleted.last?.deletedChatItem.chatInfo { + ChatModel.shared.updateChatInfo(updatedChatInfo) + } } await onSuccess() } catch { @@ -2353,7 +2517,7 @@ private func buildTheme() -> AppTheme { if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) { let perChatTheme = if case let .direct(contact) = chat.chatInfo { contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) - } else if case let .group(groupInfo) = chat.chatInfo { + } else if case let .group(groupInfo, _) = chat.chatInfo { groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) } else { nil as ThemeModeOverride? @@ -2506,7 +2670,7 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) { case var .direct(contact): contact.chatSettings = chatSettings ChatModel.shared.updateContact(contact) - case var .group(groupInfo): + case var .group(groupInfo, _): groupInfo.chatSettings = chatSettings ChatModel.shared.updateGroup(groupInfo) default: () @@ -2523,7 +2687,8 @@ struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.chatId = "@1" - ItemsModel.shared.reversedChatItems = [ + let im = ItemsModel.shared + im.reversedChatItems = [ ChatItem.getSample(1, .directSnd, .now, "hello"), ChatItem.getSample(2, .directRcv, .now, "hi"), ChatItem.getSample(3, .directRcv, .now, "hi there"), @@ -2535,7 +2700,13 @@ struct ChatView_Previews: PreviewProvider { ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") ] @State var showChatInfo = false - return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) - .environmentObject(chatModel) + return ChatView( + chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: Binding.constant(nil) + ) + .environmentObject(chatModel) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 8993de886f..68a2f6d7b1 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -323,6 +323,7 @@ struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat + @ObservedObject var im: ItemsModel @Binding var composeState: ComposeState @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @@ -356,6 +357,20 @@ struct ComposeView: View { var body: some View { VStack(spacing: 0) { Divider() + if let groupInfo = chat.chatInfo.groupInfo, + case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter, + case let .memberSupport(member) = groupScopeInfo, + let member = member, + member.memberPending, + composeState.contextItem == .noContextItem, + composeState.noPreview { + ContextPendingMemberActionsView( + groupInfo: groupInfo, + member: member + ) + Divider() + } + if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() Divider() @@ -366,8 +381,8 @@ struct ComposeView: View { Divider() } // preference checks should match checks in forwarding list - let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) - let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) + let simplexLinkProhibited = im.secondaryIMFilter == nil && hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) + let fileProhibited = im.secondaryIMFilter == nil && composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) if simplexLinkProhibited { msgNotAllowedView("SimpleX links not allowed", icon: "link") @@ -397,7 +412,8 @@ struct ComposeView: View { .padding(.bottom, 16) .padding(.leading, 12) .tint(theme.colors.primary) - if case let .group(g) = chat.chatInfo, + if im.secondaryIMFilter == nil, + case let .group(g, _) = chat.chatInfo, !g.fullGroupPreferences.files.on(for: g.membership) { b.disabled(true).onTapGesture { AlertManager.shared.showAlertMsg( @@ -438,8 +454,8 @@ struct ComposeView: View { keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, sendButtonColor: chat.chatInfo.incognito - ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) - : theme.colors.primary + ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) + : theme.colors.primary ) .padding(.trailing, 12) .disabled(!chat.chatInfo.sendMsgEnabled) @@ -939,6 +955,7 @@ struct ComposeView: View { let chatItem = try await apiUpdateChatItem( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), itemId: ei.id, updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions), live: live @@ -1001,9 +1018,9 @@ struct ComposeView: View { reportReason: reportReason, reportText: msgText ) { - await MainActor.run { - for chatItem in chatItems { - chatModel.addChatItem(chat.chatInfo, chatItem) + if showReportsInSupportChatAlertDefault.get() { + await MainActor.run { + showReportsInSupportChatAlert() } } return chatItems.first @@ -1011,7 +1028,27 @@ struct ComposeView: View { return nil } - + + func showReportsInSupportChatAlert() { + showAlert( + NSLocalizedString("Report sent to moderators", comment: "alert title"), + message: NSLocalizedString("You can view your reports in Chat with admins.", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Don't show again", comment: "alert action"), + style: .default, + handler: { _ in + showReportsInSupportChatAlertDefault.set(false) + } + ), + UIAlertAction( + title: NSLocalizedString("Ok", comment: "alert action"), + style: .default + ) + ]} + ) + } + func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? { await send( [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)], @@ -1026,6 +1063,7 @@ struct ComposeView: View { : await apiSendMessages( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), live: live, ttl: ttl, composedMessages: msgs @@ -1050,8 +1088,10 @@ struct ComposeView: View { if let chatItems = await apiForwardChatItems( toChatType: chat.chatInfo.chatType, toChatId: chat.chatInfo.apiId, + toScope: chat.chatInfo.groupChatScope(), fromChatType: fromChatInfo.chatType, fromChatId: fromChatInfo.apiId, + fromScope: fromChatInfo.groupChatScope(), itemIds: forwardedItems.map { $0.id }, ttl: ttl ) { @@ -1272,12 +1312,14 @@ struct ComposeView: View { struct ComposeView_Previews: PreviewProvider { static var previews: some View { let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) + let im = ItemsModel.shared @State var composeState = ComposeState(message: "hello") @State var selectedRange = NSRange() return Group { ComposeView( chat: chat, + im: im, composeState: $composeState, keyboardVisible: Binding.constant(true), keyboardHiddenDate: Binding.constant(Date.now), @@ -1286,6 +1328,7 @@ struct ComposeView_Previews: PreviewProvider { .environmentObject(ChatModel()) ComposeView( chat: chat, + im: im, composeState: $composeState, keyboardVisible: Binding.constant(true), keyboardHiddenDate: Binding.constant(Date.now), diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift new file mode 100644 index 0000000000..96915b342f --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift @@ -0,0 +1,108 @@ +// +// ContextPendingMemberActionsView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 02.05.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContextPendingMemberActionsView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss + var groupInfo: GroupInfo + var member: GroupMember + + var body: some View { + HStack(spacing: 0) { + ZStack { + Text("Reject") + .foregroundColor(.red) + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + showRejectMemberAlert(groupInfo, member, dismiss: dismiss) + } + + ZStack { + Text("Accept") + .foregroundColor(theme.colors.primary) + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + showAcceptMemberAlert(groupInfo, member, dismiss: dismiss) + } + } + .frame(minHeight: 54) + .frame(maxWidth: .infinity) + .background(.thinMaterial) + } +} + +func showRejectMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) { + showAlert( + title: NSLocalizedString("Reject member?", comment: "alert title"), + buttonTitle: "Reject", + buttonAction: { removeMember(groupInfo, member, dismiss: dismiss) }, + cancelButton: true + ) +} + +func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) { + showAlert( + NSLocalizedString("Accept member", comment: "alert title"), + message: NSLocalizedString("Member will join the group, accept member?", comment: "alert message"), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Accept as member", comment: "alert action"), + style: .default, + handler: { _ in + acceptMember(groupInfo, member, .member, dismiss: dismiss) + } + ), + UIAlertAction( + title: NSLocalizedString("Accept as observer", comment: "alert action"), + style: .default, + handler: { _ in + acceptMember(groupInfo, member, .observer, dismiss: dismiss) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .default + ) + ]} + ) +} + +func acceptMember(_ groupInfo: GroupInfo, _ member: GroupMember, _ role: GroupMemberRole, dismiss: DismissAction? = nil) { + Task { + do { + let (gInfo, acceptedMember) = try await apiAcceptMember(groupInfo.groupId, member.groupMemberId, role) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, acceptedMember) + ChatModel.shared.updateGroup(gInfo) + dismiss?() + } + } catch let error { + logger.error("apiAcceptMember error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error accepting member", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +#Preview { + ContextPendingMemberActionsView( + groupInfo: GroupInfo.sampleData, + member: GroupMember.sampleData + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index 7cd543af10..3154f16f5b 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -78,6 +78,12 @@ struct AddGroupMembersViewCommon: View { let count = selectedContacts.count Section { if creatingGroup { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: true + ) GroupPreferencesButton( groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 15749b0761..55d7b626fa 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -17,6 +17,7 @@ struct GroupChatInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo + @Binding var scrollToItemId: ChatItem.ID? var onSearch: () -> Void @State var localAlias: String @FocusState private var aliasTextFieldFocused: Bool @@ -87,7 +88,25 @@ struct GroupChatInfoView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - + + Section { + if groupInfo.canAddMembers && groupInfo.businessChat == nil { + groupLinkButton() + } + if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator { + memberSupportButton() + } + if groupInfo.canModerate { + GroupReportsChatNavLink(chat: chat, scrollToItemId: $scrollToItemId) + } + if groupInfo.membership.memberActive + && (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) { + UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + } + } header: { + Text("") + } + Section { if groupInfo.isOwner && groupInfo.businessChat == nil { editGroupButton() @@ -96,19 +115,6 @@ struct GroupChatInfoView: View { addOrEditWelcomeMessage() } GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences) - if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { - sendReceiptsOption() - } else { - sendReceiptsOptionDisabled() - } - - NavigationLink { - ChatWallpaperEditorSheet(chat: chat) - } label: { - Label("Chat theme", systemImage: "photo") - } - } header: { - Text("") } footer: { let label: LocalizedStringKey = ( groupInfo.businessChat == nil @@ -120,6 +126,16 @@ struct GroupChatInfoView: View { } Section { + if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { + sendReceiptsOption() + } else { + sendReceiptsOptionDisabled() + } + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } ChatTTLOption(chat: chat, progressIndicator: $progressIndicator) } footer: { Text("Delete chat messages from your device.") @@ -127,9 +143,6 @@ struct GroupChatInfoView: View { Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { if groupInfo.canAddMembers { - if groupInfo.businessChat == nil { - groupLinkButton() - } if (chat.chatInfo.incognito) { Label("Invite members", systemImage: "plus") .foregroundColor(Color(uiColor: .tertiaryLabel)) @@ -144,9 +157,16 @@ struct GroupChatInfoView: View { let filteredMembers = s == "" ? members : members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } - MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert) + MemberRowView( + chat: chat, + groupInfo: groupInfo, + groupMember: GMember(groupInfo.membership), + scrollToItemId: $scrollToItemId, + user: true, + alert: $alert + ) ForEach(filteredMembers) { member in - MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert) + MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert) } } @@ -155,7 +175,7 @@ struct GroupChatInfoView: View { if groupInfo.canDelete { deleteGroupButton() } - if groupInfo.membership.memberCurrent { + if groupInfo.membership.memberCurrentOrPending { leaveGroupButton() } } @@ -353,6 +373,7 @@ struct GroupChatInfoView: View { var chat: Chat var groupInfo: GroupInfo @ObservedObject var groupMember: GMember + @Binding var scrollToItemId: ChatItem.ID? @EnvironmentObject var theme: AppTheme var user: Bool = false @Binding var alert: GroupChatInfoViewAlert? @@ -415,7 +436,7 @@ struct GroupChatInfoView: View { } private func memberInfoView() -> some View { - GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember) + GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember, scrollToItemId: $scrollToItemId) .navigationBarHidden(false) } @@ -523,15 +544,95 @@ struct GroupChatInfoView: View { .navigationBarTitleDisplayMode(.large) } + struct UserSupportChatNavLink: View { + @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + @EnvironmentObject var chatModel: ChatModel + @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false + + var body: some View { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil) + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + HStack { + Label("Chat with admins", systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag") + Spacer() + if chat.supportUnreadCount > 0 { + UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary) + } + } + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) + } + } + } + } + + private func memberSupportButton() -> some View { + NavigationLink { + MemberSupportView(groupInfo: groupInfo, scrollToItemId: $scrollToItemId) + .navigationBarTitle("Chats with members") + .modifier(ThemedBackground()) + .navigationBarTitleDisplayMode(.large) + } label: { + HStack { + Label( + "Chats with members", + systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag" + ) + Spacer() + if chat.supportUnreadCount > 0 { + UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary) + } + } + } + } + + struct GroupReportsChatNavLink: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var navLinkActive = false + @ObservedObject var chat: Chat + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView(chat: chat, scrollToItemId: $scrollToItemId) + } label: { + HStack { + Label { + Text("Member reports") + } icon: { + Image(systemName: chat.chatStats.reportsCount > 0 ? "flag.fill" : "flag").foregroundColor(.red) + } + Spacer() + if chat.chatStats.reportsCount > 0 { + UnreadBadge(count: chat.chatStats.reportsCount, color: .red) + } + } + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(chat.id, chatFilter: .msgContentTagContext(contentTag: .report)) + } + } + } + } + private func editGroupButton() -> some View { NavigationLink { GroupProfileView( groupInfo: $groupInfo, groupProfile: groupInfo.groupProfile ) - .navigationBarTitle("Group profile") - .modifier(ThemedBackground()) - .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") } @@ -683,26 +784,36 @@ struct GroupChatInfoView: View { title: Text("Remove member?"), message: Text(messageLabel), primaryButton: .destructive(Text("Remove")) { - Task { - do { - let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) - await MainActor.run { - updatedMembers.forEach { updatedMember in - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) - } - } - } catch let error { - logger.error("apiRemoveMembers error: \(responseError(error))") - let a = getErrorAlert(error, "Error removing member") - alert = .error(title: a.title, error: a.message) - } - } + removeMember(groupInfo, mem) }, secondaryButton: .cancel() ) } } +func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) { + Task { + do { + let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) + await MainActor.run { + ChatModel.shared.updateGroup(updatedGroupInfo) + updatedMembers.forEach { updatedMember in + _ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember) + } + dismiss?() + } + } catch let error { + logger.error("apiRemoveMembers error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error removing member", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text { groupInfo.businessChat == nil ? ( groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!") @@ -796,6 +907,7 @@ struct GroupChatInfoView_Previews: PreviewProvider { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: Binding.constant(GroupInfo.sampleData), + scrollToItemId: Binding.constant(nil), onSearch: {}, localAlias: "" ) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index a11c073a42..49e4d49e86 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -35,16 +35,23 @@ struct GroupLinkView: View { } var body: some View { - if creatingGroup { - groupLinkView() - .navigationBarBackButtonHidden() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Continue") { linkCreatedCb?() } + ZStack { + if creatingGroup { + groupLinkView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Continue") { linkCreatedCb?() } + } } - } - } else { - groupLinkView() + } else { + groupLinkView() + } + if creatingLink { + ProgressView() + .scaleEffect(2) + .frame(maxWidth: .infinity) + } } } @@ -79,6 +86,14 @@ struct GroupLinkView: View { Label("Share link", systemImage: "square.and.arrow.up") } + if (groupLink.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) { + Button { + addShortLink() + } label: { + Label("Add short link", systemImage: "plus") + } + } + if !creatingGroup { Button(role: .destructive) { alert = .deleteLink } label: { Label("Delete link", systemImage: "trash") @@ -89,11 +104,6 @@ struct GroupLinkView: View { Label("Create link", systemImage: "link.badge.plus") } .disabled(creatingLink) - if creatingLink { - ProgressView() - .scaleEffect(2) - .frame(maxWidth: .infinity) - } } } header: { if let groupLink, groupLink.connShortLink != nil { @@ -160,6 +170,26 @@ struct GroupLinkView: View { } } } + + private func addShortLink() { + Task { + do { + creatingLink = true + let link = try await apiAddGroupShortLink(groupId) + await MainActor.run { + creatingLink = false + (groupLink, groupLinkMemberRole) = link + } + } catch let error { + logger.error("apiAddGroupShortLink: \(responseError(error))") + await MainActor.run { + creatingLink = false + let a = getErrorAlert(error, "Error adding short link") + alert = .error(title: a.title, error: a.message) + } + } + } + } } struct GroupLinkView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 79ad242366..c62c25c071 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -16,6 +16,7 @@ struct GroupMemberInfoView: View { @State var groupInfo: GroupInfo @ObservedObject var chat: Chat @ObservedObject var groupMember: GMember + @Binding var scrollToItemId: ChatItem.ID? var navigation: Bool = false @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil @@ -25,7 +26,6 @@ struct GroupMemberInfoView: View { @State private var knownContactConnectionStats: ConnectionStats? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? - @State private var sheet: PlanAndConnectActionSheet? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var justOpened = true @State private var progressIndicator = false @@ -40,7 +40,6 @@ struct GroupMemberInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case planAndConnectAlert(alert: PlanAndConnectAlert) case queueInfo(info: String) case someAlert(alert: SomeAlert) case error(title: LocalizedStringKey, error: LocalizedStringKey?) @@ -56,7 +55,6 @@ struct GroupMemberInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .queueInfo(info): return "queueInfo \(info)" case let .someAlert(alert): return "someAlert \(alert.id)" case let .error(title, _): return "error \(title)" @@ -103,6 +101,10 @@ struct GroupMemberInfoView: View { if member.memberActive { Section { + if groupInfo.membership.memberRole >= .moderator + && (member.memberRole < .moderator || member.supportChat != nil) { + MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) + } if let code = connectionCode { verifyCodeButton(code) } if let connStats = connectionStats, connStats.ratchetSyncAllowed { @@ -265,20 +267,18 @@ struct GroupMemberInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) - case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) case let .queueInfo(info): return queueInfoAlert(info) case let .someAlert(a): return a.alert case let .error(title, error): return mkAlert(title: title, message: error) } } - .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } if progressIndicator { ProgressView().scaleEffect(2) } } .onChange(of: chat.chatInfo) { c in - if case let .group(gI) = chat.chatInfo { + if case let .group(gI, _) = chat.chatInfo { groupInfo = gI } } @@ -345,10 +345,7 @@ struct GroupMemberInfoView: View { Button { planAndConnect( contactLink, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: nil + dismiss: true ) } label: { Label("Connect", systemImage: "link") @@ -474,6 +471,31 @@ struct GroupMemberInfoView: View { .frame(maxWidth: .infinity, alignment: .center) } + struct MemberInfoSupportChatNavLink: View { + @EnvironmentObject var theme: AppTheme + var groupInfo: GroupInfo + var member: GMember + @Binding var scrollToItemId: ChatItem.ID? + @State private var navLinkActive = false + + var body: some View { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: member.wrapped) + NavigationLink(isActive: $navLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + Label("Chat with member", systemImage: "flag") + } + .onChange(of: navLinkActive) { active in + if active { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) + } + } + } + } + private func verifyCodeButton(_ code: String) -> some View { let member = groupMember.wrapped return NavigationLink { @@ -610,10 +632,11 @@ struct GroupMemberInfoView: View { primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) + let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { + chatModel.updateGroup(updatedGroupInfo) updatedMembers.forEach { updatedMember in - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + _ = chatModel.upsertGroupMember(updatedGroupInfo, updatedMember) } dismiss() } @@ -821,7 +844,8 @@ struct GroupMemberInfoView_Previews: PreviewProvider { GroupMemberInfoView( groupInfo: GroupInfo.sampleData, chat: Chat.sampleData, - groupMember: GMember.sampleData + groupMember: GMember.sampleData, + scrollToItemId: Binding.constant(nil) ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index 9bb4a0cc35..07cc7bd217 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -17,6 +17,7 @@ let MAX_VISIBLE_MEMBER_ROWS: CGFloat = 4.8 struct GroupMentionsView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme + var im: ItemsModel var groupInfo: GroupInfo @Binding var composeState: ComposeState @Binding var selectedRange: NSRange @@ -93,12 +94,31 @@ struct GroupMentionsView: View { currentMessage = composeState.message } } - + + func contextMemberFilter(_ member: GroupMember) -> Bool { + switch im.secondaryIMFilter { + case nil: + return true + case let .groupChatScopeContext(groupScopeInfo): + switch (groupScopeInfo) { + case let .memberSupport(groupMember_): + if let scopeMember = groupMember_ { + return member.memberRole >= .moderator || member.groupMemberId == scopeMember.groupMemberId + } else { + return member.memberRole >= .moderator + } + } + case .msgContentTagContext: + return false + } + } + private func filteredMembers() -> [GMember] { let s = mentionName.lowercased() - return s.isEmpty - ? sortedMembers - : sortedMembers.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + return sortedMembers.filter { + contextMemberFilter($0.wrapped) + && (s.isEmpty || $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s)) + } } private func messageChanged(_ msg: String, _ parsedMsg: [FormattedText], _ range: NSRange) { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index ed39c401ce..55b1dc6d2e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -30,6 +30,14 @@ struct GroupPreferencesView: View { let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" VStack { List { + Section { + MemberAdmissionButton( + groupInfo: $groupInfo, + admission: groupInfo.groupProfile.memberAdmission_, + currentAdmission: groupInfo.groupProfile.memberAdmission_, + creatingGroup: creatingGroup + ) + } featureSection(.timedMessages, $preferences.timedMessages.enable) featureSection(.fullDelete, $preferences.fullDelete.enable) featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role) @@ -140,6 +148,66 @@ struct GroupPreferencesView: View { } } +struct MemberAdmissionButton: View { + @Binding var groupInfo: GroupInfo + @State var admission: GroupMemberAdmission + @State var currentAdmission: GroupMemberAdmission + var creatingGroup: Bool = false + + var body: some View { + NavigationLink { + MemberAdmissionView( + groupInfo: $groupInfo, + admission: $admission, + currentAdmission: currentAdmission, + creatingGroup: creatingGroup, + saveAdmission: saveAdmission + ) + .navigationBarTitle("Member admission") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + .onDisappear { + let saveText = NSLocalizedString( + creatingGroup ? "Save" : "Save and notify group members", + comment: "alert button" + ) + + if groupInfo.groupProfile.memberAdmission_ != admission { + showAlert( + title: NSLocalizedString("Save admission settings?", comment: "alert title"), + buttonTitle: saveText, + buttonAction: { saveAdmission() }, + cancelButton: true + ) + } + } + } label: { + if creatingGroup { + Text("Set member admission") + } else { + Label("Member admission", systemImage: "switch.2") + } + } + } + + private func saveAdmission() { + Task { + do { + var gp = groupInfo.groupProfile + gp.memberAdmission = admission + let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + currentAdmission = admission + } + } catch { + logger.error("MemberAdmissionView apiUpdateGroup error: \(responseError(error))") + } + } + } +} + struct GroupPreferencesView_Previews: PreviewProvider { static var previews: some View { GroupPreferencesView( diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 1617edd11f..4de3608735 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -26,6 +26,7 @@ struct GroupProfileView: View { @Environment(\.dismiss) var dismiss: DismissAction @Binding var groupInfo: GroupInfo @State var groupProfile: GroupProfile + @State private var currentProfileHash: Int? @State private var showChooseSource = false @State private var showImagePicker = false @State private var showTakePhoto = false @@ -34,60 +35,40 @@ struct GroupProfileView: View { @FocusState private var focusDisplayName var body: some View { - return VStack(alignment: .leading) { - Text("Group profile is stored on members' devices, not on the servers.") - .padding(.vertical) + List { + EditProfileImage(profileImage: $groupProfile.image, showChooseSource: $showChooseSource) + .if(!focusDisplayName) { $0.padding(.top) } - ZStack(alignment: .center) { - ZStack(alignment: .topTrailing) { - profileImageView(groupProfile.image) - if groupProfile.image != nil { - Button { - groupProfile.image = nil - } label: { - Image(systemName: "multiply") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 12) - } - } - } - - editImageButton { showChooseSource = true } - } - .frame(maxWidth: .infinity, alignment: .center) - - VStack(alignment: .leading) { - ZStack(alignment: .topLeading) { - if !validNewProfileName() { + Section { + HStack { + TextField("Group display name", text: $groupProfile.displayName) + .focused($focusDisplayName) + if !validNewProfileName { Button { alert = .invalidName(validName: mkValidName(groupProfile.displayName)) } label: { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } - } else { - Image(systemName: "exclamationmark.circle").foregroundColor(.clear) } - profileNameTextEdit("Group display name", $groupProfile.displayName) - .focused($focusDisplayName) } - .padding(.bottom) let fullName = groupInfo.groupProfile.fullName if fullName != "" && fullName != groupProfile.displayName { - profileNameTextEdit("Group full name (optional)", $groupProfile.fullName) - .padding(.bottom) - } - HStack(spacing: 20) { - Button("Cancel") { dismiss() } - Button("Save group profile") { saveProfile() } - .disabled(!canUpdateProfile()) + TextField("Group full name (optional)", text: $groupProfile.fullName) } + } footer: { + Text("Group profile is stored on members' devices, not on the servers.") } - .frame(maxWidth: .infinity, minHeight: 120, alignment: .leading) + Section { + Button("Reset") { + groupProfile = groupInfo.groupProfile + currentProfileHash = groupProfile.hashValue + } + .disabled(currentProfileHash == groupProfile.hashValue) + Button("Save group profile", action: saveProfile) + .disabled(!canUpdateProfile) + } } - .padding() - .frame(maxHeight: .infinity, alignment: .top) .confirmationDialog("Group image", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true @@ -95,6 +76,11 @@ struct GroupProfileView: View { Button("Choose from library") { showImagePicker = true } + if UIPasteboard.general.hasImages { + Button("Paste image") { + chosenImage = UIPasteboard.general.image + } + } } .fullScreenCover(isPresented: $showTakePhoto) { ZStack { @@ -120,8 +106,20 @@ struct GroupProfileView: View { } } .onAppear { + currentProfileHash = groupProfile.hashValue DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - focusDisplayName = true + withAnimation { focusDisplayName = true } + } + } + .onDisappear { + if canUpdateProfile { + showAlert( + title: NSLocalizedString("Save group profile?", comment: "alert title"), + message: NSLocalizedString("Group profile was changed. If you save it, the updated profile will be sent to group members.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save (and notify members)", comment: "alert button"), + buttonAction: saveProfile, + cancelButton: true + ) } } .alert(item: $alert) { a in @@ -135,30 +133,30 @@ struct GroupProfileView: View { return createInvalidNameAlert(name, $groupProfile.displayName) } } - .contentShape(Rectangle()) - .onTapGesture { hideKeyboard() } + .navigationBarTitle("Group profile") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(focusDisplayName ? .inline : .large) } - private func canUpdateProfile() -> Bool { - groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && validNewProfileName() + private var canUpdateProfile: Bool { + currentProfileHash != groupProfile.hashValue && + groupProfile.displayName.trimmingCharacters(in: .whitespaces) != "" && + validNewProfileName } - private func validNewProfileName() -> Bool { + private var validNewProfileName: Bool { groupProfile.displayName == groupInfo.groupProfile.displayName || validDisplayName(groupProfile.displayName.trimmingCharacters(in: .whitespaces)) } - func profileNameTextEdit(_ label: LocalizedStringKey, _ name: Binding) -> some View { - TextField(label, text: name) - .padding(.leading, 32) - } - func saveProfile() { Task { do { groupProfile.displayName = groupProfile.displayName.trimmingCharacters(in: .whitespaces) + groupProfile.fullName = groupProfile.fullName.trimmingCharacters(in: .whitespaces) let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) await MainActor.run { + currentProfileHash = groupProfile.hashValue groupInfo = gInfo chatModel.updateGroup(gInfo) dismiss() diff --git a/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift new file mode 100644 index 0000000000..d80615b5d2 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift @@ -0,0 +1,93 @@ +// +// MemberAdmissionView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let memberCriterias: [(criteria: MemberCriteria?, text: LocalizedStringKey)] = [ + (nil, "off"), + (.all, "all") +] + +struct MemberAdmissionView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var groupInfo: GroupInfo + @Binding var admission: GroupMemberAdmission + var currentAdmission: GroupMemberAdmission + let creatingGroup: Bool + let saveAdmission: () -> Void + @State private var showSaveDialogue = false + + var body: some View { + let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members" + VStack { + List { + admissionSection( + NSLocalizedString("Review members", comment: "admission stage"), + NSLocalizedString("Review members before admitting (\"knocking\").", comment: "admission stage description"), + $admission.review + ) + + if groupInfo.isOwner { + Section { + Button("Reset") { admission = currentAdmission } + Button(saveText) { saveAdmission() } + } + .disabled(currentAdmission == admission) + } + } + } + .modifier(BackButton(disabled: Binding.constant(false)) { + if currentAdmission == admission { + dismiss() + } else { + showSaveDialogue = true + } + }) + .confirmationDialog("Save admission settings?", isPresented: $showSaveDialogue) { + Button(saveText) { + saveAdmission() + dismiss() + } + Button("Exit without saving") { + admission = currentAdmission + dismiss() + } + } + } + + private func admissionSection(_ admissionStageStr: String, _ admissionStageDescrStr: String, _ memberCriteria: Binding) -> some View { + Section { + if groupInfo.isOwner { + Picker(admissionStageStr, selection: memberCriteria) { + ForEach(memberCriterias, id: \.criteria) { mc in + Text(mc.text) + } + } + .frame(height: 36) + } else { + infoRow(Text(admissionStageStr), memberCriteria.wrappedValue?.text ?? NSLocalizedString("off", comment: "member criteria value")) + } + } footer: { + Text(admissionStageDescrStr) + .foregroundColor(theme.colors.secondary) + } + } +} + +#Preview { + MemberAdmissionView( + groupInfo: Binding.constant(GroupInfo.sampleData), + admission: Binding.constant(GroupMemberAdmission.sampleData), + currentAdmission: GroupMemberAdmission.sampleData, + creatingGroup: false, + saveAdmission: {} + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift new file mode 100644 index 0000000000..23001e64bf --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift @@ -0,0 +1,44 @@ +// +// MemberSupportChatToolbar.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 01.05.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MemberSupportChatToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + var groupMember: GroupMember + var imageSize: CGFloat = 32 + + var body: some View { + return HStack { + MemberProfileImage(groupMember, size: imageSize) + .padding(.trailing, 4) + let t = Text(groupMember.chatViewName).font(.headline) + (groupMember.verified ? memberVerifiedShield + t : t) + .lineLimit(1) + } + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } + + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .foregroundColor(theme.colors.secondary) + .baselineOffset(1) + .kerning(-2) + } +} + +#Preview { + MemberSupportChatToolbar( + groupMember: GroupMember.sampleData + ) + .environmentObject(CurrentColors.toAppTheme()) +} diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift new file mode 100644 index 0000000000..7f3672ea17 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -0,0 +1,287 @@ +// +// MemberSupportView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 28.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MemberSupportView: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var searchText: String = "" + @FocusState private var searchFocussed + var groupInfo: GroupInfo + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + viewBody() + .onAppear { + Task { + await chatModel.loadGroupMembers(groupInfo) + } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + Task { + await chatModel.loadGroupMembers(groupInfo) + } + } label: { + Image(systemName: "arrow.clockwise") + } + } + } + } + + @ViewBuilder private func viewBody() -> some View { + let membersWithChats = sortedMembersWithChats() + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let filteredMembersWithChats = s == "" + ? membersWithChats + : membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + + if membersWithChats.isEmpty { + Text("No chats with members") + .foregroundColor(.secondary) + } else { + List { + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 8) + ForEach(filteredMembersWithChats) { memberWithChat in + MemberSupportChatNavLink( + groupInfo: groupInfo, + memberWithChat: memberWithChat, + scrollToItemId: $scrollToItemId + ) + } + } + } + } + + struct MemberSupportChatNavLink: View { + @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var memberSupportChatNavLinkActive = false + var groupInfo: GroupInfo + var memberWithChat: GMember + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + ZStack { + let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: memberWithChat.wrapped) + Button { + ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) { + memberSupportChatNavLinkActive = true + } + } label: { + SupportChatRowView(groupMember: memberWithChat, groupInfo: groupInfo) + } + + NavigationLink(isActive: $memberSupportChatNavLinkActive) { + SecondaryChatView( + chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()), + scrollToItemId: $scrollToItemId + ) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if memberWithChat.wrapped.memberPending { + Button { + showAcceptMemberAlert(groupInfo, memberWithChat.wrapped) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + } else { + Button { + showDeleteMemberSupportChatAlert(groupInfo, memberWithChat.wrapped) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + } + } + + func sortedMembersWithChats() -> [GMember] { + chatModel.groupMembers + .filter { + $0.wrapped.supportChat != nil && + $0.wrapped.memberStatus != .memLeft && + $0.wrapped.memberStatus != .memRemoved + } + .sorted { (m0: GMember, m1: GMember) -> Bool in + if m0.wrapped.memberPending != m1.wrapped.memberPending { + return m0.wrapped.memberPending + } + + let mentions0 = (m0.wrapped.supportChat?.mentions ?? 0) > 0 + let mentions1 = (m1.wrapped.supportChat?.mentions ?? 0) > 0 + if mentions0 != mentions1 { + return mentions0 + } + + let attention0 = (m0.wrapped.supportChat?.memberAttention ?? 0) > 0 + let attention1 = (m1.wrapped.supportChat?.memberAttention ?? 0) > 0 + if attention0 != attention1 { + return attention0 + } + + let unread0 = (m0.wrapped.supportChat?.unread ?? 0) > 0 + let unread1 = (m1.wrapped.supportChat?.unread ?? 0) > 0 + if unread0 != unread1 { + return unread0 + } + + return (m0.wrapped.supportChat?.chatTs ?? .distantPast) > (m1.wrapped.supportChat?.chatTs ?? .distantPast) + } + } + + private struct SupportChatRowView: View { + @EnvironmentObject var chatModel: ChatModel + @ObservedObject var groupMember: GMember + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var groupInfo: GroupInfo + + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + + var body: some View { + let member = groupMember.wrapped + HStack{ + MemberProfileImage(member, size: 38) + .padding(.trailing, 2) + VStack(alignment: .leading) { + let t = Text(member.chatViewName).foregroundColor(theme.colors.onBackground) + (member.verified ? memberVerifiedShield + t : t) + .lineLimit(1) + Text(memberStatus(member)) + .lineLimit(1) + .font(.caption) + .foregroundColor(theme.colors.secondary) + } + + Spacer() + + if member.memberPending { + Image(systemName: "flag.fill") + .resizable() + .scaledToFill() + .frame(width: dynamicChatInfoSize * 0.8, height: dynamicChatInfoSize * 0.8) + .foregroundColor(theme.colors.primary) + } + if let supportChat = member.supportChat { + SupportChatUnreadIndicator(supportChat: supportChat) + } + } + } + + private func memberStatus(_ member: GroupMember) -> LocalizedStringKey { + if member.activeConn?.connDisabled ?? false { + return "disabled" + } else if member.activeConn?.connInactive ?? false { + return "inactive" + } else if member.memberPending { + return member.memberStatus.text + } else { + return LocalizedStringKey(member.memberRole.text) + } + } + + struct SupportChatUnreadIndicator: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var supportChat: GroupSupportChat + + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + + private var indicatorTint: Color { + if supportChat.mentions > 0 || supportChat.memberAttention > 0 { + return theme.colors.primary + } else { + return theme.colors.secondary + } + } + + var body: some View { + HStack(alignment: .center, spacing: 2) { + if supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0 { + if supportChat.mentions > 0 && supportChat.unread > 1 { + Text("\(MENTION_START)") + .font(userFont <= .xxxLarge ? .body : .callout) + .foregroundColor(indicatorTint) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .cornerRadius(dynamicSize(userFont).unreadCorner) + .padding(.bottom, 1) + } + let singleUnreadIsMention = supportChat.mentions > 0 && supportChat.unread == 1 + (singleUnreadIsMention ? Text("\(MENTION_START)") : unreadCountText(supportChat.unread)) + .font(userFont <= .xxxLarge ? .caption : .caption2) + .foregroundColor(.white) + .padding(.horizontal, dynamicSize(userFont).unreadPadding) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .background(indicatorTint) + .cornerRadius(dynamicSize(userFont).unreadCorner) + } + } + .frame(height: dynamicChatInfoSize) + .frame(minWidth: 22) + } + } + + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + textSpace) + .font(.caption) + .baselineOffset(2) + .kerning(-2) + .foregroundColor(theme.colors.secondary) + } + } +} + +func showDeleteMemberSupportChatAlert(_ groupInfo: GroupInfo, _ member: GroupMember) { + showAlert( + title: NSLocalizedString("Delete chat with member?", comment: "alert title"), + buttonTitle: "Delete", + buttonAction: { deleteMemberSupportChat(groupInfo, member) }, + cancelButton: true + ) +} + +func deleteMemberSupportChat(_ groupInfo: GroupInfo, _ member: GroupMember) { + Task { + do { + let (gInfo, updatedMember) = try await apiDeleteMemberSupportChat(groupInfo.groupId, member.groupMemberId) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + ChatModel.shared.updateGroup(gInfo) + } + // TODO member row doesn't get removed from list (upsertGroupMember correctly sets supportChat to nil) - this repopulates list to fix it + await ChatModel.shared.loadGroupMembers(gInfo) + } catch let error { + logger.error("apiDeleteMemberSupportChat error: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error deleting chat with member", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +#Preview { + MemberSupportView( + groupInfo: GroupInfo.sampleData, + scrollToItemId: Binding.constant(nil) + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift new file mode 100644 index 0000000000..47c5df264f --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift @@ -0,0 +1,42 @@ +// +// SecondaryChatView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 29.04.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SecondaryChatView: View { + @EnvironmentObject var chatModel: ChatModel + @ObservedObject var chat: Chat + @Binding var scrollToItemId: ChatItem.ID? + + var body: some View { + if let im = chatModel.secondaryIM { + ChatView( + chat: chat, + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: $scrollToItemId + ) + .onDisappear { + chatModel.secondaryIM = nil + } + } + } +} + +#Preview { + SecondaryChatView( + chat: Chat( + chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData)), + chatItems: [], + chatStats: ChatStats() + ), + scrollToItemId: Binding.constant(nil) + ) +} diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift index 85d6b279c5..e397970acd 100644 --- a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift +++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift @@ -25,7 +25,7 @@ struct SelectedItemsTopToolbar: View { struct SelectedItemsBottomToolbar: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var theme: AppTheme - let chatItems: [ChatItem] + let im: ItemsModel @Binding var selectedChatItems: Set? var chatInfo: ChatInfo // Bool - delete for everyone is possible @@ -75,9 +75,9 @@ struct SelectedItemsBottomToolbar: View { .resizable() .scaledToFit() .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red) + .foregroundColor(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : .red) } - .disabled(!moderateEnabled || deleteCountProhibited) + .disabled(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil) .opacity(canModerate ? 1 : 0) Spacer() @@ -88,24 +88,24 @@ struct SelectedItemsBottomToolbar: View { .resizable() .scaledToFit() .frame(width: 20, height: 20, alignment: .center) - .foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary) + .foregroundColor(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : theme.colors.primary) } - .disabled(!forwardEnabled || forwardCountProhibited) + .disabled(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil) } .frame(maxHeight: .infinity) .padding([.leading, .trailing], 12) } .onAppear { - recheckItems(chatInfo, chatItems, selectedChatItems) + recheckItems(chatInfo, im.reversedChatItems, selectedChatItems) } .onChange(of: chatInfo) { info in - recheckItems(info, chatItems, selectedChatItems) + recheckItems(info, im.reversedChatItems, selectedChatItems) } - .onChange(of: chatItems) { items in + .onChange(of: im.reversedChatItems) { items in recheckItems(chatInfo, items, selectedChatItems) } .onChange(of: selectedChatItems) { selected in - recheckItems(chatInfo, chatItems, selected) + recheckItems(chatInfo, im.reversedChatItems, selected) } .frame(height: 55.5) .background(.thinMaterial) @@ -116,7 +116,7 @@ struct SelectedItemsBottomToolbar: View { deleteCountProhibited = count == 0 || count > 200 forwardCountProhibited = count == 0 || count > 20 canModerate = possibleToModerate(chatInfo) - let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info) = chatInfo { + let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info, _) = chatInfo { info } else { nil @@ -145,7 +145,7 @@ struct SelectedItemsBottomToolbar: View { private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool { return switch chatInfo { - case let .group(groupInfo): + case let .group(groupInfo, _): groupInfo.membership.memberRole >= .admin default: false } diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 7b01fe0300..373311073a 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -24,85 +24,70 @@ struct VerifyCodeView: View { } private func verifyCodeView(_ code: String) -> some View { - ScrollView { - let splitCode = splitToParts(code, length: 24) - VStack(alignment: .leading) { - Group { + let splitCode = splitToParts(code, length: 24) + return List { + Section { + QRCode(uri: code, small: true) + + Text(splitCode) + .multilineTextAlignment(.leading) + .font(.body.monospaced()) + .lineLimit(20) + .frame(maxWidth: .infinity, alignment: .center) + } header: { + if connectionVerified { HStack { - if connectionVerified { - Image(systemName: "checkmark.shield") - .foregroundColor(theme.colors.secondary) - Text("\(displayName) is verified") - } else { - Text("\(displayName) is not verified") - } + Image(systemName: "checkmark.shield").foregroundColor(theme.colors.secondary) + Text("\(displayName) is verified").textCase(.none) } - .frame(height: 24) - - QRCode(uri: code) - .padding(.horizontal) - - Text(splitCode) - .multilineTextAlignment(.leading) - .font(.body.monospaced()) - .lineLimit(20) - .padding(.bottom, 8) + } else { + Text("\(displayName) is not verified").textCase(.none) } - .frame(maxWidth: .infinity, alignment: .center) - + } footer: { Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.") - .padding(.bottom) + } - Group { - if connectionVerified { - Button { - verifyCode(nil) - } label: { - Label("Clear verification", systemImage: "shield") - } - .padding() - } else { - HStack { - NavigationLink { - ScanCodeView(connectionVerified: $connectionVerified, verify: verify) - .navigationBarTitleDisplayMode(.large) - .navigationTitle("Scan code") - .modifier(ThemedBackground()) - } label: { - Label("Scan code", systemImage: "qrcode") - } - .padding() - Button { - verifyCode(code) { verified in - if !verified { showCodeError = true } - } - } label: { - Label("Mark verified", systemImage: "checkmark.shield") - } - .padding() - .alert(isPresented: $showCodeError) { - Alert(title: Text("Incorrect security code!")) - } - } - } - } - .frame(maxWidth: .infinity, alignment: .center) - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { + Section { + if connectionVerified { Button { - showShareSheet(items: [splitCode]) + verifyCode(nil) } label: { - Image(systemName: "square.and.arrow.up") + Label("Clear verification", systemImage: "shield") + } + } else { + NavigationLink { + ScanCodeView(connectionVerified: $connectionVerified, verify: verify) + .navigationBarTitleDisplayMode(.large) + .navigationTitle("Scan code") + .modifier(ThemedBackground()) + } label: { + Label("Scan code", systemImage: "qrcode") + } + Button { + verifyCode(code) { verified in + if !verified { showCodeError = true } + } + } label: { + Label("Mark verified", systemImage: "checkmark.shield") + } + .alert(isPresented: $showCodeError) { + Alert(title: Text("Incorrect security code!")) } } } - .onChange(of: connectionVerified) { _ in - if connectionVerified { dismiss() } + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showShareSheet(items: [splitCode]) + } label: { + Image(systemName: "square.and.arrow.up") + } } } + .onChange(of: connectionVerified) { _ in + if connectionVerified { dismiss() } + } } private func verifyCode(_ code: String?, _ cb: ((Bool) -> Void)? = nil) { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 81d78fbadd..1e747b8019 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -66,7 +66,7 @@ struct ChatListNavLink: View { switch chat.chatInfo { case let .direct(contact): contactNavLink(contact) - case let .group(groupInfo): + case let .group(groupInfo, _): groupNavLink(groupInfo) case let .local(noteFolder): noteFolderNavLink(noteFolder) @@ -189,7 +189,7 @@ struct ChatListNavLink: View { } .swipeActions(edge: .trailing) { tagChatButton(chat) - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { leaveGroupChatButton(groupInfo) } if groupInfo.canDelete { @@ -214,7 +214,7 @@ struct ChatListNavLink: View { let showReportsButton = chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator let showClearButton = !chat.chatItems.isEmpty let showDeleteGroup = groupInfo.canDelete - let showLeaveGroup = groupInfo.membership.memberCurrent + let showLeaveGroup = groupInfo.membership.memberCurrentOrPending let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0) if showClearButton && totalNumberOfButtons <= 3 { @@ -276,7 +276,7 @@ struct ChatListNavLink: View { @ViewBuilder private func markReadButton() -> some View { if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat { Button { - Task { await markChatRead(chat) } + Task { await markChatRead(ItemsModel.shared, chat) } } label: { SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } @@ -482,12 +482,10 @@ struct ChatListNavLink: View { .tint(theme.colors.primary) } .appSheet(isPresented: $showContactConnectionInfo) { - Group { - if case let .contactConnection(contactConnection) = chat.chatInfo { - ContactConnectionInfo(contactConnection: contactConnection) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) - .modifier(ThemedBackground(grouped: true)) - } + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .modifier(ThemedBackground(grouped: true)) } } .contentShape(Rectangle()) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index f34f930c6f..58e0882e38 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -148,6 +148,7 @@ struct ChatListView: View { @State private var userPickerShown: Bool = false @State private var sheet: SomeSheet? = nil @StateObject private var chatTagsModel = ChatTagsModel.shared + @State private var scrollToItemId: ChatItem.ID? = nil // iOS 15 is required it to show/hide toolbar while chat is hidden/visible @State private var viewOnScreen = true @@ -446,7 +447,14 @@ struct ChatListView: View { @ViewBuilder private func chatView() -> some View { if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { - ChatView(chat: chat) + let im = ItemsModel.shared + ChatView( + chat: chat, + im: im, + mergedItems: BoxedValue(MergedItems.create(im, [])), + floatingButtonModel: FloatingButtonModel(im: im), + scrollToItemId: $scrollToItemId + ) } } @@ -571,8 +579,6 @@ struct ChatListSearchBar: View { @Binding var searchChatFilteredBySimplexLink: String? @Binding var parentSheet: SomeSheet? @State private var ignoreSearchTextChange = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? var body: some View { VStack(spacing: 12) { @@ -637,12 +643,6 @@ struct ChatListSearchBar: View { .onChange(of: chatTagsModel.activeFilter) { _ in searchText = "" } - .alert(item: $alert) { a in - planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) - } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) - } } private func toggleFilterButton() -> some View { @@ -668,10 +668,11 @@ struct ChatListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, dismiss: false, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } ) @@ -892,12 +893,12 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !contact.chatDeleted case .contactRequest: true case .contactConnection: true - case let .group(groupInfo): groupInfo.businessChat?.chatType == .customer + case let .group(groupInfo, _): groupInfo.businessChat?.chatType == .customer default: false } case .groups: switch chatInfo { - case let .group(groupInfo): groupInfo.businessChat == nil + case let .group(groupInfo, _): groupInfo.businessChat == nil default: false } case .business: diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index b8c8233e6e..49f629d084 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -141,7 +141,7 @@ struct ChatPreviewView: View { } else { EmptyView() } - case let .group(groupInfo): + case let .group(groupInfo, _): switch (groupInfo.membership.memberStatus) { case .memRejected: inactiveIcon() case .memLeft: inactiveIcon() @@ -165,7 +165,7 @@ struct ChatPreviewView: View { switch chat.chatInfo { case let .direct(contact): previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil) - case let .group(groupInfo): + case let .group(groupInfo, _): let v = previewTitle(t) switch (groupInfo.membership.memberStatus) { case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary) @@ -341,11 +341,12 @@ struct ChatPreviewView: View { chatPreviewInfoText("connecting…") } } - case let .group(groupInfo): + case let .group(groupInfo, _): switch (groupInfo.membership.memberStatus) { case .memRejected: chatPreviewInfoText("rejected") case .memInvited: groupInvitationPreviewText(groupInfo) case .memAccepted: chatPreviewInfoText("connecting…") + case .memPendingReview, .memPendingApproval: chatPreviewInfoText("reviewed by admins") default: EmptyView() } default: EmptyView() @@ -439,7 +440,11 @@ struct ChatPreviewView: View { if progressByTimeout { ProgressView() } else if chat.chatStats.reportsCount > 0 { - groupReportsIcon(size: size * 0.8) + flagIcon(size: size * 0.8, color: .red) + } else if chat.supportUnreadCount > 0 { + flagIcon(size: size * 0.8, color: theme.colors.primary) + } else if chat.chatInfo.groupInfo?.membership.memberPending ?? false { + flagIcon(size: size * 0.8, color: theme.colors.secondary) } else { incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } @@ -485,12 +490,12 @@ struct ChatPreviewView: View { } } -func groupReportsIcon(size: CGFloat) -> some View { +func flagIcon(size: CGFloat, color: Color) -> some View { Image(systemName: "flag") .resizable() .scaledToFit() .frame(width: size, height: size) - .foregroundColor(.red) + .foregroundColor(color) } func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View { diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index dbe10ad997..c38ddfb1da 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -124,7 +124,7 @@ struct UserPicker: View { ZStack(alignment: .topTrailing) { ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) if (u.unreadCount > 0) { - UnreadBadge(userInfo: u).offset(x: 4, y: -4) + userUnreadBadge(u, theme: theme).offset(x: 4, y: -4) } } .padding(.trailing, 6) @@ -171,19 +171,27 @@ struct UserPicker: View { } } +@inline(__always) +func userUnreadBadge(_ userInfo: UserInfo, theme: AppTheme) -> some View { + UnreadBadge( + count: userInfo.unreadCount, + color: userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary + ) +} + struct UnreadBadge: View { - var userInfo: UserInfo - @EnvironmentObject var theme: AppTheme @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + var count: Int + var color: Color var body: some View { let size = dynamicSize(userFont).chatInfoSize - unreadCountText(userInfo.unreadCount) + unreadCountText(count) .font(userFont <= .xxxLarge ? .caption : .caption2) .foregroundColor(.white) .padding(.horizontal, dynamicSize(userFont).unreadPadding) .frame(minWidth: size, minHeight: size) - .background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary) + .background(color) .cornerRadius(dynamicSize(userFont).unreadCorner) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 59eee1338b..a7e61b3105 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -21,7 +21,7 @@ enum DatabaseAlert: Identifiable { case deleteLegacyDatabase case deleteFilesAndMedia case setChatItemTTL(ttl: ChatItemTTL) - case error(title: LocalizedStringKey, error: String = "") + case error(title: String, error: String = "") var id: String { switch self { @@ -456,7 +456,7 @@ struct DatabaseView: View { } } catch let error { await MainActor.run { - alert = .error(title: "Error exporting chat database", error: responseError(error)) + alert = .error(title: NSLocalizedString("Error exporting chat database", comment: "alert title"), error: responseError(error)) progressIndicator = false } } @@ -492,10 +492,10 @@ struct DatabaseView: View { return migration } } catch let error { - await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert) + await operationEnded(.error(title: NSLocalizedString("Error importing chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert) } } catch let error { - await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert) + await operationEnded(.error(title: NSLocalizedString("Error deleting chat database", comment: "alert title"), error: responseError(error)), progressIndicator, alert) } } else { showAlert("Error accessing database file") @@ -513,7 +513,7 @@ struct DatabaseView: View { await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert) return true } catch let error { - await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert) + await DatabaseView.operationEnded(.error(title: NSLocalizedString("Error deleting database", comment: "alert title"), error: responseError(error)), $progressIndicator, $alert) return false } } @@ -522,7 +522,7 @@ struct DatabaseView: View { if removeLegacyDatabaseAndFiles() { legacyDatabase = false } else { - alert = .error(title: "Error deleting old database") + alert = .error(title: NSLocalizedString("Error deleting old database", comment: "alert title")) } } @@ -546,7 +546,7 @@ struct DatabaseView: View { let (title, message) = chatDeletedAlertText() showAlert(title, message: message, actions: { [okAlertActionWaiting] }) } else if case let .error(title, error) = dbAlert { - showAlert("\(title)", message: error, actions: { [okAlertActionWaiting] }) + showAlert(title, message: error, actions: { [okAlertActionWaiting] }) } else { alert.wrappedValue = dbAlert cont.resume() @@ -567,7 +567,7 @@ struct DatabaseView: View { } } catch { await MainActor.run { - alert = .error(title: "Error changing setting", error: responseError(error)) + alert = .error(title: NSLocalizedString("Error changing setting", comment: "alert title"), error: responseError(error)) chatItemTTL = currentChatItemTTL afterSetCiTTL() } diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift index 1e334367e8..17fe95a058 100644 --- a/apps/ios/Shared/Views/Helpers/AppSheet.swift +++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift @@ -33,7 +33,7 @@ extension View { func appSheet( isPresented: Binding, onDismiss: (() -> Void)? = nil, - content: @escaping () -> Content + @ViewBuilder content: @escaping () -> Content ) -> some View where Content: View { sheet(isPresented: isPresented, onDismiss: onDismiss) { content().modifier(PrivacySensitive()) @@ -43,7 +43,7 @@ extension View { func appSheet( item: Binding, onDismiss: (() -> Void)? = nil, - content: @escaping (T) -> Content + @ViewBuilder content: @escaping (T) -> Content ) -> some View where T: Identifiable, Content: View { sheet(item: item, onDismiss: onDismiss) { it in content(it).modifier(PrivacySensitive()) diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index b8de0e4ceb..753d92b6d9 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -65,6 +65,27 @@ func showAlert( } } +func showSheet( + _ title: String?, + message: String? = nil, + actions: () -> [UIAlertAction] = { [okAlertAction] }, + sourceView: UIView? = nil // For iPad support +) { + if let topController = getTopViewController() { + let sheet = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + for action in actions() { sheet.addAction(action) } + + // Required for iPad: Configure popover presentation + if let popover = sheet.popoverPresentationController { + popover.sourceView = sourceView ?? topController.view + popover.sourceRect = sourceView?.bounds ?? CGRect(x: topController.view.bounds.midX, y: topController.view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + topController.present(sheet, animated: true) + } +} + let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index 16ab26eff7..c21ff9be8b 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -66,6 +66,8 @@ struct LocalAuthView: View { m.chatId = nil ItemsModel.shared.reversedChatItems = [] ItemsModel.shared.chatState.clear() + ChatModel.shared.secondaryIM?.reversedChatItems = [] + ChatModel.shared.secondaryIM?.chatState.clear() m.updateChats([]) m.users = [] _ = kcAppPassword.set(password) diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 87c0b80372..b3c33e95ea 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -193,7 +193,7 @@ struct AddGroupView: View { Task { await m.loadGroupMembers(gInfo) } - let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) + let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil), chatItems: []) m.addChat(c) withAnimation { groupInfo = gInfo diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index e5263813fa..7a49c30b38 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -125,7 +125,7 @@ struct NewChatSheet: View { } NavigationLink { AddGroupView() - .navigationTitle("Create secret group") + .navigationTitle("Create group") .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { @@ -346,8 +346,6 @@ struct ContactsListSearchBar: View { @Binding var searchShowingSimplexLink: Bool @Binding var searchChatFilteredBySimplexLink: String? @State private var ignoreSearchTextChange = false - @State private var alert: PlanAndConnectAlert? - @State private var sheet: PlanAndConnectActionSheet? @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { @@ -416,12 +414,6 @@ struct ContactsListSearchBar: View { } } } - .alert(item: $alert) { a in - planAndConnectAlert(a, dismiss: true, cleanup: { searchText = "" }) - } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { searchText = "" }) - } } private func toggleFilterButton() -> some View { @@ -442,10 +434,11 @@ struct ContactsListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = $0 }, - showActionSheet: { sheet = $0 }, dismiss: true, - incognito: nil, + cleanup: { + searchText = "" + searchFocussed = false + }, filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } ) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 110eda7882..45c4f42a19 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -29,11 +29,9 @@ struct SomeSheet: Identifiable { } private enum NewChatViewAlert: Identifiable { - case planAndConnectAlert(alert: PlanAndConnectAlert) case newChatSomeAlert(alert: SomeAlert) var id: String { switch self { - case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" case let .newChatSomeAlert(alert): return "newChatSomeAlert \(alert.id)" } } @@ -165,8 +163,6 @@ struct NewChatView: View { } .alert(item: $alert) { a in switch(a) { - case let .planAndConnectAlert(alert): - return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) case let .newChatSomeAlert(a): return a.alert } @@ -593,7 +589,6 @@ private struct ConnectView: View { @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? - @State private var sheet: PlanAndConnectActionSheet? @State private var pasteboardHasStrings = UIPasteboard.general.hasStrings var body: some View { @@ -605,9 +600,6 @@ private struct ConnectView: View { ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) } } - .actionSheet(item: $sheet) { s in - planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" }) - } } @ViewBuilder private func pasteLinkView() -> some View { @@ -662,10 +654,7 @@ private struct ConnectView: View { private func connect(_ link: String) { planAndConnect( link, - showAlert: { alert = .planAndConnectAlert(alert: $0) }, - showActionSheet: { sheet = $0 }, - dismiss: true, - incognito: nil + dismiss: true ) } } @@ -839,187 +828,184 @@ func sharedProfileInfo(_ incognito: Bool) -> Text { ) } -enum PlanAndConnectAlert: Identifiable { - case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case invitationLinkConnecting(connectionLink: CreatedConnLink) - case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool) - case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?) - case error(shortOrFullLink: String, alert: Alert) - - var id: String { - switch self { - case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.connFullLink)" - case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink.connFullLink)" - case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink.connFullLink)" - case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink.connFullLink)" - case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink.connFullLink)" - case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink.connFullLink)" - case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink.connFullLink)" - case let .error(shortOrFullLink, alert): return "error \(shortOrFullLink)" - } - } +private func showInvitationLinkConnectingAlert(cleanup: (() -> Void)?) { + showAlert( + NSLocalizedString("Already connecting!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already connecting via this one-time link!", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } -func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (() -> Void)? = nil) -> Alert { - switch alert { - case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own one-time link!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case .invitationLinkConnecting: - return Alert( - title: Text("Already connecting!"), - message: Text("You are already connecting via this one-time link!"), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Connect to yourself?"), - message: Text("This is your own SimpleX address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat connection request?"), - message: Text("You have already requested connection via this address!"), - primaryButton: .destructive( - Text(incognito ? "Connect incognito" : "Connect"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Join group?"), - message: Text("You will connect to all group members."), - primaryButton: .default( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): - return Alert( - title: Text("Repeat join request?"), - message: Text("You are already joining the group via this link!"), - primaryButton: .destructive( - Text(incognito ? "Join incognito" : "Join"), - action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) } - ), - secondaryButton: .cancel() { cleanup?() } - ) - case let .groupLinkConnecting(_, groupInfo): - if let groupInfo = groupInfo { - return groupInfo.businessChat == nil - ? Alert( - title: Text("Group already exists!"), - message: Text("You are already joining the group \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } - ) - : Alert( - title: Text("Chat already exists!"), - message: Text("You are already connecting to \(groupInfo.displayName)."), - dismissButton: .default(Text("OK")) { cleanup?() } +private func showGroupLinkConnectingAlert(groupInfo: GroupInfo?, cleanup: (() -> Void)?) { + if let groupInfo = groupInfo { + if groupInfo.businessChat == nil { + showAlert( + NSLocalizedString("Group already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already joining the group %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } else { - return Alert( - title: Text("Already joining the group!"), - message: Text("You are already joining the group via this link."), - dismissButton: .default(Text("OK")) { cleanup?() } + showAlert( + NSLocalizedString("Chat already exists!", comment: "new chat sheet title"), + message: + String.localizedStringWithFormat( + NSLocalizedString("You are already connecting to %@.", comment: "new chat sheet message"), + groupInfo.displayName + ), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} ) } - case let .error(_, alert): return alert + } else { + showAlert( + NSLocalizedString("Already joining the group!", comment: "new chat sheet title"), + message: NSLocalizedString("You are already joining the group via this link.", comment: "new chat sheet message"), + actions: {[ + okCleanupAlertAction(cleanup: cleanup) + ]} + ) } } -enum PlanAndConnectActionSheet: Identifiable { - case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey) - case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) - case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) - - var id: String { - switch self { - case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)" - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)" - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" - case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)" +private func okCleanupAlertAction(cleanup: (() -> Void)?) -> UIAlertAction { + UIAlertAction( + title: NSLocalizedString("Ok", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() } - } + ) } -func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool, cleanup: (() -> Void)? = nil) -> ActionSheet { - switch sheet { - case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): - return ActionSheet( - title: Text(title), - buttons: [ - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): - return ActionSheet( - title: Text("Connect with \(contact.chatViewName)"), - buttons: [ - .default(Text("Use current profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .default(Text("Use new incognito profile")) { connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] - ) - case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo): - if let incognito = incognito { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] +private func showAskCurrentOrIncognitoProfileSheet( + title: String, + actionStyle: UIAlertAction.Style = .default, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + title, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: actionStyle, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) - } else { - return ActionSheet( - title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"), - buttons: [ - .default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) }, - .destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) }, - .destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) }, - .cancel() { cleanup?() } - ] + ]} + ) +} + +private func showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: Contact, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Connect with %@", comment: "new chat action"), + contact.chatViewName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .default, + handler: { _ in + connectContactViaAddress_(contact, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } ) - } - } + ]} + ) +} + +private func showOwnGroupLinkConfirmConnectSheet( + groupInfo: GroupInfo, + connectionLink: CreatedConnLink, + connectionPlan: ConnectionPlan?, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showSheet( + String.localizedStringWithFormat( + NSLocalizedString("Join your group?\nThis is your link for group %@!", comment: "new chat action"), + groupInfo.displayName + ), + actions: {[ + UIAlertAction( + title: NSLocalizedString("Open group", comment: "new chat action"), + style: .default, + handler: { _ in + openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) + } + ), + UIAlertAction( + title: NSLocalizedString("Use current profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Use new incognito profile", comment: "new chat action"), + style: .destructive, + handler: { _ in + connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true, cleanup: cleanup) + } + ), + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "new chat action"), + style: .default, + handler: { _ in + cleanup?() + } + ) + ]} + ) } func planAndConnect( _ shortOrFullLink: String, - showAlert: @escaping (PlanAndConnectAlert) -> Void, - showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, dismiss: Bool, - incognito: Bool?, cleanup: (() -> Void)? = nil, filterKnownContact: ((Contact) -> Void)? = nil, filterKnownGroup: ((GroupInfo) -> Void)? = nil @@ -1031,25 +1017,30 @@ func planAndConnect( case let .invitationLink(ilp): switch ilp { case .ok: - logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) - } + logger.debug("planAndConnect, .invitationLink, .ok") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .ownLink: - logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .ownLink") await MainActor.run { - if let incognito = incognito { - showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own one-time link!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connecting(contact_): - logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .connecting") await MainActor.run { if let contact = contact_ { if let f = filterKnownContact { @@ -1058,11 +1049,11 @@ func planAndConnect( openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } } } else { - showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) + showInvitationLinkConnectingAlert(cleanup: cleanup) } } case let .known(contact): - logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .invitationLink, .known") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1074,34 +1065,42 @@ func planAndConnect( case let .contactAddress(cap): switch cap { case .ok: - logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) - } + logger.debug("planAndConnect, .contactAddress, .ok") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .ownLink: - logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .ownLink") await MainActor.run { - if let incognito = incognito { - showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect to yourself?\nThis is your own SimpleX address!", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .connectingConfirmReconnect: - logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect") await MainActor.run { - if let incognito = incognito { - showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You have already requested connection!\nRepeat connection request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connectingProhibit(contact): - logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .connectingProhibit") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1110,7 +1109,7 @@ func planAndConnect( } } case let .known(contact): - logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .contactAddress, .known") await MainActor.run { if let f = filterKnownContact { f(contact) @@ -1119,49 +1118,60 @@ func planAndConnect( } } case let .contactViaAddress(contact): - logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") - if let incognito = incognito { - connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - await MainActor.run { - showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) - } + logger.debug("planAndConnect, .contactAddress, .contactViaAddress") + await MainActor.run { + showAskCurrentOrIncognitoProfileConnectContactViaAddressSheet( + contact: contact, + dismiss: dismiss, + cleanup: cleanup + ) } } case let .groupLink(glp): switch glp { case .ok: await MainActor.run { - if let incognito = incognito { - showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Join group", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .ownLink(groupInfo): - logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .ownLink") await MainActor.run { if let f = filterKnownGroup { f(groupInfo) } - showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) + showOwnGroupLinkConfirmConnectSheet( + groupInfo: groupInfo, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case .connectingConfirmReconnect: - logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect") await MainActor.run { - if let incognito = incognito { - showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) - } else { - showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("You are already joining the group!\nRepeat join request?", comment: "new chat sheet title"), + actionStyle: .destructive, + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) } case let .connectingProhibit(groupInfo_): - logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .connectingProhibit") await MainActor.run { - showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) + showGroupLinkConnectingAlert(groupInfo: groupInfo_, cleanup: cleanup) } case let .known(groupInfo): - logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") + logger.debug("planAndConnect, .groupLink, .known") await MainActor.run { if let f = filterKnownGroup { f(groupInfo) @@ -1172,15 +1182,20 @@ func planAndConnect( } case let .error(chatError): logger.debug("planAndConnect, .error \(chatErrorString(chatError))") - if let incognito = incognito { - connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) - } else { - showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) - } + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: nil, + dismiss: dismiss, + cleanup: cleanup + ) } } else if let alert { await MainActor.run { - showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert)) + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(alert) + cleanup?() + } } } } diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index 453149198b..c9054f30da 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -12,11 +12,12 @@ import SimpleXChat struct MutableQRCode: View { @Binding var uri: String + var small: Bool = false var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var body: some View { - QRCode(uri: uri, withLogo: withLogo, tintColor: tintColor) + QRCode(uri: uri, small: small, withLogo: withLogo, tintColor: tintColor) .id("simplex-qrcode-view-for-\(uri)") } } @@ -27,7 +28,7 @@ struct SimpleXCreatedLinkQRCode: View { var onShare: (() -> Void)? = nil var body: some View { - QRCode(uri: link.simplexChatUri(short: short), onShare: onShare) + QRCode(uri: link.simplexChatUri(short: short), small: short && link.connShortLink != nil, onShare: onShare) } } @@ -38,50 +39,57 @@ struct SimpleXLinkQRCode: View { var onShare: (() -> Void)? = nil var body: some View { - QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor, onShare: onShare) + QRCode(uri: simplexChatLink(uri), small: uri.count < 200, withLogo: withLogo, tintColor: tintColor, onShare: onShare) } } +private let smallQRRatio: CGFloat = 0.63 + struct QRCode: View { let uri: String + var small: Bool = false var withLogo: Bool = true var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) var onShare: (() -> Void)? = nil @State private var image: UIImage? = nil @State private var makeScreenshotFunc: () -> Void = {} + @State private var width: CGFloat = .infinity var body: some View { ZStack { if let image = image { - qrCodeImage(image) - GeometryReader { geo in + qrCodeImage(image).frame(width: width, height: width) + GeometryReader { g in + let w = g.size.width * (small ? smallQRRatio : 1) + let l = w * (small ? 0.195 : 0.16) + let m = w * 0.005 ZStack { if withLogo { - let w = geo.size.width Image("icon-light") .resizable() .scaledToFit() - .frame(width: w * 0.16, height: w * 0.16) - .frame(width: w * 0.165, height: w * 0.165) + .frame(width: l, height: l) + .frame(width: l + m, height: l + m) .background(.white) .clipShape(Circle()) } } .onAppear { + width = w makeScreenshotFunc = { let size = CGSizeMake(1024 / UIScreen.main.scale, 1024 / UIScreen.main.scale) - showShareSheet(items: [makeScreenshot(geo.frame(in: .local).origin, size)]) + showShareSheet(items: [makeScreenshot(g.frame(in: .local).origin, size)]) onShare?() } } - .frame(width: geo.size.width, height: geo.size.height) + .frame(width: g.size.width, height: g.size.height) } } else { - Color.clear.aspectRatio(1, contentMode: .fit) + Color.clear.aspectRatio(small ? 1 / smallQRRatio : 1, contentMode: .fit) } } .onTapGesture(perform: makeScreenshotFunc) - .task { image = await generateImage(uri, tintColor: tintColor) } + .task { image = await generateImage(uri, tintColor: tintColor, errorLevel: small ? "M" : "L") } .frame(maxWidth: .infinity, maxHeight: .infinity) } } @@ -94,10 +102,11 @@ private func qrCodeImage(_ image: UIImage) -> some View { .textSelection(.enabled) } -private func generateImage(_ uri: String, tintColor: UIColor) async -> UIImage? { +private func generateImage(_ uri: String, tintColor: UIColor, errorLevel: String) async -> UIImage? { let context = CIContext() let filter = CIFilter.qrCodeGenerator() filter.message = Data(uri.utf8) + filter.correctionLevel = errorLevel if let outputImage = filter.outputImage, let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { return UIImage(cgImage: cgImage).replaceColor(UIColor.black, tintColor) diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index c6d0e27289..02dec5a618 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -367,13 +367,13 @@ struct ChatThemePreview: View { let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview")) let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)) HStack { - ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: alice, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) .modifier(ChatItemClipped(alice, tailVisible: true)) Spacer() } HStack { Spacer() - ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in }) + ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: bob, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)) .modifier(ChatItemClipped(bob, tailVisible: true)) .frame(alignment: .trailing) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift index 17a0ffdd1c..c8cb2349e7 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NewServerView.swift @@ -65,7 +65,7 @@ struct NewServerView: View { useServerSection(valid) if valid { Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { - MutableQRCode(uri: $serverToEdit.server) + MutableQRCode(uri: $serverToEdit.server, small: true) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 13d01874ed..97bfd360cb 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -110,7 +110,7 @@ struct ProtocolServerView: View { useServerSection(valid) if valid { Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { - MutableQRCode(uri: $serverToEdit.server) + MutableQRCode(uri: $serverToEdit.server, small: true) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index e06b1c4dd3..50a012f4f8 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -58,6 +58,7 @@ let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert" +let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert" let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion" let DEFAULT_ONBOARDING_STAGE = "onboardingStage" let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage" @@ -117,6 +118,7 @@ let appDefaults: [String: Any] = [ DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, DEFAULT_SHOW_MUTE_PROFILE_ALERT: true, + DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true, DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue, DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300, DEFAULT_SHOW_UNREAD_AND_FAVORITES: false, @@ -144,6 +146,7 @@ let hintDefaults = [ DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, DEFAULT_SHOW_MUTE_PROFILE_ALERT, + DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE, DEFAULT_SHOW_DELETE_CONTACT_NOTICE ] @@ -195,6 +198,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE) +let showReportsInSupportChatAlertDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT) + /// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */ let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS) let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 4813edf96c..f374995f78 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -153,6 +153,9 @@ struct UserAddressView: View { } } addressSettingsButton(userAddress) + if (userAddress.connLinkContact.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) { + addShortLinkButton() + } } header: { ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink) } footer: { @@ -209,6 +212,32 @@ struct UserAddressView: View { } } + private func addShortLinkButton() -> some View { + Button { + addShortLink() + } label: { + Label("Add short link", systemImage: "plus") + } + } + + private func addShortLink() { + progressIndicator = true + Task { + do { + let userAddress = try await apiAddMyAddressShortLink() + await MainActor.run { + chatModel.userAddress = userAddress + } + await MainActor.run { progressIndicator = false } + } catch let error { + logger.error("apiAddMyAddressShortLink: \(responseError(error))") + let a = getErrorAlert(error, "Error creating address") + alert = .error(title: a.title, error: a.message) + await MainActor.run { progressIndicator = false } + } + } + } + private func createOneTimeLinkButton() -> some View { NavigationLink { NewChatView(selection: .invite) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 9aa42930bf..aa2c04ccaa 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -25,28 +25,8 @@ struct UserProfile: View { var body: some View { List { - Group { - if profile.image != nil { - ZStack(alignment: .bottomTrailing) { - ZStack(alignment: .topTrailing) { - profileImageView(profile.image) - .onTapGesture { showChooseSource = true } - overlayButton("multiply", edge: .top) { profile.image = nil } - } - overlayButton("camera", edge: .bottom) { showChooseSource = true } - } - } else { - ZStack(alignment: .center) { - profileImageView(profile.image) - editImageButton { showChooseSource = true } - } - } - } - .frame(maxWidth: .infinity, alignment: .center) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .padding(.top) - .contentShape(Rectangle()) + EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource) + .padding(.top) Section { HStack { @@ -133,25 +113,6 @@ struct UserProfile: View { .alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) } } - private func overlayButton( - _ systemName: String, - edge: Edge.Set, - action: @escaping () -> Void - ) -> some View { - Image(systemName: systemName) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 12) - .foregroundColor(theme.colors.primary) - .padding(6) - .frame(width: 36, height: 36, alignment: .center) - .background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5)) - .clipShape(Circle()) - .contentShape(Circle()) - .padding([.trailing, edge], -12) - .onTapGesture(perform: action) - } - private func showFullName(_ user: User) -> Bool { user.profile.fullName != "" && user.profile.fullName != user.profile.displayName } @@ -189,8 +150,54 @@ struct UserProfile: View { } } -func profileImageView(_ imageStr: String?) -> some View { - ProfileImage(imageStr: imageStr, size: 192) +struct EditProfileImage: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner + @Binding var profileImage: String? + @Binding var showChooseSource: Bool + + var body: some View { + Group { + if profileImage != nil { + ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: profileImage, size: 160) + .onTapGesture { showChooseSource = true } + overlayButton("multiply", edge: .top) { profileImage = nil } + } + overlayButton("camera", edge: .bottom) { showChooseSource = true } + } + } else { + ZStack(alignment: .center) { + ProfileImage(imageStr: profileImage, size: 160) + editImageButton { showChooseSource = true } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .contentShape(Rectangle()) + } + + private func overlayButton( + _ systemName: String, + edge: Edge.Set, + action: @escaping () -> Void + ) -> some View { + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 12) + .foregroundColor(theme.colors.primary) + .padding(6) + .frame(width: 36, height: 36, alignment: .center) + .background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5)) + .clipShape(Circle()) + .contentShape(Circle()) + .padding([.trailing, edge], -12) + .onTapGesture(perform: action) + } } func editImageButton(action: @escaping () -> Void) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 887023b670..ddfe59e719 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -350,7 +350,7 @@ struct UserProfilesView: View { Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) } else { if userInfo.unreadCount > 0 { - UnreadBadge(userInfo: userInfo) + userUnreadBadge(userInfo, theme: theme) } if user.hidden { Image(systemName: "lock").foregroundColor(theme.colors.secondary) 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 776199ac1f..8ba6b96b9f 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -554,6 +554,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions Приеми условията @@ -575,6 +583,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions Приети условия @@ -628,6 +640,10 @@ swipe action Добави сървъри чрез сканиране на QR кодове. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Добави членове на екипа @@ -919,12 +935,12 @@ swipe action Already connecting! В процес на свързване! - No comment provided by engineer. + new chat sheet title Already joining the group! Вече се присъединихте към групата! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1354,7 +1370,8 @@ swipe action Cancel Отказ alert action -alert button +alert button +new chat action Cancel migration @@ -1455,7 +1472,7 @@ set passcode view Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1535,11 +1552,23 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Чатове No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1743,11 +1772,6 @@ set passcode view Автоматично свъзрване No comment provided by engineer. - - Connect incognito - Свързване инкогнито - No comment provided by engineer. - Connect to desktop Свързване с настолно устройство @@ -1757,44 +1781,39 @@ set passcode view Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - Свърване със себе си? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Свърване със себе си? Това е вашият личен SimpleX адрес! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Свърване със себе си? Това е вашят еднократен линк за връзка! - No comment provided by engineer. + new chat sheet title Connect via contact address Свързване чрез адрес за контакт - No comment provided by engineer. + new chat sheet title Connect via link Свърване чрез линк - No comment provided by engineer. + new chat sheet title Connect via one-time link Свързване чрез еднократен линк за връзка - No comment provided by engineer. + new chat sheet title Connect with %@ Свързване с %@ - No comment provided by engineer. + new chat action Connected @@ -2051,11 +2070,6 @@ This is your own one-time link! Създай опашка server test step - - Create secret group - Създай тайна група - No comment provided by engineer. - Create your profile Създай своя профил @@ -2300,6 +2314,10 @@ swipe action Изтриване на чат профила? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2698,7 +2716,7 @@ swipe action Don't show again Не показвай отново - No comment provided by engineer. + alert action Done @@ -3006,6 +3024,10 @@ chat item action Грешка при приемане на заявка за контакт No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Грешка при добавяне на член(ове) @@ -3015,6 +3037,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Грешка при промяна на адреса @@ -3032,7 +3058,7 @@ chat item action Error changing setting Грешка при промяна на настройката - No comment provided by engineer. + alert title Error changing to incognito! @@ -3092,7 +3118,11 @@ chat item action Error deleting chat database Грешка при изтриване на базата данни - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3107,12 +3137,12 @@ chat item action Error deleting database Грешка при изтриване на базата данни - No comment provided by engineer. + alert title Error deleting old database Грешка при изтриване на старата база данни - No comment provided by engineer. + alert title Error deleting token @@ -3147,7 +3177,7 @@ chat item action Error exporting chat database Грешка при експортиране на базата данни - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3156,7 +3186,7 @@ chat item action Error importing chat database Грешка при импортиране на базата данни - No comment provided by engineer. + alert title Error joining group @@ -3196,7 +3226,7 @@ chat item action Error removing member Грешка при отстраняване на член - No comment provided by engineer. + alert title Error reordering lists @@ -3731,7 +3761,7 @@ Error: %2$@ Group already exists! Групата вече съществува! - No comment provided by engineer. + new chat sheet title Group display name @@ -3798,6 +3828,10 @@ Error: %2$@ Груповият профил се съхранява на устройствата на членовете, а не на сървърите. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Съобщение при посрещане в групата @@ -4277,34 +4311,24 @@ More improvements are coming soon! Join group Влез в групата - No comment provided by engineer. + new chat sheet title Join group conversations Присъединяване към групи No comment provided by engineer. - - Join group? - Влез в групата? - No comment provided by engineer. - Join incognito Влез инкогнито No comment provided by engineer. - - Join with current profile - Присъединяване с текущия профил - No comment provided by engineer. - Join your group? This is your link for group %@! Влез в твоята група? Това е вашят линк за група %@! - No comment provided by engineer. + new chat action Joining group @@ -4508,6 +4532,10 @@ This is your link for group %@! Член No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive item status text @@ -4539,6 +4567,10 @@ This is your link for group %@! Членът ще бъде премахнат от групата - това не може да бъде отменено! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Членовете на групата могат да добавят реакции към съобщенията. @@ -4931,6 +4963,10 @@ This is your link for group %@! Нова членска роля No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Ново съобщение @@ -4967,6 +5003,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Няма избрани контакти @@ -5140,7 +5180,9 @@ This is your link for group %@! Ok Ок - alert button + alert action +alert button +new chat action Old database @@ -5288,7 +5330,7 @@ Requires compatible VPN. Open group Отвори група - No comment provided by engineer. + new chat action Open link? @@ -5531,6 +5573,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -5950,6 +5996,10 @@ swipe action Отхвърли заявката за контакт No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес. @@ -6003,11 +6053,6 @@ swipe action Предоговори криптирането? No comment provided by engineer. - - Repeat connection request? - Изпрати отново заявката за свързване? - No comment provided by engineer. - Repeat download Повтори изтеглянето @@ -6018,11 +6063,6 @@ swipe action Повтори импортирането No comment provided by engineer. - - Repeat join request? - Изпрати отново заявката за присъединяване? - No comment provided by engineer. - Repeat upload Повтори качването @@ -6053,6 +6093,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6157,6 +6201,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Отзови @@ -6210,6 +6262,14 @@ chat item action Запази (и уведоми контактите) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Запази и уведоми контакта @@ -6234,6 +6294,10 @@ chat item action Запази профила на групата No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6689,6 +6753,10 @@ chat item action Задайте го вместо системната идентификация. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7432,16 +7500,6 @@ It can happen because of some bug or when the connection is compromised.Тази група вече не съществува. No comment provided by engineer. - - This is your own SimpleX address! - Това е вашият личен SimpleX адрес! - No comment provided by engineer. - - - This is your own one-time link! - Това е вашят еднократен линк за връзка! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7831,7 +7889,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Използвай текущия профил - No comment provided by engineer. + new chat action Use for files @@ -7859,7 +7917,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile Използвай нов инкогнито профил - No comment provided by engineer. + new chat action Use only local notifications? @@ -8200,12 +8258,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. Вече се свързвате с %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Вече се свързвате чрез този еднократен линк за връзка! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8215,24 +8273,19 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. Вече се присъединявате към групата %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Вие вече се присъединявате към групата чрез този линк! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Вие вече се присъединявате към групата чрез този линк. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Вече се присъединихте към групата! Изпрати отново заявката за присъединяване? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8343,6 +8396,10 @@ Repeat join request? Можете да видите отново линкът за покана в подробностите за връзката. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Не може да изпращате съобщения! @@ -8358,17 +8415,12 @@ Repeat join request? Хората могат да се свържат с вас само чрез ликовете, които споделяте. No comment provided by engineer. - - You have already requested connection via this address! - Вече сте заявили връзка през този адрес! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Вече сте направили заявката за връзка! Изпрати отново заявката за свързване? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8451,11 +8503,6 @@ Repeat connection request? Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим. No comment provided by engineer. - - You will connect to all group members. - Ще се свържете с всички членове на групата. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Все още ще получавате обаждания и известия от заглушени профили, когато са активни. @@ -8635,6 +8682,10 @@ Repeat connection request? по-горе, след това избери: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call обаждането прието @@ -8644,6 +8695,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin админ @@ -8664,6 +8719,10 @@ Repeat connection request? съгласуване на криптиране… chat item text + + all + member criteria value + all members всички членове @@ -8747,6 +8806,10 @@ marked deleted chat item preview text повикване… call status + + can't send messages + No comment provided by engineer. + cancelled %@ отменен %@ @@ -8852,6 +8915,14 @@ marked deleted chat item preview text името на контакта %1$@ е променено на %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption контактът има e2e криптиране @@ -8862,6 +8933,10 @@ marked deleted chat item preview text контактът няма e2e криптиране No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator създател @@ -9030,6 +9105,10 @@ pref value групата е изтрита No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated профилът на групата е актуализиран @@ -9153,6 +9232,10 @@ pref value свързан rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -9216,6 +9299,10 @@ pref value няма текст copied message info in history + + not synchronized + No comment provided by engineer. + observer наблюдател @@ -9226,6 +9313,7 @@ pref value изключено enabled status group pref value +member criteria value time to disappear @@ -9274,6 +9362,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption квантово устойчиво e2e криптиране @@ -9313,6 +9405,10 @@ time to disappear премахнат адрес за контакт profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture премахната профилна снимка @@ -9323,10 +9419,22 @@ time to disappear ви острани rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved запазено @@ -9508,6 +9616,10 @@ last received msg: %2$@ вие No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group вие сте поканени в групата 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 0400839cb0..86fd9fcb40 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -112,6 +112,7 @@ %@ uploaded + %@ nahrán No comment provided by engineer. @@ -126,6 +127,7 @@ %@, %@ and %lld members + %@, %@ a %lld členů No comment provided by engineer. @@ -544,6 +546,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions No comment provided by engineer. @@ -564,6 +574,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions No comment provided by engineer. @@ -612,6 +626,10 @@ swipe action Přidejte servery skenováním QR kódů. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members No comment provided by engineer. @@ -882,11 +900,11 @@ swipe action Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1281,7 +1299,8 @@ swipe action Cancel Zrušit alert action -alert button +alert button +new chat action Cancel migration @@ -1378,7 +1397,7 @@ set passcode view Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1456,11 +1475,23 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Chaty No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1658,11 +1689,6 @@ set passcode view Connect automatically No comment provided by engineer. - - Connect incognito - Spojit se inkognito - No comment provided by engineer. - Connect to desktop No comment provided by engineer. @@ -1671,37 +1697,33 @@ set passcode view Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Připojte se prostřednictvím odkazu - No comment provided by engineer. + new chat sheet title Connect via one-time link Připojit se jednorázovým odkazem - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1950,11 +1972,6 @@ This is your own one-time link! Vytvořit frontu server test step - - Create secret group - Vytvořit tajnou skupinu - No comment provided by engineer. - Create your profile Vytvořte si profil @@ -2193,6 +2210,10 @@ swipe action Smazat chat profil? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2584,7 +2605,7 @@ swipe action Don't show again Znovu neukazuj - No comment provided by engineer. + alert action Done @@ -2878,6 +2899,10 @@ chat item action Chyba při přijímání žádosti o kontakt No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Chyba přidávání člena(ů) @@ -2887,6 +2912,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Chuba změny adresy @@ -2904,7 +2933,7 @@ chat item action Error changing setting Chyba změny nastavení - No comment provided by engineer. + alert title Error changing to incognito! @@ -2963,7 +2992,11 @@ chat item action Error deleting chat database Chyba při mazání databáze chatu - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -2978,12 +3011,12 @@ chat item action Error deleting database Chyba při mazání databáze - No comment provided by engineer. + alert title Error deleting old database Chyba při mazání staré databáze - No comment provided by engineer. + alert title Error deleting token @@ -3017,7 +3050,7 @@ chat item action Error exporting chat database Chyba při exportu databáze chatu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3026,7 +3059,7 @@ chat item action Error importing chat database Chyba při importu databáze chatu - No comment provided by engineer. + alert title Error joining group @@ -3065,7 +3098,7 @@ chat item action Error removing member Chyba při odebrání člena - No comment provided by engineer. + alert title Error reordering lists @@ -3582,7 +3615,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3649,6 +3682,10 @@ Error: %2$@ Profil skupiny je uložen v zařízeních členů, nikoli na serverech. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Uvítací zpráva skupin @@ -4113,29 +4150,21 @@ More improvements are coming soon! Join group Připojit ke skupině - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito Připojit se inkognito No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4333,6 +4362,10 @@ This is your link for group %@! Člen No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive item status text @@ -4364,6 +4397,10 @@ This is your link for group %@! Člen bude odstraněn ze skupiny - toto nelze vzít zpět! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Členové skupin mohou přidávat reakce na zprávy. @@ -4739,6 +4776,10 @@ This is your link for group %@! Nová role člena No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nová zpráva @@ -4775,6 +4816,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Nebyl vybrán žádný kontakt @@ -4945,7 +4990,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5092,7 +5139,7 @@ Vyžaduje povolení sítě VPN. Open group - No comment provided by engineer. + new chat action Open link? @@ -5320,6 +5367,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -5731,6 +5782,10 @@ swipe action Odmítnout žádost o kontakt No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu. @@ -5784,10 +5839,6 @@ swipe action Znovu vyjednat šifrování? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5796,10 +5847,6 @@ swipe action Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5829,6 +5876,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -5932,6 +5983,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Odvolat @@ -5984,6 +6043,14 @@ chat item action Uložit (a informovat kontakty) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Uložit a upozornit kontakt @@ -6008,6 +6075,10 @@ chat item action Uložení profilu skupiny No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6455,6 +6526,10 @@ chat item action Nastavte jej namísto ověřování systému. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7179,14 +7254,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Tato skupina již neexistuje. No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7564,7 +7631,7 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use current profile Použít aktuální profil - No comment provided by engineer. + new chat action Use for files @@ -7591,7 +7658,7 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use new incognito profile Použít nový inkognito profil - No comment provided by engineer. + new chat action Use only local notifications? @@ -7911,11 +7978,11 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7923,20 +7990,16 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8044,6 +8107,10 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Nemůžete posílat zprávy! @@ -8059,14 +8126,10 @@ Repeat join request? Lidé se s vámi mohou spojit pouze prostřednictvím odkazu, který sdílíte. No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8148,10 +8211,6 @@ Repeat connection request? Při spuštění nebo obnovení aplikace po 30 sekundách na pozadí budete požádáni o ověření. No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní. @@ -8330,6 +8389,10 @@ Repeat connection request? výše, pak vyberte: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call přijatý hovor @@ -8339,6 +8402,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin správce @@ -8358,6 +8425,10 @@ Repeat connection request? povoluji šifrování… chat item text + + all + member criteria value + all members feature role @@ -8435,6 +8506,10 @@ marked deleted chat item preview text volání… call status + + can't send messages + No comment provided by engineer. + cancelled %@ zrušeno %@ @@ -8539,6 +8614,14 @@ marked deleted chat item preview text contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontakt má šifrování e2e @@ -8549,6 +8632,10 @@ marked deleted chat item preview text kontakt nemá šifrování e2e No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator tvůrce @@ -8715,6 +8802,10 @@ pref value skupina smazána No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated profil skupiny aktualizován @@ -8837,6 +8928,10 @@ pref value připojeno rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8900,6 +8995,10 @@ pref value žádný text copied message info in history + + not synchronized + No comment provided by engineer. + observer pozorovatel @@ -8910,6 +9009,7 @@ pref value vypnuto enabled status group pref value +member criteria value time to disappear @@ -8957,6 +9057,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8994,6 +9098,10 @@ time to disappear removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -9003,10 +9111,22 @@ time to disappear odstranil vás rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -9178,6 +9298,10 @@ last received msg: %2$@ you No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group jste pozváni do skupiny 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 06fd7c5a1d..d0f70ce2cb 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Als Mitglied übernehmen + alert action + + + Accept as observer + Als Beobachter übernehmen + alert action + Accept conditions Nutzungsbedingungen akzeptieren @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Mitglied übernehmen + alert title + Accepted conditions Akzeptierte Nutzungsbedingungen @@ -641,6 +656,10 @@ swipe action Server durch Scannen von QR Codes hinzufügen. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Team-Mitglieder aufnehmen @@ -939,12 +958,12 @@ swipe action Already connecting! Bereits verbunden! - No comment provided by engineer. + new chat sheet title Already joining the group! Sie sind bereits Mitglied der Gruppe! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1408,8 @@ swipe action Cancel Abbrechen alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1515,7 @@ set passcode view Chat already exists! Chat besteht bereits! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1602,26 @@ set passcode view Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Chat with admins + Chat mit Administratoren + chat toolbar + + + Chat with member + Chat mit einem Mitglied + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats mit Mitgliedern + No comment provided by engineer. + Check messages every 20 min. Alle 20min Nachrichten überprüfen. @@ -1812,11 +1847,6 @@ set passcode view Automatisch verbinden No comment provided by engineer. - - Connect incognito - Inkognito verbinden - No comment provided by engineer. - Connect to desktop Mit dem Desktop verbinden @@ -1827,44 +1857,39 @@ set passcode view Schneller mit Ihren Freunden verbinden. No comment provided by engineer. - - Connect to yourself? - Mit Ihnen selbst verbinden? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Sich mit Ihnen selbst verbinden? Das ist Ihre eigene SimpleX-Adresse! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Mit Ihnen selbst verbinden? Das ist Ihr eigener Einmal-Link! - No comment provided by engineer. + new chat sheet title Connect via contact address Über die Kontakt-Adresse verbinden - No comment provided by engineer. + new chat sheet title Connect via link Über einen Link verbinden - No comment provided by engineer. + new chat sheet title Connect via one-time link Über einen Einmal-Link verbinden - No comment provided by engineer. + new chat sheet title Connect with %@ Mit %@ verbinden - No comment provided by engineer. + new chat action Connected @@ -2143,11 +2168,6 @@ Das ist Ihr eigener Einmal-Link! Erzeuge Warteschlange server test step - - Create secret group - Geheime Gruppe erstellen - No comment provided by engineer. - Create your profile Erstellen Sie Ihr Profil @@ -2402,6 +2422,11 @@ swipe action Chat-Profil löschen? No comment provided by engineer. + + Delete chat with member? + Chat mit dem Mitglied löschen? + alert title + Delete chat? Chat löschen? @@ -2825,7 +2850,7 @@ swipe action Don't show again Nicht nochmals anzeigen - No comment provided by engineer. + alert action Done @@ -3143,6 +3168,11 @@ chat item action Fehler beim Annehmen der Kontaktanfrage No comment provided by engineer. + + Error accepting member + Fehler beim Übernehmen des Mitglieds + alert title + Error adding member(s) Fehler beim Hinzufügen von Mitgliedern @@ -3153,6 +3183,10 @@ chat item action Fehler beim Hinzufügen des Servers alert title + + Error adding short link + No comment provided by engineer. + Error changing address Fehler beim Wechseln der Empfängeradresse @@ -3171,7 +3205,7 @@ chat item action Error changing setting Fehler beim Ändern der Einstellung - No comment provided by engineer. + alert title Error changing to incognito! @@ -3236,7 +3270,12 @@ chat item action Error deleting chat database Fehler beim Löschen der Chat-Datenbank - No comment provided by engineer. + alert title + + + Error deleting chat with member + Fehler beim Löschen des Chats mit dem Mitglied + alert title Error deleting chat! @@ -3251,12 +3290,12 @@ chat item action Error deleting database Fehler beim Löschen der Datenbank - No comment provided by engineer. + alert title Error deleting old database Fehler beim Löschen der alten Datenbank - No comment provided by engineer. + alert title Error deleting token @@ -3291,7 +3330,7 @@ chat item action Error exporting chat database Fehler beim Exportieren der Chat-Datenbank - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3301,7 +3340,7 @@ chat item action Error importing chat database Fehler beim Importieren der Chat-Datenbank - No comment provided by engineer. + alert title Error joining group @@ -3346,7 +3385,7 @@ chat item action Error removing member Fehler beim Entfernen des Mitglieds - No comment provided by engineer. + alert title Error reordering lists @@ -3926,7 +3965,7 @@ Fehler: %2$@ Group already exists! Die Gruppe besteht bereits! - No comment provided by engineer. + new chat sheet title Group display name @@ -3993,6 +4032,10 @@ Fehler: %2$@ Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichert und nicht auf den Servern. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Gruppen-Begrüßungsmeldung @@ -4491,34 +4534,24 @@ Weitere Verbesserungen sind bald verfügbar! Join group Treten Sie der Gruppe bei - No comment provided by engineer. + new chat sheet title Join group conversations Gruppenunterhaltungen beitreten No comment provided by engineer. - - Join group? - Der Gruppe beitreten? - No comment provided by engineer. - Join incognito Inkognito beitreten No comment provided by engineer. - - Join with current profile - Mit dem aktuellen Profil beitreten - No comment provided by engineer. - Join your group? This is your link for group %@! Ihrer Gruppe beitreten? Das ist Ihr Link für die Gruppe %@! - No comment provided by engineer. + new chat action Joining group @@ -4730,6 +4763,11 @@ Das ist Ihr Link für die Gruppe %@! Mitglied No comment provided by engineer. + + Member admission + Aufnahme von Mitgliedern + No comment provided by engineer. + Member inactive Mitglied inaktiv @@ -4765,6 +4803,11 @@ Das ist Ihr Link für die Gruppe %@! Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Member will join the group, accept member? + Ein Mitglied wird der Gruppe beitreten. Übernehmen? + alert message + Members can add message reactions. Gruppenmitglieder können eine Reaktion auf Nachrichten geben. @@ -5185,6 +5228,11 @@ Das ist Ihr Link für die Gruppe %@! Neue Mitgliedsrolle No comment provided by engineer. + + New member wants to join the group. + Ein neues Mitglied will der Gruppe beitreten. + rcv group event chat item + New message Neue Nachricht @@ -5225,6 +5273,11 @@ Das ist Ihr Link für die Gruppe %@! Keine Chats in der Liste %@ No comment provided by engineer. + + No chats with members + Keine Chats mit Mitgliedern + No comment provided by engineer. + No contacts selected Keine Kontakte ausgewählt @@ -5417,7 +5470,9 @@ Das ist Ihr Link für die Gruppe %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5571,10 +5626,11 @@ Dies erfordert die Aktivierung eines VPNs. Open group Gruppe öffnen - No comment provided by engineer. + new chat action Open link? + Link öffnen? alert title @@ -5828,6 +5884,11 @@ Fehler: %@ Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren. token info + + Please wait for group moderators to review your request to join the group. + Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. + snd group event chat item + Please wait for token activation to complete. Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist. @@ -6281,6 +6342,11 @@ swipe action Kontaktanfrage ablehnen No comment provided by engineer. + + Reject member? + Mitglied ablehnen? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden. @@ -6336,11 +6402,6 @@ swipe action Verschlüsselung neu aushandeln? No comment provided by engineer. - - Repeat connection request? - Verbindungsanfrage wiederholen? - No comment provided by engineer. - Repeat download Herunterladen wiederholen @@ -6351,11 +6412,6 @@ swipe action Import wiederholen No comment provided by engineer. - - Repeat join request? - Verbindungsanfrage wiederholen? - No comment provided by engineer. - Repeat upload Hochladen wiederholen @@ -6391,6 +6447,11 @@ swipe action Grund der Meldung? No comment provided by engineer. + + Report sent to moderators + Meldung wurde an die Moderatoren gesendet + alert title + Report spam: only group moderators will see it. Spam melden: Nur Gruppenmoderatoren werden es sehen. @@ -6506,6 +6567,16 @@ swipe action Nutzungsbedingungen einsehen No comment provided by engineer. + + Review members + Überprüfung der Mitglieder + admission stage + + + Review members before admitting ("knocking"). + Überprüfung der Mitglieder vor der Aufnahme ("Anklopfen"). + admission stage description + Revoke Widerrufen @@ -6562,6 +6633,15 @@ chat item action Speichern (und Kontakte benachrichtigen) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Speichern der Aufnahme-Einstellungen? + alert title + Save and notify contact Speichern und Kontakt benachrichtigen @@ -6587,6 +6667,10 @@ chat item action Gruppenprofil speichern No comment provided by engineer. + + Save group profile? + alert title + Save list Liste speichern @@ -7077,6 +7161,11 @@ chat item action Anstelle der System-Authentifizierung festlegen. No comment provided by engineer. + + Set member admission + Aufnahme von Mitgliedern festlegen + No comment provided by engineer. + Set message expiration in chats. Verfallsdatum von Nachrichten in Chats festlegen. @@ -7873,16 +7962,6 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Gruppe existiert nicht mehr. No comment provided by engineer. - - This is your own SimpleX address! - Das ist Ihre eigene SimpleX-Adresse! - No comment provided by engineer. - - - This is your own one-time link! - Das ist Ihr eigener Einmal-Link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden. @@ -8300,7 +8379,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use current profile Aktuelles Profil nutzen - No comment provided by engineer. + new chat action Use for files @@ -8330,7 +8409,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use new incognito profile Neues Inkognito-Profil nutzen - No comment provided by engineer. + new chat action Use only local notifications? @@ -8690,12 +8769,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You are already connecting to %@. Sie sind bereits mit %@ verbunden. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Sie sind bereits über diesen Einmal-Link verbunden! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8705,24 +8784,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s You are already joining the group %@. Sie sind bereits Mitglied der Gruppe %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Sie sind über diesen Link bereits Mitglied der Gruppe! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Sie sind über diesen Link bereits Mitglied der Gruppe. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Sie sind bereits Mitglied dieser Gruppe! Verbindungsanfrage wiederholen? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8839,6 +8913,11 @@ Verbindungsanfrage wiederholen? Den Einladungslink können Sie in den Details der Verbindung nochmals sehen. alert message + + You can view your reports in Chat with admins. + Sie können Ihre Meldungen im Chat mit den Administratoren sehen. + alert message + You can't send messages! Sie können keine Nachrichten versenden! @@ -8854,17 +8933,12 @@ Verbindungsanfrage wiederholen? Sie entscheiden, wer sich mit Ihnen verbinden kann. No comment provided by engineer. - - You have already requested connection via this address! - Sie haben über diese Adresse bereits eine Verbindung beantragt! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Sie haben bereits ein Verbindungsanfrage beantragt! Verbindungsanfrage wiederholen? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8951,11 +9025,6 @@ Verbindungsanfrage wiederholen? Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen. No comment provided by engineer. - - You will connect to all group members. - Sie werden mit allen Gruppenmitgliedern verbunden. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind. @@ -9078,7 +9147,7 @@ Verbindungsanfrage wiederholen? Your profile is stored on your device and only shared with your contacts. - Das Profil wird nur mit Ihren Kontakten geteilt. + Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. No comment provided by engineer. @@ -9141,6 +9210,11 @@ Verbindungsanfrage wiederholen? Danach die gewünschte Aktion auswählen: No comment provided by engineer. + + accepted %@ + %@ übernommen + rcv group event chat item + accepted call Anruf angenommen @@ -9151,6 +9225,11 @@ Verbindungsanfrage wiederholen? Einladung angenommen chat list item title + + accepted you + hat Sie übernommen + rcv group event chat item + admin Admin @@ -9171,6 +9250,11 @@ Verbindungsanfrage wiederholen? Verschlüsselung zustimmen… chat item text + + all + alle + member criteria value + all members Alle Mitglieder @@ -9257,6 +9341,11 @@ marked deleted chat item preview text Anrufen… call status + + can't send messages + Es können keine Nachrichten gesendet werden + No comment provided by engineer. + cancelled %@ abgebrochen %@ @@ -9362,6 +9451,16 @@ marked deleted chat item preview text Der Kontaktname wurde von %1$@ auf %2$@ geändert profile update event chat item + + contact deleted + Kontakt gelöscht + No comment provided by engineer. + + + contact disabled + Kontakt deaktiviert + No comment provided by engineer. + contact has e2e encryption Kontakt nutzt E2E-Verschlüsselung @@ -9372,6 +9471,11 @@ marked deleted chat item preview text Kontakt nutzt keine E2E-Verschlüsselung No comment provided by engineer. + + contact not ready + Kontakt nicht bereit + No comment provided by engineer. + creator Ersteller @@ -9543,6 +9647,11 @@ pref value Gruppe gelöscht No comment provided by engineer. + + group is deleted + Gruppe wird gelöscht + No comment provided by engineer. + group profile updated Gruppenprofil aktualisiert @@ -9668,6 +9777,11 @@ pref value ist der Gruppe beigetreten rcv group event chat item + + member has old version + Das Mitglied hat eine alte App-Version + No comment provided by engineer. + message Nachricht @@ -9733,6 +9847,11 @@ pref value Kein Text copied message info in history + + not synchronized + Nicht synchronisiert + No comment provided by engineer. + observer Beobachter @@ -9743,6 +9862,7 @@ pref value Aus enabled status group pref value +member criteria value time to disappear @@ -9795,6 +9915,11 @@ time to disappear ausstehende Genehmigung No comment provided by engineer. + + pending review + Ausstehende Überprüfung + No comment provided by engineer. + quantum resistant e2e encryption Quantum-resistente E2E-Verschlüsselung @@ -9835,6 +9960,11 @@ time to disappear Die Kontaktadresse wurde entfernt profile update event chat item + + removed from group + Von der Gruppe entfernt + No comment provided by engineer. + removed profile picture Das Profil-Bild wurde entfernt @@ -9845,11 +9975,26 @@ time to disappear hat Sie aus der Gruppe entfernt rcv group event chat item + + request to join rejected + Beitrittsanfrage abgelehnt + No comment provided by engineer. + requested to connect Zur Verbindung aufgefordert chat list item title + + review + Überprüfung + No comment provided by engineer. + + + reviewed by admins + Von Administratoren überprüft + No comment provided by engineer. + saved abgespeichert @@ -10039,6 +10184,11 @@ Zuletzt empfangene Nachricht: %2$@ Profil No comment provided by engineer. + + you accepted this member + Sie haben dieses Mitglied übernommen + snd group event chat item + you are invited to group Sie sind zu der Gruppe eingeladen 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 fd71e0dee6..abb74d88f1 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Accept as member + alert action + + + Accept as observer + Accept as observer + alert action + Accept conditions Accept conditions @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Accept member + alert title + Accepted conditions Accepted conditions @@ -641,6 +656,11 @@ swipe action Add servers by scanning QR codes. No comment provided by engineer. + + Add short link + Add short link + No comment provided by engineer. + Add team members Add team members @@ -939,12 +959,12 @@ swipe action Already connecting! Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1409,8 @@ swipe action Cancel Cancel alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1516,7 @@ set passcode view Chat already exists! Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1603,26 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + Chat with admins + chat toolbar + + + Chat with member + Chat with member + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats with members + No comment provided by engineer. + Check messages every 20 min. Check messages every 20 min. @@ -1812,11 +1848,6 @@ set passcode view Connect automatically No comment provided by engineer. - - Connect incognito - Connect incognito - No comment provided by engineer. - Connect to desktop Connect to desktop @@ -1827,44 +1858,39 @@ set passcode view Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Connect via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Connect via one-time link - No comment provided by engineer. + new chat sheet title Connect with %@ Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -2143,11 +2169,6 @@ This is your own one-time link! Create queue server test step - - Create secret group - Create secret group - No comment provided by engineer. - Create your profile Create your profile @@ -2402,6 +2423,11 @@ swipe action Delete chat profile? No comment provided by engineer. + + Delete chat with member? + Delete chat with member? + alert title + Delete chat? Delete chat? @@ -2825,7 +2851,7 @@ swipe action Don't show again Don't show again - No comment provided by engineer. + alert action Done @@ -3143,6 +3169,11 @@ chat item action Error accepting contact request No comment provided by engineer. + + Error accepting member + Error accepting member + alert title + Error adding member(s) Error adding member(s) @@ -3153,6 +3184,11 @@ chat item action Error adding server alert title + + Error adding short link + Error adding short link + No comment provided by engineer. + Error changing address Error changing address @@ -3171,7 +3207,7 @@ chat item action Error changing setting Error changing setting - No comment provided by engineer. + alert title Error changing to incognito! @@ -3236,7 +3272,12 @@ chat item action Error deleting chat database Error deleting chat database - No comment provided by engineer. + alert title + + + Error deleting chat with member + Error deleting chat with member + alert title Error deleting chat! @@ -3251,12 +3292,12 @@ chat item action Error deleting database Error deleting database - No comment provided by engineer. + alert title Error deleting old database Error deleting old database - No comment provided by engineer. + alert title Error deleting token @@ -3291,7 +3332,7 @@ chat item action Error exporting chat database Error exporting chat database - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3301,7 +3342,7 @@ chat item action Error importing chat database Error importing chat database - No comment provided by engineer. + alert title Error joining group @@ -3346,7 +3387,7 @@ chat item action Error removing member Error removing member - No comment provided by engineer. + alert title Error reordering lists @@ -3926,7 +3967,7 @@ Error: %2$@ Group already exists! Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3993,6 +4034,11 @@ Error: %2$@ Group profile is stored on members' devices, not on the servers. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Group welcome message @@ -4491,34 +4537,24 @@ More improvements are coming soon! Join group Join group - No comment provided by engineer. + new chat sheet title Join group conversations Join group conversations No comment provided by engineer. - - Join group? - Join group? - No comment provided by engineer. - Join incognito Join incognito No comment provided by engineer. - - Join with current profile - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4730,6 +4766,11 @@ This is your link for group %@! Member No comment provided by engineer. + + Member admission + Member admission + No comment provided by engineer. + Member inactive Member inactive @@ -4765,6 +4806,11 @@ This is your link for group %@! Member will be removed from group - this cannot be undone! No comment provided by engineer. + + Member will join the group, accept member? + Member will join the group, accept member? + alert message + Members can add message reactions. Members can add message reactions. @@ -5185,6 +5231,11 @@ This is your link for group %@! New member role No comment provided by engineer. + + New member wants to join the group. + New member wants to join the group. + rcv group event chat item + New message New message @@ -5225,6 +5276,11 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No chats with members + No comment provided by engineer. + No contacts selected No contacts selected @@ -5417,7 +5473,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5571,7 +5629,7 @@ Requires compatible VPN. Open group Open group - No comment provided by engineer. + new chat action Open link? @@ -5829,6 +5887,11 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. Please wait for token activation to complete. @@ -6282,6 +6345,11 @@ swipe action Reject contact request No comment provided by engineer. + + Reject member? + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Relay server is only used if necessary. Another party can observe your IP address. @@ -6337,11 +6405,6 @@ swipe action Renegotiate encryption? No comment provided by engineer. - - Repeat connection request? - Repeat connection request? - No comment provided by engineer. - Repeat download Repeat download @@ -6352,11 +6415,6 @@ swipe action Repeat import No comment provided by engineer. - - Repeat join request? - Repeat join request? - No comment provided by engineer. - Repeat upload Repeat upload @@ -6392,6 +6450,11 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + Report sent to moderators + alert title + Report spam: only group moderators will see it. Report spam: only group moderators will see it. @@ -6507,6 +6570,16 @@ swipe action Review conditions No comment provided by engineer. + + Review members + Review members + admission stage + + + Review members before admitting ("knocking"). + Review members before admitting ("knocking"). + admission stage description + Revoke Revoke @@ -6563,6 +6636,16 @@ chat item action Save (and notify contacts) alert button + + Save (and notify members) + Save (and notify members) + alert button + + + Save admission settings? + Save admission settings? + alert title + Save and notify contact Save and notify contact @@ -6588,6 +6671,11 @@ chat item action Save group profile No comment provided by engineer. + + Save group profile? + Save group profile? + alert title + Save list Save list @@ -7078,6 +7166,11 @@ chat item action Set it instead of system authentication. No comment provided by engineer. + + Set member admission + Set member admission + No comment provided by engineer. + Set message expiration in chats. Set message expiration in chats. @@ -7874,16 +7967,6 @@ It can happen because of some bug or when the connection is compromised.This group no longer exists. No comment provided by engineer. - - This is your own SimpleX address! - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - This is your own one-time link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. @@ -8301,7 +8384,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Use current profile - No comment provided by engineer. + new chat action Use for files @@ -8331,7 +8414,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile Use new incognito profile - No comment provided by engineer. + new chat action Use only local notifications? @@ -8691,12 +8774,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8706,24 +8789,19 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8840,6 +8918,11 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + You can view your reports in Chat with admins. + alert message + You can't send messages! You can't send messages! @@ -8855,17 +8938,12 @@ Repeat join request? You decide who can connect. No comment provided by engineer. - - You have already requested connection via this address! - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8952,11 +9030,6 @@ Repeat connection request? You will be required to authenticate when you start or resume the app after 30 seconds in background. No comment provided by engineer. - - You will connect to all group members. - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. You will still receive calls and notifications from muted profiles when they are active. @@ -9142,6 +9215,11 @@ Repeat connection request? above, then choose: No comment provided by engineer. + + accepted %@ + accepted %@ + rcv group event chat item + accepted call accepted call @@ -9152,6 +9230,11 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + accepted you + rcv group event chat item + admin admin @@ -9172,6 +9255,11 @@ Repeat connection request? agreeing encryption… chat item text + + all + all + member criteria value + all members all members @@ -9258,6 +9346,11 @@ marked deleted chat item preview text calling… call status + + can't send messages + can't send messages + No comment provided by engineer. + cancelled %@ cancelled %@ @@ -9363,6 +9456,16 @@ marked deleted chat item preview text contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + contact deleted + No comment provided by engineer. + + + contact disabled + contact disabled + No comment provided by engineer. + contact has e2e encryption contact has e2e encryption @@ -9373,6 +9476,11 @@ marked deleted chat item preview text contact has no e2e encryption No comment provided by engineer. + + contact not ready + contact not ready + No comment provided by engineer. + creator creator @@ -9544,6 +9652,11 @@ pref value group deleted No comment provided by engineer. + + group is deleted + group is deleted + No comment provided by engineer. + group profile updated group profile updated @@ -9669,6 +9782,11 @@ pref value connected rcv group event chat item + + member has old version + member has old version + No comment provided by engineer. + message message @@ -9734,6 +9852,11 @@ pref value no text copied message info in history + + not synchronized + not synchronized + No comment provided by engineer. + observer observer @@ -9744,6 +9867,7 @@ pref value off enabled status group pref value +member criteria value time to disappear @@ -9796,6 +9920,11 @@ time to disappear pending approval No comment provided by engineer. + + pending review + pending review + No comment provided by engineer. + quantum resistant e2e encryption quantum resistant e2e encryption @@ -9836,6 +9965,11 @@ time to disappear removed contact address profile update event chat item + + removed from group + removed from group + No comment provided by engineer. + removed profile picture removed profile picture @@ -9846,11 +9980,26 @@ time to disappear removed you rcv group event chat item + + request to join rejected + request to join rejected + No comment provided by engineer. + requested to connect requested to connect chat list item title + + review + review + No comment provided by engineer. + + + reviewed by admins + reviewed by admins + No comment provided by engineer. + saved saved @@ -10040,6 +10189,11 @@ last received msg: %2$@ you No comment provided by engineer. + + you accepted this member + you accepted this member + snd group event chat item + you are invited to group you are invited to group 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 d39fb61249..dc327b2890 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Aceptar como miembro + alert action + + + Accept as observer + Aceptar como observador + alert action + Accept conditions Aceptar condiciones @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Aceptar miembro + alert title + Accepted conditions Condiciones aceptadas @@ -641,6 +656,10 @@ swipe action Añadir servidores mediante el escaneo de códigos QR. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Añadir miembros del equipo @@ -939,12 +958,12 @@ swipe action Already connecting! ¡Ya en proceso de conexión! - No comment provided by engineer. + new chat sheet title Already joining the group! ¡Ya en proceso de unirte al grupo! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1408,8 @@ swipe action Cancel Cancelar alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1515,7 @@ set passcode view Chat already exists! ¡El chat ya existe! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1602,26 @@ set passcode view El chat será eliminado para tí. ¡No puede deshacerse! No comment provided by engineer. + + Chat with admins + Chat con administradores + chat toolbar + + + Chat with member + Chat con miembro + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats con miembros + No comment provided by engineer. + Check messages every 20 min. Comprobar mensajes cada 20 min. @@ -1812,11 +1847,6 @@ set passcode view Conectar automáticamente No comment provided by engineer. - - Connect incognito - Conectar incognito - No comment provided by engineer. - Connect to desktop Conectar con ordenador @@ -1827,44 +1857,39 @@ set passcode view Conecta más rápido con tus amigos. No comment provided by engineer. - - Connect to yourself? - ¿Conectarte a tí mismo? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! ¿Conectarte a tí mismo? ¡Esta es tu propia dirección SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! ¿Conectarte a tí mismo? ¡Este es tu propio enlace de un solo uso! - No comment provided by engineer. + new chat sheet title Connect via contact address Conectar mediante dirección de contacto - No comment provided by engineer. + new chat sheet title Connect via link Conectar mediante enlace - No comment provided by engineer. + new chat sheet title Connect via one-time link Conectar mediante enlace de un sólo uso - No comment provided by engineer. + new chat sheet title Connect with %@ Conectar con %@ - No comment provided by engineer. + new chat action Connected @@ -2143,11 +2168,6 @@ This is your own one-time link! Crear cola server test step - - Create secret group - Crea grupo secreto - No comment provided by engineer. - Create your profile Crea tu perfil @@ -2402,6 +2422,11 @@ swipe action ¿Eliminar perfil? No comment provided by engineer. + + Delete chat with member? + ¿Eliminar el chat con el miembro? + alert title + Delete chat? ¿Eliminar chat? @@ -2825,7 +2850,7 @@ swipe action Don't show again No volver a mostrar - No comment provided by engineer. + alert action Done @@ -3143,6 +3168,11 @@ chat item action Error al aceptar solicitud del contacto No comment provided by engineer. + + Error accepting member + Error al aceptar el miembro + alert title + Error adding member(s) Error al añadir miembro(s) @@ -3153,6 +3183,10 @@ chat item action Error al añadir servidor alert title + + Error adding short link + No comment provided by engineer. + Error changing address Error al cambiar servidor @@ -3171,7 +3205,7 @@ chat item action Error changing setting Error cambiando configuración - No comment provided by engineer. + alert title Error changing to incognito! @@ -3236,7 +3270,12 @@ chat item action Error deleting chat database Error al eliminar base de datos - No comment provided by engineer. + alert title + + + Error deleting chat with member + Error al eliminar el chat con el miembro + alert title Error deleting chat! @@ -3251,12 +3290,12 @@ chat item action Error deleting database Error al eliminar base de datos - No comment provided by engineer. + alert title Error deleting old database Error al eliminar base de datos antigua - No comment provided by engineer. + alert title Error deleting token @@ -3291,7 +3330,7 @@ chat item action Error exporting chat database Error al exportar base de datos - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3301,7 +3340,7 @@ chat item action Error importing chat database Error al importar base de datos - No comment provided by engineer. + alert title Error joining group @@ -3346,7 +3385,7 @@ chat item action Error removing member Error al expulsar miembro - No comment provided by engineer. + alert title Error reordering lists @@ -3926,7 +3965,7 @@ Error: %2$@ Group already exists! ¡El grupo ya existe! - No comment provided by engineer. + new chat sheet title Group display name @@ -3993,6 +4032,10 @@ Error: %2$@ El perfil de grupo se almacena en los dispositivos, no en los servidores. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Mensaje de bienvenida en grupos @@ -4491,34 +4534,24 @@ More improvements are coming soon! Join group Unirte al grupo - No comment provided by engineer. + new chat sheet title Join group conversations Unirse a la conversación del grupo No comment provided by engineer. - - Join group? - ¿Unirte al grupo? - No comment provided by engineer. - Join incognito Unirte en modo incógnito No comment provided by engineer. - - Join with current profile - Unirte con el perfil actual - No comment provided by engineer. - Join your group? This is your link for group %@! ¿Unirse a tu grupo? ¡Este es tu enlace para el grupo %@! - No comment provided by engineer. + new chat action Joining group @@ -4730,6 +4763,11 @@ This is your link for group %@! Miembro No comment provided by engineer. + + Member admission + Admisión del miembro + No comment provided by engineer. + Member inactive Miembro inactivo @@ -4765,6 +4803,11 @@ This is your link for group %@! El miembro será expulsado del grupo. ¡No puede deshacerse! No comment provided by engineer. + + Member will join the group, accept member? + El miembro se unirá al grupo, ¿aceptas al miembro? + alert message + Members can add message reactions. Los miembros pueden añadir reacciones a los mensajes. @@ -5185,6 +5228,11 @@ This is your link for group %@! Nuevo rol de miembro No comment provided by engineer. + + New member wants to join the group. + Un miembro nuevo desea unirse al grupo. + rcv group event chat item + New message Mensaje nuevo @@ -5225,6 +5273,11 @@ This is your link for group %@! Sin chats en la lista %@ No comment provided by engineer. + + No chats with members + Sin chats con miembros + No comment provided by engineer. + No contacts selected Ningún contacto seleccionado @@ -5417,7 +5470,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5571,10 +5626,11 @@ Requiere activación de la VPN. Open group Grupo abierto - No comment provided by engineer. + new chat action Open link? + ¿Abrir enlace? alert title @@ -5800,7 +5856,7 @@ Error: %@ Please remember or store it securely - there is no way to recover a lost passcode! - Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido! + Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido! No comment provided by engineer. @@ -5828,6 +5884,11 @@ Error: %@ Por favor, intenta desactivar y reactivar las notificaciones. token info + + Please wait for group moderators to review your request to join the group. + Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. + snd group event chat item + Please wait for token activation to complete. Por favor, espera a que el token de activación se complete. @@ -6281,6 +6342,11 @@ swipe action Rechazar solicitud de contacto No comment provided by engineer. + + Reject member? + ¿Rechazar al miembro? + alert title + Relay server is only used if necessary. Another party can observe your IP address. El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. @@ -6336,11 +6402,6 @@ swipe action ¿Renegociar cifrado? No comment provided by engineer. - - Repeat connection request? - ¿Repetir solicitud de conexión? - No comment provided by engineer. - Repeat download Repetir descarga @@ -6351,11 +6412,6 @@ swipe action Repetir importación No comment provided by engineer. - - Repeat join request? - ¿Repetir solicitud de admisión? - No comment provided by engineer. - Repeat upload Repetir subida @@ -6391,6 +6447,11 @@ swipe action ¿Motivo del informe? No comment provided by engineer. + + Report sent to moderators + Informe enviado a los moderadores + alert title + Report spam: only group moderators will see it. Informar de spam: sólo los moderadores del grupo lo verán. @@ -6506,6 +6567,16 @@ swipe action Revisar condiciones No comment provided by engineer. + + Review members + Revisar miembros + admission stage + + + Review members before admitting ("knocking"). + Revisar miembros antes de admitir. + admission stage description + Revoke Revocar @@ -6562,6 +6633,15 @@ chat item action Guardar (y notificar contactos) alert button + + Save (and notify members) + alert button + + + Save admission settings? + ¿Guardar configuración? + alert title + Save and notify contact Guardar y notificar contacto @@ -6587,6 +6667,10 @@ chat item action Guardar perfil de grupo No comment provided by engineer. + + Save group profile? + alert title + Save list Guardar lista @@ -7077,6 +7161,11 @@ chat item action Úsalo en lugar de la autenticación del sistema. No comment provided by engineer. + + Set member admission + Admisión miembro + No comment provided by engineer. + Set message expiration in chats. Establece el vencimiento para los mensajes en los chats. @@ -7873,16 +7962,6 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Este grupo ya no existe. No comment provided by engineer. - - This is your own SimpleX address! - ¡Esta es tu propia dirección SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - ¡Este es tu propio enlace de un solo uso! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible. @@ -8300,7 +8379,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use current profile Usar perfil actual - No comment provided by engineer. + new chat action Use for files @@ -8330,7 +8409,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Use new incognito profile Usar nuevo perfil incógnito - No comment provided by engineer. + new chat action Use only local notifications? @@ -8690,12 +8769,12 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión You are already connecting to %@. Ya estás conectando con %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! ¡Ya estás conectando mediante este enlace de un solo uso! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8705,24 +8784,19 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión You are already joining the group %@. Ya estás uniéndote al grupo %@. - No comment provided by engineer. - - - You are already joining the group via this link! - ¡Ya estás uniéndote al grupo mediante este enlace! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Ya estás uniéndote al grupo mediante este enlace. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? ¡En proceso de unirte al grupo! ¿Repetir solicitud de admisión? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8839,6 +8913,11 @@ Repeat join request? Podrás ver el enlace de invitación en detalles de conexión. alert message + + You can view your reports in Chat with admins. + Puedes ver tus informes en Chat con administradores. + alert message + You can't send messages! ¡No puedes enviar mensajes! @@ -8854,17 +8933,12 @@ Repeat join request? Tu decides quién se conecta. No comment provided by engineer. - - You have already requested connection via this address! - ¡Ya has solicitado la conexión mediante esta dirección! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Ya has solicitado la conexión ¿Repetir solicitud? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8951,11 +9025,6 @@ Repeat connection request? Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. No comment provided by engineer. - - You will connect to all group members. - Te conectarás con todos los miembros del grupo. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. @@ -9141,6 +9210,11 @@ Repeat connection request? y después elige: No comment provided by engineer. + + accepted %@ + %@ aceptado + rcv group event chat item + accepted call llamada aceptada @@ -9151,6 +9225,11 @@ Repeat connection request? invitación aceptada chat list item title + + accepted you + te ha aceptado + rcv group event chat item + admin administrador @@ -9171,6 +9250,11 @@ Repeat connection request? acordando cifrado… chat item text + + all + todos + member criteria value + all members todos los miembros @@ -9257,6 +9341,11 @@ marked deleted chat item preview text llamando… call status + + can't send messages + no se pueden enviar mensajes + No comment provided by engineer. + cancelled %@ cancelado %@ @@ -9362,6 +9451,16 @@ marked deleted chat item preview text el contacto %1$@ ha cambiado a %2$@ profile update event chat item + + contact deleted + contacto eliminado + No comment provided by engineer. + + + contact disabled + contacto desactivado + No comment provided by engineer. + contact has e2e encryption el contacto dispone de cifrado de extremo a extremo @@ -9372,6 +9471,11 @@ marked deleted chat item preview text el contacto no dispone de cifrado de extremo a extremo No comment provided by engineer. + + contact not ready + el contacto no está listo + No comment provided by engineer. + creator creador @@ -9543,6 +9647,11 @@ pref value grupo eliminado No comment provided by engineer. + + group is deleted + el grupo ha sido eliminado + No comment provided by engineer. + group profile updated perfil de grupo actualizado @@ -9668,6 +9777,11 @@ pref value conectado rcv group event chat item + + member has old version + el miembro usa una versión antigua + No comment provided by engineer. + message mensaje @@ -9733,6 +9847,11 @@ pref value sin texto copied message info in history + + not synchronized + no sincronizado + No comment provided by engineer. + observer observador @@ -9743,6 +9862,7 @@ pref value desactivado enabled status group pref value +member criteria value time to disappear @@ -9795,6 +9915,11 @@ time to disappear pendiente de aprobación No comment provided by engineer. + + pending review + pendiente de revisión + No comment provided by engineer. + quantum resistant e2e encryption cifrado e2e resistente a tecnología cuántica @@ -9835,6 +9960,11 @@ time to disappear dirección de contacto eliminada profile update event chat item + + removed from group + expulsado del grupo + No comment provided by engineer. + removed profile picture ha eliminado la imagen del perfil @@ -9845,11 +9975,26 @@ time to disappear te ha expulsado rcv group event chat item + + request to join rejected + petición para unirse rechazada + No comment provided by engineer. + requested to connect solicitado para conectar chat list item title + + review + por revisar + No comment provided by engineer. + + + reviewed by admins + revisado por los administradores + No comment provided by engineer. + saved guardado @@ -10039,6 +10184,11 @@ last received msg: %2$@ tu No comment provided by engineer. + + you accepted this member + has aceptado al miembro + snd group event chat item + you are invited to group has sido invitado a un grupo 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 a54666bb10..5d4dabb25d 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -527,6 +527,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions No comment provided by engineer. @@ -547,6 +555,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions No comment provided by engineer. @@ -595,6 +607,10 @@ swipe action Lisää palvelimia skannaamalla QR-koodeja. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members No comment provided by engineer. @@ -865,11 +881,11 @@ swipe action Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1262,7 +1278,8 @@ swipe action Cancel Peruuta alert action -alert button +alert button +new chat action Cancel migration @@ -1359,7 +1376,7 @@ set passcode view Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1437,11 +1454,23 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Keskustelut No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1639,11 +1668,6 @@ set passcode view Connect automatically No comment provided by engineer. - - Connect incognito - Yhdistä Incognito - No comment provided by engineer. - Connect to desktop No comment provided by engineer. @@ -1652,37 +1676,33 @@ set passcode view Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link Yhdistä linkin kautta - No comment provided by engineer. + new chat sheet title Connect via one-time link Yhdistä kertalinkillä - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1931,11 +1951,6 @@ This is your own one-time link! Luo jono server test step - - Create secret group - Luo salainen ryhmä - No comment provided by engineer. - Create your profile Luo profiilisi @@ -2174,6 +2189,10 @@ swipe action Poista keskusteluprofiili? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2565,7 +2584,7 @@ swipe action Don't show again Älä näytä uudelleen - No comment provided by engineer. + alert action Done @@ -2858,6 +2877,10 @@ chat item action Virhe kontaktipyynnön hyväksymisessä No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Virhe lisättäessä jäseniä @@ -2867,6 +2890,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Virhe osoitteenvaihdossa @@ -2884,7 +2911,7 @@ chat item action Error changing setting Virhe asetuksen muuttamisessa - No comment provided by engineer. + alert title Error changing to incognito! @@ -2942,7 +2969,11 @@ chat item action Error deleting chat database Virhe keskustelujen tietokannan poistamisessa - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -2957,12 +2988,12 @@ chat item action Error deleting database Virhe tietokannan poistamisessa - No comment provided by engineer. + alert title Error deleting old database Virhe vanhan tietokannan poistamisessa - No comment provided by engineer. + alert title Error deleting token @@ -2996,7 +3027,7 @@ chat item action Error exporting chat database Virhe vietäessä keskustelujen tietokantaa - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3005,7 +3036,7 @@ chat item action Error importing chat database Virhe keskustelujen tietokannan tuonnissa - No comment provided by engineer. + alert title Error joining group @@ -3044,7 +3075,7 @@ chat item action Error removing member Virhe poistettaessa jäsentä - No comment provided by engineer. + alert title Error reordering lists @@ -3560,7 +3591,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3627,6 +3658,10 @@ Error: %2$@ Ryhmäprofiili tallennetaan jäsenten laitteille, ei palvelimille. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Ryhmän tervetuloviesti @@ -4091,29 +4126,21 @@ More improvements are coming soon! Join group Liity ryhmään - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito Liity incognito-tilassa No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4311,6 +4338,10 @@ This is your link for group %@! Jäsen No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive item status text @@ -4342,6 +4373,10 @@ This is your link for group %@! Jäsen poistetaan ryhmästä - tätä ei voi perua! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Ryhmän jäsenet voivat lisätä viestireaktioita. @@ -4716,6 +4751,10 @@ This is your link for group %@! Uusi jäsenrooli No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Uusi viesti @@ -4752,6 +4791,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Kontakteja ei ole valittu @@ -4922,7 +4965,9 @@ This is your link for group %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5068,7 +5113,7 @@ Edellyttää VPN:n sallimista. Open group - No comment provided by engineer. + new chat action Open link? @@ -5296,6 +5341,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -5707,6 +5756,10 @@ swipe action Hylkää yhteyspyyntö No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi. @@ -5760,10 +5813,6 @@ swipe action Uudelleenneuvottele salaus? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5772,10 +5821,6 @@ swipe action Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5805,6 +5850,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -5908,6 +5957,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Peruuta @@ -5960,6 +6017,14 @@ chat item action Tallenna (ja ilmoita kontakteille) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Tallenna ja ilmoita kontaktille @@ -5984,6 +6049,10 @@ chat item action Tallenna ryhmäprofiili No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6430,6 +6499,10 @@ chat item action Aseta se järjestelmän todennuksen sijaan. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7153,14 +7226,6 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tätä ryhmää ei enää ole olemassa. No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7537,7 +7602,7 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use current profile Käytä nykyistä profiilia - No comment provided by engineer. + new chat action Use for files @@ -7564,7 +7629,7 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use new incognito profile Käytä uutta incognito-profiilia - No comment provided by engineer. + new chat action Use only local notifications? @@ -7884,11 +7949,11 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7896,20 +7961,16 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8017,6 +8078,10 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Et voi lähettää viestejä! @@ -8032,14 +8097,10 @@ Repeat join request? Kimin bağlanabileceğine siz karar verirsiniz. No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8121,10 +8182,6 @@ Repeat connection request? Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen. No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia. @@ -8303,6 +8360,10 @@ Repeat connection request? edellä, valitse sitten: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call hyväksytty puhelu @@ -8312,6 +8373,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin ylläpitäjä @@ -8331,6 +8396,10 @@ Repeat connection request? hyväksyy salausta… chat item text + + all + member criteria value + all members feature role @@ -8408,6 +8477,10 @@ marked deleted chat item preview text soittaa… call status + + can't send messages + No comment provided by engineer. + cancelled %@ peruutettu %@ @@ -8511,6 +8584,14 @@ marked deleted chat item preview text contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontaktilla on e2e-salaus @@ -8521,6 +8602,10 @@ marked deleted chat item preview text kontaktilla ei ole e2e-salausta No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator luoja @@ -8687,6 +8772,10 @@ pref value ryhmä poistettu No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated ryhmäprofiili päivitetty @@ -8809,6 +8898,10 @@ pref value yhdistetty rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8872,6 +8965,10 @@ pref value ei tekstiä copied message info in history + + not synchronized + No comment provided by engineer. + observer tarkkailija @@ -8882,6 +8979,7 @@ pref value pois enabled status group pref value +member criteria value time to disappear @@ -8929,6 +9027,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8966,6 +9068,10 @@ time to disappear removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8975,10 +9081,22 @@ time to disappear poisti sinut rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -9149,6 +9267,10 @@ last received msg: %2$@ you No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group sinut on kutsuttu ryhmään diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 59bde0650e..2896a7fc17 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -565,6 +565,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions Accepter les conditions @@ -586,6 +594,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions Conditions acceptées @@ -641,6 +653,10 @@ swipe action Ajoutez des serveurs en scannant des codes QR. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Ajouter des membres à l'équipe @@ -938,12 +954,12 @@ swipe action Already connecting! Déjà en connexion ! - No comment provided by engineer. + new chat sheet title Already joining the group! Groupe déjà rejoint ! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1387,7 +1403,8 @@ swipe action Cancel Annuler alert action -alert button +alert button +new chat action Cancel migration @@ -1493,7 +1510,7 @@ set passcode view Chat already exists! La discussion existe déjà ! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1580,11 +1597,23 @@ set passcode view Le discussion sera supprimé pour vous - il n'est pas possible de revenir en arrière ! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Discussions No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. Consulter les messages toutes les 20 minutes. @@ -1810,11 +1839,6 @@ set passcode view Connexion automatique No comment provided by engineer. - - Connect incognito - Se connecter incognito - No comment provided by engineer. - Connect to desktop Connexion au bureau @@ -1825,44 +1849,39 @@ set passcode view Connectez-vous à vos amis plus rapidement. No comment provided by engineer. - - Connect to yourself? - Se connecter à soi-même ? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Se connecter à soi-même ? C'est votre propre adresse SimpleX ! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Se connecter à soi-même ? Il s'agit de votre propre lien unique ! - No comment provided by engineer. + new chat sheet title Connect via contact address Se connecter via l'adresse de contact - No comment provided by engineer. + new chat sheet title Connect via link Se connecter via un lien - No comment provided by engineer. + new chat sheet title Connect via one-time link Se connecter via un lien unique - No comment provided by engineer. + new chat sheet title Connect with %@ Se connecter avec %@ - No comment provided by engineer. + new chat action Connected @@ -2141,11 +2160,6 @@ Il s'agit de votre propre lien unique ! Créer une file d'attente server test step - - Create secret group - Créer un groupe secret - No comment provided by engineer. - Create your profile Créez votre profil @@ -2400,6 +2414,10 @@ swipe action Supprimer le profil du chat ? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? Supprimer la discussion ? @@ -2823,7 +2841,7 @@ swipe action Don't show again Ne plus afficher - No comment provided by engineer. + alert action Done @@ -3141,6 +3159,10 @@ chat item action Erreur de validation de la demande de contact No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Erreur lors de l'ajout de membre·s @@ -3151,6 +3173,10 @@ chat item action Erreur lors de l'ajout du serveur alert title + + Error adding short link + No comment provided by engineer. + Error changing address Erreur de changement d'adresse @@ -3169,7 +3195,7 @@ chat item action Error changing setting Erreur de changement de paramètre - No comment provided by engineer. + alert title Error changing to incognito! @@ -3234,7 +3260,11 @@ chat item action Error deleting chat database Erreur lors de la suppression de la base de données du chat - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3249,12 +3279,12 @@ chat item action Error deleting database Erreur lors de la suppression de la base de données - No comment provided by engineer. + alert title Error deleting old database Erreur lors de la suppression de l'ancienne base de données - No comment provided by engineer. + alert title Error deleting token @@ -3289,7 +3319,7 @@ chat item action Error exporting chat database Erreur lors de l'exportation de la base de données du chat - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3299,7 +3329,7 @@ chat item action Error importing chat database Erreur lors de l'importation de la base de données du chat - No comment provided by engineer. + alert title Error joining group @@ -3344,7 +3374,7 @@ chat item action Error removing member Erreur lors de la suppression d'un membre - No comment provided by engineer. + alert title Error reordering lists @@ -3921,7 +3951,7 @@ Erreur : %2$@ Group already exists! Ce groupe existe déjà ! - No comment provided by engineer. + new chat sheet title Group display name @@ -3988,6 +4018,10 @@ Erreur : %2$@ Le profil du groupe est stocké sur les appareils des membres, pas sur les serveurs. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Message d'accueil du groupe @@ -4476,34 +4510,24 @@ D'autres améliorations sont à venir ! Join group Rejoindre le groupe - No comment provided by engineer. + new chat sheet title Join group conversations Participez aux conversations de groupe No comment provided by engineer. - - Join group? - Rejoindre le groupe ? - No comment provided by engineer. - Join incognito Rejoindre en incognito No comment provided by engineer. - - Join with current profile - Rejoindre avec le profil actuel - No comment provided by engineer. - Join your group? This is your link for group %@! Rejoindre votre groupe ? Voici votre lien pour le groupe %@ ! - No comment provided by engineer. + new chat action Joining group @@ -4712,6 +4736,10 @@ Voici votre lien pour le groupe %@ ! Membre No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive Membre inactif @@ -4746,6 +4774,10 @@ Voici votre lien pour le groupe %@ ! Ce membre sera retiré du groupe - impossible de revenir en arrière ! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Les membres du groupe peuvent ajouter des réactions aux messages. @@ -5160,6 +5192,10 @@ Voici votre lien pour le groupe %@ ! Nouveau rôle No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nouveau message @@ -5197,6 +5233,10 @@ Voici votre lien pour le groupe %@ ! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Aucun contact sélectionné @@ -5383,7 +5423,9 @@ Voici votre lien pour le groupe %@ ! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5535,7 +5577,7 @@ Nécessite l'activation d'un VPN. Open group Ouvrir le groupe - No comment provided by engineer. + new chat action Open link? @@ -5790,6 +5832,10 @@ Erreur : %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -6234,6 +6280,10 @@ swipe action Rejeter la demande de contact No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP. @@ -6289,11 +6339,6 @@ swipe action Renégocier le chiffrement ? No comment provided by engineer. - - Repeat connection request? - Répéter la demande de connexion ? - No comment provided by engineer. - Repeat download Répéter le téléchargement @@ -6304,11 +6349,6 @@ swipe action Répéter l'importation No comment provided by engineer. - - Repeat join request? - Répéter la requête d'adhésion ? - No comment provided by engineer. - Repeat upload Répéter l'envoi @@ -6339,6 +6379,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6449,6 +6493,14 @@ swipe action Vérifier les conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Révoquer @@ -6505,6 +6557,14 @@ chat item action Enregistrer (et en informer les contacts) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Enregistrer et en informer le contact @@ -6530,6 +6590,10 @@ chat item action Enregistrer le profil du groupe No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -7017,6 +7081,10 @@ chat item action Il permet de remplacer l'authentification du système. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7805,16 +7873,6 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce groupe n'existe plus. No comment provided by engineer. - - This is your own SimpleX address! - Voici votre propre adresse SimpleX ! - No comment provided by engineer. - - - This is your own one-time link! - Voici votre propre lien unique ! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8225,7 +8283,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use current profile Utiliser le profil actuel - No comment provided by engineer. + new chat action Use for files @@ -8255,7 +8313,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use new incognito profile Utiliser un nouveau profil incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -8613,12 +8671,12 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien You are already connecting to %@. Vous êtes déjà en train de vous connecter à %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Vous êtes déjà connecté(e) via ce lien unique ! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8628,24 +8686,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien You are already joining the group %@. Vous êtes déjà en train de rejoindre le groupe %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Vous êtes déjà en train de rejoindre le groupe via ce lien ! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Vous êtes déjà en train de rejoindre le groupe via ce lien. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Vous êtes déjà membre de ce groupe ! Répéter la demande d'adhésion ? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8762,6 +8815,10 @@ Répéter la demande d'adhésion ? Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Vous ne pouvez pas envoyer de messages ! @@ -8777,17 +8834,12 @@ Répéter la demande d'adhésion ? Vous choisissez qui peut se connecter. No comment provided by engineer. - - You have already requested connection via this address! - Vous avez déjà demandé une connexion via cette adresse ! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Vous avez déjà demandé une connexion ! Répéter la demande de connexion ? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8873,11 +8925,6 @@ Répéter la demande de connexion ? Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l'application après 30 secondes en arrière-plan. No comment provided by engineer. - - You will connect to all group members. - Vous vous connecterez à tous les membres du groupe. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs. @@ -9063,6 +9110,10 @@ Répéter la demande de connexion ? ci-dessus, puis choisissez : No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call appel accepté @@ -9073,6 +9124,10 @@ Répéter la demande de connexion ? invitation acceptée chat list item title + + accepted you + rcv group event chat item + admin admin @@ -9093,6 +9148,10 @@ Répéter la demande de connexion ? négociation du chiffrement… chat item text + + all + member criteria value + all members tous les membres @@ -9178,6 +9237,10 @@ marked deleted chat item preview text appel… call status + + can't send messages + No comment provided by engineer. + cancelled %@ annulé %@ @@ -9283,6 +9346,14 @@ marked deleted chat item preview text le contact %1$@ est devenu %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption Ce contact a le chiffrement de bout en bout @@ -9293,6 +9364,10 @@ marked deleted chat item preview text Ce contact n'a pas le chiffrement de bout en bout No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator créateur @@ -9464,6 +9539,10 @@ pref value groupe supprimé No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated mise à jour du profil de groupe @@ -9589,6 +9668,10 @@ pref value est connecté·e rcv group event chat item + + member has old version + No comment provided by engineer. + message message @@ -9653,6 +9736,10 @@ pref value aucun texte copied message info in history + + not synchronized + No comment provided by engineer. + observer observateur @@ -9663,6 +9750,7 @@ pref value off enabled status group pref value +member criteria value time to disappear @@ -9713,6 +9801,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chiffrement e2e résistant post-quantique @@ -9752,6 +9844,10 @@ time to disappear suppression de l'adresse de contact profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture suppression de la photo de profil @@ -9762,11 +9858,23 @@ time to disappear vous a retiré rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect demande à se connecter chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved enregistré @@ -9956,6 +10064,10 @@ dernier message reçu : %2$@ vous No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group vous êtes invité·e au groupe 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 78bee138e4..08a6f36c83 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -337,7 +337,7 @@ **More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata. - **Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat-kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van. + **Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van. No comment provided by engineer. @@ -422,7 +422,7 @@ - custom time to disappear. - editing history. - legfeljebb 5 perc hosszúságú hangüzenetek. -- egyéni üzenet-eltűnési időkorlát. +- egyéni időkorlát beállítása az üzenetek eltűnéséhez. - előzmények szerkesztése. No comment provided by engineer. @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Befogadás tagként + alert action + + + Accept as observer + Befogadás megfigyelőként + alert action + Accept conditions Feltételek elfogadása @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Tag befogadása + alert title + Accepted conditions Elfogadott feltételek @@ -641,6 +656,10 @@ swipe action Kiszolgáló hozzáadása QR-kód beolvasásával. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Munkatársak hozzáadása @@ -939,12 +958,12 @@ swipe action Already connecting! Kapcsolódás folyamatban! - No comment provided by engineer. + new chat sheet title Already joining the group! A csatlakozás folyamatban van a csoporthoz! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1408,8 @@ swipe action Cancel Mégse alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1515,7 @@ set passcode view Chat already exists! A csevegés már létezik! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1602,26 @@ set passcode view A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza! No comment provided by engineer. + + Chat with admins + Csevegés az adminisztrátorokkal + chat toolbar + + + Chat with member + Csevegés a taggal + No comment provided by engineer. + Chats Csevegések No comment provided by engineer. + + Chats with members + Csevegés a tagokkal + No comment provided by engineer. + Check messages every 20 min. Üzenetek ellenőrzése 20 percenként. @@ -1812,11 +1847,6 @@ set passcode view Kapcsolódás automatikusan No comment provided by engineer. - - Connect incognito - Kapcsolódás inkognitóban - No comment provided by engineer. - Connect to desktop Társítás számítógéppel @@ -1827,44 +1857,39 @@ set passcode view Kapcsolódjon gyorsabban a partnereihez. No comment provided by engineer. - - Connect to yourself? - Kapcsolódik saját magához? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Kapcsolódik saját magához? Ez a saját SimpleX-címe! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Kapcsolódik saját magához? Ez a saját egyszer használható meghívója! - No comment provided by engineer. + new chat sheet title Connect via contact address Kapcsolódás a kapcsolattartási címen keresztül - No comment provided by engineer. + new chat sheet title Connect via link Kapcsolódás egy hivatkozáson keresztül - No comment provided by engineer. + new chat sheet title Connect via one-time link Kapcsolódás egyszer használható meghívón keresztül - No comment provided by engineer. + new chat sheet title Connect with %@ Kapcsolódás a következővel: %@ - No comment provided by engineer. + new chat action Connected @@ -2143,11 +2168,6 @@ Ez a saját egyszer használható meghívója! Sorba állítás létrehozása server test step - - Create secret group - Titkos csoport létrehozása - No comment provided by engineer. - Create your profile Saját profil létrehozása @@ -2240,7 +2260,7 @@ Ez a saját egyszer használható meghívója! Database IDs and Transport isolation option. - Adatbázis-azonosítók és átvitel-izolációs beállítások. + Adatbázis-azonosítók és átvitelelkülönítési beállítások. No comment provided by engineer. @@ -2338,7 +2358,7 @@ Ez a saját egyszer használható meghívója! Decryption error - Titkosítás visszafejtési hiba + Titkosításvisszafejtési hiba message decrypt error item @@ -2402,6 +2422,11 @@ swipe action Törli a csevegési profilt? No comment provided by engineer. + + Delete chat with member? + Törli a taggal való csevegést? + alert title + Delete chat? Törli a csevegést? @@ -2674,7 +2699,7 @@ swipe action Different names, avatars and transport isolation. - Különböző nevek, profilképek és átvitel-izoláció. + Különböző nevek, profilképek és átvitelizoláció. No comment provided by engineer. @@ -2825,7 +2850,7 @@ swipe action Don't show again Ne mutasd újra - No comment provided by engineer. + alert action Done @@ -3143,6 +3168,11 @@ chat item action Hiba történt a meghívási kérés elfogadásakor No comment provided by engineer. + + Error accepting member + Hiba a tag befogadásakor + alert title + Error adding member(s) Hiba történt a tag(ok) hozzáadásakor @@ -3153,6 +3183,10 @@ chat item action Hiba történt a kiszolgáló hozzáadásakor alert title + + Error adding short link + No comment provided by engineer. + Error changing address Hiba történt a cím módosításakor @@ -3171,7 +3205,7 @@ chat item action Error changing setting Hiba történt a beállítás módosításakor - No comment provided by engineer. + alert title Error changing to incognito! @@ -3236,7 +3270,12 @@ chat item action Error deleting chat database Hiba történt a csevegési adatbázis törlésekor - No comment provided by engineer. + alert title + + + Error deleting chat with member + Hiba a taggal való csevegés törlésekor + alert title Error deleting chat! @@ -3251,12 +3290,12 @@ chat item action Error deleting database Hiba történt az adatbázis törlésekor - No comment provided by engineer. + alert title Error deleting old database Hiba történt a régi adatbázis törlésekor - No comment provided by engineer. + alert title Error deleting token @@ -3265,7 +3304,7 @@ chat item action Error deleting user profile - Hiba történt a felhasználó-profil törlésekor + Hiba történt a felhasználói profil törlésekor No comment provided by engineer. @@ -3291,7 +3330,7 @@ chat item action Error exporting chat database Hiba történt a csevegési adatbázis exportálásakor - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3301,7 +3340,7 @@ chat item action Error importing chat database Hiba történt a csevegési adatbázis importálásakor - No comment provided by engineer. + alert title Error joining group @@ -3346,7 +3385,7 @@ chat item action Error removing member Hiba történt a tag eltávolításakor - No comment provided by engineer. + alert title Error reordering lists @@ -3766,7 +3805,7 @@ snd error text For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. - Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. + Például, ha a partnere egy SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux kiszolgálón keresztül fogja azokat kézbesíteni. No comment provided by engineer. @@ -3926,7 +3965,7 @@ Hiba: %2$@ Group already exists! A csoport már létezik! - No comment provided by engineer. + new chat sheet title Group display name @@ -3993,6 +4032,10 @@ Hiba: %2$@ A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message A csoport üdvözlőüzenete @@ -4449,7 +4492,7 @@ További fejlesztések hamarosan! It can happen when you or your connection used the old database backup. - Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta. No comment provided by engineer. @@ -4459,7 +4502,7 @@ További fejlesztések hamarosan! 3. The connection was compromised. Ez akkor fordulhat elő, ha: 1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak. -2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt. +2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta. 3. A kapcsolat sérült. No comment provided by engineer. @@ -4491,34 +4534,24 @@ További fejlesztések hamarosan! Join group Csatlakozás csoporthoz - No comment provided by engineer. + new chat sheet title Join group conversations Csatlakozás csoportos beszélgetésekhez No comment provided by engineer. - - Join group? - Csatlakozik a csoporthoz? - No comment provided by engineer. - Join incognito Csatlakozás inkognitóban No comment provided by engineer. - - Join with current profile - Csatlakozás a jelenlegi profillal - No comment provided by engineer. - Join your group? This is your link for group %@! Csatlakozik a csoportjához? Ez a saját hivatkozása a(z) %@ nevű csoporthoz! - No comment provided by engineer. + new chat action Joining group @@ -4730,6 +4763,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Tag No comment provided by engineer. + + Member admission + Tagbefogadás + No comment provided by engineer. + Member inactive Inaktív tag @@ -4765,6 +4803,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza! No comment provided by engineer. + + Member will join the group, accept member? + A tag csatlakozni akar a csoporthoz, befogadja a tagot? + alert message + Members can add message reactions. A tagok reakciókat adhatnak hozzá az üzenetekhez. @@ -4942,12 +4985,12 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve. + Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve. + Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve. No comment provided by engineer. @@ -5185,6 +5228,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Új tag szerepköre No comment provided by engineer. + + New member wants to join the group. + Új tag szeretne csatlakozni a csoporthoz. + rcv group event chat item + New message Új üzenet @@ -5225,6 +5273,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Nincsenek csevegések a(z) %@ nevű listában No comment provided by engineer. + + No chats with members + Nincsenek csevegések a tagokkal + No comment provided by engineer. + No contacts selected Nincs partner kijelölve @@ -5282,7 +5335,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No message servers. - Nincsenek üzenet-kiszolgálók. + Nincsenek üzenetkiszolgálók. servers error @@ -5347,7 +5400,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! No user identifiers. - Nincsenek felhasználó-azonosítók. + Nincsenek felhasználói azonosítók. No comment provided by engineer. @@ -5417,7 +5470,9 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Ok Rendben - alert button + alert action +alert button +new chat action Old database @@ -5432,20 +5487,20 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Onion hosts will be **required** for connection. Requires compatible VPN. - Onion-kiszolgálók **szükségesek** a kapcsolódáshoz. + Onion kiszolgálók **szükségesek** a kapcsolódáshoz. Kompatibilis VPN szükséges. No comment provided by engineer. Onion hosts will be used when available. Requires compatible VPN. - Onion-kiszolgálók használata, ha azok rendelkezésre állnak. + Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges. No comment provided by engineer. Onion hosts will not be used. - Az onion-kiszolgálók nem lesznek használva. + Az onion kiszolgálók nem lesznek használva. No comment provided by engineer. @@ -5571,10 +5626,11 @@ VPN engedélyezése szükséges. Open group Csoport megnyitása - No comment provided by engineer. + new chat action Open link? + Megnyitja a hivatkozást? alert title @@ -5828,6 +5884,11 @@ Hiba: %@ Próbálja meg letiltani és újra engedélyezni az értesítéseket. token info + + Please wait for group moderators to review your request to join the group. + Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérelmét. + snd group event chat item + Please wait for token activation to complete. Várjon, amíg a token aktiválása befejeződik. @@ -6281,14 +6342,19 @@ swipe action Meghívási kérés elutasítása No comment provided by engineer. + + Reject member? + Elutasítja a tagot? + alert title + Relay server is only used if necessary. Another party can observe your IP address. - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet. + A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát. + A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. No comment provided by engineer. @@ -6336,11 +6402,6 @@ swipe action Újraegyezteti a titkosítást? No comment provided by engineer. - - Repeat connection request? - Megismétli a meghívási kérést? - No comment provided by engineer. - Repeat download Letöltés ismét @@ -6351,11 +6412,6 @@ swipe action Importálás ismét No comment provided by engineer. - - Repeat join request? - Megismétli a meghívási kérést? - No comment provided by engineer. - Repeat upload Feltöltés ismét @@ -6391,6 +6447,11 @@ swipe action Jelentés indoklása? No comment provided by engineer. + + Report sent to moderators + A jelentés el lett küldve a moderátoroknak + alert title + Report spam: only group moderators will see it. Kéretlen tartalom jelentése: csak a csoport moderátorai látják. @@ -6506,6 +6567,16 @@ swipe action Feltételek felülvizsgálata No comment provided by engineer. + + Review members + Tagok áttekintése + admission stage + + + Review members before admitting ("knocking"). + Tagok áttekintése a befogadás előtt (kopogtatás). + admission stage description + Revoke Visszavonás @@ -6538,7 +6609,7 @@ swipe action SOCKS proxy - SOCKS-proxy + SOCKS proxy No comment provided by engineer. @@ -6562,6 +6633,15 @@ chat item action Mentés (és a partnerek értesítése) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Elmenti a befogadási beállításokat? + alert title + Save and notify contact Mentés és a partner értesítése @@ -6587,6 +6667,10 @@ chat item action Csoportprofil mentése No comment provided by engineer. + + Save group profile? + alert title + Save list Lista mentése @@ -7077,6 +7161,11 @@ chat item action Beállítás a rendszer-hitelesítés helyett. No comment provided by engineer. + + Set member admission + Tagbefogadás beállítása + No comment provided by engineer. + Set message expiration in chats. Üzenetek eltűnési idejének módosítása a csevegésekben. @@ -7335,7 +7424,7 @@ chat item action SimpleX protocols reviewed by Trail of Bits. - A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. + A SimpleX-protokollokat a Trail of Bits auditálta. No comment provided by engineer. @@ -7700,7 +7789,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. The code you scanned is not a SimpleX link QR code. - A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. + A beolvasott QR-kód nem egy SimpleX-hivatkozás. No comment provided by engineer. @@ -7873,16 +7962,6 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Ez a csoport már nem létezik. No comment provided by engineer. - - This is your own SimpleX address! - Ez a saját SimpleX-címe! - No comment provided by engineer. - - - This is your own one-time link! - Ez a saját egyszer használható meghívója! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől. @@ -7952,7 +8031,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To protect your privacy, SimpleX uses separate IDs for each of your contacts. - Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. + Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához. No comment provided by engineer. @@ -8027,7 +8106,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Transport isolation - Átvitel-izoláció + Átvitelelkülönítés No comment provided by engineer. @@ -8269,17 +8348,17 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Use .onion hosts - Onion-kiszolgálók használata + Onion kiszolgálók használata No comment provided by engineer. Use SOCKS proxy - SOCKS-proxy használata + SOCKS proxy használata No comment provided by engineer. Use SimpleX Chat servers? - SimpleX Chat-kiszolgálók használata? + SimpleX Chat kiszolgálók használata? No comment provided by engineer. @@ -8300,7 +8379,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Use current profile Jelenlegi profil használata - No comment provided by engineer. + new chat action Use for files @@ -8330,7 +8409,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Use new incognito profile Új inkognitóprofil használata - No comment provided by engineer. + new chat action Use only local notifications? @@ -8339,7 +8418,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Use private routing with unknown servers when IP address is not protected. - Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett. + Használjon privát útválasztást az ismeretlen kiszolgálókkal, ha az IP-cím nem védett. No comment provided by engineer. @@ -8389,7 +8468,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Using SimpleX Chat servers. - SimpleX Chat-kiszolgálók használatban. + SimpleX Chat kiszolgálók használatban. No comment provided by engineer. @@ -8624,12 +8703,12 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Without Tor or VPN, your IP address will be visible to file servers. - Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára. No comment provided by engineer. Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. - Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@. alert message @@ -8690,12 +8769,12 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso You are already connecting to %@. A kapcsolódás már folyamatban van a következőhöz: %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8705,24 +8784,19 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso You are already joining the group %@. A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz. - No comment provided by engineer. - - - You are already joining the group via this link! - A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? A csatlakozás már folyamatban van a csoporthoz! Megismétli a meghívási kérést? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8839,6 +8913,11 @@ Megismétli a meghívási kérést? A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. alert message + + You can view your reports in Chat with admins. + A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben. + alert message + You can't send messages! Nem lehet üzeneteket küldeni! @@ -8854,17 +8933,12 @@ Megismétli a meghívási kérést? Ön dönti el, hogy kivel beszélget. No comment provided by engineer. - - You have already requested connection via this address! - Már küldött egy meghívási kérést ezen a címen keresztül! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Ön már küldött egy meghívási kérést! Megismétli a meghívási kérést? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8951,11 +9025,6 @@ Megismétli a meghívási kérést? Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség. No comment provided by engineer. - - You will connect to all group members. - Kapcsolódni fog a csoport összes tagjához. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. @@ -9078,12 +9147,12 @@ Megismétli a meghívási kérést? Your profile is stored on your device and only shared with your contacts. - A profilja csak a partnereivel van megosztva. + A profilja az eszközén van tárolva és csak a partnereivel van megosztva. No comment provided by engineer. Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. - A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját. + A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX kiszolgálók nem láthatják a profilját. No comment provided by engineer. @@ -9141,6 +9210,11 @@ Megismétli a meghívási kérést? gombra fent, majd válassza ki: No comment provided by engineer. + + accepted %@ + befogadta őt: %@ + rcv group event chat item + accepted call fogadott hívás @@ -9151,6 +9225,11 @@ Megismétli a meghívási kérést? elfogadott meghívó chat list item title + + accepted you + befogadta Önt + rcv group event chat item + admin adminisztrátor @@ -9171,6 +9250,11 @@ Megismétli a meghívási kérést? titkosítás elfogadása… chat item text + + all + összes + member criteria value + all members összes tag @@ -9257,6 +9341,11 @@ marked deleted chat item preview text hívás… call status + + can't send messages + nem lehet üzeneteket küldeni + No comment provided by engineer. + cancelled %@ %@ visszavonva @@ -9362,6 +9451,16 @@ marked deleted chat item preview text %1$@ a következőre módosította a nevét: %2$@ profile update event chat item + + contact deleted + partner törölve + No comment provided by engineer. + + + contact disabled + partner letiltva + No comment provided by engineer. + contact has e2e encryption a partner e2e titkosítással rendelkezik @@ -9372,6 +9471,11 @@ marked deleted chat item preview text a partner nem rendelkezik e2e titkosítással No comment provided by engineer. + + contact not ready + a kapcsolat nem áll készen + No comment provided by engineer. + creator készítő @@ -9543,6 +9647,11 @@ pref value a csoport törölve No comment provided by engineer. + + group is deleted + csoport törölve + No comment provided by engineer. + group profile updated csoportprofil frissítve @@ -9668,6 +9777,11 @@ pref value kapcsolódott rcv group event chat item + + member has old version + a tag régi verziót használ + No comment provided by engineer. + message üzenet @@ -9733,6 +9847,11 @@ pref value nincs szöveg copied message info in history + + not synchronized + nincs szinkronizálva + No comment provided by engineer. + observer megfigyelő @@ -9743,6 +9862,7 @@ pref value kikapcsolva enabled status group pref value +member criteria value time to disappear @@ -9795,6 +9915,11 @@ time to disappear jóváhagyásra vár No comment provided by engineer. + + pending review + függőben lévő áttekintés + No comment provided by engineer. + quantum resistant e2e encryption végpontok közötti kvantumbiztos titkosítás @@ -9835,6 +9960,11 @@ time to disappear eltávolította a kapcsolattartási címet profile update event chat item + + removed from group + eltávolítva a csoportból + No comment provided by engineer. + removed profile picture eltávolította a profilképét @@ -9845,11 +9975,26 @@ time to disappear eltávolította Önt rcv group event chat item + + request to join rejected + csatlakozási kérelem elutasítva + No comment provided by engineer. + requested to connect Függőben lévő meghívási kérelem chat list item title + + review + áttekintés + No comment provided by engineer. + + + reviewed by admins + áttekintve a moderátorok által + No comment provided by engineer. + saved mentett @@ -10039,6 +10184,11 @@ utoljára fogadott üzenet: %2$@ Ön No comment provided by engineer. + + you accepted this member + Ön befogadta ezt a tagot + snd group event chat item + you are invited to group Ön meghívást kapott a csoportba 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 cf5f61918f..ab1f269ae1 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Accetta come membro + alert action + + + Accept as observer + Accetta come osservatore + alert action + Accept conditions Accetta le condizioni @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Accetta membro + alert title + Accepted conditions Condizioni accettate @@ -641,6 +656,10 @@ swipe action Aggiungi server scansionando codici QR. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Aggiungi membri del team @@ -939,12 +958,12 @@ swipe action Already connecting! Già in connessione! - No comment provided by engineer. + new chat sheet title Already joining the group! Già in ingresso nel gruppo! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1408,8 @@ swipe action Cancel Annulla alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1515,7 @@ set passcode view Chat already exists! La chat esiste già! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1602,26 @@ set passcode view La chat verrà eliminata solo per te, non è reversibile! No comment provided by engineer. + + Chat with admins + Chat con amministratori + chat toolbar + + + Chat with member + Chatta con il membro + No comment provided by engineer. + Chats Chat No comment provided by engineer. + + Chats with members + Chat con membri + No comment provided by engineer. + Check messages every 20 min. Controlla i messaggi ogni 20 min. @@ -1812,11 +1847,6 @@ set passcode view Connetti automaticamente No comment provided by engineer. - - Connect incognito - Connetti in incognito - No comment provided by engineer. - Connect to desktop Connetti al desktop @@ -1827,44 +1857,39 @@ set passcode view Connettiti più velocemente ai tuoi amici. No comment provided by engineer. - - Connect to yourself? - Connettersi a te stesso? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Connettersi a te stesso? Questo è il tuo indirizzo SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Connettersi a te stesso? Questo è il tuo link una tantum! - No comment provided by engineer. + new chat sheet title Connect via contact address Connettere via indirizzo del contatto - No comment provided by engineer. + new chat sheet title Connect via link Connetti via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Connetti via link una tantum - No comment provided by engineer. + new chat sheet title Connect with %@ Connettersi con %@ - No comment provided by engineer. + new chat action Connected @@ -2143,11 +2168,6 @@ Questo è il tuo link una tantum! Crea coda server test step - - Create secret group - Crea gruppo segreto - No comment provided by engineer. - Create your profile Crea il tuo profilo @@ -2402,6 +2422,11 @@ swipe action Eliminare il profilo di chat? No comment provided by engineer. + + Delete chat with member? + Eliminare la chat con il membro? + alert title + Delete chat? Eliminare la chat? @@ -2825,7 +2850,7 @@ swipe action Don't show again Non mostrare più - No comment provided by engineer. + alert action Done @@ -3143,6 +3168,11 @@ chat item action Errore nell'accettazione della richiesta di contatto No comment provided by engineer. + + Error accepting member + Errore di accettazione del membro + alert title + Error adding member(s) Errore di aggiunta membro/i @@ -3153,6 +3183,10 @@ chat item action Errore di aggiunta del server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Errore nella modifica dell'indirizzo @@ -3171,7 +3205,7 @@ chat item action Error changing setting Errore nella modifica dell'impostazione - No comment provided by engineer. + alert title Error changing to incognito! @@ -3236,7 +3270,12 @@ chat item action Error deleting chat database Errore nell'eliminazione del database della chat - No comment provided by engineer. + alert title + + + Error deleting chat with member + Errore di eliminazione della chat con il membro + alert title Error deleting chat! @@ -3251,12 +3290,12 @@ chat item action Error deleting database Errore nell'eliminazione del database - No comment provided by engineer. + alert title Error deleting old database Errore nell'eliminazione del database vecchio - No comment provided by engineer. + alert title Error deleting token @@ -3291,7 +3330,7 @@ chat item action Error exporting chat database Errore nell'esportazione del database della chat - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3301,7 +3340,7 @@ chat item action Error importing chat database Errore nell'importazione del database della chat - No comment provided by engineer. + alert title Error joining group @@ -3346,7 +3385,7 @@ chat item action Error removing member Errore nella rimozione del membro - No comment provided by engineer. + alert title Error reordering lists @@ -3926,7 +3965,7 @@ Errore: %2$@ Group already exists! Il gruppo esiste già! - No comment provided by engineer. + new chat sheet title Group display name @@ -3993,6 +4032,10 @@ Errore: %2$@ Il profilo del gruppo è memorizzato sui dispositivi dei membri, non sui server. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Messaggio di benvenuto del gruppo @@ -4491,34 +4534,24 @@ Altri miglioramenti sono in arrivo! Join group Entra nel gruppo - No comment provided by engineer. + new chat sheet title Join group conversations Entra in conversazioni di gruppo No comment provided by engineer. - - Join group? - Entrare nel gruppo? - No comment provided by engineer. - Join incognito Entra in incognito No comment provided by engineer. - - Join with current profile - Entra con il profilo attuale - No comment provided by engineer. - Join your group? This is your link for group %@! Entrare nel tuo gruppo? Questo è il tuo link per il gruppo %@! - No comment provided by engineer. + new chat action Joining group @@ -4730,6 +4763,11 @@ Questo è il tuo link per il gruppo %@! Membro No comment provided by engineer. + + Member admission + Ammissione del membro + No comment provided by engineer. + Member inactive Membro inattivo @@ -4765,6 +4803,11 @@ Questo è il tuo link per il gruppo %@! Il membro verrà rimosso dal gruppo, non è reversibile! No comment provided by engineer. + + Member will join the group, accept member? + Il membro entrerà nel gruppo, accettarlo? + alert message + Members can add message reactions. I membri del gruppo possono aggiungere reazioni ai messaggi. @@ -5185,6 +5228,11 @@ Questo è il tuo link per il gruppo %@! Nuovo ruolo del membro No comment provided by engineer. + + New member wants to join the group. + Un nuovo membro vuole entrare nel gruppo. + rcv group event chat item + New message Nuovo messaggio @@ -5225,6 +5273,11 @@ Questo è il tuo link per il gruppo %@! Nessuna chat nell'elenco %@ No comment provided by engineer. + + No chats with members + Nessuna chat con membri + No comment provided by engineer. + No contacts selected Nessun contatto selezionato @@ -5417,7 +5470,9 @@ Questo è il tuo link per il gruppo %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5571,10 +5626,11 @@ Richiede l'attivazione della VPN. Open group Apri gruppo - No comment provided by engineer. + new chat action Open link? + Aprire il link? alert title @@ -5828,6 +5884,11 @@ Errore: %@ Prova a disattivare e riattivare le notifiche. token info + + Please wait for group moderators to review your request to join the group. + Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. + snd group event chat item + Please wait for token activation to complete. Attendi il completamento dell'attivazione del token. @@ -6281,6 +6342,11 @@ swipe action Rifiuta la richiesta di contatto No comment provided by engineer. + + Reject member? + Rifiutare il membro? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP. @@ -6336,11 +6402,6 @@ swipe action Rinegoziare la crittografia? No comment provided by engineer. - - Repeat connection request? - Ripetere la richiesta di connessione? - No comment provided by engineer. - Repeat download Ripeti scaricamento @@ -6351,11 +6412,6 @@ swipe action Ripeti importazione No comment provided by engineer. - - Repeat join request? - Ripetere la richiesta di ingresso? - No comment provided by engineer. - Repeat upload Ripeti caricamento @@ -6391,6 +6447,11 @@ swipe action Motivo della segnalazione? No comment provided by engineer. + + Report sent to moderators + Segnalazione inviata ai moderatori + alert title + Report spam: only group moderators will see it. Segnala spam: solo i moderatori del gruppo lo vedranno. @@ -6506,6 +6567,16 @@ swipe action Leggi le condizioni No comment provided by engineer. + + Review members + Revisiona i membri + admission stage + + + Review members before admitting ("knocking"). + Revisiona i membri prima di ammetterli ("bussare"). + admission stage description + Revoke Revoca @@ -6562,6 +6633,15 @@ chat item action Salva (e avvisa i contatti) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Salvare le impostazioni di ammissione? + alert title + Save and notify contact Salva e avvisa il contatto @@ -6587,6 +6667,10 @@ chat item action Salva il profilo del gruppo No comment provided by engineer. + + Save group profile? + alert title + Save list Salva elenco @@ -7077,6 +7161,11 @@ chat item action Impostalo al posto dell'autenticazione di sistema. No comment provided by engineer. + + Set member admission + Imposta l'ammissione del membro + No comment provided by engineer. + Set message expiration in chats. Imposta la scadenza dei messaggi nelle chat. @@ -7873,16 +7962,6 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo gruppo non esiste più. No comment provided by engineer. - - This is your own SimpleX address! - Questo è il tuo indirizzo SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - Questo è il tuo link una tantum! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile. @@ -8300,7 +8379,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use current profile Usa il profilo attuale - No comment provided by engineer. + new chat action Use for files @@ -8330,7 +8409,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use new incognito profile Usa nuovo profilo in incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -8690,12 +8769,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e You are already connecting to %@. Ti stai già connettendo a %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Ti stai già connettendo tramite questo link una tantum! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8705,24 +8784,19 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e You are already joining the group %@. Stai già entrando nel gruppo %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Stai già entrando nel gruppo tramite questo link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Stai già entrando nel gruppo tramite questo link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Stai già entrando nel gruppo! Ripetere la richiesta di ingresso? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8839,6 +8913,11 @@ Ripetere la richiesta di ingresso? Puoi vedere di nuovo il link di invito nei dettagli di connessione. alert message + + You can view your reports in Chat with admins. + Puoi vedere le tue segnalazioni nella chat con gli amministratori. + alert message + You can't send messages! Non puoi inviare messaggi! @@ -8854,17 +8933,12 @@ Ripetere la richiesta di ingresso? Sei tu a decidere chi può connettersi. No comment provided by engineer. - - You have already requested connection via this address! - Hai già richiesto la connessione tramite questo indirizzo! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Hai già richiesto la connessione! Ripetere la richiesta di connessione? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8951,11 +9025,6 @@ Ripetere la richiesta di connessione? Dovrai autenticarti quando avvii o riapri l'app dopo 30 secondi in secondo piano. No comment provided by engineer. - - You will connect to all group members. - Ti connetterai a tutti i membri del gruppo. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi. @@ -9141,6 +9210,11 @@ Ripetere la richiesta di connessione? sopra, quindi scegli: No comment provided by engineer. + + accepted %@ + %@ accettato + rcv group event chat item + accepted call chiamata accettata @@ -9151,6 +9225,11 @@ Ripetere la richiesta di connessione? invito accettato chat list item title + + accepted you + ti ha accettato/a + rcv group event chat item + admin amministratore @@ -9171,6 +9250,11 @@ Ripetere la richiesta di connessione? concordando la crittografia… chat item text + + all + tutti + member criteria value + all members tutti i membri @@ -9257,6 +9341,11 @@ marked deleted chat item preview text chiamata… call status + + can't send messages + impossibile inviare messaggi + No comment provided by engineer. + cancelled %@ annullato %@ @@ -9362,6 +9451,16 @@ marked deleted chat item preview text contatto %1$@ cambiato in %2$@ profile update event chat item + + contact deleted + contatto eliminato + No comment provided by engineer. + + + contact disabled + contatto disattivato + No comment provided by engineer. + contact has e2e encryption il contatto ha la crittografia e2e @@ -9372,6 +9471,11 @@ marked deleted chat item preview text il contatto non ha la crittografia e2e No comment provided by engineer. + + contact not ready + contatto non pronto + No comment provided by engineer. + creator creatore @@ -9543,6 +9647,11 @@ pref value gruppo eliminato No comment provided by engineer. + + group is deleted + il gruppo è eliminato + No comment provided by engineer. + group profile updated profilo del gruppo aggiornato @@ -9668,6 +9777,11 @@ pref value si è connesso/a rcv group event chat item + + member has old version + il membro ha una versione vecchia + No comment provided by engineer. + message messaggio @@ -9733,6 +9847,11 @@ pref value nessun testo copied message info in history + + not synchronized + non sincronizzato + No comment provided by engineer. + observer osservatore @@ -9743,6 +9862,7 @@ pref value off enabled status group pref value +member criteria value time to disappear @@ -9795,6 +9915,11 @@ time to disappear in attesa di approvazione No comment provided by engineer. + + pending review + in attesa di revisione + No comment provided by engineer. + quantum resistant e2e encryption crittografia e2e resistente alla quantistica @@ -9835,6 +9960,11 @@ time to disappear indirizzo di contatto rimosso profile update event chat item + + removed from group + rimosso dal gruppo + No comment provided by engineer. + removed profile picture immagine del profilo rimossa @@ -9845,11 +9975,26 @@ time to disappear ti ha rimosso/a rcv group event chat item + + request to join rejected + richiesta di entrare rifiutata + No comment provided by engineer. + requested to connect richiesto di connettersi chat list item title + + review + revisiona + No comment provided by engineer. + + + reviewed by admins + revisionato dagli amministratori + No comment provided by engineer. + saved salvato @@ -10039,6 +10184,11 @@ ultimo msg ricevuto: %2$@ tu No comment provided by engineer. + + you accepted this member + hai accettato questo membro + snd group event chat item + you are invited to group sei stato/a invitato/a al gruppo 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 27134216a7..3d9955ee3d 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -561,6 +561,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions No comment provided by engineer. @@ -581,6 +589,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions No comment provided by engineer. @@ -629,6 +641,10 @@ swipe action QRコードでサーバを追加する。 No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members No comment provided by engineer. @@ -907,12 +923,12 @@ swipe action Already connecting! 既に接続中です! - No comment provided by engineer. + new chat sheet title Already joining the group! すでにグループに参加しています! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1312,7 +1328,8 @@ swipe action Cancel 中止 alert action -alert button +alert button +new chat action Cancel migration @@ -1409,7 +1426,7 @@ set passcode view Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1488,11 +1505,23 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats チャット No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1695,11 +1724,6 @@ set passcode view Connect automatically No comment provided by engineer. - - Connect incognito - シークレットモードで接続 - No comment provided by engineer. - Connect to desktop デスクトップに接続 @@ -1710,37 +1734,33 @@ set passcode view 友達ともっと速くつながりましょう。 No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link リンク経由で接続 - No comment provided by engineer. + new chat sheet title Connect via one-time link ワンタイムリンクで接続 - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1998,11 +2018,6 @@ This is your own one-time link! キューの作成 server test step - - Create secret group - シークレットグループを作成する - No comment provided by engineer. - Create your profile プロフィールを作成する @@ -2244,6 +2259,10 @@ swipe action チャットのプロフィールを削除しますか? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2637,7 +2656,7 @@ swipe action Don't show again 次から表示しない - No comment provided by engineer. + alert action Done @@ -2931,6 +2950,10 @@ chat item action 連絡先リクエストの承諾にエラー発生 No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) メンバー追加にエラー発生 @@ -2940,6 +2963,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address アドレス変更にエラー発生 @@ -2957,7 +2984,7 @@ chat item action Error changing setting 設定変更にエラー発生 - No comment provided by engineer. + alert title Error changing to incognito! @@ -3016,7 +3043,11 @@ chat item action Error deleting chat database チャットデータベース削除にエラー発生 - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3031,12 +3062,12 @@ chat item action Error deleting database データベースの削除にエラー発生 - No comment provided by engineer. + alert title Error deleting old database 古いデータベースを削除にエラー発生 - No comment provided by engineer. + alert title Error deleting token @@ -3069,7 +3100,7 @@ chat item action Error exporting chat database チャットデータベースのエキスポートにエラー発生 - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3078,7 +3109,7 @@ chat item action Error importing chat database チャットデータベースのインポートにエラー発生 - No comment provided by engineer. + alert title Error joining group @@ -3117,7 +3148,7 @@ chat item action Error removing member メンバー除名にエラー発生 - No comment provided by engineer. + alert title Error reordering lists @@ -3633,7 +3664,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3700,6 +3731,10 @@ Error: %2$@ グループのプロフィールはサーバではなく、メンバーの端末に保存されます。 No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message グループのウェルカムメッセージ @@ -4164,29 +4199,21 @@ More improvements are coming soon! Join group グループに参加 - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito シークレットモードで参加 No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4384,6 +4411,10 @@ This is your link for group %@! メンバー No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive item status text @@ -4415,6 +4446,10 @@ This is your link for group %@! メンバーをグループから除名する (※元に戻せません※)! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. グループメンバーはメッセージへのリアクションを追加できます。 @@ -4792,6 +4827,10 @@ This is your link for group %@! 新しいメンバーの役割 No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message 新しいメッセージ @@ -4828,6 +4867,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected 連絡先が選択されてません @@ -4998,7 +5041,9 @@ This is your link for group %@! Ok OK - alert button + alert action +alert button +new chat action Old database @@ -5145,7 +5190,7 @@ VPN を有効にする必要があります。 Open group - No comment provided by engineer. + new chat action Open link? @@ -5373,6 +5418,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -5784,6 +5833,10 @@ swipe action 連絡要求を拒否する No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. 中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。 @@ -5837,10 +5890,6 @@ swipe action 暗号化を再ネゴシエートしますか? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5849,10 +5898,6 @@ swipe action Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5882,6 +5927,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -5985,6 +6034,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke 取り消す @@ -6037,6 +6094,14 @@ chat item action 保存(連絡先に通知) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact 保存して、連絡先にに知らせる @@ -6061,6 +6126,10 @@ chat item action グループプロフィールの保存 No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6500,6 +6569,10 @@ chat item action システム認証の代わりに設定します。 No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7223,14 +7296,6 @@ It can happen because of some bug or when the connection is compromised.このグループはもう存在しません。 No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7607,7 +7672,7 @@ To connect, please ask your contact to create another connection link and check Use current profile 現在のプロファイルを使用する - No comment provided by engineer. + new chat action Use for files @@ -7634,7 +7699,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile 新しいシークレットプロファイルを使用する - No comment provided by engineer. + new chat action Use only local notifications? @@ -7954,11 +8019,11 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7966,20 +8031,16 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8088,6 +8149,10 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! メッセージを送信できませんでした! @@ -8103,14 +8168,10 @@ Repeat join request? あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8192,10 +8253,6 @@ Repeat connection request? 起動時、または非アクティブ状態で30秒が経った後に戻ると、認証する必要となります。 No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. ミュートされたプロフィールがアクティブな場合でも、そのプロフィールからの通話や通知は引き続き受信します。 @@ -8374,6 +8431,10 @@ Repeat connection request? 上で選んでください: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call 受けた通話 @@ -8383,6 +8444,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin 管理者 @@ -8402,6 +8467,10 @@ Repeat connection request? 暗号化に同意しています… chat item text + + all + member criteria value + all members feature role @@ -8479,6 +8548,10 @@ marked deleted chat item preview text 発信中… call status + + can't send messages + No comment provided by engineer. + cancelled %@ キャンセルされました %@ @@ -8582,6 +8655,14 @@ marked deleted chat item preview text contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption 連絡先はエンドツーエンド暗号化があります @@ -8592,6 +8673,10 @@ marked deleted chat item preview text 連絡先はエンドツーエンド暗号化がありません No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator 作成者 @@ -8758,6 +8843,10 @@ pref value グループ削除済み No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated グループのプロフィールが更新されました @@ -8880,6 +8969,10 @@ pref value 接続中 rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8943,6 +9036,10 @@ pref value テキストなし copied message info in history + + not synchronized + No comment provided by engineer. + observer オブザーバー @@ -8953,6 +9050,7 @@ pref value オフ enabled status group pref value +member criteria value time to disappear @@ -9000,6 +9098,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -9037,6 +9139,10 @@ time to disappear removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -9046,10 +9152,22 @@ time to disappear あなたを除名しました rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -9220,6 +9338,10 @@ last received msg: %2$@ you No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group グループ招待が届きました 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 4008c57ac0..2e5db29bc0 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Accepteren als lid + alert action + + + Accept as observer + Accepteren als waarnemer + alert action + Accept conditions Accepteer voorwaarden @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Lid accepteren + alert title + Accepted conditions Geaccepteerde voorwaarden @@ -641,6 +656,10 @@ swipe action Servers toevoegen door QR-codes te scannen. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Teamleden toevoegen @@ -798,6 +817,7 @@ swipe action All servers + Alle servers No comment provided by engineer. @@ -938,12 +958,12 @@ swipe action Already connecting! Al bezig met verbinden! - No comment provided by engineer. + new chat sheet title Already joining the group! Al lid van de groep! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1388,7 +1408,8 @@ swipe action Cancel Annuleren alert action -alert button +alert button +new chat action Cancel migration @@ -1494,7 +1515,7 @@ set passcode view Chat already exists! Chat bestaat al! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1581,11 +1602,26 @@ set passcode view De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Chat with admins + Chat met beheerders + chat toolbar + + + Chat with member + Chat met lid + No comment provided by engineer. + Chats Chats No comment provided by engineer. + + Chats with members + Chats met leden + No comment provided by engineer. + Check messages every 20 min. Controleer uw berichten elke 20 minuten. @@ -1811,11 +1847,6 @@ set passcode view Automatisch verbinden No comment provided by engineer. - - Connect incognito - Verbind incognito - No comment provided by engineer. - Connect to desktop Verbinden met desktop @@ -1826,44 +1857,39 @@ set passcode view Maak sneller verbinding met je vrienden. No comment provided by engineer. - - Connect to yourself? - Verbinding maken met jezelf? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Verbinding maken met jezelf? Dit is uw eigen SimpleX adres! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Verbinding maken met jezelf? Dit is uw eigen eenmalige link! - No comment provided by engineer. + new chat sheet title Connect via contact address Verbinding maken via contactadres - No comment provided by engineer. + new chat sheet title Connect via link Maak verbinding via link - No comment provided by engineer. + new chat sheet title Connect via one-time link Verbinden via een eenmalige link? - No comment provided by engineer. + new chat sheet title Connect with %@ Verbonden met %@ - No comment provided by engineer. + new chat action Connected @@ -2142,11 +2168,6 @@ Dit is uw eigen eenmalige link! Maak een wachtrij server test step - - Create secret group - Maak een geheime groep aan - No comment provided by engineer. - Create your profile Maak je profiel aan @@ -2401,6 +2422,11 @@ swipe action Chatprofiel verwijderen? No comment provided by engineer. + + Delete chat with member? + Chat met lid verwijderen? + alert title + Delete chat? Chat verwijderen? @@ -2824,7 +2850,7 @@ swipe action Don't show again Niet meer weergeven - No comment provided by engineer. + alert action Done @@ -3142,6 +3168,11 @@ chat item action Fout bij het accepteren van een contactverzoek No comment provided by engineer. + + Error accepting member + Fout bij het accepteren van lid + alert title + Error adding member(s) Fout bij het toevoegen van leden @@ -3152,6 +3183,10 @@ chat item action Fout bij toevoegen server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Fout bij wijzigen van adres @@ -3170,7 +3205,7 @@ chat item action Error changing setting Fout bij wijzigen van instelling - No comment provided by engineer. + alert title Error changing to incognito! @@ -3235,7 +3270,12 @@ chat item action Error deleting chat database Fout bij het verwijderen van de chat database - No comment provided by engineer. + alert title + + + Error deleting chat with member + Fout bij het verwijderen van chat met lid + alert title Error deleting chat! @@ -3250,12 +3290,12 @@ chat item action Error deleting database Fout bij het verwijderen van de database - No comment provided by engineer. + alert title Error deleting old database Fout bij het verwijderen van de oude database - No comment provided by engineer. + alert title Error deleting token @@ -3290,7 +3330,7 @@ chat item action Error exporting chat database Fout bij het exporteren van de chat database - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3300,7 +3340,7 @@ chat item action Error importing chat database Fout bij het importeren van de chat database - No comment provided by engineer. + alert title Error joining group @@ -3345,7 +3385,7 @@ chat item action Error removing member Fout bij verwijderen van lid - No comment provided by engineer. + alert title Error reordering lists @@ -3925,7 +3965,7 @@ Fout: %2$@ Group already exists! Groep bestaat al! - No comment provided by engineer. + new chat sheet title Group display name @@ -3992,6 +4032,10 @@ Fout: %2$@ Groep profiel wordt opgeslagen op de apparaten van de leden, niet op de servers. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Groep welkom bericht @@ -4490,34 +4534,24 @@ Binnenkort meer verbeteringen! Join group Word lid van groep - No comment provided by engineer. + new chat sheet title Join group conversations Neem deel aan groepsgesprekken No comment provided by engineer. - - Join group? - Deelnemen aan groep? - No comment provided by engineer. - Join incognito Doe incognito mee No comment provided by engineer. - - Join with current profile - Word lid met huidig profiel - No comment provided by engineer. - Join your group? This is your link for group %@! Sluit u aan bij uw groep? Dit is jouw link voor groep %@! - No comment provided by engineer. + new chat action Joining group @@ -4729,6 +4763,11 @@ Dit is jouw link voor groep %@! Lid No comment provided by engineer. + + Member admission + Toelating van leden + No comment provided by engineer. + Member inactive Lid inactief @@ -4764,6 +4803,11 @@ Dit is jouw link voor groep %@! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Member will join the group, accept member? + Lid zal toetreden tot de groep, lid accepteren? + alert message + Members can add message reactions. Groepsleden kunnen bericht reacties toevoegen. @@ -5184,6 +5228,11 @@ Dit is jouw link voor groep %@! Nieuwe leden rol No comment provided by engineer. + + New member wants to join the group. + Nieuw lid wil zich bij de groep aansluiten. + rcv group event chat item + New message nieuw bericht @@ -5224,6 +5273,11 @@ Dit is jouw link voor groep %@! Geen chats in lijst %@ No comment provided by engineer. + + No chats with members + Geen chats met leden + No comment provided by engineer. + No contacts selected Geen contacten geselecteerd @@ -5416,7 +5470,9 @@ Dit is jouw link voor groep %@! Ok OK - alert button + alert action +alert button +new chat action Old database @@ -5570,10 +5626,11 @@ Vereist het inschakelen van VPN. Open group Open groep - No comment provided by engineer. + new chat action Open link? + Link openen? alert title @@ -5827,6 +5884,11 @@ Fout: %@ Probeer meldingen uit en weer in te schakelen. token info + + Please wait for group moderators to review your request to join the group. + Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. + snd group event chat item + Please wait for token activation to complete. Wacht tot de tokenactivering voltooid is. @@ -6280,6 +6342,11 @@ swipe action Contactverzoek afwijzen No comment provided by engineer. + + Reject member? + Lid afwijzen? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien. @@ -6335,11 +6402,6 @@ swipe action Heronderhandelen over versleuteling? No comment provided by engineer. - - Repeat connection request? - Verbindingsverzoek herhalen? - No comment provided by engineer. - Repeat download Herhaal het downloaden @@ -6350,11 +6412,6 @@ swipe action Herhaal import No comment provided by engineer. - - Repeat join request? - Deelnameverzoek herhalen? - No comment provided by engineer. - Repeat upload Herhaal het uploaden @@ -6390,6 +6447,11 @@ swipe action Reden melding? No comment provided by engineer. + + Report sent to moderators + Rapport verzonden naar moderators + alert title + Report spam: only group moderators will see it. Spam melden: alleen groepsmoderators kunnen het zien. @@ -6505,6 +6567,16 @@ swipe action Voorwaarden bekijken No comment provided by engineer. + + Review members + Leden beoordelen + admission stage + + + Review members before admitting ("knocking"). + Controleer de leden voordat u ze toelaat ('knocking'). + admission stage description + Revoke Intrekken @@ -6561,6 +6633,15 @@ chat item action Bewaar (en informeer contacten) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Toegangsinstellingen opslaan? + alert title + Save and notify contact Opslaan en Contact melden @@ -6586,6 +6667,10 @@ chat item action Groep profiel opslaan No comment provided by engineer. + + Save group profile? + alert title + Save list Lijst opslaan @@ -7076,6 +7161,11 @@ chat item action Stel het in in plaats van systeemverificatie. No comment provided by engineer. + + Set member admission + Toegang voor leden instellen + No comment provided by engineer. + Set message expiration in chats. Stel de berichtvervaldatum in chats in. @@ -7189,6 +7279,7 @@ chat item action Short link + Korte link No comment provided by engineer. @@ -7293,6 +7384,7 @@ chat item action SimpleX channel link + SimpleX channel link simplex link type @@ -7870,18 +7962,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze groep bestaat niet meer. No comment provided by engineer. - - This is your own SimpleX address! - Dit is uw eigen SimpleX adres! - No comment provided by engineer. - - - This is your own one-time link! - Dit is uw eigen eenmalige link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen. No comment provided by engineer. @@ -8185,6 +8268,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Unsupported connection link + Niet-ondersteunde verbindingslink No comment provided by engineer. @@ -8284,6 +8368,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use TCP port 443 for preset servers only. + Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers. No comment provided by engineer. @@ -8294,7 +8379,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use current profile Gebruik het huidige profiel - No comment provided by engineer. + new chat action Use for files @@ -8324,7 +8409,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use new incognito profile Gebruik een nieuw incognitoprofiel - No comment provided by engineer. + new chat action Use only local notifications? @@ -8353,6 +8438,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Use short links (BETA) + Gebruik korte links (BETA) No comment provided by engineer. @@ -8683,12 +8769,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak You are already connecting to %@. U maakt al verbinding met %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Je maakt al verbinding via deze eenmalige link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8698,24 +8784,19 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak You are already joining the group %@. Je bent al lid van de groep %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Je wordt al lid van de groep via deze link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Je wordt al lid van de groep via deze link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Je sluit je al aan bij de groep! Deelnameverzoek herhalen? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8832,6 +8913,11 @@ Deelnameverzoek herhalen? U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails. alert message + + You can view your reports in Chat with admins. + U kunt uw rapporten bekijken in Chat met beheerders. + alert message + You can't send messages! Je kunt geen berichten versturen! @@ -8847,17 +8933,12 @@ Deelnameverzoek herhalen? Jij bepaalt wie er verbinding mag maken. No comment provided by engineer. - - You have already requested connection via this address! - U heeft al een verbinding aangevraagd via dit adres! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Je hebt al verbinding aangevraagd! Verbindingsverzoek herhalen? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8944,11 +9025,6 @@ Verbindingsverzoek herhalen? U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat. No comment provided by engineer. - - You will connect to all group members. - Je maakt verbinding met alle leden. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. @@ -9134,6 +9210,11 @@ Verbindingsverzoek herhalen? hier boven, kies dan: No comment provided by engineer. + + accepted %@ + geaccepteerd %@ + rcv group event chat item + accepted call geaccepteerde oproep @@ -9144,6 +9225,11 @@ Verbindingsverzoek herhalen? geaccepteerde uitnodiging chat list item title + + accepted you + heb je geaccepteerd + rcv group event chat item + admin Beheerder @@ -9164,6 +9250,11 @@ Verbindingsverzoek herhalen? versleuteling overeenkomen… chat item text + + all + alle + member criteria value + all members alle leden @@ -9250,6 +9341,11 @@ marked deleted chat item preview text bellen… call status + + can't send messages + kan geen berichten versturen + No comment provided by engineer. + cancelled %@ geannuleerd %@ @@ -9355,6 +9451,16 @@ marked deleted chat item preview text contactpersoon %1$@ gewijzigd in %2$@ profile update event chat item + + contact deleted + contact verwijderd + No comment provided by engineer. + + + contact disabled + contact uitgeschakeld + No comment provided by engineer. + contact has e2e encryption contact heeft e2e-codering @@ -9365,6 +9471,11 @@ marked deleted chat item preview text contact heeft geen e2e versleuteling No comment provided by engineer. + + contact not ready + contact niet klaar + No comment provided by engineer. + creator creator @@ -9536,6 +9647,11 @@ pref value groep verwijderd No comment provided by engineer. + + group is deleted + groep is verwijderd + No comment provided by engineer. + group profile updated groep profiel bijgewerkt @@ -9661,6 +9777,11 @@ pref value is toegetreden rcv group event chat item + + member has old version + lid heeft oude versie + No comment provided by engineer. + message bericht @@ -9726,6 +9847,11 @@ pref value geen tekst copied message info in history + + not synchronized + niet gesynchroniseerd + No comment provided by engineer. + observer Waarnemer @@ -9736,6 +9862,7 @@ pref value uit enabled status group pref value +member criteria value time to disappear @@ -9788,6 +9915,11 @@ time to disappear in afwachting van goedkeuring No comment provided by engineer. + + pending review + in afwachting van beoordeling + No comment provided by engineer. + quantum resistant e2e encryption quantum bestendige e2e-codering @@ -9828,6 +9960,11 @@ time to disappear contactadres verwijderd profile update event chat item + + removed from group + verwijderd uit de groep + No comment provided by engineer. + removed profile picture profielfoto verwijderd @@ -9838,11 +9975,26 @@ time to disappear heeft je verwijderd rcv group event chat item + + request to join rejected + verzoek tot toetreding afgewezen + No comment provided by engineer. + requested to connect verzocht om verbinding te maken chat list item title + + review + beoordeling + No comment provided by engineer. + + + reviewed by admins + beoordeeld door beheerders + No comment provided by engineer. + saved opgeslagen @@ -10032,6 +10184,11 @@ laatst ontvangen bericht: %2$@ jij No comment provided by engineer. + + you accepted this member + je hebt dit lid geaccepteerd + snd group event chat item + you are invited to group je bent uitgenodigd voor de groep @@ -10175,6 +10332,7 @@ laatst ontvangen bericht: %2$@ From %d chat(s) + Van %d chat(s) notification body diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 175c8b4112..ec6e8535d7 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -565,6 +565,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions Zaakceptuj warunki @@ -586,6 +594,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions Zaakceptowano warunki @@ -641,6 +653,10 @@ swipe action Dodaj serwery, skanując kody QR. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Dodaj członków zespołu @@ -938,12 +954,12 @@ swipe action Already connecting! Już połączony! - No comment provided by engineer. + new chat sheet title Already joining the group! Już dołączono do grupy! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1383,7 +1399,8 @@ swipe action Cancel Anuluj alert action -alert button +alert button +new chat action Cancel migration @@ -1488,7 +1505,7 @@ set passcode view Chat already exists! Czat już istnieje! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1575,11 +1592,23 @@ set passcode view Czat zostanie usunięty dla Ciebie – tej operacji nie można cofnąć! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Czaty No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. Sprawdzaj wiadomości co 20 min. @@ -1797,11 +1826,6 @@ set passcode view Łącz automatycznie No comment provided by engineer. - - Connect incognito - Połącz incognito - No comment provided by engineer. - Connect to desktop Połącz do komputera @@ -1812,44 +1836,39 @@ set passcode view Szybciej łącz się ze znajomymi. No comment provided by engineer. - - Connect to yourself? - Połączyć się ze sobą? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Połączyć się ze sobą? To jest twój własny adres SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Połączyć się ze sobą? To jest twój jednorazowy link! - No comment provided by engineer. + new chat sheet title Connect via contact address Połącz przez adres kontaktowy - No comment provided by engineer. + new chat sheet title Connect via link Połącz się przez link - No comment provided by engineer. + new chat sheet title Connect via one-time link Połącz przez jednorazowy link - No comment provided by engineer. + new chat sheet title Connect with %@ Połącz z %@ - No comment provided by engineer. + new chat action Connected @@ -2119,11 +2138,6 @@ To jest twój jednorazowy link! Utwórz kolejkę server test step - - Create secret group - Utwórz tajną grupę - No comment provided by engineer. - Create your profile Utwórz swój profil @@ -2374,6 +2388,10 @@ swipe action Usunąć profil czatu? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2787,7 +2805,7 @@ swipe action Don't show again Nie pokazuj ponownie - No comment provided by engineer. + alert action Done @@ -3100,6 +3118,10 @@ chat item action Błąd przyjmowania prośby o kontakt No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Błąd dodawania członka(ów) @@ -3109,6 +3131,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address Błąd zmiany adresu @@ -3127,7 +3153,7 @@ chat item action Error changing setting Błąd zmiany ustawienia - No comment provided by engineer. + alert title Error changing to incognito! @@ -3189,7 +3215,11 @@ chat item action Error deleting chat database Błąd usuwania bazy danych czatu - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3204,12 +3234,12 @@ chat item action Error deleting database Błąd usuwania bazy danych - No comment provided by engineer. + alert title Error deleting old database Błąd usuwania starej bazy danych - No comment provided by engineer. + alert title Error deleting token @@ -3244,7 +3274,7 @@ chat item action Error exporting chat database Błąd eksportu bazy danych czatu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3254,7 +3284,7 @@ chat item action Error importing chat database Błąd importu bazy danych czatu - No comment provided by engineer. + alert title Error joining group @@ -3297,7 +3327,7 @@ chat item action Error removing member Błąd usuwania członka - No comment provided by engineer. + alert title Error reordering lists @@ -3857,7 +3887,7 @@ Błąd: %2$@ Group already exists! Grupa już istnieje! - No comment provided by engineer. + new chat sheet title Group display name @@ -3924,6 +3954,10 @@ Błąd: %2$@ Profil grupy jest przechowywany na urządzeniach członków, a nie na serwerach. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Wiadomość powitalna grupy @@ -4407,34 +4441,24 @@ More improvements are coming soon! Join group Dołącz do grupy - No comment provided by engineer. + new chat sheet title Join group conversations Dołącz do grupowej rozmowy No comment provided by engineer. - - Join group? - Dołączyć do grupy? - No comment provided by engineer. - Join incognito Dołącz incognito No comment provided by engineer. - - Join with current profile - Dołącz z obecnym profilem - No comment provided by engineer. - Join your group? This is your link for group %@! Dołączyć do twojej grupy? To jest twój link do grupy %@! - No comment provided by engineer. + new chat action Joining group @@ -4641,6 +4665,10 @@ To jest twój link do grupy %@! Członek No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive Członek nieaktywny @@ -4673,6 +4701,10 @@ To jest twój link do grupy %@! Członek zostanie usunięty z grupy - nie można tego cofnąć! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Członkowie grupy mogą dodawać reakcje wiadomości. @@ -5083,6 +5115,10 @@ To jest twój link do grupy %@! Nowa rola członka No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Nowa wiadomość @@ -5119,6 +5155,10 @@ To jest twój link do grupy %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Nie wybrano kontaktów @@ -5298,7 +5338,9 @@ To jest twój link do grupy %@! Ok Ok - alert button + alert action +alert button +new chat action Old database @@ -5447,7 +5489,7 @@ Wymaga włączenia VPN. Open group Grupa otwarta - No comment provided by engineer. + new chat action Open link? @@ -5698,6 +5740,10 @@ Błąd: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -6140,6 +6186,10 @@ swipe action Odrzuć prośbę kontaktu No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Serwer przekaźnikowy jest używany tylko w razie potrzeby. Inna strona może obserwować Twój adres IP. @@ -6195,11 +6245,6 @@ swipe action Renegocjować szyfrowanie? No comment provided by engineer. - - Repeat connection request? - Powtórzyć prośbę połączenia? - No comment provided by engineer. - Repeat download Powtórz pobieranie @@ -6210,11 +6255,6 @@ swipe action Powtórz importowanie No comment provided by engineer. - - Repeat join request? - Powtórzyć prośbę dołączenia? - No comment provided by engineer. - Repeat upload Powtórz wgrywanie @@ -6245,6 +6285,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6354,6 +6398,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Odwołaj @@ -6410,6 +6462,14 @@ chat item action Zapisz (i powiadom kontakty) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Zapisz i powiadom kontakt @@ -6435,6 +6495,10 @@ chat item action Zapisz profil grupy No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6918,6 +6982,10 @@ chat item action Ustaw go zamiast uwierzytelniania systemowego. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7688,16 +7756,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Ta grupa już nie istnieje. 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 is your own one-time link! - To jest twój jednorazowy link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8102,7 +8160,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Use current profile Użyj obecnego profilu - No comment provided by engineer. + new chat action Use for files @@ -8130,7 +8188,7 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Use new incognito profile Użyj nowego profilu incognito - No comment provided by engineer. + new chat action Use only local notifications? @@ -8483,12 +8541,12 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc You are already connecting to %@. Już się łączysz z %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Już jesteś połączony z tym jednorazowym linkiem! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8498,24 +8556,19 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc You are already joining the group %@. Już dołączasz do grupy %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Już dołączasz do grupy przez ten link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Już dołączasz do grupy przez ten link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Już dołączasz do grupy! Powtórzyć prośbę dołączenia? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8630,6 +8683,10 @@ Powtórzyć prośbę dołączenia? Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Nie możesz wysyłać wiadomości! @@ -8645,17 +8702,12 @@ Powtórzyć prośbę dołączenia? Ty decydujesz, kto może się połączyć. No comment provided by engineer. - - You have already requested connection via this address! - Już prosiłeś o połączenie na ten adres! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Już prosiłeś o połączenie! Powtórzyć prośbę połączenia? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8741,11 +8793,6 @@ Powtórzyć prośbę połączenia? Uwierzytelnienie będzie wymagane przy uruchamianiu lub wznawianiu aplikacji po 30 sekundach w tle. No comment provided by engineer. - - You will connect to all group members. - Zostaniesz połączony ze wszystkimi członkami grupy. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne. @@ -8930,6 +8977,10 @@ Powtórzyć prośbę połączenia? powyżej, a następnie wybierz: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call zaakceptowane połączenie @@ -8939,6 +8990,10 @@ Powtórzyć prośbę połączenia? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin administrator @@ -8959,6 +9014,10 @@ Powtórzyć prośbę połączenia? uzgadnianie szyfrowania… chat item text + + all + member criteria value + all members wszyscy członkowie @@ -9044,6 +9103,10 @@ marked deleted chat item preview text dzwonie… call status + + can't send messages + No comment provided by engineer. + cancelled %@ anulowany %@ @@ -9149,6 +9212,14 @@ marked deleted chat item preview text kontakt %1$@ zmieniony na %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kontakt posiada szyfrowanie e2e @@ -9159,6 +9230,10 @@ marked deleted chat item preview text kontakt nie posiada szyfrowania e2e No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator twórca @@ -9330,6 +9405,10 @@ pref value grupa usunięta No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated zaktualizowano profil grupy @@ -9455,6 +9534,10 @@ pref value połączony rcv group event chat item + + member has old version + No comment provided by engineer. + message wiadomość @@ -9519,6 +9602,10 @@ pref value brak tekstu copied message info in history + + not synchronized + No comment provided by engineer. + observer obserwator @@ -9529,6 +9616,7 @@ pref value wyłączony enabled status group pref value +member criteria value time to disappear @@ -9579,6 +9667,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption kwantowo odporne szyfrowanie e2e @@ -9618,6 +9710,10 @@ time to disappear usunięto adres kontaktu profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture usunięto zdjęcie profilu @@ -9628,10 +9724,22 @@ time to disappear usunął cię rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved zapisane @@ -9821,6 +9929,10 @@ ostatnia otrzymana wiadomość: %2$@ Ty No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group jesteś zaproszony do grupy 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 419fa75375..6e3561ee02 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -167,7 +167,7 @@ %d hours - %d ч. + %d час. time interval @@ -565,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Принять в группу + alert action + + + Accept as observer + Принять как читателя + alert action + Accept conditions Принять условия @@ -586,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Принять члена + alert title + Accepted conditions Принятые условия @@ -641,6 +656,10 @@ swipe action Добавить серверы через QR код. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Добавить сотрудников @@ -798,6 +817,7 @@ swipe action All servers + Все серверы No comment provided by engineer. @@ -938,12 +958,12 @@ swipe action Already connecting! Уже соединяется! - No comment provided by engineer. + new chat sheet title Already joining the group! Вступление в группу уже начато! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1388,7 +1408,8 @@ swipe action Cancel Отменить alert action -alert button +alert button +new chat action Cancel migration @@ -1494,7 +1515,7 @@ set passcode view Chat already exists! Разговор уже существует! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1581,11 +1602,26 @@ set passcode view Разговор будет удален для Вас - это действие нельзя отменить! No comment provided by engineer. + + Chat with admins + Чат с админами + chat toolbar + + + Chat with member + Чат с членом группы + No comment provided by engineer. + Chats Чаты No comment provided by engineer. + + Chats with members + Чаты с членами группы + No comment provided by engineer. + Check messages every 20 min. Проверять сообщения каждые 20 минут. @@ -1811,11 +1847,6 @@ set passcode view Соединяться автоматически No comment provided by engineer. - - Connect incognito - Соединиться Инкогнито - No comment provided by engineer. - Connect to desktop Подключиться к компьютеру @@ -1826,44 +1857,39 @@ set passcode view Соединяйтесь с друзьями быстрее. No comment provided by engineer. - - Connect to yourself? - Соединиться с самим собой? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Соединиться с самим собой? Это ваш собственный адрес SimpleX! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Соединиться с самим собой? Это ваша собственная одноразовая ссылка! - No comment provided by engineer. + new chat sheet title Connect via contact address Соединиться через адрес - No comment provided by engineer. + new chat sheet title Connect via link Соединиться через ссылку - No comment provided by engineer. + new chat sheet title Connect via one-time link Соединиться через одноразовую ссылку - No comment provided by engineer. + new chat sheet title Connect with %@ Соединиться с %@ - No comment provided by engineer. + new chat action Connected @@ -2142,11 +2168,6 @@ This is your own one-time link! Создание очереди server test step - - Create secret group - Создать скрытую группу - No comment provided by engineer. - Create your profile Создать профиль @@ -2401,6 +2422,11 @@ swipe action Удалить профиль? No comment provided by engineer. + + Delete chat with member? + Удалить чат с членом группы? + alert title + Delete chat? Удалить разговор? @@ -2688,6 +2714,7 @@ swipe action Direct messages between members are prohibited. + Прямые сообщения между членами запрещены. No comment provided by engineer. @@ -2792,6 +2819,7 @@ swipe action Do not send history to new members. + Не отправлять историю новым членам. No comment provided by engineer. @@ -2822,7 +2850,7 @@ swipe action Don't show again Не показывать - No comment provided by engineer. + alert action Done @@ -2917,6 +2945,7 @@ chat item action Enable Flux in Network & servers settings for better metadata privacy. + Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных. No comment provided by engineer. @@ -3139,8 +3168,14 @@ chat item action Ошибка при принятии запроса на соединение No comment provided by engineer. + + Error accepting member + Ошибка вступления члена группы + alert title + Error adding member(s) + Ошибка при добавлении членов группы No comment provided by engineer. @@ -3148,6 +3183,10 @@ chat item action Ошибка добавления сервера alert title + + Error adding short link + No comment provided by engineer. + Error changing address Ошибка при изменении адреса @@ -3166,7 +3205,7 @@ chat item action Error changing setting Ошибка при изменении настройки - No comment provided by engineer. + alert title Error changing to incognito! @@ -3205,6 +3244,7 @@ chat item action Error creating member contact + Ошибка при создании контакта No comment provided by engineer. @@ -3230,7 +3270,12 @@ chat item action Error deleting chat database Ошибка при удалении данных чата - No comment provided by engineer. + alert title + + + Error deleting chat with member + Ошибка при удалении чата с членом группы + alert title Error deleting chat! @@ -3245,12 +3290,12 @@ chat item action Error deleting database Ошибка при удалении данных чата - No comment provided by engineer. + alert title Error deleting old database Ошибка при удалении предыдущей версии данных чата - No comment provided by engineer. + alert title Error deleting token @@ -3285,7 +3330,7 @@ chat item action Error exporting chat database Ошибка при экспорте архива чата - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3295,7 +3340,7 @@ chat item action Error importing chat database Ошибка при импорте архива чата - No comment provided by engineer. + alert title Error joining group @@ -3339,7 +3384,8 @@ chat item action Error removing member - No comment provided by engineer. + Ошибка при удалении члена группы + alert title Error reordering lists @@ -3403,6 +3449,7 @@ chat item action Error sending member contact invitation + Ошибка при отправке приглашения члену No comment provided by engineer. @@ -3738,6 +3785,7 @@ snd error text Fix not supported by group member + Починка не поддерживается членом группы. No comment provided by engineer. @@ -3871,6 +3919,7 @@ Error: %2$@ Fully decentralized – visible only to members. + Группа полностью децентрализована – она видна только членам. No comment provided by engineer. @@ -3916,7 +3965,7 @@ Error: %2$@ Group already exists! Группа уже существует! - No comment provided by engineer. + new chat sheet title Group display name @@ -3980,8 +4029,13 @@ Error: %2$@ Group profile is stored on members' devices, not on the servers. + Профиль группы хранится на устройствах членов, а не на серверах. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Приветственное сообщение группы @@ -3989,6 +4043,7 @@ Error: %2$@ Group will be deleted for all members - this cannot be undone! + Группа будет удалена для всех членов - это действие нельзя отменить! No comment provided by engineer. @@ -4053,6 +4108,7 @@ Error: %2$@ History is not sent to new members. + История не отправляется новым членам. No comment provided by engineer. @@ -4400,6 +4456,7 @@ More improvements are coming soon! Invite members + Пригласить членов группы No comment provided by engineer. @@ -4476,34 +4533,24 @@ More improvements are coming soon! Join group Вступить в группу - No comment provided by engineer. + new chat sheet title Join group conversations Присоединяйтесь к разговорам в группах No comment provided by engineer. - - Join group? - Вступить в группу? - No comment provided by engineer. - Join incognito Вступить инкогнито No comment provided by engineer. - - Join with current profile - Вступить с активным профилем - No comment provided by engineer. - Join your group? This is your link for group %@! Вступить в вашу группу? Это ваша ссылка на группу %@! - No comment provided by engineer. + new chat action Joining group @@ -4712,10 +4759,17 @@ This is your link for group %@! Member + Член группы + No comment provided by engineer. + + + Member admission + Приём членов в группу No comment provided by engineer. Member inactive + Член неактивен item status text @@ -4730,54 +4784,72 @@ This is your link for group %@! Member role will be changed to "%@". All group members will be notified. + Роль члена будет изменена на "%@". Все члены группы получат уведомление. No comment provided by engineer. Member role will be changed to "%@". The member will receive a new invitation. + Роль члена будет изменена на "%@". Будет отправлено новое приглашение. No comment provided by engineer. Member will be removed from chat - this cannot be undone! + Член будет удален из разговора - это действие нельзя отменить! No comment provided by engineer. Member will be removed from group - this cannot be undone! + Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. + + Member will join the group, accept member? + Участник хочет присоединиться к группе. Принять? + alert message + Members can add message reactions. + Члены могут добавлять реакции на сообщения. No comment provided by engineer. Members can irreversibly delete sent messages. (24 hours) + Члены могут необратимо удалять отправленные сообщения. (24 часа) No comment provided by engineer. Members can report messsages to moderators. + Члены группы могут пожаловаться модераторам. No comment provided by engineer. Members can send SimpleX links. + Члены могут отправлять ссылки SimpleX. No comment provided by engineer. Members can send direct messages. + Члены могут посылать прямые сообщения. No comment provided by engineer. Members can send disappearing messages. + Члены могут посылать исчезающие сообщения. No comment provided by engineer. Members can send files and media. + Члены могут слать файлы и медиа. No comment provided by engineer. Members can send voice messages. + Члены могут отправлять голосовые сообщения. No comment provided by engineer. Mention members 👋 + Упоминайте участников 👋 No comment provided by engineer. @@ -4812,6 +4884,7 @@ This is your link for group %@! Message may be delivered later if member becomes active. + Сообщение может быть доставлено позже, если член группы станет активным. item status description @@ -5151,8 +5224,14 @@ This is your link for group %@! New member role + Роль члена группы No comment provided by engineer. + + New member wants to join the group. + Новый участник хочет присоединиться к группе. + rcv group event chat item + New message Новое сообщение @@ -5193,6 +5272,11 @@ This is your link for group %@! Нет чатов в списке %@ No comment provided by engineer. + + No chats with members + Нет чатов с членами группы + No comment provided by engineer. + No contacts selected Контакты не выбраны @@ -5367,6 +5451,9 @@ This is your link for group %@! Now admins can: - delete members' messages. - disable members ("observer" role) + Теперь админы могут: +- удалять сообщения членов. +- приостанавливать членов (роль наблюдатель) No comment provided by engineer. @@ -5382,7 +5469,9 @@ This is your link for group %@! Ok Ок - alert button + alert action +alert button +new chat action Old database @@ -5536,10 +5625,11 @@ Requires compatible VPN. Open group Открыть группу - No comment provided by engineer. + new chat action Open link? + Открыть ссылку? alert title @@ -5656,6 +5746,7 @@ Requires compatible VPN. Past member %@ + Бывший член %@ past/unknown group member @@ -5792,6 +5883,11 @@ Error: %@ Попробуйте выключить и снова включить уведомления. token info + + Please wait for group moderators to review your request to join the group. + Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. + snd group event chat item + Please wait for token activation to complete. Пожалуйста, дождитесь завершения активации токена. @@ -5964,6 +6060,7 @@ Error: %@ Prohibit sending direct messages to members. + Запретить посылать прямые сообщения членам группы. No comment provided by engineer. @@ -6244,6 +6341,11 @@ swipe action Отклонить запрос No comment provided by engineer. + + Reject member? + Отклонить участника? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес. @@ -6271,10 +6373,12 @@ swipe action Remove member + Удалить члена группы No comment provided by engineer. Remove member? + Удалить члена группы? No comment provided by engineer. @@ -6297,11 +6401,6 @@ swipe action Пересогласовать шифрование? No comment provided by engineer. - - Repeat connection request? - Повторить запрос на соединение? - No comment provided by engineer. - Repeat download Повторить загрузку @@ -6312,11 +6411,6 @@ swipe action Повторить импорт No comment provided by engineer. - - Repeat join request? - Повторить запрос на вступление? - No comment provided by engineer. - Repeat upload Повторить загрузку @@ -6352,6 +6446,11 @@ swipe action Причина сообщения? No comment provided by engineer. + + Report sent to moderators + Жалоба отправлена модераторам + alert title + Report spam: only group moderators will see it. Пожаловаться на спам: увидят только модераторы группы. @@ -6467,6 +6566,16 @@ swipe action Посмотреть условия No comment provided by engineer. + + Review members + Одобрять членов + admission stage + + + Review members before admitting ("knocking"). + Одобрять членов для вступления в группу. + admission stage description + Revoke Отозвать @@ -6523,6 +6632,15 @@ chat item action Сохранить (и уведомить контакты) alert button + + Save (and notify members) + alert button + + + Save admission settings? + Сохранить настройки вступления? + alert title + Save and notify contact Сохранить и уведомить контакт @@ -6530,6 +6648,7 @@ chat item action Save and notify group members + Сохранить и уведомить членов группы No comment provided by engineer. @@ -6547,6 +6666,10 @@ chat item action Сохранить профиль группы No comment provided by engineer. + + Save group profile? + alert title + Save list Сохранить список @@ -6814,6 +6937,7 @@ chat item action Send up to 100 last messages to new members. + Отправить до 100 последних сообщений новым членам. No comment provided by engineer. @@ -7036,6 +7160,11 @@ chat item action Установите код вместо системной аутентификации. No comment provided by engineer. + + Set member admission + Приём членов в группу + No comment provided by engineer. + Set message expiration in chats. Установите срок хранения сообщений в чатах. @@ -7058,6 +7187,7 @@ chat item action Set the message shown to new members! + Установить сообщение для новых членов группы! No comment provided by engineer. @@ -7148,6 +7278,7 @@ chat item action Short link + Короткая ссылка No comment provided by engineer. @@ -7252,6 +7383,7 @@ chat item action SimpleX channel link + SimpleX ссылка канала simplex link type @@ -7696,18 +7828,22 @@ It can happen because of some bug or when the connection is compromised. The message will be deleted for all members. + Сообщение будет удалено для всех членов группы. No comment provided by engineer. The message will be marked as moderated for all members. + Сообщение будет помечено как удаленное для всех членов группы. No comment provided by engineer. The messages will be deleted for all members. + Сообщения будут удалены для всех членов группы. No comment provided by engineer. The messages will be marked as moderated for all members. + Сообщения будут помечены как удаленные для всех членов группы. No comment provided by engineer. @@ -7817,6 +7953,7 @@ It can happen because of some bug or when the connection is compromised. This group has over %lld members, delivery receipts are not sent. + В этой группе более %lld членов, отчёты о доставке не отправляются. No comment provided by engineer. @@ -7824,18 +7961,9 @@ It can happen because of some bug or when the connection is compromised.Эта группа больше не существует. No comment provided by engineer. - - This is your own SimpleX address! - Это ваш собственный адрес SimpleX! - No comment provided by engineer. - - - This is your own one-time link! - Это ваша собственная одноразовая ссылка! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. No comment provided by engineer. @@ -8027,14 +8155,17 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member + Разблокировать члена группы No comment provided by engineer. Unblock member for all? + Разблокировать члена для всех? No comment provided by engineer. Unblock member? + Разблокировать члена группы? No comment provided by engineer. @@ -8136,10 +8267,12 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link + Ссылка не поддерживается No comment provided by engineer. Up to 100 last messages are sent to new members. + До 100 последних сообщений отправляются новым членам. No comment provided by engineer. @@ -8234,6 +8367,7 @@ To connect, please ask your contact to create another connection link and check Use TCP port 443 for preset servers only. + Использовать TCP-порт 443 только для серверов по умолчанию. No comment provided by engineer. @@ -8244,7 +8378,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Использовать активный профиль - No comment provided by engineer. + new chat action Use for files @@ -8274,7 +8408,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile Использовать новый Инкогнито профиль - No comment provided by engineer. + new chat action Use only local notifications? @@ -8303,6 +8437,7 @@ To connect, please ask your contact to create another connection link and check Use short links (BETA) + Короткие ссылки (БЕТА) No comment provided by engineer. @@ -8633,12 +8768,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. Вы уже соединяетесь с %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Вы уже соединяетесь по этой одноразовой ссылке! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8648,24 +8783,19 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. Вы уже вступаете в группу %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Вы уже вступаете в группу по этой ссылке! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Вы уже вступаете в группу по этой ссылке. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Вы уже вступаете в группу! Повторить запрос на вступление? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8749,6 +8879,7 @@ Repeat join request? You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. + Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились. No comment provided by engineer. @@ -8781,6 +8912,11 @@ Repeat join request? Вы можете увидеть ссылку-приглашение снова открыв соединение. alert message + + You can view your reports in Chat with admins. + Вы можете найти Ваши жалобы в Чате с админами. + alert message + You can't send messages! Вы не можете отправлять сообщения! @@ -8796,17 +8932,12 @@ Repeat join request? Вы определяете, кто может соединиться. No comment provided by engineer. - - You have already requested connection via this address! - Вы уже запросили соединение через этот адрес! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Вы уже запросили соединение! Повторить запрос? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8825,6 +8956,7 @@ Repeat connection request? You joined this group. Connecting to inviting group member. + Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. No comment provided by engineer. @@ -8892,10 +9024,6 @@ Repeat connection request? Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме. No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Вы все равно получите звонки и уведомления в профилях без звука, когда они активные. @@ -9081,6 +9209,11 @@ Repeat connection request? наверху, затем выберите: No comment provided by engineer. + + accepted %@ + принят %@ + rcv group event chat item + accepted call принятый звонок @@ -9091,6 +9224,11 @@ Repeat connection request? принятое приглашение chat list item title + + accepted you + Вы приняты + rcv group event chat item + admin админ @@ -9111,8 +9249,14 @@ Repeat connection request? шифрование согласовывается… chat item text + + all + все + member criteria value + all members + все члены feature role @@ -9196,6 +9340,11 @@ marked deleted chat item preview text входящий звонок… call status + + can't send messages + нельзя отправлять + No comment provided by engineer. + cancelled %@ отменил(a) %@ @@ -9301,6 +9450,16 @@ marked deleted chat item preview text контакт %1$@ изменён на %2$@ profile update event chat item + + contact deleted + контакт удален + No comment provided by engineer. + + + contact disabled + контакт выключен + No comment provided by engineer. + contact has e2e encryption у контакта есть e2e шифрование @@ -9311,6 +9470,11 @@ marked deleted chat item preview text у контакта нет e2e шифрования No comment provided by engineer. + + contact not ready + контакт не готов + No comment provided by engineer. + creator создатель @@ -9482,6 +9646,11 @@ pref value группа удалена No comment provided by engineer. + + group is deleted + группа удалена + No comment provided by engineer. + group profile updated профиль группы обновлен @@ -9594,10 +9763,12 @@ pref value member + член группы member role member %1$@ changed to %2$@ + член %1$@ изменился на %2$@ profile update event chat item @@ -9605,6 +9776,11 @@ pref value соединен(а) rcv group event chat item + + member has old version + член имеет старую версию + No comment provided by engineer. + message написать @@ -9670,6 +9846,11 @@ pref value нет текста copied message info in history + + not synchronized + не синхронизирован + No comment provided by engineer. + observer читатель @@ -9680,6 +9861,7 @@ pref value нет enabled status group pref value +member criteria value time to disappear @@ -9732,6 +9914,11 @@ time to disappear ожидает утверждения No comment provided by engineer. + + pending review + ожидает одобрения + No comment provided by engineer. + quantum resistant e2e encryption квантово-устойчивое e2e шифрование @@ -9772,6 +9959,11 @@ time to disappear удалён адрес контакта profile update event chat item + + removed from group + удален из группы + No comment provided by engineer. + removed profile picture удалена картинка профиля @@ -9782,11 +9974,26 @@ time to disappear удалил(а) Вас из группы rcv group event chat item + + request to join rejected + запрос на вступление отклонён + No comment provided by engineer. + requested to connect запрошено соединение chat list item title + + review + рассмотрение + No comment provided by engineer. + + + reviewed by admins + одобрен админами + No comment provided by engineer. + saved сохранено @@ -9976,6 +10183,11 @@ last received msg: %2$@ Вы No comment provided by engineer. + + you accepted this member + Вы приняли этого члена + snd group event chat item + you are invited to group Вы приглашены в группу @@ -10119,6 +10331,7 @@ last received msg: %2$@ From %d chat(s) + Из %d чатов notification body diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 671dd87d7d..e2fcf6ec73 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -520,6 +520,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions No comment provided by engineer. @@ -539,6 +547,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions No comment provided by engineer. @@ -587,6 +599,10 @@ swipe action เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members No comment provided by engineer. @@ -857,11 +873,11 @@ swipe action Already connecting! - No comment provided by engineer. + new chat sheet title Already joining the group! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1254,7 +1270,8 @@ swipe action Cancel ยกเลิก alert action -alert button +alert button +new chat action Cancel migration @@ -1351,7 +1368,7 @@ set passcode view Chat already exists! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1429,11 +1446,23 @@ set passcode view Chat will be deleted for you - this cannot be undone! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats แชท No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. No comment provided by engineer. @@ -1631,10 +1660,6 @@ set passcode view Connect automatically No comment provided by engineer. - - Connect incognito - No comment provided by engineer. - Connect to desktop No comment provided by engineer. @@ -1643,36 +1668,32 @@ set passcode view Connect to your friends faster. No comment provided by engineer. - - Connect to yourself? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! - No comment provided by engineer. + new chat sheet title Connect via contact address - No comment provided by engineer. + new chat sheet title Connect via link เชื่อมต่อผ่านลิงก์ - No comment provided by engineer. + new chat sheet title Connect via one-time link - No comment provided by engineer. + new chat sheet title Connect with %@ - No comment provided by engineer. + new chat action Connected @@ -1919,11 +1940,6 @@ This is your own one-time link! สร้างคิว server test step - - Create secret group - สร้างกลุ่มลับ - No comment provided by engineer. - Create your profile สร้างโปรไฟล์ของคุณ @@ -2162,6 +2178,10 @@ swipe action ลบโปรไฟล์แชทไหม? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? No comment provided by engineer. @@ -2551,7 +2571,7 @@ swipe action Don't show again ไม่ต้องแสดงอีก - No comment provided by engineer. + alert action Done @@ -2843,6 +2863,10 @@ chat item action เกิดข้อผิดพลาดในการรับคำขอติดต่อ No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) เกิดข้อผิดพลาดในการเพิ่มสมาชิก @@ -2852,6 +2876,10 @@ chat item action Error adding server alert title + + Error adding short link + No comment provided by engineer. + Error changing address เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่ @@ -2869,7 +2897,7 @@ chat item action Error changing setting เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า - No comment provided by engineer. + alert title Error changing to incognito! @@ -2926,7 +2954,11 @@ chat item action Error deleting chat database เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -2941,12 +2973,12 @@ chat item action Error deleting database เกิดข้อผิดพลาดในการลบฐานข้อมูล - No comment provided by engineer. + alert title Error deleting old database เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า - No comment provided by engineer. + alert title Error deleting token @@ -2980,7 +3012,7 @@ chat item action Error exporting chat database เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -2989,7 +3021,7 @@ chat item action Error importing chat database เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท - No comment provided by engineer. + alert title Error joining group @@ -3028,7 +3060,7 @@ chat item action Error removing member เกิดข้อผิดพลาดในการลบสมาชิก - No comment provided by engineer. + alert title Error reordering lists @@ -3544,7 +3576,7 @@ Error: %2$@ Group already exists! - No comment provided by engineer. + new chat sheet title Group display name @@ -3611,6 +3643,10 @@ Error: %2$@ โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์ No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message ข้อความต้อนรับกลุ่ม @@ -4073,29 +4109,21 @@ More improvements are coming soon! Join group เข้าร่วมกลุ่ม - No comment provided by engineer. + new chat sheet title Join group conversations No comment provided by engineer. - - Join group? - No comment provided by engineer. - Join incognito เข้าร่วมแบบไม่ระบุตัวตน No comment provided by engineer. - - Join with current profile - No comment provided by engineer. - Join your group? This is your link for group %@! - No comment provided by engineer. + new chat action Joining group @@ -4293,6 +4321,10 @@ This is your link for group %@! สมาชิก No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive item status text @@ -4324,6 +4356,10 @@ This is your link for group %@! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้ @@ -4697,6 +4733,10 @@ This is your link for group %@! บทบาทของสมาชิกใหม่ No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message ข้อความใหม่ @@ -4733,6 +4773,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected ไม่ได้เลือกผู้ติดต่อ @@ -4902,7 +4946,9 @@ This is your link for group %@! Ok ตกลง - alert button + alert action +alert button +new chat action Old database @@ -5046,7 +5092,7 @@ Requires compatible VPN. Open group - No comment provided by engineer. + new chat action Open link? @@ -5274,6 +5320,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -5683,6 +5733,10 @@ swipe action ปฏิเสธคำขอติดต่อ No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้ @@ -5736,10 +5790,6 @@ swipe action เจรจา enryption ใหม่หรือไม่? No comment provided by engineer. - - Repeat connection request? - No comment provided by engineer. - Repeat download No comment provided by engineer. @@ -5748,10 +5798,6 @@ swipe action Repeat import No comment provided by engineer. - - Repeat join request? - No comment provided by engineer. - Repeat upload No comment provided by engineer. @@ -5781,6 +5827,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -5884,6 +5934,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke ถอน @@ -5936,6 +5994,14 @@ chat item action บันทึก (และแจ้งผู้ติดต่อ) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact บันทึกและแจ้งผู้ติดต่อ @@ -5960,6 +6026,10 @@ chat item action บันทึกโปรไฟล์กลุ่ม No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6404,6 +6474,10 @@ chat item action ตั้งแทนการรับรองความถูกต้องของระบบ No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7124,14 +7198,6 @@ It can happen because of some bug or when the connection is compromised.ไม่มีกลุ่มนี้แล้ว No comment provided by engineer. - - This is your own SimpleX address! - No comment provided by engineer. - - - This is your own one-time link! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -7507,7 +7573,7 @@ To connect, please ask your contact to create another connection link and check Use current profile - No comment provided by engineer. + new chat action Use for files @@ -7533,7 +7599,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile - No comment provided by engineer. + new chat action Use only local notifications? @@ -7853,11 +7919,11 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -7865,20 +7931,16 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. - No comment provided by engineer. - - - You are already joining the group via this link! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -7986,6 +8048,10 @@ Repeat join request? You can view invitation link again in connection details. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! คุณไม่สามารถส่งข้อความได้! @@ -8001,14 +8067,10 @@ Repeat join request? ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น No comment provided by engineer. - - You have already requested connection via this address! - No comment provided by engineer. - You have already requested connection! Repeat connection request? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8089,10 +8151,6 @@ Repeat connection request? คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง No comment provided by engineer. - - You will connect to all group members. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน @@ -8270,6 +8328,10 @@ Repeat connection request? ด้านบน จากนั้นเลือก: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call รับสายแล้ว @@ -8279,6 +8341,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin ผู้ดูแลระบบ @@ -8298,6 +8364,10 @@ Repeat connection request? เห็นด้วยกับการ encryption… chat item text + + all + member criteria value + all members feature role @@ -8375,6 +8445,10 @@ marked deleted chat item preview text กำลังโทร… call status + + can't send messages + No comment provided by engineer. + cancelled %@ ยกเลิก %@ @@ -8478,6 +8552,14 @@ marked deleted chat item preview text contact %1$@ changed to %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption ผู้ติดต่อมีการ encrypt จากต้นจนจบ @@ -8488,6 +8570,10 @@ marked deleted chat item preview text ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator ผู้สร้าง @@ -8653,6 +8739,10 @@ pref value ลบกลุ่มแล้ว No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated อัปเดตโปรไฟล์กลุ่มแล้ว @@ -8775,6 +8865,10 @@ pref value เชื่อมต่อสำเร็จ rcv group event chat item + + member has old version + No comment provided by engineer. + message No comment provided by engineer. @@ -8838,6 +8932,10 @@ pref value ไม่มีข้อความ copied message info in history + + not synchronized + No comment provided by engineer. + observer ผู้สังเกตการณ์ @@ -8848,6 +8946,7 @@ pref value ปิด enabled status group pref value +member criteria value time to disappear @@ -8895,6 +8994,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption chat item text @@ -8932,6 +9035,10 @@ time to disappear removed contact address profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profile update event chat item @@ -8941,10 +9048,22 @@ time to disappear ลบคุณออกแล้ว rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved No comment provided by engineer. @@ -9115,6 +9234,10 @@ last received msg: %2$@ you No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group คุณได้รับเชิญให้เข้าร่วมกลุ่ม 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 bbee40c2b9..5abc28ab5f 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -192,6 +192,7 @@ %d seconds(s) + %d saniye(ler) delete after time @@ -465,6 +466,7 @@ time interval 1 year + 1 yıl delete after time @@ -563,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Üye olarak kabul et + alert action + + + Accept as observer + Gözlemci olarak kabul et + alert action + Accept conditions Koşulları kabul et @@ -584,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Üyeyi kabul et + alert title + Accepted conditions Kabul edilmiş koşullar @@ -601,6 +618,7 @@ swipe action Active + Aktif token status text @@ -620,6 +638,7 @@ swipe action Add list + Liste ekle No comment provided by engineer. @@ -637,6 +656,10 @@ swipe action Karekod taratarak sunucuları ekleyin. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Takım üyesi ekle @@ -649,6 +672,7 @@ swipe action Add to list + Listeye ekle No comment provided by engineer. @@ -728,6 +752,7 @@ swipe action All + Hepsi No comment provided by engineer. @@ -929,12 +954,12 @@ swipe action Already connecting! Zaten bağlanılıyor! - No comment provided by engineer. + new chat sheet title Already joining the group! Zaten gruba bağlanılıyor! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1365,7 +1390,8 @@ swipe action Cancel İptal et alert action -alert button +alert button +new chat action Cancel migration @@ -1470,7 +1496,7 @@ set passcode view Chat already exists! Sohbet zaten mevcut! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1557,11 +1583,23 @@ set passcode view Sohbet senden silinecek - bu geri alınamaz! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats Sohbetler No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. Her 20 dakikada mesajları kontrol et. @@ -1782,11 +1820,6 @@ set passcode view Otomatik olarak bağlan No comment provided by engineer. - - Connect incognito - Gizli bağlan - No comment provided by engineer. - Connect to desktop Bilgisayara bağlan @@ -1797,44 +1830,39 @@ set passcode view Arkadaşlarınıza daha hızlı bağlanın. No comment provided by engineer. - - Connect to yourself? - Kendine mi bağlanacaksın? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! Kendine mi bağlanacaksın? Bu senin kendi SimpleX adresin! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Kendine mi bağlanacaksın? Bu senin kendi tek kullanımlık bağlantın! - No comment provided by engineer. + new chat sheet title Connect via contact address Kişi adresi aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect via link Bağlantı aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect via one-time link Tek kullanımlık bağlantı aracılığıyla bağlan - No comment provided by engineer. + new chat sheet title Connect with %@ %@ ile bağlan - No comment provided by engineer. + new chat action Connected @@ -2106,11 +2134,6 @@ Bu senin kendi tek kullanımlık bağlantın! Sıra oluştur server test step - - Create secret group - Gizli grup oluştur - No comment provided by engineer. - Create your profile Profilini oluştur @@ -2364,6 +2387,10 @@ swipe action Sohbet profili silinsin mi? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? Sohbet silinsin mi? @@ -2780,7 +2807,7 @@ swipe action Don't show again Yeniden gösterme - No comment provided by engineer. + alert action Done @@ -3095,6 +3122,10 @@ chat item action Bağlantı isteği kabul edilirken hata oluştu No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) Üye(ler) eklenirken hata oluştu @@ -3105,6 +3136,10 @@ chat item action Sunucu eklenirken hata oluştu alert title + + Error adding short link + No comment provided by engineer. + Error changing address Adres değiştirilirken hata oluştu @@ -3123,7 +3158,7 @@ chat item action Error changing setting Ayar değiştirilirken hata oluştu - No comment provided by engineer. + alert title Error changing to incognito! @@ -3185,7 +3220,11 @@ chat item action Error deleting chat database Sohbet veritabanı silinirken sorun oluştu - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3200,12 +3239,12 @@ chat item action Error deleting database Veritabanı silinirken hata oluştu - No comment provided by engineer. + alert title Error deleting old database Eski veritabanı silinirken hata oluştu - No comment provided by engineer. + alert title Error deleting token @@ -3240,7 +3279,7 @@ chat item action Error exporting chat database Sohbet veritabanı dışa aktarılırken hata oluştu - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3250,7 +3289,7 @@ chat item action Error importing chat database Sohbet veritabanı içe aktarılırken hata oluştu - No comment provided by engineer. + alert title Error joining group @@ -3294,7 +3333,7 @@ chat item action Error removing member Kişiyi silerken sorun oluştu - No comment provided by engineer. + alert title Error reordering lists @@ -3862,7 +3901,7 @@ Hata: %2$@ Group already exists! Grup çoktan mevcut! - No comment provided by engineer. + new chat sheet title Group display name @@ -3929,6 +3968,10 @@ Hata: %2$@ Grup profili üyelerin cihazlarında saklanır, sunucularda değil. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Grup hoşgeldin mesajı @@ -4417,34 +4460,24 @@ Daha fazla iyileştirme yakında geliyor! Join group Gruba katıl - No comment provided by engineer. + new chat sheet title Join group conversations Grup sohbetlerine katıl No comment provided by engineer. - - Join group? - Gruba katılınsın mı? - No comment provided by engineer. - Join incognito Gizli katıl No comment provided by engineer. - - Join with current profile - Şu anki profille katıl - No comment provided by engineer. - Join your group? This is your link for group %@! Bu gruba katılınsın mı? Bu senin grup için bağlantın %@! - No comment provided by engineer. + new chat action Joining group @@ -4653,6 +4686,10 @@ Bu senin grup için bağlantın %@! Kişi No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive Üye inaktif @@ -4686,6 +4723,10 @@ Bu senin grup için bağlantın %@! Üye gruptan çıkarılacaktır - bu geri alınamaz! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Grup üyeleri mesaj tepkileri ekleyebilir. @@ -5096,6 +5137,10 @@ Bu senin grup için bağlantın %@! Yeni üye rolü No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Yeni mesaj @@ -5132,6 +5177,10 @@ Bu senin grup için bağlantın %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Hiçbir kişi seçilmedi @@ -5311,7 +5360,9 @@ Bu senin grup için bağlantın %@! Ok Tamam - alert button + alert action +alert button +new chat action Old database @@ -5460,7 +5511,7 @@ VPN'nin etkinleştirilmesi gerekir. Open group Grubu aç - No comment provided by engineer. + new chat action Open link? @@ -5711,6 +5762,10 @@ Hata: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -6153,6 +6208,10 @@ swipe action Bağlanma isteğini reddet No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. @@ -6208,11 +6267,6 @@ swipe action Şifreleme yeniden müzakere edilsin mi? No comment provided by engineer. - - Repeat connection request? - Bağlantı isteği tekrarlansın mı? - No comment provided by engineer. - Repeat download Tekrar indir @@ -6223,11 +6277,6 @@ swipe action İthalatı tekrarla No comment provided by engineer. - - Repeat join request? - Katılma isteği tekrarlansın mı? - No comment provided by engineer. - Repeat upload Yüklemeyi tekrarla @@ -6258,6 +6307,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6367,6 +6420,14 @@ swipe action Review conditions No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke İptal et @@ -6423,6 +6484,14 @@ chat item action Kaydet (ve kişilere bildir) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Kaydet ve kişilere bildir @@ -6448,6 +6517,10 @@ chat item action Grup profilini kaydet No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6931,6 +7004,10 @@ chat item action Sistem kimlik doğrulaması yerine ayarla. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7704,16 +7781,6 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu grup artık mevcut değildir. No comment provided by engineer. - - This is your own SimpleX address! - Bu senin kendi SimpleX adresin! - No comment provided by engineer. - - - This is your own one-time link! - Bu senin kendi tek kullanımlık bağlantın! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8118,7 +8185,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use current profile Şu anki profili kullan - No comment provided by engineer. + new chat action Use for files @@ -8146,7 +8213,7 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Use new incognito profile Yeni gizli profilden kullan - No comment provided by engineer. + new chat action Use only local notifications? @@ -8499,12 +8566,12 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste You are already connecting to %@. Zaten %@'a bağlanıyorsunuz. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8514,24 +8581,19 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste You are already joining the group %@. Zaten %@ grubuna katılıyorsunuz. - No comment provided by engineer. - - - You are already joining the group via this link! - Bu bağlantı üzerinden gruba zaten katılıyorsunuz! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Gruba zaten bu bağlantı üzerinden katılıyorsunuz. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Gruba zaten katılıyorsunuz! Katılma isteği tekrarlansın mı? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8646,6 +8708,10 @@ Katılma isteği tekrarlansın mı? Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Mesajlar gönderemezsiniz! @@ -8661,17 +8727,12 @@ Katılma isteği tekrarlansın mı? Kimin bağlanabileceğine siz karar verirsiniz. No comment provided by engineer. - - You have already requested connection via this address! - Bu adres üzerinden zaten bağlantı talebinde bulundunuz! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Zaten bağlantı isteğinde bulundunuz! Bağlantı isteği tekrarlansın mı? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8757,11 +8818,6 @@ Bağlantı isteği tekrarlansın mı? Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir. No comment provided by engineer. - - You will connect to all group members. - Bütün grup üyelerine bağlanacaksın. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz. @@ -8945,6 +9001,10 @@ Bağlantı isteği tekrarlansın mı? yukarı çıkın, ardından seçin: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call kabul edilen arama @@ -8954,6 +9014,10 @@ Bağlantı isteği tekrarlansın mı? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin yönetici @@ -8974,6 +9038,10 @@ Bağlantı isteği tekrarlansın mı? şifreleme kabul ediliyor… chat item text + + all + member criteria value + all members bütün üyeler @@ -9059,6 +9127,10 @@ marked deleted chat item preview text aranıyor… call status + + can't send messages + No comment provided by engineer. + cancelled %@ %@ iptal edildi @@ -9164,6 +9236,14 @@ marked deleted chat item preview text %1$@ kişisi %2$@ olarak değişti profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption kişi uçtan uca şifrelemeye sahiptir @@ -9174,6 +9254,10 @@ marked deleted chat item preview text kişi uçtan uca şifrelemeye sahip değildir No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator oluşturan @@ -9345,6 +9429,10 @@ pref value grup silindi No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated grup profili güncellendi @@ -9470,6 +9558,10 @@ pref value bağlanıldı rcv group event chat item + + member has old version + No comment provided by engineer. + message mesaj @@ -9534,6 +9626,10 @@ pref value metin yok copied message info in history + + not synchronized + No comment provided by engineer. + observer gözlemci @@ -9544,6 +9640,7 @@ pref value kapalı enabled status group pref value +member criteria value time to disappear @@ -9594,6 +9691,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption kuantuma dayanıklı e2e şifreleme @@ -9633,6 +9734,10 @@ time to disappear kişi adresi silindi profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture profil fotoğrafı silindi @@ -9643,10 +9748,22 @@ time to disappear sen kaldırıldın rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved kaydedildi @@ -9836,6 +9953,10 @@ son alınan msj: %2$@ sen No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group gruba davet edildiniz 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 c0375e3b02..c90b8ce02c 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -192,6 +192,7 @@ %d seconds(s) + %d секунд(и) delete after time @@ -465,6 +466,7 @@ time interval 1 year + 1 рік delete after time @@ -563,6 +565,16 @@ time interval accept incoming call via notification swipe action + + Accept as member + Прийняти як учасника + alert action + + + Accept as observer + Прийняти як спостерігача + alert action + Accept conditions Прийняти умови @@ -584,6 +596,11 @@ swipe action accept contact request via notification swipe action + + Accept member + Прийняти учасника + alert title + Accepted conditions Прийняті умови @@ -601,6 +618,7 @@ swipe action Active + Активний token status text @@ -620,6 +638,7 @@ swipe action Add list + Додати список No comment provided by engineer. @@ -637,6 +656,10 @@ swipe action Додайте сервери, відсканувавши QR-код. No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members Додайте учасників команди @@ -649,6 +672,7 @@ swipe action Add to list + Додати до списку No comment provided by engineer. @@ -728,6 +752,7 @@ swipe action All + Всі No comment provided by engineer. @@ -742,6 +767,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. + Всі чати будуть видалені з списку %@, і список буде видалений. alert message @@ -786,10 +812,12 @@ swipe action All reports will be archived for you. + Всі скарги будуть заархівовані для вас. No comment provided by engineer. All servers + Всі сервери No comment provided by engineer. @@ -869,6 +897,7 @@ swipe action Allow to report messsages to moderators. + Дозволити надсилати скаргу на повідомлення модераторам. No comment provided by engineer. @@ -929,12 +958,12 @@ swipe action Already connecting! Вже підключаємось! - No comment provided by engineer. + new chat sheet title Already joining the group! Вже приєднуємося до групи! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -953,6 +982,7 @@ swipe action Another reason + Інша причина report reason @@ -982,6 +1012,7 @@ swipe action App group: + Група застосунків: No comment provided by engineer. @@ -1031,14 +1062,17 @@ swipe action Archive + Архівувати No comment provided by engineer. Archive %lld reports? + Архівувати %lld скарг? No comment provided by engineer. Archive all reports? + Архівувати всі скарги? No comment provided by engineer. @@ -1053,14 +1087,17 @@ swipe action Archive report + Архівувати скаргу No comment provided by engineer. Archive report? + Архівувати скаргу? No comment provided by engineer. Archive reports + Архівувати скарги swipe action @@ -1175,6 +1212,7 @@ swipe action Better groups performance + Краща продуктивність груп No comment provided by engineer. @@ -1199,6 +1237,7 @@ swipe action Better privacy and security + Краща конфіденційність і безпека No comment provided by engineer. @@ -1303,6 +1342,7 @@ swipe action Businesses + Бізнеси No comment provided by engineer. @@ -1314,6 +1354,9 @@ swipe action By using SimpleX Chat you agree to: - send only legal content in public groups. - respect other users – no spam. + Використовуючи SimpleX Chat, ви погоджуєтеся: +- надсилати лише легальний контент у публічних групах. +- поважати інших користувачів - без спаму. No comment provided by engineer. @@ -1365,7 +1408,8 @@ swipe action Cancel Скасувати alert action -alert button +alert button +new chat action Cancel migration @@ -1404,6 +1448,7 @@ alert button Change automatic message deletion? + Змінити автоматичне видалення повідомлень? alert title @@ -1428,7 +1473,7 @@ alert button Change passcode - Змінити пароль + Змінити код доступу authentication reason @@ -1470,7 +1515,7 @@ set passcode view Chat already exists! Чат вже існує! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1557,11 +1602,26 @@ set passcode view Чат буде видалено для вас - цю дію неможливо скасувати! No comment provided by engineer. + + Chat with admins + Чат з адміністраторами + chat toolbar + + + Chat with member + Чат з учасником + No comment provided by engineer. + Chats Чати No comment provided by engineer. + + Chats with members + Чати з учасниками + No comment provided by engineer. + Check messages every 20 min. Перевіряйте повідомлення кожні 20 хв. @@ -1629,10 +1689,12 @@ set passcode view Clear group? + Очистити групу? No comment provided by engineer. Clear or delete group? + Очистити чи видалити групу? No comment provided by engineer. @@ -1657,6 +1719,7 @@ set passcode view Community guidelines violation + Порушення правил спільноти report reason @@ -1716,6 +1779,7 @@ set passcode view Configure server operators + Налаштувати операторів сервера No comment provided by engineer. @@ -1770,6 +1834,7 @@ set passcode view Confirmed + Підтверджений token status text @@ -1782,11 +1847,6 @@ set passcode view Підключення автоматично No comment provided by engineer. - - Connect incognito - Підключайтеся інкогніто - No comment provided by engineer. - Connect to desktop Підключення до комп'ютера @@ -1797,44 +1857,39 @@ set passcode view Швидше спілкуйтеся з друзями. No comment provided by engineer. - - Connect to yourself? - З'єднатися з самим собою? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! З'єднатися з самим собою? Це ваша власна SimpleX-адреса! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! Підключитися до себе? Це ваше власне одноразове посилання! - No comment provided by engineer. + new chat sheet title Connect via contact address Підключіться за контактною адресою - No comment provided by engineer. + new chat sheet title Connect via link Підключіться за посиланням - No comment provided by engineer. + new chat sheet title Connect via one-time link Під'єднатися за одноразовим посиланням - No comment provided by engineer. + new chat sheet title Connect with %@ Підключитися до %@ - No comment provided by engineer. + new chat action Connected @@ -1893,6 +1948,7 @@ This is your own one-time link! Connection blocked + Підключення заблоковано No comment provided by engineer. @@ -1908,10 +1964,13 @@ This is your own one-time link! Connection is blocked by server operator: %@ + Підключення заблоковано оператором сервера: +%@ No comment provided by engineer. Connection not ready. + Підключення не готове. No comment provided by engineer. @@ -1926,6 +1985,7 @@ This is your own one-time link! Connection requires encryption renegotiation. + Підключення вимагає повторного узгодження шифрування. No comment provided by engineer. @@ -2010,6 +2070,7 @@ This is your own one-time link! Content violates conditions of use + Вміст порушує умови використання blocking reason @@ -2089,6 +2150,7 @@ This is your own one-time link! Create list + Створити список No comment provided by engineer. @@ -2106,11 +2168,6 @@ This is your own one-time link! Створити чергу server test step - - Create secret group - Створити секретну групу - No comment provided by engineer. - Create your profile Створіть свій профіль @@ -2352,6 +2409,7 @@ swipe action Delete chat messages from your device. + Видалити повідомлення чату з вашого пристрою. No comment provided by engineer. @@ -2364,6 +2422,11 @@ swipe action Видалити профіль чату? No comment provided by engineer. + + Delete chat with member? + Видалити чат з учасником? + alert title + Delete chat? Видалити чат? @@ -2446,6 +2509,7 @@ swipe action Delete list? + Видалити список? alert title @@ -2500,6 +2564,7 @@ swipe action Delete report + Видалити скаргу No comment provided by engineer. @@ -2664,10 +2729,12 @@ swipe action Disable automatic message deletion? + Вимкнути автоматичне видалення повідомлень? alert title Disable delete messages + Вимкнути видалення повідомлень alert button @@ -2762,6 +2829,7 @@ swipe action Documents: + Документи: No comment provided by engineer. @@ -2776,15 +2844,17 @@ swipe action Don't miss important messages. + Не пропускайте важливі повідомлення. No comment provided by engineer. Don't show again Більше не показувати - No comment provided by engineer. + alert action Done + Готово No comment provided by engineer. @@ -2875,6 +2945,7 @@ chat item action Enable Flux in Network & servers settings for better metadata privacy. + Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих. No comment provided by engineer. @@ -3019,6 +3090,7 @@ chat item action Encryption renegotiation in progress. + Виконується повторне узгодження шифрування. No comment provided by engineer. @@ -3096,6 +3168,11 @@ chat item action Помилка при прийнятті запиту на контакт No comment provided by engineer. + + Error accepting member + Помилка при прийомі учасника + alert title + Error adding member(s) Помилка додавання користувача(ів) @@ -3106,6 +3183,10 @@ chat item action Помилка додавання сервера alert title + + Error adding short link + No comment provided by engineer. + Error changing address Помилка зміни адреси @@ -3124,7 +3205,7 @@ chat item action Error changing setting Помилка зміни налаштування - No comment provided by engineer. + alert title Error changing to incognito! @@ -3133,6 +3214,7 @@ chat item action Error checking token status + Помилка перевірки статусу токена No comment provided by engineer. @@ -3157,6 +3239,7 @@ chat item action Error creating list + Помилка при створенні списку alert title @@ -3176,6 +3259,7 @@ chat item action Error creating report + Помилка при створенні скарги No comment provided by engineer. @@ -3186,7 +3270,12 @@ chat item action Error deleting chat database Помилка видалення бази даних чату - No comment provided by engineer. + alert title + + + Error deleting chat with member + Помилка при видаленні чату з учасником + alert title Error deleting chat! @@ -3201,12 +3290,12 @@ chat item action Error deleting database Помилка видалення бази даних - No comment provided by engineer. + alert title Error deleting old database Помилка видалення старої бази даних - No comment provided by engineer. + alert title Error deleting token @@ -3241,7 +3330,7 @@ chat item action Error exporting chat database Помилка експорту бази даних чату - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3251,7 +3340,7 @@ chat item action Error importing chat database Помилка імпорту бази даних чату - No comment provided by engineer. + alert title Error joining group @@ -3290,15 +3379,17 @@ chat item action Error registering for notifications + Помилка під час реєстрації для отримання сповіщень alert title Error removing member Помилка видалення учасника - No comment provided by engineer. + alert title Error reordering lists + Помилка при переупорядкуванні списків alert title @@ -3313,6 +3404,7 @@ chat item action Error saving chat list + Помилка під час збереження списку чатів alert title @@ -3397,6 +3489,7 @@ chat item action Error testing server connection + Помилка під час перевірки з'єднання з сервером No comment provided by engineer. @@ -3483,6 +3576,7 @@ snd error text Expired + Термін дії закінчився token status text @@ -3527,6 +3621,7 @@ snd error text Faster deletion of groups. + Швидше видалення груп. No comment provided by engineer. @@ -3536,6 +3631,7 @@ snd error text Faster sending messages. + Швидше надсилання повідомлень. No comment provided by engineer. @@ -3545,6 +3641,7 @@ snd error text Favorites + Вибране No comment provided by engineer. @@ -3691,6 +3788,7 @@ snd error text For all moderators + Для всіх модераторів No comment provided by engineer. @@ -3710,6 +3808,7 @@ snd error text For me + Для мене No comment provided by engineer. @@ -3838,6 +3937,7 @@ Error: %2$@ Get notified when mentioned. + Отримуйте сповіщення, коли вас згадують. No comment provided by engineer. @@ -3863,7 +3963,7 @@ Error: %2$@ Group already exists! Група вже існує! - No comment provided by engineer. + new chat sheet title Group display name @@ -3930,6 +4030,10 @@ Error: %2$@ Профіль групи зберігається на пристроях учасників, а не на серверах. No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message Привітальне повідомлення групи @@ -3947,6 +4051,7 @@ Error: %2$@ Groups + Групи No comment provided by engineer. @@ -3956,6 +4061,7 @@ Error: %2$@ Help admins moderating their groups. + Допоможіть адміністраторам модерувати їхні групи. No comment provided by engineer. @@ -4020,6 +4126,7 @@ Error: %2$@ How it works + Як це працює alert button @@ -4166,10 +4273,12 @@ More improvements are coming soon! Inappropriate content + Невідповідний вміст report reason Inappropriate profile + Невідповідний профіль report reason @@ -4266,22 +4375,27 @@ More improvements are coming soon! Invalid + Недійсний token status text Invalid (bad token) + Недійсний (неправильний токен) token status text Invalid (expired) + Недійсний (термін дії закінчився) token status text Invalid (unregistered) + Недійсний (незареєстрований) token status text Invalid (wrong topic) + Недійсний (неправильна тема) token status text @@ -4418,34 +4532,24 @@ More improvements are coming soon! Join group Приєднуйтесь до групи - No comment provided by engineer. + new chat sheet title Join group conversations Приєднуйтесь до групових розмов No comment provided by engineer. - - Join group? - Приєднатися до групи? - No comment provided by engineer. - Join incognito Приєднуйтесь інкогніто No comment provided by engineer. - - Join with current profile - Приєднатися з поточним профілем - No comment provided by engineer. - Join your group? This is your link for group %@! Приєднатися до групи? Це ваше посилання на групу %@! - No comment provided by engineer. + new chat action Joining group @@ -4559,10 +4663,12 @@ This is your link for group %@! List + Список swipe action List name and emoji should be different for all lists. + Назва списку та емодзі повинні бути різними для всіх списків. No comment provided by engineer. @@ -4654,6 +4760,10 @@ This is your link for group %@! Учасник No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive Користувач неактивний @@ -4688,6 +4798,10 @@ This is your link for group %@! Учасник буде видалений з групи - це неможливо скасувати! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. Учасники групи можуть додавати реакції на повідомлення. @@ -5102,6 +5216,10 @@ This is your link for group %@! Нова роль учасника No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message Нове повідомлення @@ -5139,6 +5257,10 @@ This is your link for group %@! No chats in list %@ No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected Не вибрано жодного контакту @@ -5325,7 +5447,9 @@ This is your link for group %@! Ok Гаразд - alert button + alert action +alert button +new chat action Old database @@ -5477,7 +5601,7 @@ Requires compatible VPN. Open group Відкрита група - No comment provided by engineer. + new chat action Open link? @@ -5732,6 +5856,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -6176,6 +6304,10 @@ swipe action Відхилити запит на контакт No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу. @@ -6231,11 +6363,6 @@ swipe action Переузгодьте шифрування? No comment provided by engineer. - - Repeat connection request? - Повторити запит на підключення? - No comment provided by engineer. - Repeat download Повторити завантаження @@ -6246,11 +6373,6 @@ swipe action Повторний імпорт No comment provided by engineer. - - Repeat join request? - Повторити запит на приєднання? - No comment provided by engineer. - Repeat upload Повторне завантаження @@ -6281,6 +6403,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6391,6 +6517,14 @@ swipe action Умови перегляду No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke Відкликати @@ -6447,6 +6581,14 @@ chat item action Зберегти (і повідомити контактам) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact Зберегти та повідомити контакт @@ -6472,6 +6614,10 @@ chat item action Зберегти профіль групи No comment provided by engineer. + + Save group profile? + alert title + Save list No comment provided by engineer. @@ -6959,6 +7105,10 @@ chat item action Встановіть його замість аутентифікації системи. No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7747,16 +7897,6 @@ It can happen because of some bug or when the connection is compromised.Цієї групи більше не існує. No comment provided by engineer. - - This is your own SimpleX address! - Це ваша власна SimpleX-адреса! - No comment provided by engineer. - - - This is your own one-time link! - Це ваше власне одноразове посилання! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8167,7 +8307,7 @@ To connect, please ask your contact to create another connection link and check Use current profile Використовувати поточний профіль - No comment provided by engineer. + new chat action Use for files @@ -8197,7 +8337,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile Використовуйте новий профіль інкогніто - No comment provided by engineer. + new chat action Use only local notifications? @@ -8555,12 +8695,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. Ви вже з'єднані з %@. - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8570,24 +8710,19 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. Ви вже приєдналися до групи %@. - No comment provided by engineer. - - - You are already joining the group via this link! - Ви вже приєдналися до групи за цим посиланням! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. Ви вже приєдналися до групи за цим посиланням. - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? Ви вже приєдналися до групи! Повторити запит на приєднання? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8704,6 +8839,10 @@ Repeat join request? Ви можете переглянути посилання на запрошення ще раз у деталях підключення. alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! Ви не можете надсилати повідомлення! @@ -8719,17 +8858,12 @@ Repeat join request? Ви вирішуєте, хто може під'єднатися. No comment provided by engineer. - - You have already requested connection via this address! - Ви вже надсилали запит на підключення за цією адресою! - No comment provided by engineer. - You have already requested connection! Repeat connection request? Ви вже надіслали запит на підключення! Повторити запит на підключення? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8815,11 +8949,6 @@ Repeat connection request? Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі. No comment provided by engineer. - - You will connect to all group members. - Ви з'єднаєтеся з усіма учасниками групи. - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні. @@ -9005,6 +9134,10 @@ Repeat connection request? вище, а потім обирайте: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call прийнято виклик @@ -9015,6 +9148,10 @@ Repeat connection request? прийняте запрошення chat list item title + + accepted you + rcv group event chat item + admin адмін @@ -9035,6 +9172,10 @@ Repeat connection request? узгодження шифрування… chat item text + + all + member criteria value + all members всі учасники @@ -9120,6 +9261,10 @@ marked deleted chat item preview text дзвоніть… call status + + can't send messages + No comment provided by engineer. + cancelled %@ скасовано %@ @@ -9225,6 +9370,14 @@ marked deleted chat item preview text контакт %1$@ змінено на %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption контакт має шифрування e2e @@ -9235,6 +9388,10 @@ marked deleted chat item preview text контакт не має шифрування e2e No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator творець @@ -9406,6 +9563,10 @@ pref value групу видалено No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated оновлено профіль групи @@ -9531,6 +9692,10 @@ pref value з'єднаний rcv group event chat item + + member has old version + No comment provided by engineer. + message повідомлення @@ -9595,6 +9760,10 @@ pref value без тексту copied message info in history + + not synchronized + No comment provided by engineer. + observer спостерігач @@ -9605,6 +9774,7 @@ pref value вимкнено enabled status group pref value +member criteria value time to disappear @@ -9655,6 +9825,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption квантово-стійке шифрування e2e @@ -9694,6 +9868,10 @@ time to disappear видалено контактну адресу profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture видалено зображення профілю @@ -9704,11 +9882,23 @@ time to disappear прибрали вас rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect запит на підключення chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved збережено @@ -9898,6 +10088,10 @@ last received msg: %2$@ ти No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group вас запрошують до групи 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 d5411f86e3..007c609362 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 @@ -565,6 +565,14 @@ time interval accept incoming call via notification swipe action + + Accept as member + alert action + + + Accept as observer + alert action + Accept conditions 接受条款 @@ -586,6 +594,10 @@ swipe action accept contact request via notification swipe action + + Accept member + alert title + Accepted conditions 已接受的条款 @@ -641,6 +653,10 @@ swipe action 扫描二维码来添加服务器。 No comment provided by engineer. + + Add short link + No comment provided by engineer. + Add team members 添加团队成员 @@ -939,12 +955,12 @@ swipe action Already connecting! 已经在连接了! - No comment provided by engineer. + new chat sheet title Already joining the group! 已经加入了该群组! - No comment provided by engineer. + new chat sheet title Always use private routing. @@ -1389,7 +1405,8 @@ swipe action Cancel 取消 alert action -alert button +alert button +new chat action Cancel migration @@ -1495,7 +1512,7 @@ set passcode view Chat already exists! 聊天已存在! - No comment provided by engineer. + new chat sheet title Chat colors @@ -1582,11 +1599,23 @@ set passcode view 将为你删除聊天 - 此操作无法撤销! No comment provided by engineer. + + Chat with admins + chat toolbar + + + Chat with member + No comment provided by engineer. + Chats 聊天 No comment provided by engineer. + + Chats with members + No comment provided by engineer. + Check messages every 20 min. 每 20 分钟检查消息。 @@ -1812,11 +1841,6 @@ set passcode view 自动连接 No comment provided by engineer. - - Connect incognito - 在隐身状态下连接 - No comment provided by engineer. - Connect to desktop 连接到桌面 @@ -1827,44 +1851,39 @@ set passcode view 更快地与您的朋友联系。 No comment provided by engineer. - - Connect to yourself? - 连接到你自己? - No comment provided by engineer. - Connect to yourself? This is your own SimpleX address! 与自己建立联系? 这是您自己的 SimpleX 地址! - No comment provided by engineer. + new chat sheet title Connect to yourself? This is your own one-time link! 与自己建立联系? 这是您自己的一次性链接! - No comment provided by engineer. + new chat sheet title Connect via contact address 通过联系地址连接 - No comment provided by engineer. + new chat sheet title Connect via link 通过链接连接 - No comment provided by engineer. + new chat sheet title Connect via one-time link 通过一次性链接连接 - No comment provided by engineer. + new chat sheet title Connect with %@ 与 %@连接 - No comment provided by engineer. + new chat action Connected @@ -2142,11 +2161,6 @@ This is your own one-time link! 创建队列 server test step - - Create secret group - 创建私密群组 - No comment provided by engineer. - Create your profile 创建您的资料 @@ -2401,6 +2415,10 @@ swipe action 删除聊天资料? No comment provided by engineer. + + Delete chat with member? + alert title + Delete chat? 删除聊天? @@ -2824,7 +2842,7 @@ swipe action Don't show again 不再显示 - No comment provided by engineer. + alert action Done @@ -3142,6 +3160,10 @@ chat item action 接受联系人请求错误 No comment provided by engineer. + + Error accepting member + alert title + Error adding member(s) 添加成员错误 @@ -3152,6 +3174,10 @@ chat item action 添加服务器出错 alert title + + Error adding short link + No comment provided by engineer. + Error changing address 更改地址错误 @@ -3170,7 +3196,7 @@ chat item action Error changing setting 更改设置错误 - No comment provided by engineer. + alert title Error changing to incognito! @@ -3234,7 +3260,11 @@ chat item action Error deleting chat database 删除聊天数据库错误 - No comment provided by engineer. + alert title + + + Error deleting chat with member + alert title Error deleting chat! @@ -3249,12 +3279,12 @@ chat item action Error deleting database 删除数据库错误 - No comment provided by engineer. + alert title Error deleting old database 删除旧数据库错误 - No comment provided by engineer. + alert title Error deleting token @@ -3289,7 +3319,7 @@ chat item action Error exporting chat database 导出聊天数据库错误 - No comment provided by engineer. + alert title Error exporting theme: %@ @@ -3299,7 +3329,7 @@ chat item action Error importing chat database 导入聊天数据库错误 - No comment provided by engineer. + alert title Error joining group @@ -3344,7 +3374,7 @@ chat item action Error removing member 删除成员错误 - No comment provided by engineer. + alert title Error reordering lists @@ -3924,7 +3954,7 @@ Error: %2$@ Group already exists! 群已存在! - No comment provided by engineer. + new chat sheet title Group display name @@ -3991,6 +4021,10 @@ Error: %2$@ 群组资料存储在成员的设备上,而不是服务器上。 No comment provided by engineer. + + Group profile was changed. If you save it, the updated profile will be sent to group members. + alert message + Group welcome message 群欢迎词 @@ -4489,34 +4523,24 @@ More improvements are coming soon! Join group 加入群组 - No comment provided by engineer. + new chat sheet title Join group conversations 加入群对话 No comment provided by engineer. - - Join group? - 加入群组? - No comment provided by engineer. - Join incognito 加入隐身聊天 No comment provided by engineer. - - Join with current profile - 使用当前档案加入 - No comment provided by engineer. - Join your group? This is your link for group %@! 加入您的群组? 这是您组 %@ 的链接! - No comment provided by engineer. + new chat action Joining group @@ -4728,6 +4752,10 @@ This is your link for group %@! 成员 No comment provided by engineer. + + Member admission + No comment provided by engineer. + Member inactive 成员不活跃 @@ -4763,6 +4791,10 @@ This is your link for group %@! 成员将被移出群组——此操作无法撤消! No comment provided by engineer. + + Member will join the group, accept member? + alert message + Members can add message reactions. 群组成员可以添加信息回应。 @@ -5183,6 +5215,10 @@ This is your link for group %@! 新成员角色 No comment provided by engineer. + + New member wants to join the group. + rcv group event chat item + New message 新消息 @@ -5223,6 +5259,10 @@ This is your link for group %@! 列表 %@ 中无聊天 No comment provided by engineer. + + No chats with members + No comment provided by engineer. + No contacts selected 未选择联系人 @@ -5415,7 +5455,9 @@ This is your link for group %@! Ok 好的 - alert button + alert action +alert button +new chat action Old database @@ -5569,7 +5611,7 @@ Requires compatible VPN. Open group 打开群 - No comment provided by engineer. + new chat action Open link? @@ -5823,6 +5865,10 @@ Error: %@ Please try to disable and re-enable notfications. token info + + Please wait for group moderators to review your request to join the group. + snd group event chat item + Please wait for token activation to complete. token info @@ -6265,6 +6311,10 @@ swipe action 拒绝联系人请求 No comment provided by engineer. + + Reject member? + alert title + Relay server is only used if necessary. Another party can observe your IP address. 中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。 @@ -6319,11 +6369,6 @@ swipe action 重新协商加密? No comment provided by engineer. - - Repeat connection request? - 重复连接请求吗? - No comment provided by engineer. - Repeat download 重复下载 @@ -6334,11 +6379,6 @@ swipe action 重复导入 No comment provided by engineer. - - Repeat join request? - 重复加入请求吗? - No comment provided by engineer. - Repeat upload 重复上传 @@ -6369,6 +6409,10 @@ swipe action Report reason? No comment provided by engineer. + + Report sent to moderators + alert title + Report spam: only group moderators will see it. report reason @@ -6479,6 +6523,14 @@ swipe action 审阅条款 No comment provided by engineer. + + Review members + admission stage + + + Review members before admitting ("knocking"). + admission stage description + Revoke 吊销 @@ -6534,6 +6586,14 @@ chat item action 保存(并通知联系人) alert button + + Save (and notify members) + alert button + + + Save admission settings? + alert title + Save and notify contact 保存并通知联系人 @@ -6559,6 +6619,10 @@ chat item action 保存群组资料 No comment provided by engineer. + + Save group profile? + alert title + Save list 保存列表 @@ -7041,6 +7105,10 @@ chat item action 设置它以代替系统身份验证。 No comment provided by engineer. + + Set member admission + No comment provided by engineer. + Set message expiration in chats. No comment provided by engineer. @@ -7812,16 +7880,6 @@ It can happen because of some bug or when the connection is compromised.该群组已不存在。 No comment provided by engineer. - - This is your own SimpleX address! - 这是你自己的 SimpleX 地址! - No comment provided by engineer. - - - This is your own one-time link! - 这是你自己的一次性链接! - No comment provided by engineer. - This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. No comment provided by engineer. @@ -8223,7 +8281,7 @@ To connect, please ask your contact to create another connection link and check Use current profile 使用当前配置文件 - No comment provided by engineer. + new chat action Use for files @@ -8251,7 +8309,7 @@ To connect, please ask your contact to create another connection link and check Use new incognito profile 使用新的隐身配置文件 - No comment provided by engineer. + new chat action Use only local notifications? @@ -8603,12 +8661,12 @@ To connect, please ask your contact to create another connection link and check You are already connecting to %@. 您已连接到 %@。 - No comment provided by engineer. + new chat sheet message You are already connecting via this one-time link! 你已经在通过这个一次性链接进行连接! - No comment provided by engineer. + new chat sheet message You are already in group %@. @@ -8618,24 +8676,19 @@ To connect, please ask your contact to create another connection link and check You are already joining the group %@. 您已加入组 %@。 - No comment provided by engineer. - - - You are already joining the group via this link! - 您已经通过此链接加入群组! - No comment provided by engineer. + new chat sheet message You are already joining the group via this link. 你已经在通过此链接加入该群。 - No comment provided by engineer. + new chat sheet message You are already joining the group! Repeat join request? 您已经加入了这个群组! 重复加入请求? - No comment provided by engineer. + new chat sheet title You are connected to the server used to receive messages from this contact. @@ -8750,6 +8803,10 @@ Repeat join request? 您可以在连接详情中再次查看邀请链接。 alert message + + You can view your reports in Chat with admins. + alert message + You can't send messages! 您无法发送消息! @@ -8765,17 +8822,12 @@ Repeat join request? 你决定谁可以连接。 No comment provided by engineer. - - You have already requested connection via this address! - 你已经请求通过此地址进行连接! - No comment provided by engineer. - You have already requested connection! Repeat connection request? 您已经请求连接了! 重复连接请求? - No comment provided by engineer. + new chat sheet title You have to enter passphrase every time the app starts - it is not stored on the device. @@ -8861,11 +8913,6 @@ Repeat connection request? 当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。 No comment provided by engineer. - - You will connect to all group members. - 你将连接到所有群成员。 - No comment provided by engineer. - You will still receive calls and notifications from muted profiles when they are active. 当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。 @@ -9045,6 +9092,10 @@ Repeat connection request? 上面,然后选择: No comment provided by engineer. + + accepted %@ + rcv group event chat item + accepted call 已接受通话 @@ -9054,6 +9105,10 @@ Repeat connection request? accepted invitation chat list item title + + accepted you + rcv group event chat item + admin 管理员 @@ -9074,6 +9129,10 @@ Repeat connection request? 同意加密… chat item text + + all + member criteria value + all members 所有成员 @@ -9159,6 +9218,10 @@ marked deleted chat item preview text 呼叫中…… call status + + can't send messages + No comment provided by engineer. + cancelled %@ 已取消 %@ @@ -9264,6 +9327,14 @@ marked deleted chat item preview text 联系人 %1$@ 已更改为 %2$@ profile update event chat item + + contact deleted + No comment provided by engineer. + + + contact disabled + No comment provided by engineer. + contact has e2e encryption 联系人具有端到端加密 @@ -9274,6 +9345,10 @@ marked deleted chat item preview text 联系人没有端到端加密 No comment provided by engineer. + + contact not ready + No comment provided by engineer. + creator 创建者 @@ -9445,6 +9520,10 @@ pref value 群组已删除 No comment provided by engineer. + + group is deleted + No comment provided by engineer. + group profile updated 群组资料已更新 @@ -9570,6 +9649,10 @@ pref value 已连接 rcv group event chat item + + member has old version + No comment provided by engineer. + message 消息 @@ -9634,6 +9717,10 @@ pref value 无文本 copied message info in history + + not synchronized + No comment provided by engineer. + observer 观察者 @@ -9644,6 +9731,7 @@ pref value 关闭 enabled status group pref value +member criteria value time to disappear @@ -9694,6 +9782,10 @@ time to disappear pending approval No comment provided by engineer. + + pending review + No comment provided by engineer. + quantum resistant e2e encryption 抗量子端到端加密 @@ -9733,6 +9825,10 @@ time to disappear 删除了联系地址 profile update event chat item + + removed from group + No comment provided by engineer. + removed profile picture 删除了资料图片 @@ -9743,10 +9839,22 @@ time to disappear 已将您移除 rcv group event chat item + + request to join rejected + No comment provided by engineer. + requested to connect chat list item title + + review + No comment provided by engineer. + + + reviewed by admins + No comment provided by engineer. + saved 已保存 @@ -9936,6 +10044,10 @@ last received msg: %2$@ No comment provided by engineer. + + you accepted this member + snd group event chat item + you are invited to group 您被邀请加入群组 diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 3ea46ee364..0a76757aed 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -4783,7 +4783,7 @@ Available in v5.1 Migrations: - 遷移:%@ + 遷移: No comment provided by engineer. diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings index 12d1e01f1d..83d42926d1 100644 --- a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "‐%d nieuwe gebeurtenissen"; +/* notification body */ +"From %d chat(s)" = "Van %d chat(s)"; + /* notification body */ "From: %@" = "Van: %@"; diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings index 7205b37e7f..cf082a166d 100644 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d новых сообщений"; +/* notification body */ +"From %d chat(s)" = "Из %d чатов"; + /* notification body */ "From: %@" = "От: %@"; diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift index 3e901c73eb..2b3e8068ae 100644 --- a/apps/ios/SimpleX SE/ShareAPI.swift +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -67,6 +67,7 @@ func apiSendMessages( : SEChatCommand.apiSendMessages( type: chatInfo.chatType, id: chatInfo.apiId, + scope: chatInfo.groupChatScope(), live: false, ttl: nil, composedMessages: composedMessages @@ -123,7 +124,7 @@ enum SEChatCommand: ChatCmdProtocol { case apiSetEncryptLocalFiles(enable: Bool) case apiGetChats(userId: Int64) case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) - case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) var cmdString: String { switch self { @@ -139,15 +140,27 @@ enum SEChatCommand: ChatCmdProtocol { case let .apiCreateChatItems(noteFolderId, composedMessages): let msgs = encodeJSON(composedMessages) return "/_create *\(noteFolderId) json \(msgs)" - case let .apiSendMessages(type, id, live, ttl, composedMessages): + case let .apiSendMessages(type, id, scope, live, ttl, composedMessages): let msgs = encodeJSON(composedMessages) let ttlStr = ttl != nil ? "\(ttl!)" : "default" - return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" } } - func ref(_ type: ChatType, _ id: Int64) -> String { - "\(type.rawValue)\(id)" + func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String { + "\(type.rawValue)\(id)\(scopeRef(scope: scope))" + } + + func scopeRef(scope: GroupChatScope?) -> String { + switch (scope) { + case .none: "" + case let .memberSupport(groupMemberId_): + if let groupMemberId = groupMemberId_ { + "(_support:\(groupMemberId))" + } else { + "(_support)" + } + } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 9326ae9abe..bea839c019 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -168,14 +168,19 @@ 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; + 64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */; }; + 64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */; }; + 64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */; }; + 64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */; }; + 64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; }; 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -527,14 +532,19 @@ 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; + 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAdmissionView.swift; sourceTree = ""; }; + 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportView.swift; sourceTree = ""; }; + 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryChatView.swift; sourceTree = ""; }; + 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportChatToolbar.swift; sourceTree = ""; }; + 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextPendingMemberActionsView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; }; 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -692,8 +702,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -778,8 +788,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */, ); path = Libraries; sourceTree = ""; @@ -1076,6 +1086,7 @@ 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */, D72A9087294BD7A70047C86D /* NativeTextEditor.swift */, 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */, + 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */, ); path = ComposeMessage; sourceTree = ""; @@ -1117,6 +1128,10 @@ 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */, 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */, B70CE9E52D4BE5930080F36D /* GroupMentions.swift */, + 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */, + 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */, + 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */, + 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */, ); path = Group; sourceTree = ""; @@ -1431,8 +1446,10 @@ 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */, + 64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, + 64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */, 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */, @@ -1467,6 +1484,7 @@ 5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */, 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */, 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */, + 64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */, 3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */, 648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, @@ -1509,6 +1527,7 @@ 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, + 64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */, 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, @@ -1587,6 +1606,7 @@ 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */, 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */, 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */, + 64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */, 18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */, 8CB15EA02CFDA30600C28209 /* ChatItemsMerger.swift in Sources */, 184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */, @@ -1971,7 +1991,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1996,7 +2016,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2021,7 +2041,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2046,7 +2066,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2063,11 +2083,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2083,11 +2103,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2108,7 +2128,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2123,7 +2143,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2145,7 +2165,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2160,7 +2180,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2182,7 +2202,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2208,7 +2228,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2233,7 +2253,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2259,7 +2279,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2284,7 +2304,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2299,7 +2319,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2318,7 +2338,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 282; + CURRENT_PROJECT_VERSION = 283; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2333,7 +2353,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.3.6; + MARKETING_VERSION = 6.4; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 88246465e1..9a82c912dd 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -15,6 +15,9 @@ public let CREATE_MEMBER_CONTACT_VERSION = 2 // version to receive reports (MCReport) public let REPORTS_VERSION = 12 +// support group knocking (MsgScope) +public let GROUP_KNOCKING_VERSION = 15 + public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html#preventing-server-abuse-without-compromising-e2e-encryption")! public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { @@ -1197,7 +1200,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable { public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case direct(contact: Contact) - case group(groupInfo: GroupInfo) + case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?) case local(noteFolder: NoteFolder) case contactRequest(contactRequest: UserContactRequest) case contactConnection(contactConnection: PendingContactConnection) @@ -1211,7 +1214,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.localDisplayName - case let .group(groupInfo): return groupInfo.localDisplayName + case let .group(groupInfo, _): return groupInfo.localDisplayName case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localDisplayName case let .contactConnection(contactConnection): return contactConnection.localDisplayName @@ -1224,7 +1227,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.displayName - case let .group(groupInfo): return groupInfo.displayName + case let .group(groupInfo, _): return groupInfo.displayName case .local: return ChatInfo.privateNotesChatName case let .contactRequest(contactRequest): return contactRequest.displayName case let .contactConnection(contactConnection): return contactConnection.displayName @@ -1237,7 +1240,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.fullName - case let .group(groupInfo): return groupInfo.fullName + case let .group(groupInfo, _): return groupInfo.fullName case .local: return "" case let .contactRequest(contactRequest): return contactRequest.fullName case let .contactConnection(contactConnection): return contactConnection.fullName @@ -1250,7 +1253,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.image - case let .group(groupInfo): return groupInfo.image + case let .group(groupInfo, _): return groupInfo.image case .local: return nil case let .contactRequest(contactRequest): return contactRequest.image case let .contactConnection(contactConnection): return contactConnection.image @@ -1263,7 +1266,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.localAlias - case let .group(groupInfo): return groupInfo.localAlias + case let .group(groupInfo, _): return groupInfo.localAlias case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localAlias case let .contactConnection(contactConnection): return contactConnection.localAlias @@ -1276,7 +1279,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.id - case let .group(groupInfo): return groupInfo.id + case let .group(groupInfo, _): return groupInfo.id case let .local(noteFolder): return noteFolder.id case let .contactRequest(contactRequest): return contactRequest.id case let .contactConnection(contactConnection): return contactConnection.id @@ -1302,7 +1305,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.apiId - case let .group(groupInfo): return groupInfo.apiId + case let .group(groupInfo, _): return groupInfo.apiId case let .local(noteFolder): return noteFolder.apiId case let .contactRequest(contactRequest): return contactRequest.apiId case let .contactConnection(contactConnection): return contactConnection.apiId @@ -1315,7 +1318,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { get { switch self { case let .direct(contact): return contact.ready - case let .group(groupInfo): return groupInfo.ready + case let .group(groupInfo, _): return groupInfo.ready case let .local(noteFolder): return noteFolder.ready case let .contactRequest(contactRequest): return contactRequest.ready case let .contactConnection(contactConnection): return contactConnection.ready @@ -1336,34 +1339,57 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { get { switch self { - case let .direct(contact): return contact.userCantSendReason - case let .group(groupInfo): return groupInfo.userCantSendReason - case let .local(noteFolder): return noteFolder.userCantSendReason - case let .contactRequest(contactRequest): return contactRequest.userCantSendReason - case let .contactConnection(contactConnection): return contactConnection.userCantSendReason - case .invalidJSON: return ("can't send messages", nil) + case let .direct(contact): + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if contact.nextSendGrpInv { return nil } + if !contact.active { return ("contact deleted", nil) } + if !contact.sndReady { return ("contact not ready", nil) } + if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } + if contact.activeConn?.connDisabled ?? true { return ("contact disabled", nil) } + return nil + case let .group(groupInfo, groupChatScope): + if groupInfo.membership.memberActive { + switch(groupChatScope) { + case .none: + if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } + if groupInfo.membership.memberRole == .observer { return ("you are observer", "Please contact group admin.") } + return nil + case let .some(.memberSupport(groupMember_: .some(supportMember))): + if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending { + return ("member has old version", nil) + } + return nil + case .some(.memberSupport(groupMember_: .none)): + return nil + } + } else { + switch groupInfo.membership.memberStatus { + case .memRejected: return ("request to join rejected", nil) + case .memGroupDeleted: return ("group is deleted", nil) + case .memRemoved: return ("removed from group", nil) + case .memLeft: return ("you left", nil) + default: return ("can't send messages", nil) + } + } + case .local: + return nil + case .contactRequest: + return ("can't send messages", nil) + case .contactConnection: + return ("can't send messages", nil) + case .invalidJSON: + return ("can't send messages", nil) } } } - public var sendMsgEnabled: Bool { - get { - switch self { - case let .direct(contact): return contact.sendMsgEnabled - case let .group(groupInfo): return groupInfo.sendMsgEnabled - case let .local(noteFolder): return noteFolder.sendMsgEnabled - case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled - case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled - case .invalidJSON: return false - } - } - } + public var sendMsgEnabled: Bool { userCantSendReason == nil } public var incognito: Bool { get { switch self { case let .direct(contact): return contact.contactConnIncognito - case let .group(groupInfo): return groupInfo.membership.memberIncognito + case let .group(groupInfo, _): return groupInfo.membership.memberIncognito case .local: return false case .contactRequest: return false case let .contactConnection(contactConnection): return contactConnection.incognito @@ -1388,7 +1414,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupInfo: GroupInfo? { switch self { - case let .group(groupInfo): return groupInfo + case let .group(groupInfo, _): return groupInfo default: return nil } } @@ -1405,7 +1431,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .voice: return cups.voice.enabled.forUser case .calls: return cups.calls.enabled.forUser } - case let .group(groupInfo): + case let .group(groupInfo, _): let prefs = groupInfo.fullGroupPreferences switch feature { case .timedMessages: return prefs.timedMessages.on @@ -1428,7 +1454,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case let .direct(contact): let pref = contact.mergedPreferences.timedMessages return pref.enabled.forUser ? pref.userPreference.preference.ttl : nil - case let .group(groupInfo): + case let .group(groupInfo, _): let pref = groupInfo.fullGroupPreferences.timedMessages return pref.on ? pref.ttl : nil default: @@ -1453,7 +1479,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } else { return .other } - case let .group(groupInfo): + case let .group(groupInfo, _): if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) { return .groupOwnerCan } else { @@ -1484,7 +1510,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { return .other } } - + + public func groupChatScope() -> GroupChatScope? { + switch self { + case let .group(_, groupChatScope): groupChatScope?.toChatScope() + default: nil + } + } + public func ntfsEnabled(chatItem: ChatItem) -> Bool { ntfsEnabled(chatItem.meta.userMention) } @@ -1500,7 +1533,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var chatSettings: ChatSettings? { switch self { case let .direct(contact): return contact.chatSettings - case let .group(groupInfo): return groupInfo.chatSettings + case let .group(groupInfo, _): return groupInfo.chatSettings default: return nil } } @@ -1516,7 +1549,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var chatTags: [Int64]? { switch self { case let .direct(contact): return contact.chatTags - case let .group(groupInfo): return groupInfo.chatTags + case let .group(groupInfo, _): return groupInfo.chatTags default: return nil } } @@ -1524,7 +1557,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { var createdAt: Date { switch self { case let .direct(contact): return contact.createdAt - case let .group(groupInfo): return groupInfo.createdAt + case let .group(groupInfo, _): return groupInfo.createdAt case let .local(noteFolder): return noteFolder.createdAt case let .contactRequest(contactRequest): return contactRequest.createdAt case let .contactConnection(contactConnection): return contactConnection.createdAt @@ -1535,7 +1568,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var updatedAt: Date { switch self { case let .direct(contact): return contact.updatedAt - case let .group(groupInfo): return groupInfo.updatedAt + case let .group(groupInfo, _): return groupInfo.updatedAt case let .local(noteFolder): return noteFolder.updatedAt case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt @@ -1546,7 +1579,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public var chatTs: Date { switch self { case let .direct(contact): return contact.chatTs ?? contact.updatedAt - case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt + case let .group(groupInfo, _): return groupInfo.chatTs ?? groupInfo.updatedAt case let .local(noteFolder): return noteFolder.chatTs case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt @@ -1562,7 +1595,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } else { ChatTTL.userDefault(globalTTL) } - case let .group(groupInfo): + case let .group(groupInfo, _): return if let ciTTL = groupInfo.chatItemTTL { ChatTTL.chat(ChatItemTTL(ciTTL)) } else { @@ -1582,7 +1615,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { public static var sampleData: ChatInfo.SampleData = SampleData( direct: ChatInfo.direct(contact: Contact.sampleData), - group: ChatInfo.group(groupInfo: GroupInfo.sampleData), + group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil), local: ChatInfo.local(noteFolder: NoteFolder.sampleData), contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData), contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData()) @@ -1612,7 +1645,13 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { } public struct ChatStats: Decodable, Hashable { - public init(unreadCount: Int = 0, unreadMentions: Int = 0, reportsCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) { + public init( + unreadCount: Int = 0, + unreadMentions: Int = 0, + reportsCount: Int = 0, + minUnreadItemId: Int64 = 0, + unreadChat: Bool = false + ) { self.unreadCount = unreadCount self.unreadMentions = unreadMentions self.reportsCount = reportsCount @@ -1629,6 +1668,32 @@ public struct ChatStats: Decodable, Hashable { public var unreadChat: Bool = false } +public enum GroupChatScope: Decodable { + case memberSupport(groupMemberId_: Int64?) +} + +public func sameChatScope(_ scope1: GroupChatScope, _ scope2: GroupChatScope) -> Bool { + switch (scope1, scope2) { + case let (.memberSupport(groupMemberId1_), .memberSupport(groupMemberId2_)): + return groupMemberId1_ == groupMemberId2_ + } +} + +public enum GroupChatScopeInfo: Decodable, Hashable { + case memberSupport(groupMember_: GroupMember?) + + public func toChatScope() -> GroupChatScope { + switch self { + case let .memberSupport(groupMember_): + if let groupMember = groupMember_ { + return .memberSupport(groupMemberId_: groupMember.groupMemberId) + } else { + return .memberSupport(groupMemberId_: nil) + } + } + } +} + public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var contactId: Int64 var localDisplayName: ContactName @@ -1655,16 +1720,6 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var ready: Bool { get { activeConn?.connStatus == .ready } } public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { - // TODO [short links] this will have additional statuses for pending contact requests before they are accepted - if nextSendGrpInv { return nil } - if !active { return ("contact deleted", nil) } - if !sndReady { return ("contact not ready", nil) } - if activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } - if activeConn?.connDisabled ?? true { return ("contact disabled", nil) } - return nil - } - public var sendMsgEnabled: Bool { userCantSendReason == nil } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } @@ -1843,8 +1898,6 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { public var id: ChatId { get { "<@\(contactRequestId)" } } public var apiId: Int64 { get { contactRequestId } } var ready: Bool { get { true } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } - public var sendMsgEnabled: Bool { get { false } } public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } @@ -1876,8 +1929,6 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var id: ChatId { get { ":\(pccConnId)" } } public var apiId: Int64 { get { pccConnId } } var ready: Bool { get { false } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) } - public var sendMsgEnabled: Bool { get { false } } var localDisplayName: String { get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) } } @@ -2002,24 +2053,11 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { var updatedAt: Date var chatTs: Date? public var uiThemes: ThemeModeOverrides? + public var membersRequireAttention: Int public var id: ChatId { get { "#\(groupId)" } } public var apiId: Int64 { get { groupId } } public var ready: Bool { get { true } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { - return if membership.memberActive { - membership.memberRole == .observer ? ("you are observer", "Please contact group admin.") : nil - } else { - switch membership.memberStatus { - case .memRejected: ("request to join rejected", nil) - case .memGroupDeleted: ("group is deleted", nil) - case .memRemoved: ("removed from group", nil) - case .memLeft: ("you left", nil) - default: ("can't send messages", nil) - } - } - } - public var sendMsgEnabled: Bool { userCantSendReason == nil } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } @@ -2032,13 +2070,17 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { } public var canDelete: Bool { - return membership.memberRole == .owner || !membership.memberCurrent + return membership.memberRole == .owner || !membership.memberCurrentOrPending } public var canAddMembers: Bool { return membership.memberRole >= .admin && membership.memberActive } + public var canModerate: Bool { + return membership.memberRole >= .moderator && membership.memberActive + } + public static let sampleData = GroupInfo( groupId: 1, localDisplayName: "team", @@ -2048,6 +2090,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { chatSettings: ChatSettings.defaults, createdAt: .now, updatedAt: .now, + membersRequireAttention: 0, chatTags: [], localAlias: "" ) @@ -2059,12 +2102,20 @@ public struct GroupRef: Decodable, Hashable { } public struct GroupProfile: Codable, NamedChat, Hashable { - public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) { + public init( + displayName: String, + fullName: String, + description: String? = nil, + image: String? = nil, + groupPreferences: GroupPreferences? = nil, + memberAdmission: GroupMemberAdmission? = nil + ) { self.displayName = displayName self.fullName = fullName self.description = description self.image = image self.groupPreferences = groupPreferences + self.memberAdmission = memberAdmission } public var displayName: String @@ -2072,14 +2123,48 @@ public struct GroupProfile: Codable, NamedChat, Hashable { public var description: String? public var image: String? public var groupPreferences: GroupPreferences? + public var memberAdmission: GroupMemberAdmission? public var localAlias: String { "" } + public var memberAdmission_: GroupMemberAdmission { + get { self.memberAdmission ?? GroupMemberAdmission() } + set { memberAdmission = newValue } + } + public static let sampleData = GroupProfile( displayName: "team", fullName: "My Team" ) } +public struct GroupMemberAdmission: Codable, Hashable { + public var review: MemberCriteria? + + public init( + review: MemberCriteria? = nil + ) { + self.review = review + } + + public static let sampleData = GroupMemberAdmission( + review: .all + ) +} + +public enum MemberCriteria: String, Codable, Identifiable, Hashable { + case all + + public static var values: [MemberCriteria] { [.all] } + + public var id: Self { self } + + public var text: String { + switch self { + case .all: return NSLocalizedString("all", comment: "member criteria value") + } + } +} + public struct BusinessChatInfo: Decodable, Hashable { public var chatType: BusinessChatType public var businessId: String @@ -2106,6 +2191,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var memberContactId: Int64? public var memberContactProfileId: Int64 public var activeConn: Connection? + public var supportChat: GroupSupportChat? public var memberChatVRange: VersionRange public var id: String { "#\(groupId) @\(groupMemberId)" } @@ -2177,6 +2263,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { case .memUnknown: return false case .memInvited: return false case .memPendingApproval: return true + case .memPendingReview: return true case .memIntroduced: return false case .memIntroInvited: return false case .memAccepted: return false @@ -2196,6 +2283,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { case .memUnknown: return false case .memInvited: return false case .memPendingApproval: return false + case .memPendingReview: return false case .memIntroduced: return true case .memIntroInvited: return true case .memAccepted: return true @@ -2206,6 +2294,18 @@ public struct GroupMember: Identifiable, Decodable, Hashable { } } + public var memberPending: Bool { + switch memberStatus { + case .memPendingApproval: return true + case .memPendingReview: return true + default: return false + } + } + + public var memberCurrentOrPending: Bool { + memberCurrent || memberPending + } + public func canBeRemoved(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft @@ -2259,6 +2359,13 @@ public struct GroupMember: Identifiable, Decodable, Hashable { ) } +public struct GroupSupportChat: Codable, Hashable { + public var chatTs: Date + public var unread: Int + public var memberAttention: Int + public var mentions: Int +} + public struct GroupMemberSettings: Codable, Hashable { public var showMessages: Bool } @@ -2328,6 +2435,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case memUnknown = "unknown" case memInvited = "invited" case memPendingApproval = "pending_approval" + case memPendingReview = "pending_review" case memIntroduced = "introduced" case memIntroInvited = "intro-inv" case memAccepted = "accepted" @@ -2345,6 +2453,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case .memUnknown: return "unknown status" case .memInvited: return "invited" case .memPendingApproval: return "pending approval" + case .memPendingReview: return "pending review" case .memIntroduced: return "connecting (introduced)" case .memIntroInvited: return "connecting (introduction invitation)" case .memAccepted: return "connecting (accepted)" @@ -2364,6 +2473,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable { case .memUnknown: return "unknown" case .memInvited: return "invited" case .memPendingApproval: return "pending" + case .memPendingReview: return "review" case .memIntroduced: return "connecting" case .memIntroInvited: return "connecting" case .memAccepted: return "connecting" @@ -2386,8 +2496,6 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable { public var id: ChatId { get { "*\(noteFolderId)" } } public var apiId: Int64 { get { noteFolderId } } public var ready: Bool { get { true } } - public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { nil } - public var sendMsgEnabled: Bool { get { true } } public var displayName: String { get { ChatInfo.privateNotesChatName } } public var fullName: String { get { "" } } public var image: String? { get { nil } } @@ -2647,12 +2755,15 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .userDeleted: nil case .groupDeleted: nil case .memberCreatedContact: nil + case .newMemberPendingReview: nil default: .rcvGroupEvent } case let .sndGroupEvent(event): switch event { case .userRole: nil case .userLeft: nil + case .memberAccepted: nil + case .userPendingReview: nil default: .sndGroupEvent } default: @@ -2685,6 +2796,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable { switch rcvGroupEvent { case .groupUpdated: return false case .memberConnected: return false + case .memberAccepted: return false + case .userAccepted: return false case .memberRole: return false case .memberBlocked: return false case .userRole: return true @@ -2696,6 +2809,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .invitedViaGroupLink: return false case .memberCreatedContact: return false case .memberProfileUpdated: return false + case .newMemberPendingReview: return true } case .sndGroupEvent: return false case .rcvConnEvent: return false @@ -2764,12 +2878,12 @@ public struct ChatItem: Identifiable, Decodable, Hashable { public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? { switch (chatInfo, chatDir) { - case let (.group(groupInfo), .groupRcv(groupMember)): + 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): + case let (.group(groupInfo, _), .groupSnd): let m = groupInfo.membership return m.memberRole >= .admin ? (groupInfo, nil) : nil default: return nil @@ -3151,6 +3265,21 @@ public enum CIStatus: Decodable, Hashable { } } + // as in corresponds to SENT response from agent, opposed to `sent` which means snd status + public var isSent: Bool { + switch self { + case .sndNew: false + case .sndSent: true + case .sndRcvd: false + case .sndErrorAuth: true + case .sndError: true + case .sndWarning: true + case .rcvNew: false + case .rcvRead: false + case .invalid: false + } + } + public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? { switch self { case .sndNew: nil @@ -3204,6 +3333,17 @@ public enum CIStatus: Decodable, Hashable { } } +public func shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus) -> Bool { + switch (oldStatus, newStatus) { + case (.sndRcvd, let new) where !new.isSndRcvd: + return true + case (let old, .sndNew) where old.isSent: + return true + default: + return false + } +} + public enum SndError: Decodable, Hashable { case auth case quota @@ -4103,6 +4243,16 @@ extension MsgContent: Encodable { } } +public enum MsgContentTag: String { + case text + case link + case image + case video + case voice + case file + case report +} + public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? @@ -4440,6 +4590,8 @@ public enum RcvDirectEvent: Decodable, Hashable { public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected + case memberAccepted(groupMemberId: Int64, profile: Profile) + case userAccepted case memberLeft case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) @@ -4451,12 +4603,16 @@ public enum RcvGroupEvent: Decodable, Hashable { case invitedViaGroupLink case memberCreatedContact case memberProfileUpdated(fromProfile: Profile, toProfile: Profile) + case newMemberPendingReview var text: String { switch self { case let .memberAdded(_, profile): return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName) case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item") + case let .memberAccepted(_, profile): + return String.localizedStringWithFormat(NSLocalizedString("accepted %@", comment: "rcv group event chat item"), profile.profileViewName) + case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberRole(_, profile, role): return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text) @@ -4476,6 +4632,7 @@ public enum RcvGroupEvent: Decodable, Hashable { case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item") case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item") case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + case .newMemberPendingReview: return NSLocalizedString("New member wants to join the group.", comment: "rcv group event chat item") } } @@ -4500,6 +4657,8 @@ public enum SndGroupEvent: Decodable, Hashable { case memberDeleted(groupMemberId: Int64, profile: Profile) case userLeft case groupUpdated(groupProfile: GroupProfile) + case memberAccepted(groupMemberId: Int64, profile: Profile) + case userPendingReview var text: String { switch self { @@ -4517,6 +4676,9 @@ public enum SndGroupEvent: Decodable, Hashable { return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName) case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item") case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item") + case .memberAccepted: return NSLocalizedString("you accepted this member", comment: "snd group event chat item") + case .userPendingReview: + return NSLocalizedString("Please wait for group moderators to review your request to join the group.", comment: "snd group event chat item") } } } diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 6cbc76ec98..6f80629932 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -16,7 +16,7 @@ public protocol ChatLike { extension ChatLike { public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { - if case let .group(groupInfo) = self.chatInfo { + if case let .group(groupInfo, _) = self.chatInfo { let p = groupInfo.fullGroupPreferences return switch feature { case .timedMessages: p.timedMessages.on @@ -82,9 +82,9 @@ public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { switch cInfo { - case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv - case let .group(groupInfo): groupInfo.sendMsgEnabled - case let .local(noteFolder): noteFolder.sendMsgEnabled + case let .direct(contact): cInfo.sendMsgEnabled && !contact.nextSendGrpInv + case .group: cInfo.sendMsgEnabled + case .local: cInfo.sendMsgEnabled case .contactRequest: false case .contactConnection: false case .invalidJSON: false @@ -94,7 +94,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { public func chatIconName(_ cInfo: ChatInfo) -> String { switch cInfo { case .direct: "person.crop.circle.fill" - case let .group(groupInfo): + case let .group(groupInfo, _): switch groupInfo.businessChat?.chatType { case .none: "person.2.circle.fill" case .business: "briefcase.circle.fill" diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 5579449caa..70db4476d5 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -62,7 +62,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact, public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent { let previewMode = ntfPreviewModeGroupDefault.get() var title: String - if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { + if case let .group(groupInfo, _) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden) } else { title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):" diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index e4bc8f2150..d643936e53 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -526,10 +526,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Вече сте свързани?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "В процес на свързване!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вече се присъединихте към групата!"; /* pref value */ @@ -795,7 +795,8 @@ marked deleted chat item preview text */ "Can't invite contacts!" = "Не може да поканят контактите!"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Отказ"; /* No comment provided by engineer. */ @@ -973,34 +974,28 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Автоматично свъзрване"; -/* No comment provided by engineer. */ -"Connect incognito" = "Свързване инкогнито"; - /* No comment provided by engineer. */ "Connect to desktop" = "Свързване с настолно устройство"; /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "свържете се с разработчиците на SimpleX Chat."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Свърване със себе си?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Свърване със себе си?\nТова е вашят еднократен линк за връзка!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Свърване със себе си?\nТова е вашият личен SimpleX адрес!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Свързване чрез адрес за контакт"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Свърване чрез линк"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Свързване чрез еднократен линк за връзка"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Свързване с %@"; /* No comment provided by engineer. */ @@ -1144,9 +1139,6 @@ set passcode view */ /* server test step */ "Create queue" = "Създай опашка"; -/* No comment provided by engineer. */ -"Create secret group" = "Създай тайна група"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Създаване на адрес в SimpleX"; @@ -1485,7 +1477,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "Не активирай"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Не показвай отново"; /* No comment provided by engineer. */ @@ -1705,7 +1697,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Грешка при промяна на ролята"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Грешка при промяна на настройката"; /* No comment provided by engineer. */ @@ -1729,7 +1721,7 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Грешка при декриптирането на файла"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Грешка при изтриване на базата данни"; /* No comment provided by engineer. */ @@ -1738,10 +1730,10 @@ chat item action */ /* No comment provided by engineer. */ "Error deleting connection" = "Грешка при изтриване на връзката"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Грешка при изтриване на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Грешка при изтриване на старата база данни"; /* No comment provided by engineer. */ @@ -1762,10 +1754,10 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Грешка при криптиране на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Грешка при експортиране на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Грешка при импортиране на базата данни"; /* No comment provided by engineer. */ @@ -1777,7 +1769,7 @@ chat item action */ /* alert title */ "Error receiving file" = "Грешка при получаване на файл"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Грешка при отстраняване на член"; /* No comment provided by engineer. */ @@ -2001,7 +1993,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Групата вече съществува"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Групата вече съществува!"; /* No comment provided by engineer. */ @@ -2337,22 +2329,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "присъединяване като %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Влез в групата"; /* No comment provided by engineer. */ "Join group conversations" = "Присъединяване към групи"; -/* No comment provided by engineer. */ -"Join group?" = "Влез в групата?"; - /* No comment provided by engineer. */ "Join incognito" = "Влез инкогнито"; -/* No comment provided by engineer. */ -"Join with current profile" = "Присъединяване с текущия профил"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Влез в твоята група?\nТова е вашят линк за група %@!"; /* No comment provided by engineer. */ @@ -2747,6 +2733,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "изключено"; @@ -2759,7 +2746,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "предлага %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -2834,7 +2823,7 @@ time to disappear */ /* authentication reason */ "Open chat console" = "Отвори конзолата"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Отвори група"; /* authentication reason */ @@ -3186,18 +3175,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Предоговори криптирането?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Изпрати отново заявката за свързване?"; - /* No comment provided by engineer. */ "Repeat download" = "Повтори изтеглянето"; /* No comment provided by engineer. */ "Repeat import" = "Повтори импортирането"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Изпрати отново заявката за присъединяване?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повтори качването"; @@ -3776,9 +3759,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; @@ -3827,12 +3807,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Тази група вече не съществува."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Това е вашят еднократен линк за връзка!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Това е вашият личен SimpleX адрес!"; - /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Тази настройка се прилага за съобщения в текущия ви профил **%@**."; @@ -4007,7 +3981,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Използвай чата"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Използвай текущия профил"; /* No comment provided by engineer. */ @@ -4019,7 +3993,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Използвай интерфейса за повикване на iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Използвай нов инкогнито профил"; /* No comment provided by engineer. */ @@ -4220,25 +4194,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected to %@." = "Вече сте вече свързани с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Вече се свързвате с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Вече се свързвате чрез този еднократен линк за връзка!"; /* No comment provided by engineer. */ "You are already in group %@." = "Вече сте в група %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Вече се присъединявате към групата %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Вие вече се присъединявате към групата чрез този линк!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Вие вече се присъединявате към групата чрез този линк."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Вече се присъединихте към групата!\nИзпрати отново заявката за присъединяване?"; /* No comment provided by engineer. */ @@ -4322,10 +4293,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Вече сте заявили връзка през този адрес!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вече сте направили заявката за връзка!\nИзпрати отново заявката за свързване?"; /* No comment provided by engineer. */ @@ -4382,9 +4350,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Ще се свържете с всички членове на групата."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Все още ще получавате обаждания и известия от заглушени профили, когато са активни."; @@ -4446,10 +4411,10 @@ chat item action */ "Your profile **%@** will be shared." = "Вашият профил **%@** ще бъде споделен."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил."; +"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти."; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил."; /* No comment provided by engineer. */ "Your random profile" = "Вашият автоматично генериран профил"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 08a94615a3..18a6b95a58 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -109,12 +109,18 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ servery"; +/* No comment provided by engineer. */ +"%@ uploaded" = "%@ nahrán"; + /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; /* format for date separator in chat */ "%@, %@" = "%1$@, %2$@"; +/* No comment provided by engineer. */ +"%@, %@ and %lld members" = "%@, %@ a %lld členů"; + /* No comment provided by engineer. */ "%@, %@ and %lld other members connected" = "%@, %@ a %lld ostatní členové připojeni"; @@ -560,7 +566,8 @@ swipe action */ "Can't invite contacts!" = "Nelze pozvat kontakty!"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Zrušit"; /* feature offered item */ @@ -702,16 +709,13 @@ set passcode view */ /* server test step */ "Connect" = "Připojit"; -/* No comment provided by engineer. */ -"Connect incognito" = "Spojit se inkognito"; - /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "připojit se k vývojářům SimpleX Chat."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Připojte se prostřednictvím odkazu"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Připojit se jednorázovým odkazem"; /* No comment provided by engineer. */ @@ -828,9 +832,6 @@ set passcode view */ /* server test step */ "Create queue" = "Vytvořit frontu"; -/* No comment provided by engineer. */ -"Create secret group" = "Vytvořit tajnou skupinu"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Vytvořit SimpleX adresu"; @@ -1127,7 +1128,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "Nepovolovat"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Znovu neukazuj"; /* No comment provided by engineer. */ @@ -1304,7 +1305,7 @@ swipe action */ /* No comment provided by engineer. */ "Error changing role" = "Chyba při změně role"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Chyba změny nastavení"; /* No comment provided by engineer. */ @@ -1325,7 +1326,7 @@ swipe action */ /* No comment provided by engineer. */ "Error decrypting file" = "Chyba dešifrování souboru"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Chyba při mazání databáze chatu"; /* No comment provided by engineer. */ @@ -1334,10 +1335,10 @@ swipe action */ /* No comment provided by engineer. */ "Error deleting connection" = "Chyba při mazání připojení"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Chyba při mazání databáze"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Chyba při mazání staré databáze"; /* No comment provided by engineer. */ @@ -1355,10 +1356,10 @@ swipe action */ /* No comment provided by engineer. */ "Error encrypting database" = "Chyba šifrování databáze"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Chyba při exportu databáze chatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Chyba při importu databáze chatu"; /* No comment provided by engineer. */ @@ -1367,7 +1368,7 @@ swipe action */ /* alert title */ "Error receiving file" = "Chyba při příjmu souboru"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Chyba při odebrání člena"; /* No comment provided by engineer. */ @@ -1825,7 +1826,7 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "připojit se jako %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Připojit ke skupině"; /* No comment provided by engineer. */ @@ -2145,6 +2146,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "vypnuto"; @@ -2157,7 +2159,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "nabídl %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2988,9 +2992,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; @@ -3153,7 +3154,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Použijte chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Použít aktuální profil"; /* No comment provided by engineer. */ @@ -3162,7 +3163,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Použít rozhraní volání iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Použít nový inkognito profil"; /* No comment provided by engineer. */ @@ -3472,10 +3473,10 @@ chat item action */ "Your profile **%@** will be shared." = "Váš profil **%@** bude sdílen."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil."; +"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil."; /* No comment provided by engineer. */ "Your random profile" = "Váš náhodný profil"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 8da7835c43..f2ffabca1d 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Annehmen"; +/* alert action */ +"Accept as member" = "Als Mitglied übernehmen"; + +/* alert action */ +"Accept as observer" = "Als Beobachter übernehmen"; + /* No comment provided by engineer. */ "Accept conditions" = "Nutzungsbedingungen akzeptieren"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Inkognito akzeptieren"; +/* alert title */ +"Accept member" = "Mitglied übernehmen"; + +/* rcv group event chat item */ +"accepted %@" = "%@ übernommen"; + /* call status */ "accepted call" = "Anruf angenommen"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "Einladung angenommen"; +/* rcv group event chat item */ +"accepted you" = "hat Sie übernommen"; + /* No comment provided by engineer. */ "Acknowledged" = "Bestätigt"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "Verschlüsselung zustimmen…"; +/* member criteria value */ +"all" = "alle"; + /* No comment provided by engineer. */ "All" = "Alle"; @@ -589,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Sind Sie bereits verbunden?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Bereits verbunden!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Sie sind bereits Mitglied der Gruppe!"; /* pref value */ @@ -905,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "Mitglied kann nicht benachrichtigt werden"; +/* No comment provided by engineer. */ +"can't send messages" = "Es können keine Nachrichten gesendet werden"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ @@ -988,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Chat besteht bereits"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Chat besteht bereits!"; /* No comment provided by engineer. */ @@ -1042,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; +/* chat toolbar */ +"Chat with admins" = "Chat mit Administratoren"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat mit einem Mitglied"; + /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chats mit Mitgliedern"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Alle 20min Nachrichten überprüfen."; @@ -1186,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Automatisch verbinden"; -/* No comment provided by engineer. */ -"Connect incognito" = "Inkognito verbinden"; - /* No comment provided by engineer. */ "Connect to desktop" = "Mit dem Desktop verbinden"; @@ -1198,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Schneller mit Ihren Freunden verbinden."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Mit Ihnen selbst verbinden?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Mit Ihnen selbst verbinden?\nDas ist Ihr eigener Einmal-Link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Sich mit Ihnen selbst verbinden?\nDas ist Ihre eigene SimpleX-Adresse!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Über die Kontakt-Adresse verbinden"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Über einen Link verbinden"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Über einen Einmal-Link verbinden"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Mit %@ verbinden"; /* No comment provided by engineer. */ @@ -1333,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "Der Kontakt ist bereits vorhanden"; +/* No comment provided by engineer. */ +"contact deleted" = "Kontakt gelöscht"; + /* No comment provided by engineer. */ "Contact deleted!" = "Kontakt gelöscht!"; +/* No comment provided by engineer. */ +"contact disabled" = "Kontakt deaktiviert"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Kontakt nutzt E2E-Verschlüsselung"; @@ -1354,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Kontaktname"; +/* No comment provided by engineer. */ +"contact not ready" = "Kontakt nicht bereit"; + /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; @@ -1423,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Erzeuge Warteschlange"; -/* No comment provided by engineer. */ -"Create secret group" = "Geheime Gruppe erstellen"; - /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-Adresse erstellen"; @@ -1602,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Chat-Profil löschen?"; +/* alert title */ +"Delete chat with member?" = "Chat mit dem Mitglied löschen?"; + /* No comment provided by engineer. */ "Delete chat?" = "Chat löschen?"; @@ -1872,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Verpassen Sie keine wichtigen Nachrichten."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Nicht nochmals anzeigen"; /* No comment provided by engineer. */ @@ -2116,6 +2150,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; +/* alert title */ +"Error accepting member" = "Fehler beim Übernehmen des Mitglieds"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern"; @@ -2131,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Fehler beim Ändern der Rolle"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Fehler beim Ändern der Einstellung"; /* No comment provided by engineer. */ @@ -2170,19 +2207,22 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Fehler beim Entschlüsseln der Datei"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; +/* alert title */ +"Error deleting chat with member" = "Fehler beim Löschen des Chats mit dem Mitglied"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Fehler beim Löschen des Chats!"; /* No comment provided by engineer. */ "Error deleting connection" = "Fehler beim Löschen der Verbindung"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Fehler beim Löschen der Datenbank"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Fehler beim Löschen der alten Datenbank"; /* No comment provided by engineer. */ @@ -2203,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Fehler beim Verschlüsseln der Datenbank"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Fehler beim Exportieren der Chat-Datenbank"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Fehler beim Exportieren des Designs: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Fehler beim Importieren der Chat-Datenbank"; /* No comment provided by engineer. */ @@ -2236,7 +2276,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Fehler beim Registrieren für Benachrichtigungen"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; /* alert title */ @@ -2586,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Die Gruppe besteht bereits"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Die Gruppe besteht bereits!"; /* No comment provided by engineer. */ @@ -2610,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde."; +/* No comment provided by engineer. */ +"group is deleted" = "Gruppe wird gelöscht"; + /* No comment provided by engineer. */ "Group link" = "Gruppen-Link"; @@ -2982,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "beitreten als %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Treten Sie der Gruppe bei"; /* No comment provided by engineer. */ "Join group conversations" = "Gruppenunterhaltungen beitreten"; -/* No comment provided by engineer. */ -"Join group?" = "Der Gruppe beitreten?"; - /* No comment provided by engineer. */ "Join incognito" = "Inkognito beitreten"; -/* No comment provided by engineer. */ -"Join with current profile" = "Mit dem aktuellen Profil beitreten"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Ihrer Gruppe beitreten?\nDas ist Ihr Link für die Gruppe %@!"; /* No comment provided by engineer. */ @@ -3138,9 +3175,15 @@ snd error text */ /* profile update event chat item */ "member %@ changed to %@" = "Der Mitgliedsname von %1$@ wurde auf %2$@ geändert"; +/* No comment provided by engineer. */ +"Member admission" = "Aufnahme von Mitgliedern"; + /* rcv group event chat item */ "member connected" = "ist der Gruppe beigetreten"; +/* No comment provided by engineer. */ +"member has old version" = "Das Mitglied hat eine alte App-Version"; + /* item status text */ "Member inactive" = "Mitglied inaktiv"; @@ -3162,6 +3205,9 @@ snd error text */ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; +/* alert message */ +"Member will join the group, accept member?" = "Ein Mitglied wird der Gruppe beitreten. Übernehmen?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben."; @@ -3432,6 +3478,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Neue Mitgliedsrolle"; +/* rcv group event chat item */ +"New member wants to join the group." = "Ein neues Mitglied will der Gruppe beitreten."; + /* notification */ "new message" = "Neue Nachricht"; @@ -3471,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Keine Chats in der Liste %@"; +/* No comment provided by engineer. */ +"No chats with members" = "Keine Chats mit Mitgliedern"; + /* No comment provided by engineer. */ "No contacts selected" = "Keine Kontakte ausgewählt"; @@ -3555,6 +3607,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "Nicht kompatibel!"; +/* No comment provided by engineer. */ +"not synchronized" = "Nicht synchronisiert"; + /* No comment provided by engineer. */ "Notes" = "Anmerkungen"; @@ -3587,6 +3642,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "Aus"; @@ -3599,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "angeboten %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3692,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Nutzungsbedingungen öffnen"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Gruppe öffnen"; +/* alert title */ +"Open link?" = "Link öffnen?"; + /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; @@ -3797,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "ausstehende Genehmigung"; +/* No comment provided by engineer. */ +"pending review" = "Ausstehende Überprüfung"; + /* No comment provided by engineer. */ "Periodic" = "Periodisch"; @@ -3866,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können."; + /* token info */ "Please wait for token activation to complete." = "Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist."; @@ -4146,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Kontaktanfrage ablehnen"; +/* alert title */ +"Reject member?" = "Mitglied ablehnen?"; + /* No comment provided by engineer. */ "rejected" = "abgelehnt"; @@ -4185,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "Die Kontaktadresse wurde entfernt"; +/* No comment provided by engineer. */ +"removed from group" = "Von der Gruppe entfernt"; + /* profile update event chat item */ "removed profile picture" = "Das Profil-Bild wurde entfernt"; @@ -4200,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Verschlüsselung neu aushandeln?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Verbindungsanfrage wiederholen?"; - /* No comment provided by engineer. */ "Repeat download" = "Herunterladen wiederholen"; /* No comment provided by engineer. */ "Repeat import" = "Import wiederholen"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Verbindungsanfrage wiederholen?"; - /* No comment provided by engineer. */ "Repeat upload" = "Hochladen wiederholen"; @@ -4233,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "Grund der Meldung?"; +/* alert title */ +"Report sent to moderators" = "Meldung wurde an die Moderatoren gesendet"; + /* report reason */ "Report spam: only group moderators will see it." = "Spam melden: Nur Gruppenmoderatoren werden es sehen."; @@ -4248,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Meldungen"; +/* No comment provided by engineer. */ +"request to join rejected" = "Beitrittsanfrage abgelehnt"; + /* chat list item title */ "requested to connect" = "Zur Verbindung aufgefordert"; @@ -4302,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Aufdecken"; +/* No comment provided by engineer. */ +"review" = "Überprüfung"; + /* No comment provided by engineer. */ "Review conditions" = "Nutzungsbedingungen einsehen"; +/* admission stage */ +"Review members" = "Überprüfung der Mitglieder"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\")."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "Von Administratoren überprüft"; + /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -4333,6 +4418,9 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)"; +/* alert title */ +"Save admission settings?" = "Speichern der Aufnahme-Einstellungen?"; + /* alert button */ "Save and notify contact" = "Speichern und Kontakt benachrichtigen"; @@ -4669,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen."; +/* No comment provided by engineer. */ +"Set member admission" = "Aufnahme von Mitgliedern festlegen"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Verfallsdatum von Nachrichten in Chats festlegen."; @@ -5100,9 +5191,6 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**."; @@ -5172,12 +5260,6 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Diese Gruppe existiert nicht mehr."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Das ist Ihr eigener Einmal-Link!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; - /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Für diesen Link wird eine neuere App-Version benötigt. Bitte aktualisieren Sie die App oder bitten Sie Ihren Kontakt einen kompatiblen Link zu senden."; @@ -5433,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "Verwenden Sie Chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Aktuelles Profil nutzen"; /* No comment provided by engineer. */ @@ -5451,7 +5533,7 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Neues Inkognito-Profil nutzen"; /* No comment provided by engineer. */ @@ -5712,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Sie haben die Verbindung akzeptiert"; +/* snd group event chat item */ +"you accepted this member" = "Sie haben dieses Mitglied übernommen"; + /* No comment provided by engineer. */ "You allow" = "Sie erlauben"; @@ -5724,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "Sie sind bereits mit %@ verbunden."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Sie sind bereits mit %@ verbunden."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Sie sind bereits über diesen Einmal-Link verbunden!"; /* No comment provided by engineer. */ "You are already in group %@." = "Sie sind bereits Mitglied der Gruppe %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Sie sind bereits Mitglied der Gruppe %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Sie sind über diesen Link bereits Mitglied der Gruppe!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Sie sind über diesen Link bereits Mitglied der Gruppe."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Sie sind bereits Mitglied dieser Gruppe!\nVerbindungsanfrage wiederholen?"; /* No comment provided by engineer. */ @@ -5823,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "Den Einladungslink können Sie in den Details der Verbindung nochmals sehen."; +/* alert message */ +"You can view your reports in Chat with admins." = "Sie können Ihre Meldungen im Chat mit den Administratoren sehen."; + /* No comment provided by engineer. */ "You can't send messages!" = "Sie können keine Nachrichten versenden!"; @@ -5844,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Sie entscheiden, wer sich mit Ihnen verbinden kann."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Sie haben über diese Adresse bereits eine Verbindung beantragt!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Sie haben bereits ein Verbindungsanfrage beantragt!\nVerbindungsanfrage wiederholen?"; /* No comment provided by engineer. */ @@ -5916,9 +5998,6 @@ report reason */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Sie werden mit allen Gruppenmitgliedern verbunden."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind."; @@ -5991,15 +6070,15 @@ report reason */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert."; - /* No comment provided by engineer. */ "Your random profile" = "Ihr Zufallsprofil"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 28ba0f0642..bdcd864a5f 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Aceptar"; +/* alert action */ +"Accept as member" = "Aceptar como miembro"; + +/* alert action */ +"Accept as observer" = "Aceptar como observador"; + /* No comment provided by engineer. */ "Accept conditions" = "Aceptar condiciones"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Aceptar incógnito"; +/* alert title */ +"Accept member" = "Aceptar miembro"; + +/* rcv group event chat item */ +"accepted %@" = "%@ aceptado"; + /* call status */ "accepted call" = "llamada aceptada"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "invitación aceptada"; +/* rcv group event chat item */ +"accepted you" = "te ha aceptado"; + /* No comment provided by engineer. */ "Acknowledged" = "Confirmaciones"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "acordando cifrado…"; +/* member criteria value */ +"all" = "todos"; + /* No comment provided by engineer. */ "All" = "Todo"; @@ -589,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "¿Ya está conectado?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "¡Ya en proceso de conexión!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "¡Ya en proceso de unirte al grupo!"; /* pref value */ @@ -905,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "No se pueden enviar mensajes al miembro"; +/* No comment provided by engineer. */ +"can't send messages" = "no se pueden enviar mensajes"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ @@ -988,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "El chat ya existe"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "¡El chat ya existe!"; /* No comment provided by engineer. */ @@ -1042,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; +/* chat toolbar */ +"Chat with admins" = "Chat con administradores"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat con miembro"; + /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chats con miembros"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Comprobar mensajes cada 20 min."; @@ -1186,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Conectar automáticamente"; -/* No comment provided by engineer. */ -"Connect incognito" = "Conectar incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Conectar con ordenador"; @@ -1198,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Conecta más rápido con tus amigos."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "¿Conectarte a tí mismo?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "¿Conectarte a tí mismo?\n¡Este es tu propio enlace de un solo uso!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "¿Conectarte a tí mismo?\n¡Esta es tu propia dirección SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Conectar mediante dirección de contacto"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Conectar mediante enlace"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Conectar mediante enlace de un sólo uso"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Conectar con %@"; /* No comment provided by engineer. */ @@ -1333,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "El contácto ya existe"; +/* No comment provided by engineer. */ +"contact deleted" = "contacto eliminado"; + /* No comment provided by engineer. */ "Contact deleted!" = "¡Contacto eliminado!"; +/* No comment provided by engineer. */ +"contact disabled" = "contacto desactivado"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "el contacto dispone de cifrado de extremo a extremo"; @@ -1354,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Contacto"; +/* No comment provided by engineer. */ +"contact not ready" = "el contacto no está listo"; + /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; @@ -1423,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Crear cola"; -/* No comment provided by engineer. */ -"Create secret group" = "Crea grupo secreto"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Crear dirección SimpleX"; @@ -1602,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "¿Eliminar perfil?"; +/* alert title */ +"Delete chat with member?" = "¿Eliminar el chat con el miembro?"; + /* No comment provided by engineer. */ "Delete chat?" = "¿Eliminar chat?"; @@ -1872,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "No pierdas los mensajes importantes."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "No volver a mostrar"; /* No comment provided by engineer. */ @@ -2116,6 +2150,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Error al aceptar solicitud del contacto"; +/* alert title */ +"Error accepting member" = "Error al aceptar el miembro"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Error al añadir miembro(s)"; @@ -2131,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Error al cambiar rol"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Error cambiando configuración"; /* No comment provided by engineer. */ @@ -2170,19 +2207,22 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Error al descifrar el archivo"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Error al eliminar base de datos"; +/* alert title */ +"Error deleting chat with member" = "Error al eliminar el chat con el miembro"; + /* No comment provided by engineer. */ "Error deleting chat!" = "¡Error al eliminar chat!"; /* No comment provided by engineer. */ "Error deleting connection" = "Error al eliminar conexión"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Error al eliminar base de datos"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Error al eliminar base de datos antigua"; /* No comment provided by engineer. */ @@ -2203,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Error al cifrar base de datos"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Error al importar base de datos"; /* No comment provided by engineer. */ @@ -2236,7 +2276,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Error al registrarse para notificaciones"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Error al expulsar miembro"; /* alert title */ @@ -2586,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "El grupo ya existe"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "¡El grupo ya existe!"; /* No comment provided by engineer. */ @@ -2610,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "La invitación al grupo ya no es válida, ha sido eliminada por el remitente."; +/* No comment provided by engineer. */ +"group is deleted" = "el grupo ha sido eliminado"; + /* No comment provided by engineer. */ "Group link" = "Enlace de grupo"; @@ -2982,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "unirte como %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Unirte al grupo"; /* No comment provided by engineer. */ "Join group conversations" = "Unirse a la conversación del grupo"; -/* No comment provided by engineer. */ -"Join group?" = "¿Unirte al grupo?"; - /* No comment provided by engineer. */ "Join incognito" = "Unirte en modo incógnito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Unirte con el perfil actual"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "¿Unirse a tu grupo?\n¡Este es tu enlace para el grupo %@!"; /* No comment provided by engineer. */ @@ -3138,9 +3175,15 @@ snd error text */ /* profile update event chat item */ "member %@ changed to %@" = "el miembro %1$@ ha cambiado a %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Admisión del miembro"; + /* rcv group event chat item */ "member connected" = "conectado"; +/* No comment provided by engineer. */ +"member has old version" = "el miembro usa una versión antigua"; + /* item status text */ "Member inactive" = "Miembro inactivo"; @@ -3162,6 +3205,9 @@ snd error text */ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No puede deshacerse!"; +/* alert message */ +"Member will join the group, accept member?" = "El miembro se unirá al grupo, ¿aceptas al miembro?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "Los miembros pueden añadir reacciones a los mensajes."; @@ -3432,6 +3478,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Nuevo rol de miembro"; +/* rcv group event chat item */ +"New member wants to join the group." = "Un miembro nuevo desea unirse al grupo."; + /* notification */ "new message" = "mensaje nuevo"; @@ -3471,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Sin chats en la lista %@"; +/* No comment provided by engineer. */ +"No chats with members" = "Sin chats con miembros"; + /* No comment provided by engineer. */ "No contacts selected" = "Ningún contacto seleccionado"; @@ -3555,6 +3607,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "¡No compatible!"; +/* No comment provided by engineer. */ +"not synchronized" = "no sincronizado"; + /* No comment provided by engineer. */ "Notes" = "Notas"; @@ -3587,6 +3642,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "desactivado"; @@ -3599,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "ofrecido %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3692,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Abrir condiciones"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Grupo abierto"; +/* alert title */ +"Open link?" = "¿Abrir enlace?"; + /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; @@ -3797,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "pendiente de aprobación"; +/* No comment provided by engineer. */ +"pending review" = "pendiente de revisión"; + /* No comment provided by engineer. */ "Periodic" = "Periódicamente"; @@ -3849,7 +3913,7 @@ time to disappear */ "Please enter the previous password after restoring database backup. This action can not be undone." = "Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer."; /* No comment provided by engineer. */ -"Please remember or store it securely - there is no way to recover a lost passcode!" = "Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido!"; +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido!"; /* No comment provided by engineer. */ "Please report it to the developers." = "Por favor, informa a los desarrolladores."; @@ -3866,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Por favor, intenta desactivar y reactivar las notificaciones."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo."; + /* token info */ "Please wait for token activation to complete." = "Por favor, espera a que el token de activación se complete."; @@ -4146,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Rechazar solicitud de contacto"; +/* alert title */ +"Reject member?" = "¿Rechazar al miembro?"; + /* No comment provided by engineer. */ "rejected" = "rechazado"; @@ -4185,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "dirección de contacto eliminada"; +/* No comment provided by engineer. */ +"removed from group" = "expulsado del grupo"; + /* profile update event chat item */ "removed profile picture" = "ha eliminado la imagen del perfil"; @@ -4200,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "¿Renegociar cifrado?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "¿Repetir solicitud de conexión?"; - /* No comment provided by engineer. */ "Repeat download" = "Repetir descarga"; /* No comment provided by engineer. */ "Repeat import" = "Repetir importación"; -/* No comment provided by engineer. */ -"Repeat join request?" = "¿Repetir solicitud de admisión?"; - /* No comment provided by engineer. */ "Repeat upload" = "Repetir subida"; @@ -4233,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "¿Motivo del informe?"; +/* alert title */ +"Report sent to moderators" = "Informe enviado a los moderadores"; + /* report reason */ "Report spam: only group moderators will see it." = "Informar de spam: sólo los moderadores del grupo lo verán."; @@ -4248,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Informes"; +/* No comment provided by engineer. */ +"request to join rejected" = "petición para unirse rechazada"; + /* chat list item title */ "requested to connect" = "solicitado para conectar"; @@ -4302,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Revelar"; +/* No comment provided by engineer. */ +"review" = "por revisar"; + /* No comment provided by engineer. */ "Review conditions" = "Revisar condiciones"; +/* admission stage */ +"Review members" = "Revisar miembros"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Revisar miembros antes de admitir."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "revisado por los administradores"; + /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -4333,6 +4418,9 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Guardar (y notificar contactos)"; +/* alert title */ +"Save admission settings?" = "¿Guardar configuración?"; + /* alert button */ "Save and notify contact" = "Guardar y notificar contacto"; @@ -4669,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Úsalo en lugar de la autenticación del sistema."; +/* No comment provided by engineer. */ +"Set member admission" = "Admisión miembro"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Establece el vencimiento para los mensajes en los chats."; @@ -5100,9 +5191,6 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**."; @@ -5172,12 +5260,6 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Este grupo ya no existe."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "¡Este es tu propio enlace de un solo uso!"; - -/* 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 requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Este enlace requiere una versión más reciente de la aplicación. Por favor, actualiza la aplicación o pide a tu contacto un enlace compatible."; @@ -5433,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "Usar Chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Usar perfil actual"; /* No comment provided by engineer. */ @@ -5451,7 +5533,7 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "Usar interfaz de llamada de iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Usar nuevo perfil incógnito"; /* No comment provided by engineer. */ @@ -5712,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Has aceptado la conexión"; +/* snd group event chat item */ +"you accepted this member" = "has aceptado al miembro"; + /* No comment provided by engineer. */ "You allow" = "Permites"; @@ -5724,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "Ya estás conectado con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Ya estás conectando con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "¡Ya estás conectando mediante este enlace de un solo uso!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ya estás en el grupo %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Ya estás uniéndote al grupo %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "¡Ya estás uniéndote al grupo mediante este enlace!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Ya estás uniéndote al grupo mediante este enlace."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "¡En proceso de unirte al grupo!\n¿Repetir solicitud de admisión?"; /* No comment provided by engineer. */ @@ -5823,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "Podrás ver el enlace de invitación en detalles de conexión."; +/* alert message */ +"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con administradores."; + /* No comment provided by engineer. */ "You can't send messages!" = "¡No puedes enviar mensajes!"; @@ -5844,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Tu decides quién se conecta."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "¡Ya has solicitado la conexión mediante esta dirección!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ya has solicitado la conexión\n¿Repetir solicitud?"; /* No comment provided by engineer. */ @@ -5916,9 +5998,6 @@ report reason */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Te conectarás con todos los miembros del grupo."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; @@ -5991,15 +6070,15 @@ report reason */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "El perfil **%@** será compartido."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo."; - /* No comment provided by engineer. */ "Your random profile" = "Tu perfil aleatorio"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 4891c7fb26..6feb2087d3 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -509,7 +509,8 @@ swipe action */ "Can't invite contacts!" = "Kontakteja ei voi kutsua!"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Peruuta"; /* feature offered item */ @@ -651,16 +652,13 @@ set passcode view */ /* server test step */ "Connect" = "Yhdistä"; -/* No comment provided by engineer. */ -"Connect incognito" = "Yhdistä Incognito"; - /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "ole yhteydessä SimpleX Chat -kehittäjiin."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Yhdistä linkin kautta"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Yhdistä kertalinkillä"; /* No comment provided by engineer. */ @@ -774,9 +772,6 @@ set passcode view */ /* server test step */ "Create queue" = "Luo jono"; -/* No comment provided by engineer. */ -"Create secret group" = "Luo salainen ryhmä"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Luo SimpleX-osoite"; @@ -1073,7 +1068,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "Älä salli"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Älä näytä uudelleen"; /* No comment provided by engineer. */ @@ -1247,7 +1242,7 @@ swipe action */ /* No comment provided by engineer. */ "Error changing role" = "Virhe roolin vaihdossa"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Virhe asetuksen muuttamisessa"; /* No comment provided by engineer. */ @@ -1265,7 +1260,7 @@ swipe action */ /* No comment provided by engineer. */ "Error decrypting file" = "Virhe tiedoston salauksen purussa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Virhe keskustelujen tietokannan poistamisessa"; /* No comment provided by engineer. */ @@ -1274,10 +1269,10 @@ swipe action */ /* No comment provided by engineer. */ "Error deleting connection" = "Virhe yhteyden poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Virhe tietokannan poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Virhe vanhan tietokannan poistamisessa"; /* No comment provided by engineer. */ @@ -1295,10 +1290,10 @@ swipe action */ /* No comment provided by engineer. */ "Error encrypting database" = "Virhe tietokannan salauksessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Virhe vietäessä keskustelujen tietokantaa"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Virhe keskustelujen tietokannan tuonnissa"; /* No comment provided by engineer. */ @@ -1307,7 +1302,7 @@ swipe action */ /* alert title */ "Error receiving file" = "Virhe tiedoston vastaanottamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Virhe poistettaessa jäsentä"; /* No comment provided by engineer. */ @@ -1762,7 +1757,7 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "Liity %@:nä"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Liity ryhmään"; /* No comment provided by engineer. */ @@ -2079,6 +2074,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "pois"; @@ -2091,7 +2087,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "tarjottu %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -2910,9 +2908,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; @@ -3072,7 +3067,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Käytä chattia"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Käytä nykyistä profiilia"; /* No comment provided by engineer. */ @@ -3081,7 +3076,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Käytä iOS:n puhelujen käyttöliittymää"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Käytä uutta incognito-profiilia"; /* No comment provided by engineer. */ @@ -3391,10 +3386,10 @@ chat item action */ "Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi."; +"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi."; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi."; /* No comment provided by engineer. */ "Your random profile" = "Satunnainen profiilisi"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 4dd75039dc..db6af254f0 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -586,10 +586,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Déjà connecté ?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Déjà en connexion !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Groupe déjà rejoint !"; /* pref value */ @@ -897,7 +897,8 @@ marked deleted chat item preview text */ "Can't message member" = "Impossible d'envoyer un message à ce membre"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ @@ -979,7 +980,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "La discussion existe déjà"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "La discussion existe déjà !"; /* No comment provided by engineer. */ @@ -1177,9 +1178,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Connexion automatique"; -/* No comment provided by engineer. */ -"Connect incognito" = "Se connecter incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Connexion au bureau"; @@ -1189,25 +1187,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Connectez-vous à vos amis plus rapidement."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Se connecter à soi-même ?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Se connecter à soi-même ?\nIl s'agit de votre propre lien unique !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Se connecter à soi-même ?\nC'est votre propre adresse SimpleX !"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Se connecter via l'adresse de contact"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Se connecter via un lien"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Se connecter via un lien unique"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Se connecter avec %@"; /* No comment provided by engineer. */ @@ -1414,9 +1409,6 @@ set passcode view */ /* server test step */ "Create queue" = "Créer une file d'attente"; -/* No comment provided by engineer. */ -"Create secret group" = "Créer un groupe secret"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Créer une adresse SimpleX"; @@ -1863,7 +1855,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Ne manquez pas les messages importants."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Ne plus afficher"; /* No comment provided by engineer. */ @@ -2122,7 +2114,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Erreur lors du changement de rôle"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Erreur de changement de paramètre"; /* No comment provided by engineer. */ @@ -2161,7 +2153,7 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Erreur lors du déchiffrement du fichier"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Erreur lors de la suppression de la base de données du chat"; /* No comment provided by engineer. */ @@ -2170,10 +2162,10 @@ chat item action */ /* No comment provided by engineer. */ "Error deleting connection" = "Erreur lors de la suppression de la connexion"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Erreur lors de la suppression de la base de données"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Erreur lors de la suppression de l'ancienne base de données"; /* No comment provided by engineer. */ @@ -2194,13 +2186,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Erreur lors du chiffrement de la base de données"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Erreur lors de l'importation de la base de données du chat"; /* No comment provided by engineer. */ @@ -2227,7 +2219,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Erreur lors de l'inscription aux notifications"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Erreur lors de la suppression d'un membre"; /* alert title */ @@ -2568,7 +2560,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Le groupe existe déjà"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Ce groupe existe déjà !"; /* No comment provided by engineer. */ @@ -2934,22 +2926,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "rejoindre entant que %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Rejoindre le groupe"; /* No comment provided by engineer. */ "Join group conversations" = "Participez aux conversations de groupe"; -/* No comment provided by engineer. */ -"Join group?" = "Rejoindre le groupe ?"; - /* No comment provided by engineer. */ "Join incognito" = "Rejoindre en incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Rejoindre avec le profil actuel"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Rejoindre votre groupe ?\nVoici votre lien pour le groupe %@ !"; /* No comment provided by engineer. */ @@ -3479,6 +3465,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "off"; @@ -3491,7 +3478,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "propose %1$@ : %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3578,7 +3567,7 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Ouvrir les conditions"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Ouvrir le groupe"; /* authentication reason */ @@ -4044,18 +4033,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Renégocier le chiffrement ?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Répéter la demande de connexion ?"; - /* No comment provided by engineer. */ "Repeat download" = "Répéter le téléchargement"; /* No comment provided by engineer. */ "Repeat import" = "Répéter l'importation"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Répéter la requête d'adhésion ?"; - /* No comment provided by engineer. */ "Repeat upload" = "Répéter l'envoi"; @@ -4883,9 +4866,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Les mêmes conditions s'appliquent à l'opérateur **%@**."; @@ -4952,12 +4932,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Ce groupe n'existe plus."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Voici votre propre lien unique !"; - -/* 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."; @@ -5198,7 +5172,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Utiliser le chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Utiliser le profil actuel"; /* No comment provided by engineer. */ @@ -5216,7 +5190,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Utiliser l'interface d'appel d'iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Utiliser un nouveau profil incognito"; /* No comment provided by engineer. */ @@ -5477,25 +5451,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected with %@." = "Vous êtes déjà connecté avec %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Vous êtes déjà en train de vous connecter à %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Vous êtes déjà connecté(e) via ce lien unique !"; /* No comment provided by engineer. */ "You are already in group %@." = "Vous êtes déjà dans le groupe %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Vous êtes déjà en train de rejoindre le groupe %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Vous êtes déjà en train de rejoindre le groupe via ce lien !"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Vous êtes déjà en train de rejoindre le groupe via ce lien."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Vous êtes déjà membre de ce groupe !\nRépéter la demande d'adhésion ?"; /* No comment provided by engineer. */ @@ -5597,10 +5568,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "Vous choisissez qui peut se connecter."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Vous avez déjà demandé une connexion via cette adresse !"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; /* No comment provided by engineer. */ @@ -5666,9 +5634,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l'application après 30 secondes en arrière-plan."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Vous vous connecterez à tous les membres du groupe."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu'ils sont actifs."; @@ -5741,15 +5706,15 @@ chat item action */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Votre profil **%@** sera partagé."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil."; - /* No comment provided by engineer. */ "Your random profile" = "Votre profil aléatoire"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 5a9b6b4e38..d541715edf 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -14,7 +14,7 @@ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- partnerek értesítése a törlésről (nem kötelező)\n- profilnevek szóközökkel\n- és még sok más!"; /* No comment provided by engineer. */ -"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése."; +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni időkorlát beállítása az üzenetek eltűnéséhez.\n- előzmények szerkesztése."; /* No comment provided by engineer. */ "!1 colored!" = "!1 színezett!"; @@ -47,7 +47,7 @@ "**e2e encrypted** video call" = "**e2e titkosított** videóhívás"; /* No comment provided by engineer. */ -"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat-kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van."; +"**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Privátabb:** 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken meg lesz osztva a SimpleX Chat kiszolgálóval, de az nem, hogy hány partnere vagy üzenete van."; /* No comment provided by engineer. */ "**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Legprivátabb:** ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Elfogadás"; +/* alert action */ +"Accept as member" = "Befogadás tagként"; + +/* alert action */ +"Accept as observer" = "Befogadás megfigyelőként"; + /* No comment provided by engineer. */ "Accept conditions" = "Feltételek elfogadása"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Elfogadás inkognitóban"; +/* alert title */ +"Accept member" = "Tag befogadása"; + +/* rcv group event chat item */ +"accepted %@" = "befogadta őt: %@"; + /* call status */ "accepted call" = "fogadott hívás"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "elfogadott meghívó"; +/* rcv group event chat item */ +"accepted you" = "befogadta Önt"; + /* No comment provided by engineer. */ "Acknowledged" = "Visszaigazolt"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "titkosítás elfogadása…"; +/* member criteria value */ +"all" = "összes"; + /* No comment provided by engineer. */ "All" = "Összes"; @@ -589,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Már kapcsolódott?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Kapcsolódás folyamatban!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "A csatlakozás folyamatban van a csoporthoz!"; /* pref value */ @@ -905,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; +/* No comment provided by engineer. */ +"can't send messages" = "nem lehet üzeneteket küldeni"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Mégse"; /* No comment provided by engineer. */ @@ -988,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "A csevegés már létezik"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "A csevegés már létezik!"; /* No comment provided by engineer. */ @@ -1042,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; +/* chat toolbar */ +"Chat with admins" = "Csevegés az adminisztrátorokkal"; + +/* No comment provided by engineer. */ +"Chat with member" = "Csevegés a taggal"; + /* No comment provided by engineer. */ "Chats" = "Csevegések"; +/* No comment provided by engineer. */ +"Chats with members" = "Csevegés a tagokkal"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Üzenetek ellenőrzése 20 percenként."; @@ -1186,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Kapcsolódás automatikusan"; -/* No comment provided by engineer. */ -"Connect incognito" = "Kapcsolódás inkognitóban"; - /* No comment provided by engineer. */ "Connect to desktop" = "Társítás számítógéppel"; @@ -1198,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Kapcsolódjon gyorsabban a partnereihez."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Kapcsolódik saját magához?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódik saját magához?\nEz a saját egyszer használható meghívója!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódik saját magához?\nEz a saját SimpleX-címe!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Kapcsolódás a kapcsolattartási címen keresztül"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Kapcsolódás egyszer használható meghívón keresztül"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Kapcsolódás a következővel: %@"; /* No comment provided by engineer. */ @@ -1333,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "A partner már létezik"; +/* No comment provided by engineer. */ +"contact deleted" = "partner törölve"; + /* No comment provided by engineer. */ "Contact deleted!" = "Partner törölve!"; +/* No comment provided by engineer. */ +"contact disabled" = "partner letiltva"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "a partner e2e titkosítással rendelkezik"; @@ -1354,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Csak név"; +/* No comment provided by engineer. */ +"contact not ready" = "a kapcsolat nem áll készen"; + /* No comment provided by engineer. */ "Contact preferences" = "Partnerbeállítások"; @@ -1423,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Sorba állítás létrehozása"; -/* No comment provided by engineer. */ -"Create secret group" = "Titkos csoport létrehozása"; - /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-cím létrehozása"; @@ -1505,7 +1536,7 @@ set passcode view */ "Database ID: %d" = "Adatbázis-azonosító: %d"; /* No comment provided by engineer. */ -"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások."; +"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitelelkülönítési beállítások."; /* No comment provided by engineer. */ "Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat."; @@ -1550,7 +1581,7 @@ set passcode view */ "Decentralized" = "Decentralizált"; /* message decrypt error item */ -"Decryption error" = "Titkosítás visszafejtési hiba"; +"Decryption error" = "Titkosításvisszafejtési hiba"; /* No comment provided by engineer. */ "decryption errors" = "visszafejtési hibák"; @@ -1602,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Törli a csevegési profilt?"; +/* alert title */ +"Delete chat with member?" = "Törli a taggal való csevegést?"; + /* No comment provided by engineer. */ "Delete chat?" = "Törli a csevegést?"; @@ -1777,7 +1811,7 @@ swipe action */ "different migration in the app/database: %@ / %@" = "különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@"; /* No comment provided by engineer. */ -"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció."; +"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitelizoláció."; /* connection level description */ "direct" = "közvetlen"; @@ -1872,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Ne maradjon le a fontos üzenetekről."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Ne mutasd újra"; /* No comment provided by engineer. */ @@ -2116,6 +2150,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Hiba történt a meghívási kérés elfogadásakor"; +/* alert title */ +"Error accepting member" = "Hiba a tag befogadásakor"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor"; @@ -2131,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Hiba történt a szerepkör módosításakor"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Hiba történt a beállítás módosításakor"; /* No comment provided by engineer. */ @@ -2170,26 +2207,29 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Hiba történt a fájl visszafejtésekor"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor"; +/* alert title */ +"Error deleting chat with member" = "Hiba a taggal való csevegés törlésekor"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; /* No comment provided by engineer. */ "Error deleting connection" = "Hiba történt a kapcsolat törlésekor"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Hiba történt az adatbázis törlésekor"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Hiba történt a régi adatbázis törlésekor"; /* No comment provided by engineer. */ "Error deleting token" = "Hiba történt a token törlésekor"; /* No comment provided by engineer. */ -"Error deleting user profile" = "Hiba történt a felhasználó-profil törlésekor"; +"Error deleting user profile" = "Hiba történt a felhasználói profil törlésekor"; /* No comment provided by engineer. */ "Error downloading the archive" = "Hiba történt az archívum letöltésekor"; @@ -2203,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Hiba történt az adatbázis titkosításakor"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Hiba történt a csevegési adatbázis exportálásakor"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Hiba történt a téma exportálásakor: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Hiba történt a csevegési adatbázis importálásakor"; /* No comment provided by engineer. */ @@ -2236,7 +2276,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Hiba történt a tag eltávolításakor"; /* alert title */ @@ -2491,7 +2531,7 @@ snd error text */ "For console" = "Konzolhoz"; /* No comment provided by engineer. */ -"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni."; +"For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Például, ha a partnere egy SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux kiszolgálón keresztül fogja azokat kézbesíteni."; /* No comment provided by engineer. */ "For me" = "Csak magamnak"; @@ -2586,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "A csoport már létezik"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "A csoport már létezik!"; /* No comment provided by engineer. */ @@ -2610,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "A csoportmeghívó már nem érvényes, a küldője eltávolította."; +/* No comment provided by engineer. */ +"group is deleted" = "csoport törölve"; + /* No comment provided by engineer. */ "Group link" = "Csoporthivatkozás"; @@ -2956,10 +2999,10 @@ snd error text */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegési profilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; /* No comment provided by engineer. */ -"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy a partnere 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 a partnere egy régi adatbázis biztonsági mentését használta."; /* 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. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; +"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. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.\n3. A kapcsolat sérült."; /* No comment provided by engineer. */ "It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatait."; @@ -2982,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "csatlakozás mint %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Csatlakozás csoporthoz"; /* No comment provided by engineer. */ "Join group conversations" = "Csatlakozás csoportos beszélgetésekhez"; -/* No comment provided by engineer. */ -"Join group?" = "Csatlakozik a csoporthoz?"; - /* No comment provided by engineer. */ "Join incognito" = "Csatlakozás inkognitóban"; -/* No comment provided by engineer. */ -"Join with current profile" = "Csatlakozás a jelenlegi profillal"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Csatlakozik a csoportjához?\nEz a saját hivatkozása a(z) %@ nevű csoporthoz!"; /* No comment provided by engineer. */ @@ -3138,9 +3175,15 @@ snd error text */ /* profile update event chat item */ "member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Tagbefogadás"; + /* rcv group event chat item */ "member connected" = "kapcsolódott"; +/* No comment provided by engineer. */ +"member has old version" = "a tag régi verziót használ"; + /* item status text */ "Member inactive" = "Inaktív tag"; @@ -3162,6 +3205,9 @@ snd error text */ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!"; +/* alert message */ +"Member will join the group, accept member?" = "A tag csatlakozni akar a csoporthoz, befogadja a tagot?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez."; @@ -3274,10 +3320,10 @@ snd error text */ "Messages were deleted after you selected them." = "Az üzeneteket törölték miután kijelölte őket."; /* 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." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve."; +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve."; /* No comment provided by engineer. */ "Migrate device" = "Eszköz átköltöztetése"; @@ -3432,6 +3478,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Új tag szerepköre"; +/* rcv group event chat item */ +"New member wants to join the group." = "Új tag szeretne csatlakozni a csoporthoz."; + /* notification */ "new message" = "új üzenet"; @@ -3471,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Nincsenek csevegések a(z) %@ nevű listában"; +/* No comment provided by engineer. */ +"No chats with members" = "Nincsenek csevegések a tagokkal"; + /* No comment provided by engineer. */ "No contacts selected" = "Nincs partner kijelölve"; @@ -3508,7 +3560,7 @@ snd error text */ "No message" = "Nincs üzenet"; /* servers error */ -"No message servers." = "Nincsenek üzenet-kiszolgálók."; +"No message servers." = "Nincsenek üzenetkiszolgálók."; /* No comment provided by engineer. */ "No network connection" = "Nincs hálózati kapcsolat"; @@ -3550,11 +3602,14 @@ snd error text */ "No unread chats" = "Nincsenek olvasatlan csevegések"; /* No comment provided by engineer. */ -"No user identifiers." = "Nincsenek felhasználó-azonosítók."; +"No user identifiers." = "Nincsenek felhasználói azonosítók."; /* No comment provided by engineer. */ "Not compatible!" = "Nem kompatibilis!"; +/* No comment provided by engineer. */ +"not synchronized" = "nincs szinkronizálva"; + /* No comment provided by engineer. */ "Notes" = "Jegyzetek"; @@ -3587,6 +3642,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "kikapcsolva"; @@ -3599,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "ajánlotta: %1$@, ekkor: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Rendben"; /* No comment provided by engineer. */ @@ -3615,13 +3673,13 @@ time to disappear */ "One-time invitation link" = "Egyszer használható meghívó"; /* No comment provided by engineer. */ -"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion-kiszolgálók **szükségesek** a kapcsolódáshoz.\nKompatibilis VPN szükséges."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion kiszolgálók **szükségesek** a kapcsolódáshoz.\nKompatibilis VPN szükséges."; /* No comment provided by engineer. */ -"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion-kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN engedélyezése szükséges."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN engedélyezése szükséges."; /* No comment provided by engineer. */ -"Onion hosts will not be used." = "Az onion-kiszolgálók nem lesznek használva."; +"Onion hosts will not be used." = "Az onion kiszolgálók nem lesznek használva."; /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Csak a csevegés tulajdonosai módosíthatják a csevegési beállításokat."; @@ -3692,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Feltételek megnyitása"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Csoport megnyitása"; +/* alert title */ +"Open link?" = "Megnyitja a hivatkozást?"; + /* authentication reason */ "Open migration to another device" = "Átköltöztetés indítása egy másik eszközre"; @@ -3797,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "jóváhagyásra vár"; +/* No comment provided by engineer. */ +"pending review" = "függőben lévő áttekintés"; + /* No comment provided by engineer. */ "Periodic" = "Időszakos"; @@ -3866,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Próbálja meg letiltani és újra engedélyezni az értesítéseket."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérelmét."; + /* token info */ "Please wait for token activation to complete." = "Várjon, amíg a token aktiválása befejeződik."; @@ -4146,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Meghívási kérés elutasítása"; +/* alert title */ +"Reject member?" = "Elutasítja a tagot?"; + /* No comment provided by engineer. */ "rejected" = "elutasítva"; @@ -4153,10 +4223,10 @@ swipe action */ "rejected call" = "elutasított hívás"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet."; +"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát."; +"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát."; /* No comment provided by engineer. */ "Remove" = "Eltávolítás"; @@ -4185,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "eltávolította a kapcsolattartási címet"; +/* No comment provided by engineer. */ +"removed from group" = "eltávolítva a csoportból"; + /* profile update event chat item */ "removed profile picture" = "eltávolította a profilképét"; @@ -4200,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Újraegyezteti a titkosítást?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Megismétli a meghívási kérést?"; - /* No comment provided by engineer. */ "Repeat download" = "Letöltés ismét"; /* No comment provided by engineer. */ "Repeat import" = "Importálás ismét"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Megismétli a meghívási kérést?"; - /* No comment provided by engineer. */ "Repeat upload" = "Feltöltés ismét"; @@ -4233,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "Jelentés indoklása?"; +/* alert title */ +"Report sent to moderators" = "A jelentés el lett küldve a moderátoroknak"; + /* report reason */ "Report spam: only group moderators will see it." = "Kéretlen tartalom jelentése: csak a csoport moderátorai látják."; @@ -4248,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Jelentések"; +/* No comment provided by engineer. */ +"request to join rejected" = "csatlakozási kérelem elutasítva"; + /* chat list item title */ "requested to connect" = "Függőben lévő meghívási kérelem"; @@ -4302,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Felfedés"; +/* No comment provided by engineer. */ +"review" = "áttekintés"; + /* No comment provided by engineer. */ "Review conditions" = "Feltételek felülvizsgálata"; +/* admission stage */ +"Review members" = "Tagok áttekintése"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Tagok áttekintése a befogadás előtt (kopogtatás)."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "áttekintve a moderátorok által"; + /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -4333,6 +4418,9 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Mentés (és a partnerek értesítése)"; +/* alert title */ +"Save admission settings?" = "Elmenti a befogadási beállításokat?"; + /* alert button */ "Save and notify contact" = "Mentés és a partner értesítése"; @@ -4669,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Beállítás a rendszer-hitelesítés helyett."; +/* No comment provided by engineer. */ +"Set member admission" = "Tagbefogadás beállítása"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Üzenetek eltűnési idejének módosítása a csevegésekben."; @@ -4830,7 +4921,7 @@ chat item action */ "SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó"; /* No comment provided by engineer. */ -"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva."; +"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX-protokollokat a Trail of Bits auditálta."; /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognitómód"; @@ -4851,7 +4942,7 @@ chat item action */ "SMP server" = "SMP-kiszolgáló"; /* No comment provided by engineer. */ -"SOCKS proxy" = "SOCKS-proxy"; +"SOCKS proxy" = "SOCKS proxy"; /* blur media */ "Soft" = "Enyhe"; @@ -5059,7 +5150,7 @@ report reason */ "The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be."; /* No comment provided by engineer. */ -"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás."; +"The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-hivatkozás."; /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "A kapcsolat elérte a kézbesítetlen üzenetek számának határát, a partnere lehet, hogy offline állapotban van."; @@ -5100,9 +5191,6 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "A profilja csak a partnereivel van megosztva."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**."; @@ -5172,12 +5260,6 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Ez a csoport már nem létezik."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Ez a saját egyszer használható meghívója!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Ez a saját SimpleX-címe!"; - /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Ez a hivatkozás újabb alkalmazásverziót igényel. Frissítse az alkalmazást vagy kérjen egy kompatibilis hivatkozást a partnerétől."; @@ -5218,7 +5300,7 @@ report reason */ "To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez."; /* No comment provided by engineer. */ -"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához."; +"To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához."; /* No comment provided by engineer. */ "To receive" = "A fogadáshoz"; @@ -5263,7 +5345,7 @@ report reason */ "Total" = "Összes kapcsolat"; /* No comment provided by engineer. */ -"Transport isolation" = "Átvitel-izoláció"; +"Transport isolation" = "Átvitelelkülönítés"; /* No comment provided by engineer. */ "Transport sessions" = "Munkamenetek átvitele"; @@ -5425,7 +5507,7 @@ report reason */ "Uploading archive" = "Archívum feltöltése"; /* No comment provided by engineer. */ -"Use .onion hosts" = "Onion-kiszolgálók használata"; +"Use .onion hosts" = "Onion kiszolgálók használata"; /* No comment provided by engineer. */ "Use %@" = "%@ használata"; @@ -5433,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "SimpleX Chat használata"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Jelenlegi profil használata"; /* No comment provided by engineer. */ @@ -5451,14 +5533,14 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "Az iOS hívási felületét használata"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Új inkognitóprofil használata"; /* No comment provided by engineer. */ "Use only local notifications?" = "Csak helyi értesítések használata?"; /* No comment provided by engineer. */ -"Use private routing with unknown servers when IP address is not protected." = "Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett."; +"Use private routing with unknown servers when IP address is not protected." = "Használjon privát útválasztást az ismeretlen kiszolgálókkal, ha az IP-cím nem védett."; /* No comment provided by engineer. */ "Use private routing with unknown servers." = "Használjon privát útválasztást ismeretlen kiszolgálókkal."; @@ -5473,10 +5555,10 @@ report reason */ "Use short links (BETA)" = "Rövid hivatkozások használata (béta)"; /* No comment provided by engineer. */ -"Use SimpleX Chat servers?" = "SimpleX Chat-kiszolgálók használata?"; +"Use SimpleX Chat servers?" = "SimpleX Chat kiszolgálók használata?"; /* No comment provided by engineer. */ -"Use SOCKS proxy" = "SOCKS-proxy használata"; +"Use SOCKS proxy" = "SOCKS proxy használata"; /* No comment provided by engineer. */ "Use TCP port %@ when no port is specified." = "A következő TCP-port használata, amikor nincs port megadva: %@."; @@ -5500,7 +5582,7 @@ report reason */ "Username" = "Felhasználónév"; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "SimpleX Chat-kiszolgálók használatban."; +"Using SimpleX Chat servers." = "SimpleX Chat kiszolgálók használatban."; /* No comment provided by engineer. */ "v%@" = "v%@"; @@ -5680,10 +5762,10 @@ report reason */ "With reduced battery usage." = "Csökkentett akkumulátor-használattal."; /* No comment provided by engineer. */ -"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára."; +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára."; /* alert message */ -"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@."; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@."; /* No comment provided by engineer. */ "Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; @@ -5712,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Kapcsolat létrehozása"; +/* snd group event chat item */ +"you accepted this member" = "Ön befogadta ezt a tagot"; + /* No comment provided by engineer. */ "You allow" = "Ön engedélyezi"; @@ -5724,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "Ön már kapcsolódva van vele: %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "A kapcsolódás már folyamatban van a következőhöz: %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "A kapcsolódás már folyamatban van ezen az egyszer használható meghívón keresztül!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ön már a(z) %@ nevű csoport tagja."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "A csatlakozás már folyamatban van a(z) %@ nevű csoporthoz."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "A csatlakozás már folyamatban van a csoporthoz!\nMegismétli a meghívási kérést?"; /* No comment provided by engineer. */ @@ -5823,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél."; +/* alert message */ +"You can view your reports in Chat with admins." = "A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben."; + /* No comment provided by engineer. */ "You can't send messages!" = "Nem lehet üzeneteket küldeni!"; @@ -5844,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Ön dönti el, hogy kivel beszélget."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Már küldött egy meghívási kérést ezen a címen keresztül!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ön már küldött egy meghívási kérést!\nMegismétli a meghívási kérést?"; /* No comment provided by engineer. */ @@ -5916,9 +5998,6 @@ report reason */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Az alkalmazás elindításához vagy 30 másodpercnyi háttérben töltött idő után, az alkalmazáshoz való visszatéréshez hitelesítésre lesz szükség."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Kapcsolódni fog a csoport összes tagjához."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak."; @@ -5992,14 +6071,14 @@ report reason */ "Your profile **%@** will be shared." = "A(z) **%@** nevű profilja meg lesz osztva."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját."; +"Your profile is stored on your device and only shared with your contacts." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX kiszolgálók nem láthatják a profilját."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve a partnerei számára."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva."; - /* No comment provided by engineer. */ "Your random profile" = "Véletlenszerű profil"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index b914a06079..d53329134d 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Accetta"; +/* alert action */ +"Accept as member" = "Accetta come membro"; + +/* alert action */ +"Accept as observer" = "Accetta come osservatore"; + /* No comment provided by engineer. */ "Accept conditions" = "Accetta le condizioni"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Accetta in incognito"; +/* alert title */ +"Accept member" = "Accetta membro"; + +/* rcv group event chat item */ +"accepted %@" = "%@ accettato"; + /* call status */ "accepted call" = "chiamata accettata"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "invito accettato"; +/* rcv group event chat item */ +"accepted you" = "ti ha accettato/a"; + /* No comment provided by engineer. */ "Acknowledged" = "Riconosciuto"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "concordando la crittografia…"; +/* member criteria value */ +"all" = "tutti"; + /* No comment provided by engineer. */ "All" = "Tutte"; @@ -589,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Già connesso/a?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Già in connessione!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Già in ingresso nel gruppo!"; /* pref value */ @@ -905,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "Impossibile inviare un messaggio al membro"; +/* No comment provided by engineer. */ +"can't send messages" = "impossibile inviare messaggi"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ @@ -988,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "La chat esiste già"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "La chat esiste già!"; /* No comment provided by engineer. */ @@ -1042,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!"; +/* chat toolbar */ +"Chat with admins" = "Chat con amministratori"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chatta con il membro"; + /* No comment provided by engineer. */ "Chats" = "Chat"; +/* No comment provided by engineer. */ +"Chats with members" = "Chat con membri"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Controlla i messaggi ogni 20 min."; @@ -1186,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Connetti automaticamente"; -/* No comment provided by engineer. */ -"Connect incognito" = "Connetti in incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Connetti al desktop"; @@ -1198,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Connettiti più velocemente ai tuoi amici."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Connettersi a te stesso?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Connettersi a te stesso?\nQuesto è il tuo link una tantum!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Connettersi a te stesso?\nQuesto è il tuo indirizzo SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Connettere via indirizzo del contatto"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Connetti via link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Connetti via link una tantum"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Connettersi con %@"; /* No comment provided by engineer. */ @@ -1333,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "Il contatto esiste già"; +/* No comment provided by engineer. */ +"contact deleted" = "contatto eliminato"; + /* No comment provided by engineer. */ "Contact deleted!" = "Contatto eliminato!"; +/* No comment provided by engineer. */ +"contact disabled" = "contatto disattivato"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "il contatto ha la crittografia e2e"; @@ -1354,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Nome del contatto"; +/* No comment provided by engineer. */ +"contact not ready" = "contatto non pronto"; + /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; @@ -1423,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Crea coda"; -/* No comment provided by engineer. */ -"Create secret group" = "Crea gruppo segreto"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Crea indirizzo SimpleX"; @@ -1602,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Eliminare il profilo di chat?"; +/* alert title */ +"Delete chat with member?" = "Eliminare la chat con il membro?"; + /* No comment provided by engineer. */ "Delete chat?" = "Eliminare la chat?"; @@ -1872,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Non perdere messaggi importanti."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Non mostrare più"; /* No comment provided by engineer. */ @@ -2116,6 +2150,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto"; +/* alert title */ +"Error accepting member" = "Errore di accettazione del membro"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Errore di aggiunta membro/i"; @@ -2131,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Errore nel cambio di ruolo"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Errore nella modifica dell'impostazione"; /* No comment provided by engineer. */ @@ -2170,19 +2207,22 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Errore decifrando il file"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Errore nell'eliminazione del database della chat"; +/* alert title */ +"Error deleting chat with member" = "Errore di eliminazione della chat con il membro"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Errore nell'eliminazione della chat!"; /* No comment provided by engineer. */ "Error deleting connection" = "Errore nell'eliminazione della connessione"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Errore nell'eliminazione del database"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Errore nell'eliminazione del database vecchio"; /* No comment provided by engineer. */ @@ -2203,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Errore nella crittografia del database"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Errore nell'esportazione del database della chat"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Errore di esportazione del tema: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Errore nell'importazione del database della chat"; /* No comment provided by engineer. */ @@ -2236,7 +2276,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Errore di registrazione per le notifiche"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Errore nella rimozione del membro"; /* alert title */ @@ -2586,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Il gruppo esiste già"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Il gruppo esiste già!"; /* No comment provided by engineer. */ @@ -2610,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "L'invito al gruppo non è più valido, è stato rimosso dal mittente."; +/* No comment provided by engineer. */ +"group is deleted" = "il gruppo è eliminato"; + /* No comment provided by engineer. */ "Group link" = "Link del gruppo"; @@ -2982,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "entra come %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Entra nel gruppo"; /* No comment provided by engineer. */ "Join group conversations" = "Entra in conversazioni di gruppo"; -/* No comment provided by engineer. */ -"Join group?" = "Entrare nel gruppo?"; - /* No comment provided by engineer. */ "Join incognito" = "Entra in incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Entra con il profilo attuale"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Entrare nel tuo gruppo?\nQuesto è il tuo link per il gruppo %@!"; /* No comment provided by engineer. */ @@ -3138,9 +3175,15 @@ snd error text */ /* profile update event chat item */ "member %@ changed to %@" = "il membro %1$@ è diventato %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Ammissione del membro"; + /* rcv group event chat item */ "member connected" = "si è connesso/a"; +/* No comment provided by engineer. */ +"member has old version" = "il membro ha una versione vecchia"; + /* item status text */ "Member inactive" = "Membro inattivo"; @@ -3162,6 +3205,9 @@ snd error text */ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* alert message */ +"Member will join the group, accept member?" = "Il membro entrerà nel gruppo, accettarlo?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi."; @@ -3432,6 +3478,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Nuovo ruolo del membro"; +/* rcv group event chat item */ +"New member wants to join the group." = "Un nuovo membro vuole entrare nel gruppo."; + /* notification */ "new message" = "messaggio nuovo"; @@ -3471,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Nessuna chat nell'elenco %@"; +/* No comment provided by engineer. */ +"No chats with members" = "Nessuna chat con membri"; + /* No comment provided by engineer. */ "No contacts selected" = "Nessun contatto selezionato"; @@ -3555,6 +3607,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "Non compatibile!"; +/* No comment provided by engineer. */ +"not synchronized" = "non sincronizzato"; + /* No comment provided by engineer. */ "Notes" = "Note"; @@ -3587,6 +3642,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "off"; @@ -3599,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "offerto %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3692,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Apri le condizioni"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Apri gruppo"; +/* alert title */ +"Open link?" = "Aprire il link?"; + /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; @@ -3797,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "in attesa di approvazione"; +/* No comment provided by engineer. */ +"pending review" = "in attesa di revisione"; + /* No comment provided by engineer. */ "Periodic" = "Periodicamente"; @@ -3866,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Prova a disattivare e riattivare le notifiche."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo."; + /* token info */ "Please wait for token activation to complete." = "Attendi il completamento dell'attivazione del token."; @@ -4146,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Rifiuta la richiesta di contatto"; +/* alert title */ +"Reject member?" = "Rifiutare il membro?"; + /* No comment provided by engineer. */ "rejected" = "rifiutato"; @@ -4185,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "indirizzo di contatto rimosso"; +/* No comment provided by engineer. */ +"removed from group" = "rimosso dal gruppo"; + /* profile update event chat item */ "removed profile picture" = "immagine del profilo rimossa"; @@ -4200,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Rinegoziare la crittografia?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Ripetere la richiesta di connessione?"; - /* No comment provided by engineer. */ "Repeat download" = "Ripeti scaricamento"; /* No comment provided by engineer. */ "Repeat import" = "Ripeti importazione"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Ripetere la richiesta di ingresso?"; - /* No comment provided by engineer. */ "Repeat upload" = "Ripeti caricamento"; @@ -4233,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "Motivo della segnalazione?"; +/* alert title */ +"Report sent to moderators" = "Segnalazione inviata ai moderatori"; + /* report reason */ "Report spam: only group moderators will see it." = "Segnala spam: solo i moderatori del gruppo lo vedranno."; @@ -4248,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Segnalazioni"; +/* No comment provided by engineer. */ +"request to join rejected" = "richiesta di entrare rifiutata"; + /* chat list item title */ "requested to connect" = "richiesto di connettersi"; @@ -4302,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Rivela"; +/* No comment provided by engineer. */ +"review" = "revisiona"; + /* No comment provided by engineer. */ "Review conditions" = "Leggi le condizioni"; +/* admission stage */ +"Review members" = "Revisiona i membri"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Revisiona i membri prima di ammetterli (\"bussare\")."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "revisionato dagli amministratori"; + /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -4333,6 +4418,9 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Salva (e avvisa i contatti)"; +/* alert title */ +"Save admission settings?" = "Salvare le impostazioni di ammissione?"; + /* alert button */ "Save and notify contact" = "Salva e avvisa il contatto"; @@ -4669,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Impostalo al posto dell'autenticazione di sistema."; +/* No comment provided by engineer. */ +"Set member admission" = "Imposta l'ammissione del membro"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Imposta la scadenza dei messaggi nelle chat."; @@ -5100,9 +5191,6 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**."; @@ -5172,12 +5260,6 @@ report reason */ /* No comment provided by engineer. */ "This group no longer exists." = "Questo gruppo non esiste più."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Questo è il tuo link una tantum!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; - /* No comment provided by engineer. */ "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Questo link richiede una versione più recente dell'app. Aggiornala o chiedi al tuo contatto di inviare un link compatibile."; @@ -5433,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "Usa la chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Usa il profilo attuale"; /* No comment provided by engineer. */ @@ -5451,7 +5533,7 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "Usa interfaccia di chiamata iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Usa nuovo profilo in incognito"; /* No comment provided by engineer. */ @@ -5712,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Hai accettato la connessione"; +/* snd group event chat item */ +"you accepted this member" = "hai accettato questo membro"; + /* No comment provided by engineer. */ "You allow" = "Lo consenti"; @@ -5724,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "Sei già connesso/a con %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Ti stai già connettendo a %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Ti stai già connettendo tramite questo link una tantum!"; /* No comment provided by engineer. */ "You are already in group %@." = "Sei già nel gruppo %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Stai già entrando nel gruppo %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Stai già entrando nel gruppo tramite questo link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Stai già entrando nel gruppo tramite questo link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Stai già entrando nel gruppo!\nRipetere la richiesta di ingresso?"; /* No comment provided by engineer. */ @@ -5823,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "Puoi vedere di nuovo il link di invito nei dettagli di connessione."; +/* alert message */ +"You can view your reports in Chat with admins." = "Puoi vedere le tue segnalazioni nella chat con gli amministratori."; + /* No comment provided by engineer. */ "You can't send messages!" = "Non puoi inviare messaggi!"; @@ -5844,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Sei tu a decidere chi può connettersi."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Hai già richiesto la connessione tramite questo indirizzo!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Hai già richiesto la connessione!\nRipetere la richiesta di connessione?"; /* No comment provided by engineer. */ @@ -5916,9 +5998,6 @@ report reason */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Dovrai autenticarti quando avvii o riapri l'app dopo 30 secondi in secondo piano."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Ti connetterai a tutti i membri del gruppo."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi."; @@ -5991,15 +6070,15 @@ report reason */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo."; - /* No comment provided by engineer. */ "Your random profile" = "Il tuo profilo casuale"; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index d214f88e1c..206794c0c0 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -484,10 +484,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "すでに接続済みですか?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "既に接続中です!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "すでにグループに参加しています!"; /* pref value */ @@ -647,7 +647,8 @@ swipe action */ "Can't invite contacts!" = "連絡先を招待できません!"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "中止"; /* feature offered item */ @@ -807,9 +808,6 @@ set passcode view */ /* server test step */ "Connect" = "接続"; -/* No comment provided by engineer. */ -"Connect incognito" = "シークレットモードで接続"; - /* No comment provided by engineer. */ "Connect to desktop" = "デスクトップに接続"; @@ -819,10 +817,10 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "友達ともっと速くつながりましょう。"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "リンク経由で接続"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "ワンタイムリンクで接続"; /* No comment provided by engineer. */ @@ -963,9 +961,6 @@ set passcode view */ /* server test step */ "Create queue" = "キューの作成"; -/* No comment provided by engineer. */ -"Create secret group" = "シークレットグループを作成する"; - /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleXアドレスの作成"; @@ -1277,7 +1272,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "有効にしない"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "次から表示しない"; /* No comment provided by engineer. */ @@ -1454,7 +1449,7 @@ swipe action */ /* No comment provided by engineer. */ "Error changing role" = "役割変更にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "設定変更にエラー発生"; /* No comment provided by engineer. */ @@ -1475,7 +1470,7 @@ swipe action */ /* No comment provided by engineer. */ "Error decrypting file" = "ファイルの復号エラー"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "チャットデータベース削除にエラー発生"; /* No comment provided by engineer. */ @@ -1484,10 +1479,10 @@ swipe action */ /* No comment provided by engineer. */ "Error deleting connection" = "接続の削除エラー"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "データベースの削除にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "古いデータベースを削除にエラー発生"; /* No comment provided by engineer. */ @@ -1502,10 +1497,10 @@ swipe action */ /* No comment provided by engineer. */ "Error encrypting database" = "データベース暗号化ににエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "チャットデータベースのエキスポートにエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "チャットデータベースのインポートにエラー発生"; /* No comment provided by engineer. */ @@ -1514,7 +1509,7 @@ swipe action */ /* alert title */ "Error receiving file" = "ファイル受信にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "メンバー除名にエラー発生"; /* No comment provided by engineer. */ @@ -1969,7 +1964,7 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "%@ として参加"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "グループに参加"; /* No comment provided by engineer. */ @@ -2295,6 +2290,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "オフ"; @@ -2307,7 +2303,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "提供された %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "OK"; /* No comment provided by engineer. */ @@ -3111,9 +3109,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。"; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "長らくお待たせしました! ✅"; @@ -3270,7 +3265,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "チャット"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "現在のプロファイルを使用する"; /* No comment provided by engineer. */ @@ -3279,7 +3274,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "iOS通話インターフェースを使用する"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "新しいシークレットプロファイルを使用する"; /* No comment provided by engineer. */ @@ -3592,10 +3587,10 @@ chat item action */ "Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。"; +"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。"; /* No comment provided by engineer. */ "Your random profile" = "あなたのランダム・プロフィール"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 232de56641..1dff6a5a7f 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Accepteer"; +/* alert action */ +"Accept as member" = "Accepteren als lid"; + +/* alert action */ +"Accept as observer" = "Accepteren als waarnemer"; + /* No comment provided by engineer. */ "Accept conditions" = "Accepteer voorwaarden"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Accepteer incognito"; +/* alert title */ +"Accept member" = "Lid accepteren"; + +/* rcv group event chat item */ +"accepted %@" = "geaccepteerd %@"; + /* call status */ "accepted call" = "geaccepteerde oproep"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "geaccepteerde uitnodiging"; +/* rcv group event chat item */ +"accepted you" = "heb je geaccepteerd"; + /* No comment provided by engineer. */ "Acknowledged" = "Erkend"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "versleuteling overeenkomen…"; +/* member criteria value */ +"all" = "alle"; + /* No comment provided by engineer. */ "All" = "alle"; @@ -505,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "All reports will be archived for you." = "Alle rapporten worden voor u gearchiveerd."; +/* No comment provided by engineer. */ +"All servers" = "Alle servers"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; @@ -586,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Al verbonden?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Al bezig met verbinden!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Al lid van de groep!"; /* pref value */ @@ -902,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "Kan geen bericht sturen naar lid"; +/* No comment provided by engineer. */ +"can't send messages" = "kan geen berichten versturen"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Annuleren"; /* No comment provided by engineer. */ @@ -985,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Chat bestaat al"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Chat bestaat al!"; /* No comment provided by engineer. */ @@ -1039,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!"; +/* chat toolbar */ +"Chat with admins" = "Chat met beheerders"; + +/* No comment provided by engineer. */ +"Chat with member" = "Chat met lid"; + /* No comment provided by engineer. */ "Chats" = "Chats"; +/* No comment provided by engineer. */ +"Chats with members" = "Chats met leden"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Controleer uw berichten elke 20 minuten."; @@ -1183,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Automatisch verbinden"; -/* No comment provided by engineer. */ -"Connect incognito" = "Verbind incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Verbinden met desktop"; @@ -1195,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Maak sneller verbinding met je vrienden."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Verbinding maken met jezelf?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Verbinding maken met jezelf?\nDit is uw eigen eenmalige link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Verbinding maken met jezelf?\nDit is uw eigen SimpleX adres!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Verbinding maken via contactadres"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Maak verbinding via link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Verbinden via een eenmalige link?"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Verbonden met %@"; /* No comment provided by engineer. */ @@ -1330,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "Contact bestaat al"; +/* No comment provided by engineer. */ +"contact deleted" = "contact verwijderd"; + /* No comment provided by engineer. */ "Contact deleted!" = "Contact verwijderd!"; +/* No comment provided by engineer. */ +"contact disabled" = "contact uitgeschakeld"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "contact heeft e2e-codering"; @@ -1351,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Contact naam"; +/* No comment provided by engineer. */ +"contact not ready" = "contact niet klaar"; + /* No comment provided by engineer. */ "Contact preferences" = "Contact voorkeuren"; @@ -1420,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Maak een wachtrij"; -/* No comment provided by engineer. */ -"Create secret group" = "Maak een geheime groep aan"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Maak een SimpleX adres aan"; @@ -1599,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Chatprofiel verwijderen?"; +/* alert title */ +"Delete chat with member?" = "Chat met lid verwijderen?"; + /* No comment provided by engineer. */ "Delete chat?" = "Chat verwijderen?"; @@ -1869,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Mis geen belangrijke berichten."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Niet meer weergeven"; /* No comment provided by engineer. */ @@ -2113,6 +2150,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Fout bij het accepteren van een contactverzoek"; +/* alert title */ +"Error accepting member" = "Fout bij het accepteren van lid"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Fout bij het toevoegen van leden"; @@ -2128,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Fout bij wisselen van rol"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Fout bij wijzigen van instelling"; /* No comment provided by engineer. */ @@ -2167,19 +2207,22 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Fout bij het ontsleutelen van bestand"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Fout bij het verwijderen van de chat database"; +/* alert title */ +"Error deleting chat with member" = "Fout bij het verwijderen van chat met lid"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Fout bij verwijderen gesprek!"; /* No comment provided by engineer. */ "Error deleting connection" = "Fout bij verwijderen van verbinding"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Fout bij het verwijderen van de database"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Fout bij het verwijderen van de oude database"; /* No comment provided by engineer. */ @@ -2200,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Fout bij het versleutelen van de database"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Fout bij het importeren van de chat database"; /* No comment provided by engineer. */ @@ -2233,7 +2276,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Fout bij registreren voor meldingen"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Fout bij verwijderen van lid"; /* alert title */ @@ -2583,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Groep bestaat al"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Groep bestaat al!"; /* No comment provided by engineer. */ @@ -2607,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Groep uitnodiging is niet meer geldig, deze is verwijderd door de afzender."; +/* No comment provided by engineer. */ +"group is deleted" = "groep is verwijderd"; + /* No comment provided by engineer. */ "Group link" = "Groep link"; @@ -2979,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "deelnemen als %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Word lid van groep"; /* No comment provided by engineer. */ "Join group conversations" = "Neem deel aan groepsgesprekken"; -/* No comment provided by engineer. */ -"Join group?" = "Deelnemen aan groep?"; - /* No comment provided by engineer. */ "Join incognito" = "Doe incognito mee"; -/* No comment provided by engineer. */ -"Join with current profile" = "Word lid met huidig profiel"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Sluit u aan bij uw groep?\nDit is jouw link voor groep %@!"; /* No comment provided by engineer. */ @@ -3135,9 +3175,15 @@ snd error text */ /* profile update event chat item */ "member %@ changed to %@" = "lid %1$@ gewijzigd in %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Toelating van leden"; + /* rcv group event chat item */ "member connected" = "is toegetreden"; +/* No comment provided by engineer. */ +"member has old version" = "lid heeft oude versie"; + /* item status text */ "Member inactive" = "Lid inactief"; @@ -3159,6 +3205,9 @@ snd error text */ /* 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!"; +/* alert message */ +"Member will join the group, accept member?" = "Lid zal toetreden tot de groep, lid accepteren?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; @@ -3429,6 +3478,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Nieuwe leden rol"; +/* rcv group event chat item */ +"New member wants to join the group." = "Nieuw lid wil zich bij de groep aansluiten."; + /* notification */ "new message" = "nieuw bericht"; @@ -3468,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Geen chats in lijst %@"; +/* No comment provided by engineer. */ +"No chats with members" = "Geen chats met leden"; + /* No comment provided by engineer. */ "No contacts selected" = "Geen contacten geselecteerd"; @@ -3552,6 +3607,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "Niet compatibel!"; +/* No comment provided by engineer. */ +"not synchronized" = "niet gesynchroniseerd"; + /* No comment provided by engineer. */ "Notes" = "Notities"; @@ -3584,6 +3642,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "uit"; @@ -3596,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "voorgesteld %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "OK"; /* No comment provided by engineer. */ @@ -3689,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Open voorwaarden"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Open groep"; +/* alert title */ +"Open link?" = "Link openen?"; + /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; @@ -3794,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "in afwachting van goedkeuring"; +/* No comment provided by engineer. */ +"pending review" = "in afwachting van beoordeling"; + /* No comment provided by engineer. */ "Periodic" = "Periodiek"; @@ -3863,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Probeer meldingen uit en weer in te schakelen."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld."; + /* token info */ "Please wait for token activation to complete." = "Wacht tot de tokenactivering voltooid is."; @@ -4143,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Contactverzoek afwijzen"; +/* alert title */ +"Reject member?" = "Lid afwijzen?"; + /* No comment provided by engineer. */ "rejected" = "afgewezen"; @@ -4182,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "contactadres verwijderd"; +/* No comment provided by engineer. */ +"removed from group" = "verwijderd uit de groep"; + /* profile update event chat item */ "removed profile picture" = "profielfoto verwijderd"; @@ -4197,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Heronderhandelen over versleuteling?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Verbindingsverzoek herhalen?"; - /* No comment provided by engineer. */ "Repeat download" = "Herhaal het downloaden"; /* No comment provided by engineer. */ "Repeat import" = "Herhaal import"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Deelnameverzoek herhalen?"; - /* No comment provided by engineer. */ "Repeat upload" = "Herhaal het uploaden"; @@ -4230,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "Reden melding?"; +/* alert title */ +"Report sent to moderators" = "Rapport verzonden naar moderators"; + /* report reason */ "Report spam: only group moderators will see it." = "Spam melden: alleen groepsmoderators kunnen het zien."; @@ -4245,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Rapporten"; +/* No comment provided by engineer. */ +"request to join rejected" = "verzoek tot toetreding afgewezen"; + /* chat list item title */ "requested to connect" = "verzocht om verbinding te maken"; @@ -4299,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Onthullen"; +/* No comment provided by engineer. */ +"review" = "beoordeling"; + /* No comment provided by engineer. */ "Review conditions" = "Voorwaarden bekijken"; +/* admission stage */ +"Review members" = "Leden beoordelen"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Controleer de leden voordat u ze toelaat ('knocking')."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "beoordeeld door beheerders"; + /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -4330,6 +4418,9 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Bewaar (en informeer contacten)"; +/* alert title */ +"Save admission settings?" = "Toegangsinstellingen opslaan?"; + /* alert button */ "Save and notify contact" = "Opslaan en Contact melden"; @@ -4666,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Stel het in in plaats van systeemverificatie."; +/* No comment provided by engineer. */ +"Set member admission" = "Toegang voor leden instellen"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Stel de berichtvervaldatum in chats in."; @@ -4739,6 +4833,9 @@ chat item action */ /* No comment provided by engineer. */ "Share with contacts" = "Delen met contacten"; +/* No comment provided by engineer. */ +"Short link" = "Korte link"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Toon → bij berichten verzonden via privéroutering."; @@ -4781,6 +4878,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "SimpleX adres of eenmalige link?"; +/* simplex link type */ +"SimpleX channel link" = "SimpleX channel link"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "Simplex-chat en flux hebben een overeenkomst gemaakt om door flux geëxploiteerde servers in de app op te nemen."; @@ -5091,9 +5191,6 @@ report reason */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**."; @@ -5164,10 +5261,7 @@ report reason */ "This group no longer exists." = "Deze groep bestaat niet meer."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Dit is uw eigen eenmalige link!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Dit is uw eigen SimpleX adres!"; +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Voor deze link is een nieuwere app-versie vereist. Werk de app bij of vraag je contactpersoon om een compatibele link te sturen."; /* 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."; @@ -5361,6 +5455,9 @@ report reason */ /* swipe action */ "Unread" = "Ongelezen"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Niet-ondersteunde verbindingslink"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Er worden maximaal 100 laatste berichten naar nieuwe leden verzonden."; @@ -5418,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "Gebruik chat"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Gebruik het huidige profiel"; /* No comment provided by engineer. */ @@ -5436,7 +5533,7 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "De iOS-oproepinterface gebruiken"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Gebruik een nieuw incognitoprofiel"; /* No comment provided by engineer. */ @@ -5454,6 +5551,9 @@ report reason */ /* No comment provided by engineer. */ "Use servers" = "Gebruik servers"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Gebruik korte links (BETA)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "SimpleX Chat servers gebruiken?"; @@ -5463,6 +5563,9 @@ report reason */ /* No comment provided by engineer. */ "Use TCP port %@ when no port is specified." = "Gebruik TCP-poort %@ als er geen poort is opgegeven."; +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Gebruik de app tijdens het gesprek."; @@ -5691,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Je hebt de verbinding geaccepteerd"; +/* snd group event chat item */ +"you accepted this member" = "je hebt dit lid geaccepteerd"; + /* No comment provided by engineer. */ "You allow" = "Jij staat toe"; @@ -5703,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "U bent al verbonden met %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "U maakt al verbinding met %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Je maakt al verbinding via deze eenmalige link!"; /* No comment provided by engineer. */ "You are already in group %@." = "Je zit al in groep %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Je bent al lid van de groep %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Je wordt al lid van de groep via deze link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Je wordt al lid van de groep via deze link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Je sluit je al aan bij de groep!\nDeelnameverzoek herhalen?"; /* No comment provided by engineer. */ @@ -5802,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails."; +/* alert message */ +"You can view your reports in Chat with admins." = "U kunt uw rapporten bekijken in Chat met beheerders."; + /* No comment provided by engineer. */ "You can't send messages!" = "Je kunt geen berichten versturen!"; @@ -5823,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Jij bepaalt wie er verbinding mag maken."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "U heeft al een verbinding aangevraagd via dit adres!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Je hebt al verbinding aangevraagd!\nVerbindingsverzoek herhalen?"; /* No comment provided by engineer. */ @@ -5895,9 +5998,6 @@ report reason */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Je maakt verbinding met alle leden."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn."; @@ -5970,15 +6070,15 @@ report reason */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen."; - /* No comment provided by engineer. */ "Your random profile" = "Je willekeurige profiel"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 31a9b87662..8f322548dd 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -583,10 +583,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Już połączony?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Już połączony!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Już dołączono do grupy!"; /* pref value */ @@ -888,7 +888,8 @@ marked deleted chat item preview text */ "Can't message member" = "Nie można wysłać wiadomości do członka"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ @@ -967,7 +968,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Czat już istnieje"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Czat już istnieje!"; /* No comment provided by engineer. */ @@ -1141,9 +1142,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Łącz automatycznie"; -/* No comment provided by engineer. */ -"Connect incognito" = "Połącz incognito"; - /* No comment provided by engineer. */ "Connect to desktop" = "Połącz do komputera"; @@ -1153,25 +1151,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Szybciej łącz się ze znajomymi."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Połączyć się ze sobą?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Połączyć się ze sobą?\nTo jest twój jednorazowy link!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Połączyć się ze sobą?\nTo jest twój własny adres SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Połącz przez adres kontaktowy"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Połącz się przez link"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Połącz przez jednorazowy link"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Połącz z %@"; /* No comment provided by engineer. */ @@ -1354,9 +1349,6 @@ set passcode view */ /* server test step */ "Create queue" = "Utwórz kolejkę"; -/* No comment provided by engineer. */ -"Create secret group" = "Utwórz tajną grupę"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Utwórz adres SimpleX"; @@ -1761,7 +1753,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "Nie włączaj"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Nie pokazuj ponownie"; /* No comment provided by engineer. */ @@ -2002,7 +1994,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Błąd zmiany roli"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Błąd zmiany ustawienia"; /* No comment provided by engineer. */ @@ -2032,7 +2024,7 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Błąd odszyfrowania pliku"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Błąd usuwania bazy danych czatu"; /* No comment provided by engineer. */ @@ -2041,10 +2033,10 @@ chat item action */ /* No comment provided by engineer. */ "Error deleting connection" = "Błąd usuwania połączenia"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Błąd usuwania bazy danych"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Błąd usuwania starej bazy danych"; /* No comment provided by engineer. */ @@ -2065,13 +2057,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Błąd szyfrowania bazy danych"; -/* No comment provided by engineer. */ +/* alert title */ "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. */ +/* alert title */ "Error importing chat database" = "Błąd importu bazy danych czatu"; /* No comment provided by engineer. */ @@ -2092,7 +2084,7 @@ chat item action */ /* No comment provided by engineer. */ "Error reconnecting servers" = "Błąd ponownego łączenia serwerów"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Błąd usuwania członka"; /* No comment provided by engineer. */ @@ -2385,7 +2377,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Grupa już istnieje"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Grupa już istnieje!"; /* No comment provided by engineer. */ @@ -2739,22 +2731,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "dołącz jako %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Dołącz do grupy"; /* No comment provided by engineer. */ "Join group conversations" = "Dołącz do grupowej rozmowy"; -/* No comment provided by engineer. */ -"Join group?" = "Dołączyć do grupy?"; - /* No comment provided by engineer. */ "Join incognito" = "Dołącz incognito"; -/* No comment provided by engineer. */ -"Join with current profile" = "Dołącz z obecnym profilem"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Dołączyć do twojej grupy?\nTo jest twój link do grupy %@!"; /* No comment provided by engineer. */ @@ -3236,6 +3222,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "wyłączony"; @@ -3248,7 +3235,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "zaoferował %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ok"; /* No comment provided by engineer. */ @@ -3326,7 +3315,7 @@ time to disappear */ /* authentication reason */ "Open chat console" = "Otwórz konsolę czatu"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Grupa otwarta"; /* authentication reason */ @@ -3774,18 +3763,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Renegocjować szyfrowanie?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Powtórzyć prośbę połączenia?"; - /* No comment provided by engineer. */ "Repeat download" = "Powtórz pobieranie"; /* No comment provided by engineer. */ "Repeat import" = "Powtórz importowanie"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Powtórzyć prośbę dołączenia?"; - /* No comment provided by engineer. */ "Repeat upload" = "Powtórz wgrywanie"; @@ -4556,9 +4539,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅"; @@ -4613,12 +4593,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Ta grupa już nie istnieje."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "To jest twój jednorazowy link!"; - -/* 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."; @@ -4841,7 +4815,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Użyj czatu"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Użyj obecnego profilu"; /* No comment provided by engineer. */ @@ -4853,7 +4827,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Użyj interfejsu połączeń iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Użyj nowego profilu incognito"; /* No comment provided by engineer. */ @@ -5099,25 +5073,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected to %@." = "Jesteś już połączony z %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Już się łączysz z %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Już jesteś połączony z tym jednorazowym linkiem!"; /* No comment provided by engineer. */ "You are already in group %@." = "Już jesteś w grupie %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Już dołączasz do grupy %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Już dołączasz do grupy przez ten link!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Już dołączasz do grupy przez ten link."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Już dołączasz do grupy!\nPowtórzyć prośbę dołączenia?"; /* No comment provided by engineer. */ @@ -5213,10 +5184,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "Ty decydujesz, kto może się połączyć."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Już prosiłeś o połączenie na ten adres!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Już prosiłeś o połączenie!\nPowtórzyć prośbę połączenia?"; /* No comment provided by engineer. */ @@ -5282,9 +5250,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Uwierzytelnienie będzie wymagane przy uruchamianiu lub wznawianiu aplikacji po 30 sekundach w tle."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Zostaniesz połączony ze wszystkimi członkami grupy."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne."; @@ -5354,15 +5319,15 @@ chat item action */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu."; - /* No comment provided by engineer. */ "Your random profile" = "Twój losowy profil"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index cb837836ff..ac0122118b 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -164,7 +164,7 @@ "%d file(s) were not downloaded." = "%d файлов не было загружено."; /* time interval */ -"%d hours" = "%d ч."; +"%d hours" = "%d час."; /* alert title */ "%d messages not forwarded" = "%d сообщений не переслано"; @@ -345,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Принять"; +/* alert action */ +"Accept as member" = "Принять в группу"; + +/* alert action */ +"Accept as observer" = "Принять как читателя"; + /* No comment provided by engineer. */ "Accept conditions" = "Принять условия"; @@ -358,6 +364,12 @@ swipe action */ swipe action */ "Accept incognito" = "Принять инкогнито"; +/* alert title */ +"Accept member" = "Принять члена"; + +/* rcv group event chat item */ +"accepted %@" = "принят %@"; + /* call status */ "accepted call" = "принятый звонок"; @@ -367,6 +379,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "принятое приглашение"; +/* rcv group event chat item */ +"accepted you" = "Вы приняты"; + /* No comment provided by engineer. */ "Acknowledged" = "Подтверждено"; @@ -463,6 +478,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "шифрование согласовывается…"; +/* member criteria value */ +"all" = "все"; + /* No comment provided by engineer. */ "All" = "Все"; @@ -484,6 +502,9 @@ swipe action */ /* No comment provided by engineer. */ "All group members will remain connected." = "Все члены группы останутся соединены."; +/* feature role */ +"all members" = "все члены"; + /* No comment provided by engineer. */ "All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах."; @@ -502,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "All reports will be archived for you." = "Все сообщения о нарушениях будут заархивированы для вас."; +/* No comment provided by engineer. */ +"All servers" = "Все серверы"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Все контакты, которые соединились через этот адрес, сохранятся."; @@ -583,10 +607,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Соединение уже установлено?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Уже соединяется!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вступление в группу уже начато!"; /* pref value */ @@ -899,8 +923,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't message member" = "Не удаётся отправить сообщение члену группы"; +/* No comment provided by engineer. */ +"can't send messages" = "нельзя отправлять"; + /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ @@ -982,7 +1010,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Разговор уже существует"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Разговор уже существует!"; /* No comment provided by engineer. */ @@ -1036,9 +1064,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Разговор будет удален для Вас - это действие нельзя отменить!"; +/* chat toolbar */ +"Chat with admins" = "Чат с админами"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат с членом группы"; + /* No comment provided by engineer. */ "Chats" = "Чаты"; +/* No comment provided by engineer. */ +"Chats with members" = "Чаты с членами группы"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Проверять сообщения каждые 20 минут."; @@ -1180,9 +1217,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Соединяться автоматически"; -/* No comment provided by engineer. */ -"Connect incognito" = "Соединиться Инкогнито"; - /* No comment provided by engineer. */ "Connect to desktop" = "Подключиться к компьютеру"; @@ -1192,25 +1226,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Соединяйтесь с друзьями быстрее."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Соединиться с самим собой?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Соединиться с самим собой?\nЭто ваша собственная одноразовая ссылка!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Соединиться с самим собой?\nЭто ваш собственный адрес SimpleX!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Соединиться через адрес"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Соединиться через ссылку"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Соединиться через одноразовую ссылку"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Соединиться с %@"; /* No comment provided by engineer. */ @@ -1327,9 +1358,15 @@ set passcode view */ /* No comment provided by engineer. */ "Contact already exists" = "Существующий контакт"; +/* No comment provided by engineer. */ +"contact deleted" = "контакт удален"; + /* No comment provided by engineer. */ "Contact deleted!" = "Контакт удален!"; +/* No comment provided by engineer. */ +"contact disabled" = "контакт выключен"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "у контакта есть e2e шифрование"; @@ -1348,6 +1385,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; +/* No comment provided by engineer. */ +"contact not ready" = "контакт не готов"; + /* No comment provided by engineer. */ "Contact preferences" = "Предпочтения контакта"; @@ -1417,9 +1457,6 @@ set passcode view */ /* server test step */ "Create queue" = "Создание очереди"; -/* No comment provided by engineer. */ -"Create secret group" = "Создать скрытую группу"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Создать адрес SimpleX"; @@ -1596,6 +1633,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Удалить профиль?"; +/* alert title */ +"Delete chat with member?" = "Удалить чат с членом группы?"; + /* No comment provided by engineer. */ "Delete chat?" = "Удалить разговор?"; @@ -1782,6 +1822,9 @@ swipe action */ /* No comment provided by engineer. */ "Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе."; +/* No comment provided by engineer. */ +"Direct messages between members are prohibited." = "Прямые сообщения между членами запрещены."; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Выключить (кроме исключений)"; @@ -1836,6 +1879,9 @@ swipe action */ /* No comment provided by engineer. */ "Do it later" = "Отложить"; +/* No comment provided by engineer. */ +"Do not send history to new members." = "Не отправлять историю новым членам."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку."; @@ -1860,7 +1906,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "Не пропустите важные сообщения."; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Не показывать"; /* No comment provided by engineer. */ @@ -1933,6 +1979,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "Включить доступ к камере"; +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных."; + /* No comment provided by engineer. */ "Enable for all" = "Включить для всех"; @@ -2101,6 +2150,12 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Ошибка при принятии запроса на соединение"; +/* alert title */ +"Error accepting member" = "Ошибка вступления члена группы"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Ошибка при добавлении членов группы"; + /* alert title */ "Error adding server" = "Ошибка добавления сервера"; @@ -2113,7 +2168,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Ошибка при изменении роли"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Ошибка при изменении настройки"; /* No comment provided by engineer. */ @@ -2137,6 +2192,9 @@ chat item action */ /* alert title */ "Error creating list" = "Ошибка создания списка"; +/* No comment provided by engineer. */ +"Error creating member contact" = "Ошибка при создании контакта"; + /* No comment provided by engineer. */ "Error creating message" = "Ошибка создания сообщения"; @@ -2149,19 +2207,22 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Ошибка расшифровки файла"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Ошибка при удалении данных чата"; +/* alert title */ +"Error deleting chat with member" = "Ошибка при удалении чата с членом группы"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Ошибка при удалении чата!"; /* No comment provided by engineer. */ "Error deleting connection" = "Ошибка при удалении соединения"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Ошибка при удалении данных чата"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Ошибка при удалении предыдущей версии данных чата"; /* No comment provided by engineer. */ @@ -2182,13 +2243,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Ошибка при шифровании"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Ошибка при экспорте архива чата"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Ошибка экспорта темы: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Ошибка при импорте архива чата"; /* No comment provided by engineer. */ @@ -2215,6 +2276,9 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Ошибка регистрации для уведомлений"; +/* alert title */ +"Error removing member" = "Ошибка при удалении члена группы"; + /* alert title */ "Error reordering lists" = "Ошибка сортировки списков"; @@ -2251,6 +2315,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending email" = "Ошибка отправки email"; +/* No comment provided by engineer. */ +"Error sending member contact invitation" = "Ошибка при отправке приглашения члену"; + /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; @@ -2451,6 +2518,9 @@ snd error text */ /* No comment provided by engineer. */ "Fix not supported by contact" = "Починка не поддерживается контактом"; +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Починка не поддерживается членом группы."; + /* No comment provided by engineer. */ "For all moderators" = "Для всех модераторов"; @@ -2529,6 +2599,9 @@ snd error text */ /* No comment provided by engineer. */ "Full name (optional)" = "Полное имя (не обязательно)"; +/* No comment provided by engineer. */ +"Fully decentralized – visible only to members." = "Группа полностью децентрализована – она видна только членам."; + /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Полностью обновлены - работают в фоне!"; @@ -2553,7 +2626,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Группа уже существует"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Группа уже существует!"; /* No comment provided by engineer. */ @@ -2577,6 +2650,9 @@ snd error text */ /* No comment provided by engineer. */ "Group invitation is no longer valid, it was removed by sender." = "Приглашение в группу больше не действительно, оно было удалено отправителем."; +/* No comment provided by engineer. */ +"group is deleted" = "группа удалена"; + /* No comment provided by engineer. */ "Group link" = "Ссылка группы"; @@ -2595,12 +2671,18 @@ snd error text */ /* No comment provided by engineer. */ "Group profile" = "Профиль группы"; +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Профиль группы хранится на устройствах членов, а не на серверах."; + /* snd group event chat item */ "group profile updated" = "профиль группы обновлен"; /* No comment provided by engineer. */ "Group welcome message" = "Приветственное сообщение группы"; +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Группа будет удалена для всех членов - это действие нельзя отменить!"; + /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Группа будет удалена для Вас - это действие нельзя отменить!"; @@ -2637,6 +2719,9 @@ snd error text */ /* No comment provided by engineer. */ "History" = "История"; +/* No comment provided by engineer. */ +"History is not sent to new members." = "История не отправляется новым членам."; + /* time unit */ "hours" = "часов"; @@ -2871,6 +2956,9 @@ snd error text */ /* No comment provided by engineer. */ "Invite friends" = "Пригласить друзей"; +/* No comment provided by engineer. */ +"Invite members" = "Пригласить членов группы"; + /* No comment provided by engineer. */ "Invite to chat" = "Пригласить в разговор"; @@ -2937,22 +3025,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "вступить как %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Вступить в группу"; /* No comment provided by engineer. */ "Join group conversations" = "Присоединяйтесь к разговорам в группах"; -/* No comment provided by engineer. */ -"Join group?" = "Вступить в группу?"; - /* No comment provided by engineer. */ "Join incognito" = "Вступить инкогнито"; -/* No comment provided by engineer. */ -"Join with current profile" = "Вступить с активным профилем"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Вступить в вашу группу?\nЭто ваша ссылка на группу %@!"; /* No comment provided by engineer. */ @@ -3084,15 +3166,75 @@ snd error text */ /* blur media */ "Medium" = "Среднее"; +/* member role */ +"member" = "член группы"; + +/* No comment provided by engineer. */ +"Member" = "Член группы"; + +/* profile update event chat item */ +"member %@ changed to %@" = "член %1$@ изменился на %2$@"; + +/* No comment provided by engineer. */ +"Member admission" = "Приём членов в группу"; + /* rcv group event chat item */ "member connected" = "соединен(а)"; +/* No comment provided by engineer. */ +"member has old version" = "член имеет старую версию"; + +/* item status text */ +"Member inactive" = "Член неактивен"; + /* chat feature */ "Member reports" = "Сообщения о нарушениях"; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Роль члена будет изменена на \"%@\". Все члены группы получат уведомление."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение."; + +/* No comment provided by engineer. */ +"Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!"; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; + +/* alert message */ +"Member will join the group, accept member?" = "Участник хочет присоединиться к группе. Принять?"; + +/* No comment provided by engineer. */ +"Members can add message reactions." = "Члены могут добавлять реакции на сообщения."; + +/* No comment provided by engineer. */ +"Members can irreversibly delete sent messages. (24 hours)" = "Члены могут необратимо удалять отправленные сообщения. (24 часа)"; + +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Члены группы могут пожаловаться модераторам."; + +/* No comment provided by engineer. */ +"Members can send direct messages." = "Члены могут посылать прямые сообщения."; + +/* No comment provided by engineer. */ +"Members can send disappearing messages." = "Члены могут посылать исчезающие сообщения."; + +/* No comment provided by engineer. */ +"Members can send files and media." = "Члены могут слать файлы и медиа."; + +/* No comment provided by engineer. */ +"Members can send SimpleX links." = "Члены могут отправлять ссылки SimpleX."; + +/* No comment provided by engineer. */ +"Members can send voice messages." = "Члены могут отправлять голосовые сообщения."; + +/* No comment provided by engineer. */ +"Mention members 👋" = "Упоминайте участников 👋"; + /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -3114,6 +3256,9 @@ snd error text */ /* item status text */ "Message forwarded" = "Сообщение переслано"; +/* item status description */ +"Message may be delivered later if member becomes active." = "Сообщение может быть доставлено позже, если член группы станет активным."; + /* No comment provided by engineer. */ "Message queue info" = "Информация об очереди сообщений"; @@ -3330,6 +3475,12 @@ snd error text */ /* No comment provided by engineer. */ "New media options" = "Новые медиа-опции"; +/* No comment provided by engineer. */ +"New member role" = "Роль члена группы"; + +/* rcv group event chat item */ +"New member wants to join the group." = "Новый участник хочет присоединиться к группе."; + /* notification */ "new message" = "новое сообщение"; @@ -3369,6 +3520,9 @@ snd error text */ /* No comment provided by engineer. */ "No chats in list %@" = "Нет чатов в списке %@"; +/* No comment provided by engineer. */ +"No chats with members" = "Нет чатов с членами группы"; + /* No comment provided by engineer. */ "No contacts selected" = "Контакты не выбраны"; @@ -3453,6 +3607,9 @@ snd error text */ /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; +/* No comment provided by engineer. */ +"not synchronized" = "не синхронизирован"; + /* No comment provided by engineer. */ "Notes" = "Заметки"; @@ -3477,11 +3634,15 @@ snd error text */ /* alert title */ "Notifications status" = "Статус уведомлений"; +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль наблюдатель)"; + /* member role */ "observer" = "читатель"; /* enabled status group pref value +member criteria value time to disappear */ "off" = "нет"; @@ -3494,7 +3655,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "предложил(a) %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Ок"; /* No comment provided by engineer. */ @@ -3587,9 +3750,12 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Открыть условия"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Открыть группу"; +/* alert title */ +"Open link?" = "Открыть ссылку?"; + /* authentication reason */ "Open migration to another device" = "Открытие миграции на другое устройство"; @@ -3665,6 +3831,9 @@ time to disappear */ /* No comment provided by engineer. */ "Password to show" = "Пароль чтобы раскрыть"; +/* past/unknown group member */ +"Past member %@" = "Бывший член %@"; + /* No comment provided by engineer. */ "Paste desktop address" = "Вставить адрес компьютера"; @@ -3689,6 +3858,9 @@ time to disappear */ /* No comment provided by engineer. */ "pending approval" = "ожидает утверждения"; +/* No comment provided by engineer. */ +"pending review" = "ожидает одобрения"; + /* No comment provided by engineer. */ "Periodic" = "Периодически"; @@ -3758,6 +3930,9 @@ time to disappear */ /* token info */ "Please try to disable and re-enable notfications." = "Попробуйте выключить и снова включить уведомления."; +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление."; + /* token info */ "Please wait for token activation to complete." = "Пожалуйста, дождитесь завершения активации токена."; @@ -3857,6 +4032,9 @@ time to disappear */ /* No comment provided by engineer. */ "Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Запретить посылать исчезающие сообщения."; @@ -4035,6 +4213,9 @@ swipe action */ /* No comment provided by engineer. */ "Reject contact request" = "Отклонить запрос"; +/* alert title */ +"Reject member?" = "Отклонить участника?"; + /* No comment provided by engineer. */ "rejected" = "отклонён"; @@ -4056,6 +4237,12 @@ swipe action */ /* No comment provided by engineer. */ "Remove image" = "Удалить изображение"; +/* No comment provided by engineer. */ +"Remove member" = "Удалить члена группы"; + +/* No comment provided by engineer. */ +"Remove member?" = "Удалить члена группы?"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; @@ -4068,6 +4255,9 @@ swipe action */ /* profile update event chat item */ "removed contact address" = "удалён адрес контакта"; +/* No comment provided by engineer. */ +"removed from group" = "удален из группы"; + /* profile update event chat item */ "removed profile picture" = "удалена картинка профиля"; @@ -4083,18 +4273,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Пересогласовать шифрование?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Повторить запрос на соединение?"; - /* No comment provided by engineer. */ "Repeat download" = "Повторить загрузку"; /* No comment provided by engineer. */ "Repeat import" = "Повторить импорт"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Повторить запрос на вступление?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повторить загрузку"; @@ -4116,6 +4300,9 @@ swipe action */ /* No comment provided by engineer. */ "Report reason?" = "Причина сообщения?"; +/* alert title */ +"Report sent to moderators" = "Жалоба отправлена модераторам"; + /* report reason */ "Report spam: only group moderators will see it." = "Пожаловаться на спам: увидят только модераторы группы."; @@ -4131,6 +4318,9 @@ swipe action */ /* No comment provided by engineer. */ "Reports" = "Сообщения о нарушениях"; +/* No comment provided by engineer. */ +"request to join rejected" = "запрос на вступление отклонён"; + /* chat list item title */ "requested to connect" = "запрошено соединение"; @@ -4185,9 +4375,21 @@ swipe action */ /* chat item action */ "Reveal" = "Показать"; +/* No comment provided by engineer. */ +"review" = "рассмотрение"; + /* No comment provided by engineer. */ "Review conditions" = "Посмотреть условия"; +/* admission stage */ +"Review members" = "Одобрять членов"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Одобрять членов для вступления в группу."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "одобрен админами"; + /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -4216,9 +4418,15 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Сохранить (и уведомить контакты)"; +/* alert title */ +"Save admission settings?" = "Сохранить настройки вступления?"; + /* alert button */ "Save and notify contact" = "Сохранить и уведомить контакт"; +/* No comment provided by engineer. */ +"Save and notify group members" = "Сохранить и уведомить членов группы"; + /* No comment provided by engineer. */ "Save and reconnect" = "Сохранить и переподключиться"; @@ -4411,6 +4619,9 @@ chat item action */ /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new members." = "Отправить до 100 последних сообщений новым членам."; + /* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; @@ -4546,6 +4757,9 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Установите код вместо системной аутентификации."; +/* No comment provided by engineer. */ +"Set member admission" = "Приём членов в группу"; + /* No comment provided by engineer. */ "Set message expiration in chats." = "Установите срок хранения сообщений в чатах."; @@ -4564,6 +4778,9 @@ chat item action */ /* No comment provided by engineer. */ "Set passphrase to export" = "Установите пароль"; +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Установить сообщение для новых членов группы!"; + /* No comment provided by engineer. */ "Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN"; @@ -4616,6 +4833,9 @@ chat item action */ /* No comment provided by engineer. */ "Share with contacts" = "Поделиться с контактами"; +/* No comment provided by engineer. */ +"Short link" = "Короткая ссылка"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Показать → на сообщениях доставленных конфиденциально."; @@ -4658,6 +4878,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?"; +/* simplex link type */ +"SimpleX channel link" = "SimpleX ссылка канала"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение."; @@ -4954,10 +5177,19 @@ report reason */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Неправильный ID предыдущего сообщения (меньше или равен предыдущему).\nЭто может произойти из-за ошибки программы, или когда соединение компроментировано."; /* No comment provided by engineer. */ -"The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; +"The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы."; /* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам."; +"The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы."; + +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Сообщения будут удалены для всех членов группы."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить."; /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**."; @@ -5022,14 +5254,14 @@ report reason */ /* No comment provided by engineer. */ "This display name is invalid. Please choose another name." = "Ошибка имени профиля. Пожалуйста, выберите другое имя."; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "В этой группе более %lld членов, отчёты о доставке не отправляются."; + /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; /* No comment provided by engineer. */ -"This is your own one-time link!" = "Это ваша собственная одноразовая ссылка!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Это ваш собственный адрес SimpleX!"; +"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку."; /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере."; @@ -5142,6 +5374,15 @@ report reason */ /* No comment provided by engineer. */ "Unblock for all" = "Разблокировать для всех"; +/* No comment provided by engineer. */ +"Unblock member" = "Разблокировать члена группы"; + +/* No comment provided by engineer. */ +"Unblock member for all?" = "Разблокировать члена для всех?"; + +/* No comment provided by engineer. */ +"Unblock member?" = "Разблокировать члена группы?"; + /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; @@ -5214,6 +5455,12 @@ report reason */ /* swipe action */ "Unread" = "Не прочитано"; +/* No comment provided by engineer. */ +"Unsupported connection link" = "Ссылка не поддерживается"; + +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; + /* No comment provided by engineer. */ "Update" = "Обновить"; @@ -5268,7 +5515,7 @@ report reason */ /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Использовать активный профиль"; /* No comment provided by engineer. */ @@ -5286,7 +5533,7 @@ report reason */ /* No comment provided by engineer. */ "Use iOS call interface" = "Использовать интерфейс iOS для звонков"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Использовать новый Инкогнито профиль"; /* No comment provided by engineer. */ @@ -5304,6 +5551,9 @@ report reason */ /* No comment provided by engineer. */ "Use servers" = "Использовать серверы"; +/* No comment provided by engineer. */ +"Use short links (BETA)" = "Короткие ссылки (БЕТА)"; + /* No comment provided by engineer. */ "Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?"; @@ -5313,6 +5563,9 @@ report reason */ /* No comment provided by engineer. */ "Use TCP port %@ when no port is specified." = "Использовать TCP-порт %@, когда порт не указан."; +/* No comment provided by engineer. */ +"Use TCP port 443 for preset servers only." = "Использовать TCP-порт 443 только для серверов по умолчанию."; + /* No comment provided by engineer. */ "Use the app while in the call." = "Используйте приложение во время звонка."; @@ -5541,6 +5794,9 @@ report reason */ /* No comment provided by engineer. */ "You accepted connection" = "Вы приняли приглашение соединиться"; +/* snd group event chat item */ +"you accepted this member" = "Вы приняли этого члена"; + /* No comment provided by engineer. */ "You allow" = "Вы разрешаете"; @@ -5553,25 +5809,22 @@ report reason */ /* No comment provided by engineer. */ "You are already connected with %@." = "Вы уже соединены с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Вы уже соединяетесь с %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Вы уже соединяетесь по этой одноразовой ссылке!"; /* No comment provided by engineer. */ "You are already in group %@." = "Вы уже состоите в группе %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Вы уже вступаете в группу %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Вы уже вступаете в группу по этой ссылке!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Вы уже вступаете в группу по этой ссылке."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Вы уже вступаете в группу!\nПовторить запрос на вступление?"; /* No comment provided by engineer. */ @@ -5631,6 +5884,9 @@ report reason */ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились."; + /* No comment provided by engineer. */ "You can share this address with your contacts to let them connect with **%@**." = "Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**."; @@ -5649,6 +5905,9 @@ report reason */ /* alert message */ "You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение."; +/* alert message */ +"You can view your reports in Chat with admins." = "Вы можете найти Ваши жалобы в Чате с админами."; + /* No comment provided by engineer. */ "You can't send messages!" = "Вы не можете отправлять сообщения!"; @@ -5670,10 +5929,7 @@ report reason */ /* No comment provided by engineer. */ "You decide who can connect." = "Вы определяете, кто может соединиться."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Вы уже запросили соединение через этот адрес!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Вы уже запросили соединение!\nПовторить запрос?"; /* No comment provided by engineer. */ @@ -5685,6 +5941,9 @@ report reason */ /* No comment provided by engineer. */ "You joined this group" = "Вы вступили в эту группу"; +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы."; + /* snd group event chat item */ "you left" = "Вы покинули группу"; @@ -5811,15 +6070,15 @@ report reason */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; - /* No comment provided by engineer. */ "Your random profile" = "Случайный профиль"; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 57c0466eb9..8e04b62361 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -485,7 +485,8 @@ swipe action */ "Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "ยกเลิก"; /* feature offered item */ @@ -630,7 +631,7 @@ set passcode view */ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "เชื่อมต่อกับนักพัฒนา SimpleX Chat"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "เชื่อมต่อผ่านลิงก์"; /* No comment provided by engineer. */ @@ -738,9 +739,6 @@ set passcode view */ /* server test step */ "Create queue" = "สร้างคิว"; -/* No comment provided by engineer. */ -"Create secret group" = "สร้างกลุ่มลับ"; - /* No comment provided by engineer. */ "Create SimpleX address" = "สร้างที่อยู่ SimpleX"; @@ -1028,7 +1026,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "อย่าเปิดใช้งาน"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "ไม่ต้องแสดงอีก"; /* No comment provided by engineer. */ @@ -1199,7 +1197,7 @@ swipe action */ /* No comment provided by engineer. */ "Error changing role" = "เกิดข้อผิดพลาดในการเปลี่ยนบทบาท"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า"; /* No comment provided by engineer. */ @@ -1214,7 +1212,7 @@ swipe action */ /* No comment provided by engineer. */ "Error creating profile!" = "เกิดข้อผิดพลาดในการสร้างโปรไฟล์!"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท"; /* No comment provided by engineer. */ @@ -1223,10 +1221,10 @@ swipe action */ /* No comment provided by engineer. */ "Error deleting connection" = "เกิดข้อผิดพลาดในการลบการเชื่อมต่อ"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า"; /* No comment provided by engineer. */ @@ -1244,10 +1242,10 @@ swipe action */ /* No comment provided by engineer. */ "Error encrypting database" = "เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท"; /* No comment provided by engineer. */ @@ -1256,7 +1254,7 @@ swipe action */ /* alert title */ "Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก"; /* No comment provided by engineer. */ @@ -1705,7 +1703,7 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "เข้าร่วมเป็น %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "เข้าร่วมกลุ่ม"; /* No comment provided by engineer. */ @@ -2016,6 +2014,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "ปิด"; @@ -2028,7 +2027,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "เสนอแล้ว %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "ตกลง"; /* No comment provided by engineer. */ @@ -2829,9 +2830,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; @@ -3292,10 +3290,10 @@ chat item action */ "Your privacy" = "ความเป็นส่วนตัวของคุณ"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; +"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; /* No comment provided by engineer. */ "Your random profile" = "โปรไฟล์แบบสุ่มของคุณ"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e3bb11d1cc..9cd3ae216a 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d saniye"; +/* delete after time */ +"%d seconds(s)" = "%d saniye(ler)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d okunmamış mesaj(lar)"; @@ -283,6 +286,9 @@ time interval */ time interval */ "1 week" = "1 hafta"; +/* delete after time */ +"1 year" = "1 yıl"; + /* No comment provided by engineer. */ "1-time link" = "tek kullanımlık bağlantı"; @@ -339,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Kabul et"; +/* alert action */ +"Accept as member" = "Üye olarak kabul et"; + +/* alert action */ +"Accept as observer" = "Gözlemci olarak kabul et"; + /* No comment provided by engineer. */ "Accept conditions" = "Koşulları kabul et"; @@ -352,6 +364,9 @@ swipe action */ swipe action */ "Accept incognito" = "Takma adla kabul et"; +/* alert title */ +"Accept member" = "Üyeyi kabul et"; + /* call status */ "accepted call" = "kabul edilen arama"; @@ -364,6 +379,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Onay hataları"; +/* token status text */ +"Active" = "Aktif"; + /* No comment provided by engineer. */ "Active connections" = "Aktif bağlantılar"; @@ -373,6 +391,9 @@ swipe action */ /* No comment provided by engineer. */ "Add friends" = "Arkadaş ekle"; +/* No comment provided by engineer. */ +"Add list" = "Liste ekle"; + /* No comment provided by engineer. */ "Add profile" = "Profil ekle"; @@ -388,6 +409,9 @@ swipe action */ /* No comment provided by engineer. */ "Add to another device" = "Başka bir cihaza ekle"; +/* No comment provided by engineer. */ +"Add to list" = "Listeye ekle"; + /* No comment provided by engineer. */ "Add welcome message" = "Karşılama mesajı ekleyin"; @@ -445,6 +469,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "şifreleme kabul ediliyor…"; +/* No comment provided by engineer. */ +"All" = "Hepsi"; + /* No comment provided by engineer. */ "All app data is deleted." = "Tüm uygulama verileri silinir."; @@ -556,10 +583,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Zaten bağlandı?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Zaten bağlanılıyor!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Zaten gruba bağlanılıyor!"; /* pref value */ @@ -834,7 +861,8 @@ marked deleted chat item preview text */ "Can't message member" = "Üyeye mesaj gönderilemiyor"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "İptal et"; /* No comment provided by engineer. */ @@ -913,7 +941,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Sohbet zaten mevcut"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Sohbet zaten mevcut!"; /* No comment provided by engineer. */ @@ -1096,9 +1124,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Otomatik olarak bağlan"; -/* No comment provided by engineer. */ -"Connect incognito" = "Gizli bağlan"; - /* No comment provided by engineer. */ "Connect to desktop" = "Bilgisayara bağlan"; @@ -1108,25 +1133,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Arkadaşlarınıza daha hızlı bağlanın."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "Kendine mi bağlanacaksın?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Kendine mi bağlanacaksın?\nBu senin kendi tek kullanımlık bağlantın!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "Kendine mi bağlanacaksın?\nBu senin kendi SimpleX adresin!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Kişi adresi aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Bağlantı aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Tek kullanımlık bağlantı aracılığıyla bağlan"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "%@ ile bağlan"; /* No comment provided by engineer. */ @@ -1315,9 +1337,6 @@ set passcode view */ /* server test step */ "Create queue" = "Sıra oluştur"; -/* No comment provided by engineer. */ -"Create secret group" = "Gizli grup oluştur"; - /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX adresi oluştur"; @@ -1740,7 +1759,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't enable" = "Etkinleştirme"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "Yeniden gösterme"; /* No comment provided by engineer. */ @@ -1990,7 +2009,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Rol değiştirilirken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Ayar değiştirilirken hata oluştu"; /* No comment provided by engineer. */ @@ -2020,7 +2039,7 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Dosya şifresi çözülürken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "Sohbet veritabanı silinirken sorun oluştu"; /* No comment provided by engineer. */ @@ -2029,10 +2048,10 @@ chat item action */ /* No comment provided by engineer. */ "Error deleting connection" = "Bağlantı silinirken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Veritabanı silinirken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Eski veritabanı silinirken hata oluştu"; /* No comment provided by engineer. */ @@ -2053,13 +2072,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Veritabanı şifrelemesi çözülürken hata oluştu"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Sohbet veritabanı dışa aktarılırken hata oluştu"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Tema dışa aktarılırken hata oluştu: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Sohbet veritabanı içe aktarılırken hata oluştu"; /* No comment provided by engineer. */ @@ -2083,7 +2102,7 @@ chat item action */ /* No comment provided by engineer. */ "Error reconnecting servers" = "Hata sunuculara yeniden bağlanılıyor"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "Kişiyi silerken sorun oluştu"; /* No comment provided by engineer. */ @@ -2400,7 +2419,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Grup çoktan mevcut"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Grup çoktan mevcut!"; /* No comment provided by engineer. */ @@ -2766,22 +2785,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "%@ olarak katıl"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Gruba katıl"; /* No comment provided by engineer. */ "Join group conversations" = "Grup sohbetlerine katıl"; -/* No comment provided by engineer. */ -"Join group?" = "Gruba katılınsın mı?"; - /* No comment provided by engineer. */ "Join incognito" = "Gizli katıl"; -/* No comment provided by engineer. */ -"Join with current profile" = "Şu anki profille katıl"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Bu gruba katılınsın mı?\nBu senin grup için bağlantın %@!"; /* No comment provided by engineer. */ @@ -3272,6 +3285,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "kapalı"; @@ -3284,7 +3298,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "%1$@: %2$@ teklif etti"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Tamam"; /* No comment provided by engineer. */ @@ -3362,7 +3378,7 @@ time to disappear */ /* authentication reason */ "Open chat console" = "Sohbet konsolunu aç"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Grubu aç"; /* authentication reason */ @@ -3810,18 +3826,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Şifreleme yeniden müzakere edilsin mi?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Bağlantı isteği tekrarlansın mı?"; - /* No comment provided by engineer. */ "Repeat download" = "Tekrar indir"; /* No comment provided by engineer. */ "Repeat import" = "İthalatı tekrarla"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Katılma isteği tekrarlansın mı?"; - /* No comment provided by engineer. */ "Repeat upload" = "Yüklemeyi tekrarla"; @@ -4601,9 +4611,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅"; @@ -4658,12 +4665,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Bu grup artık mevcut değildir."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Bu senin kendi tek kullanımlık bağlantın!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Bu senin kendi SimpleX adresin!"; - /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Bu bağlantı başka bir mobil cihazda kullanıldı, lütfen masaüstünde yeni bir bağlantı oluşturun."; @@ -4886,7 +4887,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Sohbeti kullan"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Şu anki profili kullan"; /* No comment provided by engineer. */ @@ -4898,7 +4899,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "iOS arama arayüzünden kullan"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Yeni gizli profilden kullan"; /* No comment provided by engineer. */ @@ -5144,25 +5145,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected to %@." = "Zaten %@'a bağlısınız."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Zaten %@'a bağlanıyorsunuz."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Bu tek seferlik bağlantı üzerinden zaten bağlanıyorsunuz!"; /* No comment provided by engineer. */ "You are already in group %@." = "Zaten %@ grubundasın."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Zaten %@ grubuna katılıyorsunuz."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Bu bağlantı üzerinden gruba zaten katılıyorsunuz!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Gruba zaten bu bağlantı üzerinden katılıyorsunuz."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Gruba zaten katılıyorsunuz!\nKatılma isteği tekrarlansın mı?"; /* No comment provided by engineer. */ @@ -5258,10 +5256,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "Kimin bağlanabileceğine siz karar verirsiniz."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Bu adres üzerinden zaten bağlantı talebinde bulundunuz!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Zaten bağlantı isteğinde bulundunuz!\nBağlantı isteği tekrarlansın mı?"; /* No comment provided by engineer. */ @@ -5327,9 +5322,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Arka planda 30 saniye kaldıktan sonra uygulamayı başlattığınızda veya devam ettirdiğinizde kimlik doğrulaması yapmanız gerekecektir."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Bütün grup üyelerine bağlanacaksın."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Aktif olduklarında sessize alınmış profillerden arama ve bildirim almaya devam edersiniz."; @@ -5399,15 +5391,15 @@ chat item action */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Profiliniz **%@** paylaşılacaktır."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır."; - /* No comment provided by engineer. */ "Your random profile" = "Rasgele profiliniz"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 734b8dda82..e1039b9d0e 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -178,6 +178,9 @@ /* time interval */ "%d sec" = "%d сек"; +/* delete after time */ +"%d seconds(s)" = "%d секунд(и)"; + /* integrity error chat item */ "%d skipped message(s)" = "%d пропущено повідомлення(ь)"; @@ -283,6 +286,9 @@ time interval */ time interval */ "1 week" = "1 тиждень"; +/* delete after time */ +"1 year" = "1 рік"; + /* No comment provided by engineer. */ "1-time link" = "Одноразове посилання"; @@ -339,6 +345,12 @@ accept incoming call via notification swipe action */ "Accept" = "Прийняти"; +/* alert action */ +"Accept as member" = "Прийняти як учасника"; + +/* alert action */ +"Accept as observer" = "Прийняти як спостерігача"; + /* No comment provided by engineer. */ "Accept conditions" = "Прийняти умови"; @@ -352,6 +364,9 @@ swipe action */ swipe action */ "Accept incognito" = "Прийняти інкогніто"; +/* alert title */ +"Accept member" = "Прийняти учасника"; + /* call status */ "accepted call" = "прийнято виклик"; @@ -367,6 +382,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledgement errors" = "Помилки підтвердження"; +/* token status text */ +"Active" = "Активний"; + /* No comment provided by engineer. */ "Active connections" = "Активні з'єднання"; @@ -376,6 +394,9 @@ swipe action */ /* No comment provided by engineer. */ "Add friends" = "Додайте друзів"; +/* No comment provided by engineer. */ +"Add list" = "Додати список"; + /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; @@ -391,6 +412,9 @@ swipe action */ /* No comment provided by engineer. */ "Add to another device" = "Додати до іншого пристрою"; +/* No comment provided by engineer. */ +"Add to list" = "Додати до списку"; + /* No comment provided by engineer. */ "Add welcome message" = "Додати вітальне повідомлення"; @@ -448,12 +472,18 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "узгодження шифрування…"; +/* No comment provided by engineer. */ +"All" = "Всі"; + /* No comment provided by engineer. */ "All app data is deleted." = "Всі дані програми видаляються."; /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "Всі чати та повідомлення будуть видалені - це неможливо скасувати!"; +/* alert message */ +"All chats will be removed from the list %@, and the list deleted." = "Всі чати будуть видалені з списку %@, і список буде видалений."; + /* No comment provided by engineer. */ "All data is erased when it is entered." = "Всі дані стираються при введенні."; @@ -481,6 +511,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Всі профілі"; +/* No comment provided by engineer. */ +"All reports will be archived for you." = "Всі скарги будуть заархівовані для вас."; + +/* No comment provided by engineer. */ +"All servers" = "Всі сервери"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Всі ваші контакти залишаться на зв'язку."; @@ -526,6 +562,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)"; +/* No comment provided by engineer. */ +"Allow to report messsages to moderators." = "Дозволити надсилати скаргу на повідомлення модераторам."; + /* No comment provided by engineer. */ "Allow to send files and media." = "Дозволяє надсилати файли та медіа."; @@ -559,10 +598,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "Вже підключено?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "Вже підключаємось!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "Вже приєднуємося до групи!"; /* pref value */ @@ -580,6 +619,9 @@ swipe action */ /* No comment provided by engineer. */ "and %lld other events" = "та %lld інших подій"; +/* report reason */ +"Another reason" = "Інша причина"; + /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; @@ -595,6 +637,9 @@ swipe action */ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "Додаток шифрує нові локальні файли (крім відео)."; +/* No comment provided by engineer. */ +"App group:" = "Група застосунків:"; + /* No comment provided by engineer. */ "App icon" = "Іконка програми"; @@ -622,12 +667,30 @@ swipe action */ /* No comment provided by engineer. */ "Apply to" = "Звертатися до"; +/* No comment provided by engineer. */ +"Archive" = "Архівувати"; + +/* No comment provided by engineer. */ +"Archive %lld reports?" = "Архівувати %lld скарг?"; + +/* No comment provided by engineer. */ +"Archive all reports?" = "Архівувати всі скарги?"; + /* No comment provided by engineer. */ "Archive and upload" = "Архівування та завантаження"; /* No comment provided by engineer. */ "Archive contacts to chat later." = "Архівуйте контакти, щоб поспілкуватися пізніше."; +/* No comment provided by engineer. */ +"Archive report" = "Архівувати скаргу"; + +/* No comment provided by engineer. */ +"Archive report?" = "Архівувати скаргу?"; + +/* swipe action */ +"Archive reports" = "Архівувати скарги"; + /* No comment provided by engineer. */ "Archived contacts" = "Архівні контакти"; @@ -709,6 +772,9 @@ swipe action */ /* No comment provided by engineer. */ "Better groups" = "Кращі групи"; +/* No comment provided by engineer. */ +"Better groups performance" = "Краща продуктивність груп"; + /* No comment provided by engineer. */ "Better message dates." = "Кращі дати повідомлень."; @@ -721,6 +787,9 @@ swipe action */ /* No comment provided by engineer. */ "Better notifications" = "Кращі сповіщення"; +/* No comment provided by engineer. */ +"Better privacy and security" = "Краща конфіденційність і безпека"; + /* No comment provided by engineer. */ "Better security ✅" = "Краща безпека ✅"; @@ -794,9 +863,15 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Business chats" = "Ділові чати"; +/* No comment provided by engineer. */ +"Businesses" = "Бізнеси"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"By using SimpleX Chat you agree to:\n- send only legal content in public groups.\n- respect other users – no spam." = "Використовуючи SimpleX Chat, ви погоджуєтеся:\n- надсилати лише легальний контент у публічних групах.\n- поважати інших користувачів - без спаму."; + /* No comment provided by engineer. */ "call" = "дзвонити"; @@ -837,7 +912,8 @@ marked deleted chat item preview text */ "Can't message member" = "Не можу надіслати повідомлення користувачеві"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "Скасувати"; /* No comment provided by engineer. */ @@ -864,6 +940,9 @@ alert button */ /* No comment provided by engineer. */ "Change" = "Зміна"; +/* alert title */ +"Change automatic message deletion?" = "Змінити автоматичне видалення повідомлень?"; + /* authentication reason */ "Change chat profiles" = "Зміна профілів користувачів"; @@ -877,7 +956,7 @@ alert button */ "Change member role?" = "Змінити роль учасника?"; /* authentication reason */ -"Change passcode" = "Змінити пароль"; +"Change passcode" = "Змінити код доступу"; /* No comment provided by engineer. */ "Change receiving address" = "Змінити адресу отримання"; @@ -916,7 +995,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "Чат вже існує"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "Чат вже існує!"; /* No comment provided by engineer. */ @@ -970,9 +1049,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Чат буде видалено для вас - цю дію неможливо скасувати!"; +/* chat toolbar */ +"Chat with admins" = "Чат з адміністраторами"; + +/* No comment provided by engineer. */ +"Chat with member" = "Чат з учасником"; + /* No comment provided by engineer. */ "Chats" = "Чати"; +/* No comment provided by engineer. */ +"Chats with members" = "Чати з учасниками"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Перевіряйте повідомлення кожні 20 хв."; @@ -1012,6 +1100,12 @@ set passcode view */ /* No comment provided by engineer. */ "Clear conversation?" = "Відверта розмова?"; +/* No comment provided by engineer. */ +"Clear group?" = "Очистити групу?"; + +/* No comment provided by engineer. */ +"Clear or delete group?" = "Очистити чи видалити групу?"; + /* No comment provided by engineer. */ "Clear private notes?" = "Чисті приватні нотатки?"; @@ -1027,6 +1121,9 @@ set passcode view */ /* No comment provided by engineer. */ "colored" = "кольоровий"; +/* report reason */ +"Community guidelines violation" = "Порушення правил спільноти"; + /* server test step */ "Compare file" = "Порівняти файл"; @@ -1063,6 +1160,9 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; +/* No comment provided by engineer. */ +"Configure server operators" = "Налаштувати операторів сервера"; + /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; @@ -1093,15 +1193,15 @@ set passcode view */ /* No comment provided by engineer. */ "Confirm upload" = "Підтвердити завантаження"; +/* token status text */ +"Confirmed" = "Підтверджений"; + /* server test step */ "Connect" = "Підключіться"; /* No comment provided by engineer. */ "Connect automatically" = "Підключення автоматично"; -/* No comment provided by engineer. */ -"Connect incognito" = "Підключайтеся інкогніто"; - /* No comment provided by engineer. */ "Connect to desktop" = "Підключення до комп'ютера"; @@ -1111,25 +1211,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "Швидше спілкуйтеся з друзями."; -/* No comment provided by engineer. */ -"Connect to yourself?" = "З'єднатися з самим собою?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "Підключитися до себе? \nЦе ваше власне одноразове посилання!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "З'єднатися з самим собою? \nЦе ваша власна SimpleX-адреса!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "Підключіться за контактною адресою"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "Підключіться за посиланням"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "Підключитися до %@"; /* No comment provided by engineer. */ @@ -1192,6 +1289,9 @@ set passcode view */ /* No comment provided by engineer. */ "Connection and servers status." = "Стан з'єднання та серверів."; +/* No comment provided by engineer. */ +"Connection blocked" = "Підключення заблоковано"; + /* No comment provided by engineer. */ "Connection error" = "Помилка підключення"; @@ -1201,12 +1301,21 @@ set passcode view */ /* chat list item title (it should not be shown */ "connection established" = "з'єднання встановлене"; +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "Підключення заблоковано оператором сервера:\n%@"; + +/* No comment provided by engineer. */ +"Connection not ready." = "Підключення не готове."; + /* No comment provided by engineer. */ "Connection notifications" = "Сповіщення про підключення"; /* No comment provided by engineer. */ "Connection request sent!" = "Запит на підключення відправлено!"; +/* No comment provided by engineer. */ +"Connection requires encryption renegotiation." = "Підключення вимагає повторного узгодження шифрування."; + /* No comment provided by engineer. */ "Connection security" = "Безпека з'єднання"; @@ -1267,6 +1376,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути."; +/* blocking reason */ +"Content violates conditions of use" = "Вміст порушує умови використання"; + /* No comment provided by engineer. */ "Continue" = "Продовжуйте"; @@ -1309,6 +1421,9 @@ set passcode view */ /* No comment provided by engineer. */ "Create link" = "Створити посилання"; +/* No comment provided by engineer. */ +"Create list" = "Створити список"; + /* No comment provided by engineer. */ "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Створіть новий профіль у [desktop app](https://simplex.chat/downloads/). 💻"; @@ -1318,9 +1433,6 @@ set passcode view */ /* server test step */ "Create queue" = "Створити чергу"; -/* No comment provided by engineer. */ -"Create secret group" = "Створити секретну групу"; - /* No comment provided by engineer. */ "Create SimpleX address" = "Створіть адресу SimpleX"; @@ -1488,12 +1600,18 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat" = "Видалити чат"; +/* No comment provided by engineer. */ +"Delete chat messages from your device." = "Видалити повідомлення чату з вашого пристрою."; + /* No comment provided by engineer. */ "Delete chat profile" = "Видалити профіль чату"; /* No comment provided by engineer. */ "Delete chat profile?" = "Видалити профіль чату?"; +/* alert title */ +"Delete chat with member?" = "Видалити чат з учасником?"; + /* No comment provided by engineer. */ "Delete chat?" = "Видалити чат?"; @@ -1542,6 +1660,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete link?" = "Видалити посилання?"; +/* alert title */ +"Delete list?" = "Видалити список?"; + /* No comment provided by engineer. */ "Delete member message?" = "Видалити повідомлення учасника?"; @@ -1572,6 +1693,9 @@ swipe action */ /* server test step */ "Delete queue" = "Видалити чергу"; +/* No comment provided by engineer. */ +"Delete report" = "Видалити скаргу"; + /* No comment provided by engineer. */ "Delete up to 20 messages at once." = "Видаляйте до 20 повідомлень одночасно."; @@ -1680,6 +1804,12 @@ swipe action */ /* No comment provided by engineer. */ "Disable (keep overrides)" = "Вимкнути (зберегти перевизначення)"; +/* alert title */ +"Disable automatic message deletion?" = "Вимкнути автоматичне видалення повідомлень?"; + +/* alert button */ +"Disable delete messages" = "Вимкнути видалення повідомлень"; + /* No comment provided by engineer. */ "Disable for all" = "Вимкнути для всіх"; @@ -1740,6 +1870,9 @@ swipe action */ /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "НЕ використовуйте SimpleX для екстрених викликів."; +/* No comment provided by engineer. */ +"Documents:" = "Документи:"; + /* No comment provided by engineer. */ "Don't create address" = "Не створювати адресу"; @@ -1747,8 +1880,14 @@ swipe action */ "Don't enable" = "Не вмикати"; /* No comment provided by engineer. */ +"Don't miss important messages." = "Не пропускайте важливі повідомлення."; + +/* alert action */ "Don't show again" = "Більше не показувати"; +/* No comment provided by engineer. */ +"Done" = "Готово"; + /* No comment provided by engineer. */ "Downgrade and open chat" = "Пониження та відкритий чат"; @@ -1816,6 +1955,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "Увімкніть доступ до камери"; +/* No comment provided by engineer. */ +"Enable Flux in Network & servers settings for better metadata privacy." = "Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих."; + /* No comment provided by engineer. */ "Enable for all" = "Увімкнути для всіх"; @@ -1927,6 +2069,9 @@ chat item action */ /* chat item text */ "encryption re-negotiation required for %@" = "для %@ потрібне повторне узгодження шифрування"; +/* No comment provided by engineer. */ +"Encryption renegotiation in progress." = "Виконується повторне узгодження шифрування."; + /* No comment provided by engineer. */ "ended" = "закінчився"; @@ -1981,6 +2126,9 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; +/* alert title */ +"Error accepting member" = "Помилка при прийомі учасника"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Помилка додавання користувача(ів)"; @@ -1996,12 +2144,15 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "Помилка зміни ролі"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "Помилка зміни налаштування"; /* No comment provided by engineer. */ "Error changing to incognito!" = "Помилка переходу на інкогніто!"; +/* No comment provided by engineer. */ +"Error checking token status" = "Помилка перевірки статусу токена"; + /* No comment provided by engineer. */ "Error connecting to forwarding server %@. Please try later." = "Помилка підключення до сервера переадресації %@. Спробуйте пізніше."; @@ -2014,6 +2165,9 @@ chat item action */ /* No comment provided by engineer. */ "Error creating group link" = "Помилка створення посилання на групу"; +/* alert title */ +"Error creating list" = "Помилка при створенні списку"; + /* No comment provided by engineer. */ "Error creating member contact" = "Помилка при створенні контакту користувача"; @@ -2024,21 +2178,27 @@ chat item action */ "Error creating profile!" = "Помилка створення профілю!"; /* No comment provided by engineer. */ -"Error decrypting file" = "Помилка розшифрування файлу"; +"Error creating report" = "Помилка при створенні скарги"; /* No comment provided by engineer. */ +"Error decrypting file" = "Помилка розшифрування файлу"; + +/* alert title */ "Error deleting chat database" = "Помилка видалення бази даних чату"; +/* alert title */ +"Error deleting chat with member" = "Помилка при видаленні чату з учасником"; + /* No comment provided by engineer. */ "Error deleting chat!" = "Помилка видалення чату!"; /* No comment provided by engineer. */ "Error deleting connection" = "Помилка видалення з'єднання"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "Помилка видалення бази даних"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "Помилка видалення старої бази даних"; /* No comment provided by engineer. */ @@ -2059,13 +2219,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "Помилка шифрування бази даних"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "Помилка експорту бази даних чату"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "Помилка експорту теми: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "Помилка імпорту бази даних чату"; /* No comment provided by engineer. */ @@ -2089,12 +2249,21 @@ chat item action */ /* No comment provided by engineer. */ "Error reconnecting servers" = "Помилка перепідключення серверів"; -/* No comment provided by engineer. */ +/* alert title */ +"Error registering for notifications" = "Помилка під час реєстрації для отримання сповіщень"; + +/* alert title */ "Error removing member" = "Помилка видалення учасника"; +/* alert title */ +"Error reordering lists" = "Помилка при переупорядкуванні списків"; + /* No comment provided by engineer. */ "Error resetting statistics" = "Статистика скидання помилок"; +/* alert title */ +"Error saving chat list" = "Помилка під час збереження списку чатів"; + /* No comment provided by engineer. */ "Error saving group profile" = "Помилка збереження профілю групи"; @@ -2146,6 +2315,9 @@ chat item action */ /* No comment provided by engineer. */ "Error synchronizing connection" = "Помилка синхронізації з'єднання"; +/* No comment provided by engineer. */ +"Error testing server connection" = "Помилка під час перевірки з'єднання з сервером"; + /* No comment provided by engineer. */ "Error updating group link" = "Помилка оновлення посилання на групу"; @@ -2199,6 +2371,9 @@ snd error text */ /* No comment provided by engineer. */ "expired" = "закінчився"; +/* token status text */ +"Expired" = "Термін дії закінчився"; + /* No comment provided by engineer. */ "Export database" = "Експорт бази даних"; @@ -2223,12 +2398,21 @@ snd error text */ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Швидко і без очікування, поки відправник буде онлайн!"; +/* No comment provided by engineer. */ +"Faster deletion of groups." = "Швидше видалення груп."; + /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Швидше приєднання та надійніші повідомлення."; +/* No comment provided by engineer. */ +"Faster sending messages." = "Швидше надсилання повідомлень."; + /* swipe action */ "Favorite" = "Улюблений"; +/* No comment provided by engineer. */ +"Favorites" = "Вибране"; + /* file error alert title */ "File error" = "Помилка файлу"; @@ -2310,6 +2494,9 @@ snd error text */ /* No comment provided by engineer. */ "Fix not supported by group member" = "Виправлення не підтримується учасником групи"; +/* No comment provided by engineer. */ +"For all moderators" = "Для всіх модераторів"; + /* servers error */ "For chat profile %@:" = "Для профілю чату %@:"; @@ -2319,6 +2506,9 @@ snd error text */ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Наприклад, якщо ваш контакт отримує повідомлення через сервер SimpleX Chat, ваш додаток доставлятиме їх через сервер Flux."; +/* No comment provided by engineer. */ +"For me" = "Для мене"; + /* No comment provided by engineer. */ "For private routing" = "Для приватної маршрутизації"; @@ -2391,6 +2581,9 @@ snd error text */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Подальше зменшення використання акумулятора"; +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Отримуйте сповіщення, коли вас згадують."; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-файли та наклейки"; @@ -2406,7 +2599,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "Група вже існує"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "Група вже існує!"; /* No comment provided by engineer. */ @@ -2463,9 +2656,15 @@ snd error text */ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Група буде видалена для вас - це не може бути скасовано!"; +/* No comment provided by engineer. */ +"Groups" = "Групи"; + /* No comment provided by engineer. */ "Help" = "Довідка"; +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Допоможіть адміністраторам модерувати їхні групи."; + /* No comment provided by engineer. */ "Hidden" = "Приховано"; @@ -2502,6 +2701,9 @@ snd error text */ /* No comment provided by engineer. */ "How it helps privacy" = "Як це захищає приватність"; +/* alert button */ +"How it works" = "Як це працює"; + /* No comment provided by engineer. */ "How SimpleX works" = "Як працює SimpleX"; @@ -2589,6 +2791,12 @@ snd error text */ /* No comment provided by engineer. */ "inactive" = "неактивний"; +/* report reason */ +"Inappropriate content" = "Невідповідний вміст"; + +/* report reason */ +"Inappropriate profile" = "Невідповідний профіль"; + /* No comment provided by engineer. */ "Incognito" = "Інкогніто"; @@ -2655,6 +2863,21 @@ snd error text */ /* No comment provided by engineer. */ "Interface colors" = "Кольори інтерфейсу"; +/* token status text */ +"Invalid" = "Недійсний"; + +/* token status text */ +"Invalid (bad token)" = "Недійсний (неправильний токен)"; + +/* token status text */ +"Invalid (expired)" = "Недійсний (термін дії закінчився)"; + +/* token status text */ +"Invalid (unregistered)" = "Недійсний (незареєстрований)"; + +/* token status text */ +"Invalid (wrong topic)" = "Недійсний (неправильна тема)"; + /* invalid chat data */ "invalid chat" = "недійсний чат"; @@ -2772,22 +2995,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "приєднатися як %@"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "Приєднуйтесь до групи"; /* No comment provided by engineer. */ "Join group conversations" = "Приєднуйтесь до групових розмов"; -/* No comment provided by engineer. */ -"Join group?" = "Приєднатися до групи?"; - /* No comment provided by engineer. */ "Join incognito" = "Приєднуйтесь інкогніто"; -/* No comment provided by engineer. */ -"Join with current profile" = "Приєднатися з поточним профілем"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "Приєднатися до групи?\nЦе ваше посилання на групу %@!"; /* No comment provided by engineer. */ @@ -2856,6 +3073,12 @@ snd error text */ /* No comment provided by engineer. */ "Linked desktops" = "Пов'язані робочі столи"; +/* swipe action */ +"List" = "Список"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Назва списку та емодзі повинні бути різними для всіх списків."; + /* No comment provided by engineer. */ "LIVE" = "НАЖИВО"; @@ -3317,6 +3540,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "вимкнено"; @@ -3329,7 +3553,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "запропонував %1$@: %2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "Гаразд"; /* No comment provided by engineer. */ @@ -3416,7 +3642,7 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "Відкриті умови"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "Відкрита група"; /* authentication reason */ @@ -3882,18 +4108,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "Переузгодьте шифрування?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "Повторити запит на підключення?"; - /* No comment provided by engineer. */ "Repeat download" = "Повторити завантаження"; /* No comment provided by engineer. */ "Repeat import" = "Повторний імпорт"; -/* No comment provided by engineer. */ -"Repeat join request?" = "Повторити запит на приєднання?"; - /* No comment provided by engineer. */ "Repeat upload" = "Повторне завантаження"; @@ -4721,9 +4941,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам."; - /* No comment provided by engineer. */ "The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**."; @@ -4790,12 +5007,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Цієї групи більше не існує."; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "Це ваше власне одноразове посилання!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Це ваша власна SimpleX-адреса!"; - /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі."; @@ -5036,7 +5247,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "Використовуйте чат"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "Використовувати поточний профіль"; /* No comment provided by engineer. */ @@ -5054,7 +5265,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "Використовуйте новий профіль інкогніто"; /* No comment provided by engineer. */ @@ -5315,25 +5526,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected with %@." = "Ви вже підключені до %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "Ви вже з'єднані з %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "Ви вже підключаєтеся до %@.Ви вже підключаєтеся за цим одноразовим посиланням!"; /* No comment provided by engineer. */ "You are already in group %@." = "Ви вже перебуваєте в групі %@."; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "Ви вже приєдналися до групи %@."; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "Ви вже приєдналися до групи за цим посиланням!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "Ви вже приєдналися до групи за цим посиланням."; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "Ви вже приєдналися до групи!\nПовторити запит на приєднання?"; /* No comment provided by engineer. */ @@ -5435,10 +5643,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "Ви вирішуєте, хто може під'єднатися."; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Ви вже надсилали запит на підключення за цією адресою!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "Ви вже надіслали запит на підключення!\nПовторити запит на підключення?"; /* No comment provided by engineer. */ @@ -5504,9 +5709,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі."; -/* No comment provided by engineer. */ -"You will connect to all group members." = "Ви з'єднаєтеся з усіма учасниками групи."; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні."; @@ -5579,15 +5781,15 @@ chat item action */ /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; +/* No comment provided by engineer. */ +"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль."; /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам."; -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; - /* No comment provided by engineer. */ "Your random profile" = "Ваш випадковий профіль"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index e3f9669d9f..59dc22a6ad 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -586,10 +586,10 @@ swipe action */ /* No comment provided by engineer. */ "Already connected?" = "已连接?"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already connecting!" = "已经在连接了!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Already joining the group!" = "已经加入了该群组!"; /* pref value */ @@ -900,7 +900,8 @@ marked deleted chat item preview text */ "Can't message member" = "无法向成员发送消息"; /* alert action -alert button */ +alert button +new chat action */ "Cancel" = "取消"; /* No comment provided by engineer. */ @@ -982,7 +983,7 @@ set passcode view */ /* No comment provided by engineer. */ "Chat already exists" = "聊天已存在"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Chat already exists!" = "聊天已存在!"; /* No comment provided by engineer. */ @@ -1180,9 +1181,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "自动连接"; -/* No comment provided by engineer. */ -"Connect incognito" = "在隐身状态下连接"; - /* No comment provided by engineer. */ "Connect to desktop" = "连接到桌面"; @@ -1192,25 +1190,22 @@ set passcode view */ /* No comment provided by engineer. */ "Connect to your friends faster." = "更快地与您的朋友联系。"; -/* No comment provided by engineer. */ -"Connect to yourself?" = "连接到你自己?"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own one-time link!" = "与自己建立联系?\n这是您自己的一次性链接!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect to yourself?\nThis is your own SimpleX address!" = "与自己建立联系?\n这是您自己的 SimpleX 地址!"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via contact address" = "通过联系地址连接"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via link" = "通过链接连接"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Connect via one-time link" = "通过一次性链接连接"; -/* No comment provided by engineer. */ +/* new chat action */ "Connect with %@" = "与 %@连接"; /* No comment provided by engineer. */ @@ -1417,9 +1412,6 @@ set passcode view */ /* server test step */ "Create queue" = "创建队列"; -/* No comment provided by engineer. */ -"Create secret group" = "创建私密群组"; - /* No comment provided by engineer. */ "Create SimpleX address" = "创建 SimpleX 地址"; @@ -1866,7 +1858,7 @@ swipe action */ /* No comment provided by engineer. */ "Don't miss important messages." = "不错过重要消息。"; -/* No comment provided by engineer. */ +/* alert action */ "Don't show again" = "不再显示"; /* No comment provided by engineer. */ @@ -2125,7 +2117,7 @@ chat item action */ /* No comment provided by engineer. */ "Error changing role" = "更改角色错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error changing setting" = "更改设置错误"; /* No comment provided by engineer. */ @@ -2161,7 +2153,7 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "解密文件时出错"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat database" = "删除聊天数据库错误"; /* No comment provided by engineer. */ @@ -2170,10 +2162,10 @@ chat item action */ /* No comment provided by engineer. */ "Error deleting connection" = "删除连接错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting database" = "删除数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting old database" = "删除旧数据库错误"; /* No comment provided by engineer. */ @@ -2194,13 +2186,13 @@ chat item action */ /* No comment provided by engineer. */ "Error encrypting database" = "加密数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error exporting chat database" = "导出聊天数据库错误"; /* No comment provided by engineer. */ "Error exporting theme: %@" = "导出主题时出错: %@"; -/* No comment provided by engineer. */ +/* alert title */ "Error importing chat database" = "导入聊天数据库错误"; /* No comment provided by engineer. */ @@ -2227,7 +2219,7 @@ chat item action */ /* alert title */ "Error registering for notifications" = "注册消息推送出错"; -/* No comment provided by engineer. */ +/* alert title */ "Error removing member" = "删除成员错误"; /* alert title */ @@ -2577,7 +2569,7 @@ snd error text */ /* No comment provided by engineer. */ "Group already exists" = "群组已存在"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Group already exists!" = "群已存在!"; /* No comment provided by engineer. */ @@ -2973,22 +2965,16 @@ snd error text */ /* No comment provided by engineer. */ "join as %@" = "以 %@ 身份加入"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "Join group" = "加入群组"; /* No comment provided by engineer. */ "Join group conversations" = "加入群对话"; -/* No comment provided by engineer. */ -"Join group?" = "加入群组?"; - /* No comment provided by engineer. */ "Join incognito" = "加入隐身聊天"; -/* No comment provided by engineer. */ -"Join with current profile" = "使用当前档案加入"; - -/* No comment provided by engineer. */ +/* new chat action */ "Join your group?\nThis is your link for group %@!" = "加入您的群组?\n这是您组 %@ 的链接!"; /* No comment provided by engineer. */ @@ -3575,6 +3561,7 @@ snd error text */ /* enabled status group pref value +member criteria value time to disappear */ "off" = "关闭"; @@ -3587,7 +3574,9 @@ time to disappear */ /* feature offered item */ "offered %@: %@" = "已提供 %1$@:%2$@"; -/* alert button */ +/* alert action +alert button +new chat action */ "Ok" = "好的"; /* No comment provided by engineer. */ @@ -3680,7 +3669,7 @@ time to disappear */ /* No comment provided by engineer. */ "Open conditions" = "打开条款"; -/* No comment provided by engineer. */ +/* new chat action */ "Open group" = "打开群"; /* authentication reason */ @@ -4137,18 +4126,12 @@ swipe action */ /* No comment provided by engineer. */ "Renegotiate encryption?" = "重新协商加密?"; -/* No comment provided by engineer. */ -"Repeat connection request?" = "重复连接请求吗?"; - /* No comment provided by engineer. */ "Repeat download" = "重复下载"; /* No comment provided by engineer. */ "Repeat import" = "重复导入"; -/* No comment provided by engineer. */ -"Repeat join request?" = "重复加入请求吗?"; - /* No comment provided by engineer. */ "Repeat upload" = "重复上传"; @@ -4922,9 +4905,6 @@ chat item action */ /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。"; -/* No comment provided by engineer. */ -"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。"; - /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; @@ -4976,12 +4956,6 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; -/* No comment provided by engineer. */ -"This is your own one-time link!" = "这是你自己的一次性链接!"; - -/* No comment provided by engineer. */ -"This is your own SimpleX address!" = "这是你自己的 SimpleX 地址!"; - /* No comment provided by engineer. */ "This link was used with another mobile device, please create a new link on the desktop." = "此链接已在其他移动设备上使用,请在桌面上创建新链接。"; @@ -5198,7 +5172,7 @@ chat item action */ /* No comment provided by engineer. */ "Use chat" = "使用聊天"; -/* No comment provided by engineer. */ +/* new chat action */ "Use current profile" = "使用当前配置文件"; /* No comment provided by engineer. */ @@ -5210,7 +5184,7 @@ chat item action */ /* No comment provided by engineer. */ "Use iOS call interface" = "使用 iOS 通话界面"; -/* No comment provided by engineer. */ +/* new chat action */ "Use new incognito profile" = "使用新的隐身配置文件"; /* No comment provided by engineer. */ @@ -5450,25 +5424,22 @@ chat item action */ /* No comment provided by engineer. */ "You are already connected to %@." = "您已经连接到 %@。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting to %@." = "您已连接到 %@。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already connecting via this one-time link!" = "你已经在通过这个一次性链接进行连接!"; /* No comment provided by engineer. */ "You are already in group %@." = "您已在组 %@ 中。"; -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group %@." = "您已加入组 %@。"; -/* No comment provided by engineer. */ -"You are already joining the group via this link!" = "您已经通过此链接加入群组!"; - -/* No comment provided by engineer. */ +/* new chat sheet message */ "You are already joining the group via this link." = "你已经在通过此链接加入该群。"; -/* No comment provided by engineer. */ +/* new chat sheet title */ "You are already joining the group!\nRepeat join request?" = "您已经加入了这个群组!\n重复加入请求?"; /* No comment provided by engineer. */ @@ -5564,10 +5535,7 @@ chat item action */ /* No comment provided by engineer. */ "You decide who can connect." = "你决定谁可以连接。"; -/* No comment provided by engineer. */ -"You have already requested connection via this address!" = "你已经请求通过此地址进行连接!"; - -/* No comment provided by engineer. */ +/* new chat sheet title */ "You have already requested connection!\nRepeat connection request?" = "您已经请求连接了!\n重复连接请求?"; /* No comment provided by engineer. */ @@ -5633,9 +5601,6 @@ chat item action */ /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。"; -/* No comment provided by engineer. */ -"You will connect to all group members." = "你将连接到所有群成员。"; - /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。"; @@ -5697,10 +5662,10 @@ chat item action */ "Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。"; +"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。"; /* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "您的资料、联系人和发送的消息存储在您的设备上。"; +"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。"; /* No comment provided by engineer. */ "Your random profile" = "您的随机资料"; diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt index fc323f6ffd..4f47fda130 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt @@ -101,13 +101,13 @@ actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this actual fun ImageBitmap.hasAlpha(): Boolean = hasAlpha -actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas { - val radius = (width * 0.16f) / 2 +actual fun ImageBitmap.addLogo(size: Float): ImageBitmap = asAndroidBitmap().applyCanvas { + val radius = (width * size) / 2 val paint = android.graphics.Paint() paint.color = android.graphics.Color.WHITE drawCircle(width / 2f, height / 2f, radius, paint) val logo = androidAppContext.resources.getDrawable(R.drawable.icon_foreground_android_common, null).toBitmap() - val logoSize = (width * 0.24).toInt() + val logoSize = (width * size * 1.5).toInt() translate((width - logoSize) / 2f, (height - logoSize) / 2f) drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null) }.asImageBitmap() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt index 54e3061d25..a09ca2792b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -91,7 +91,7 @@ fun UserPickerUserBox( ProfileImageForActiveCall(size = USER_PICKER_IMAGE_SIZE, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant) if (userInfo.unreadCount > 0 && !userInfo.user.activeUser) { - unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) + userUnreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) } } val user = userInfo.user 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 61c20587bf..270b3a73b2 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 @@ -295,14 +295,13 @@ object ChatModel { } } - class ChatsContext(val contentTag: MsgContentTag?) { + class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) { val chats = mutableStateOf(SnapshotStateList()) /** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on. * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. * If you use api call to get the items, use just [add] instead of [addToChatItems]. * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ val chatItems = mutableStateOf(SnapshotStateList()) - val chatItemStatuses = mutableMapOf() // set listener here that will be notified on every add/delete of a chat item val chatState = ActiveChatState() @@ -310,6 +309,26 @@ object ChatModel { fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } + val contentTag: MsgContentTag? = + when (secondaryContextFilter) { + null -> null + is SecondaryContextFilter.GroupChatScopeContext -> null + is SecondaryContextFilter.MsgContentTagContext -> secondaryContextFilter.contentTag + } + + val groupScopeInfo: GroupChatScopeInfo? = + when (secondaryContextFilter) { + null -> null + is SecondaryContextFilter.GroupChatScopeContext -> secondaryContextFilter.groupScopeInfo + is SecondaryContextFilter.MsgContentTagContext -> null + } + + val isUserSupportChat: Boolean = + when (groupScopeInfo) { + null -> false + is GroupChatScopeInfo.MemberSupport -> groupScopeInfo.groupMember_ == null + } + suspend fun addChat(chat: Chat) { chats.add(index = 0, chat) popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) @@ -342,6 +361,8 @@ object ChatModel { ) ) } + } else if (currentCInfo is ChatInfo.Group && newCInfo is ChatInfo.Group && newCInfo.groupChatScope != null) { + newCInfo = newCInfo.copy(groupInfo = newCInfo.groupInfo, groupChatScope = null) } chats[i] = chats[i].copy(chatInfo = newCInfo) } @@ -364,7 +385,7 @@ object ChatModel { updateContact(rhId, updatedContact) } - suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo, groupChatScope = null)) private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { if (hasChat(rhId, cInfo.id)) { @@ -416,55 +437,64 @@ object ChatModel { } suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + // updates membersRequireAttention + updateChatInfo(rhId, cInfo) // mark chat non deleted if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { val updatedContact = cInfo.contact.copy(chatDeleted = false) updateContact(rhId, updatedContact) } - // update previews + // update chat list 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 + chat = chatsContext.chats[i] + // update preview (for chat from main scope to show new items for invitee in pending status) + if (cInfo.groupChatScope() == null || cInfo.groupInfo_?.membership?.memberPending == true) { + 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 { - currentPreviewItem + cItem } - } else { - cItem } - } - else -> cItem - } - val wasUnread = chat.unreadTag - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = - if (cItem.meta.itemStatus is CIStatus.RcvNew) { - increaseUnreadCounter(rhId, currentUser.value!!) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions) - } - else - chat.chatStats - ) - updateChatTagReadNoContentTag(chats[i], wasUnread) + else -> cItem + } + val wasUnread = chat.unreadTag + chatsContext.chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = + if (cItem.meta.itemStatus is CIStatus.RcvNew) { + increaseUnreadCounter(rhId, currentUser.value!!) + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions) + } else + chat.chatStats + ) + updateChatTagReadInPrimaryContext(chatsContext.chats[i], wasUnread) + } + // pop chat if (appPlatform.isDesktop && cItem.chatDir.sent) { - reorderChat(chats[i], 0) + reorderChat(chatsContext.chats[i], 0) } else { popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) } } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + if (cInfo.groupChatScope() == null) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = emptyList())) + } } + // add to current scope withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { + if (chatItemBelongsToScope(cInfo, cItem)) { // 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) { @@ -476,84 +506,101 @@ object ChatModel { } } } - - 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 - decreaseCounterInChatNoContentTag(rhId, cInfo.id) + + private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean = + when (secondaryContextFilter) { + null -> + chatId.value == cInfo.id && cInfo.groupChatScope() == null + is SecondaryContextFilter.GroupChatScopeContext -> { + val cInfoScope = cInfo.groupChatScope() + if (cInfoScope != null) { + chatId.value == cInfo.id && sameChatScope(cInfoScope, secondaryContextFilter.groupScopeInfo.toChatScope()) + } else { + false } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true + is SecondaryContextFilter.MsgContentTagContext -> + chatId.value == cInfo.id && cItem.isReport } - return withContext(Dispatchers.Main) { - // update current chat - if (chatId.value == cInfo.id) { + + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + var itemAdded = false + // update chat list + if (cInfo.groupChatScope() == null) { + 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(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInPrimaryContext(rhId, cInfo.id) + } + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + itemAdded = true + } + } + // update current scope + withContext(Dispatchers.Main) { + if (chatItemBelongsToScope(cInfo, cItem)) { if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) { AudioPlayer.stop(cItem) } 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)) + val oldStatus = items[itemIndex].meta.itemStatus + val newStatus = cItem.meta.itemStatus + val ci = if (shouldKeepOldSndCIStatus(oldStatus, newStatus)) { + cItem.copy(meta = cItem.meta.copy(itemStatus = oldStatus)) } else { cItem } - addToChatItems(ci) - true + items[itemIndex] = ci + } else { + addToChatItems(cItem) + itemAdded = true } - } else { - res } } + return itemAdded } suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) { withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { + if (chatItemBelongsToScope(cInfo, cItem)) { val items = chatItems.value val itemIndex = atIndex ?: 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) { - decreaseCounterInChatNoContentTag(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)) + // update chat list + if (cInfo.groupChatScope() == null) { + if (cItem.isRcvNew) { + decreaseCounterInPrimaryContext(rhId, cInfo.id) + } + // update preview + 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) { + // remove from current scope + if (chatItemBelongsToScope(cInfo, cItem)) { chatItems.removeAllAndNotify { // We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted // (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id) @@ -586,7 +633,7 @@ object ChatModel { Log.d(TAG, "exiting removeMemberItems") return } - val cInfo = ChatInfo.Group(groupInfo) + val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review if (chatId.value == groupInfo.id) { for (i in 0 until chatItems.value.size) { val updatedItem = removedUpdatedItem(chatItems.value[i]) @@ -617,7 +664,6 @@ object ChatModel { } // clear current chat if (chatId.value == cInfo.id) { - chatItemStatuses.clear() chatItems.clearAndNotify() } } @@ -688,7 +734,7 @@ object ChatModel { chats[chatIdx] = chat.copy( chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions) ) - updateChatTagReadNoContentTag(chats[chatIdx], wasUnread) + updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread) } } } @@ -729,9 +775,9 @@ object ChatModel { return markedRead to mentionsMarkedRead } - private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) { + private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val chatIndex = getChatIndex(rhId, chatId) if (chatIndex == -1) return @@ -745,7 +791,7 @@ object ChatModel { unreadCount = unreadCount, ) ) - updateChatTagReadNoContentTag(chats[chatIndex], wasUnread) + updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread) } fun removeChat(rhId: Long?, id: String) { @@ -814,16 +860,16 @@ object ChatModel { } fun increaseUnreadCounter(rhId: Long?, user: UserLike) { - changeUnreadCounterNoContentTag(rhId, user, 1) + changeUnreadCounterInPrimaryContext(rhId, user, 1) } fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { - changeUnreadCounterNoContentTag(rhId, user, -by) + changeUnreadCounterInPrimaryContext(rhId, user, -by) } - private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) { + private fun changeUnreadCounterInPrimaryContext(rhId: Long?, user: UserLike, by: Int) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } if (i != -1) { @@ -831,9 +877,9 @@ object ChatModel { } } - fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) { + fun updateChatTagReadInPrimaryContext(chat: Chat, wasUnread: Boolean) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return val tags = chat.chatInfo.chatTags ?: return val nowUnread = chat.unreadTag @@ -843,21 +889,21 @@ object ChatModel { unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 } } else if (!nowUnread && wasUnread) { - markChatTagReadNoContentTag_(chat, tags) + markChatTagReadInPrimaryContext_(chat, tags) } } fun markChatTagRead(chat: Chat) { if (chat.unreadTag) { chat.chatInfo.chatTags?.let { tags -> - markChatTagReadNoContentTag_(chat, tags) + markChatTagReadInPrimaryContext_(chat, tags) } } } - private fun markChatTagReadNoContentTag_(chat: Chat, tags: List) { + private fun markChatTagReadInPrimaryContext_(chat: Chat, tags: List) { // updates anything only in main ChatView, not GroupReportsView or anything else from the future - if (contentTag != null) return + if (secondaryContextFilter != null) return for (tag in tags) { val count = unreadTags[tag] @@ -889,12 +935,12 @@ object ChatModel { val wasReportsCount = chat.chatStats.reportsCount val nowReportsCount = chats[i].chatStats.reportsCount val by = if (wasReportsCount == 0 && nowReportsCount > 0) 1 else if (wasReportsCount > 0 && nowReportsCount == 0) -1 else 0 - changeGroupReportsTagNoContentTag(by) + changeGroupReportsTagInPrimaryContext(by) } } - private fun changeGroupReportsTagNoContentTag(by: Int = 0) { - if (by == 0 || contentTag != null) return + private fun changeGroupReportsTagInPrimaryContext(by: Int = 0) { + if (by == 0 || secondaryContextFilter != null) return presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by) clearActiveChatFilterIfNeeded() } @@ -1099,6 +1145,28 @@ enum class ChatType(val type: String) { ContactConnection(":"); } +sealed class GroupChatScope { + class MemberSupport(val groupMemberId_: Long?): GroupChatScope() +} + +fun sameChatScope(scope1: GroupChatScope, scope2: GroupChatScope) = + scope1 is GroupChatScope.MemberSupport + && scope2 is GroupChatScope.MemberSupport + && scope1.groupMemberId_ == scope2.groupMemberId_ + +@Serializable +sealed class GroupChatScopeInfo { + @Serializable @SerialName("memberSupport") data class MemberSupport(val groupMember_: GroupMember?) : GroupChatScopeInfo() + + fun toChatScope(): GroupChatScope = + when (this) { + is MemberSupport -> when (groupMember_) { + null -> GroupChatScope.MemberSupport(groupMemberId_ = null) + else -> GroupChatScope.MemberSupport(groupMemberId_ = groupMember_.groupMemberId) + } + } +} + @Serializable data class User( val remoteHostId: Long?, @@ -1204,8 +1272,6 @@ interface SomeChat { val apiId: Long val ready: Boolean val chatDeleted: Boolean - val userCantSendReason: Pair? - val sendMsgEnabled: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -1237,6 +1303,16 @@ data class Chat( val id: String get() = chatInfo.id + val supportUnreadCount: Int get() = when (chatInfo) { + is ChatInfo.Group -> + if (chatInfo.groupInfo.canModerate) { + chatInfo.groupInfo.membersRequireAttention + } else { + chatInfo.groupInfo.membership.supportChat?.unread ?: 0 + } + else -> 0 + } + fun groupFeatureEnabled(feature: GroupFeature): Boolean = if (chatInfo is ChatInfo.Group) { chatInfo.groupInfo.groupFeatureEnabled(feature) @@ -1275,8 +1351,6 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val chatDeleted get() = contact.chatDeleted - override val userCantSendReason get() = contact.userCantSendReason - override val sendMsgEnabled get() = contact.sendMsgEnabled override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL @@ -1294,15 +1368,13 @@ sealed class ChatInfo: SomeChat, NamedChat { } @Serializable @SerialName("group") - data class Group(val groupInfo: GroupInfo): ChatInfo() { + data class Group(val groupInfo: GroupInfo, val groupChatScope: GroupChatScopeInfo?): ChatInfo() { override val chatType get() = ChatType.Group override val localDisplayName get() = groupInfo.localDisplayName 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 userCantSendReason get() = groupInfo.userCantSendReason - override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL @@ -1314,7 +1386,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val localAlias get() = groupInfo.localAlias companion object { - val sampleData = Group(GroupInfo.sampleData) + val sampleData = Group(GroupInfo.sampleData, groupChatScope = null) } } @@ -1326,8 +1398,6 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready override val chatDeleted get() = noteFolder.chatDeleted - override val userCantSendReason get() = noteFolder.userCantSendReason - override val sendMsgEnabled get() = noteFolder.sendMsgEnabled override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL @@ -1351,8 +1421,6 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val chatDeleted get() = contactRequest.chatDeleted - override val userCantSendReason get() = contactRequest.userCantSendReason - override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL @@ -1376,8 +1444,6 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val chatDeleted get() = contactConnection.chatDeleted - override val userCantSendReason get() = contactConnection.userCantSendReason - override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL @@ -1406,8 +1472,6 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = "?$apiId" override val ready get() = false override val chatDeleted get() = false - override val userCantSendReason get() = generalGetString(MR.strings.cant_send_message_generic) to null - override val sendMsgEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -1422,6 +1486,71 @@ sealed class ChatInfo: SomeChat, NamedChat { } } + val userCantSendReason: Pair? + get() { + when (this) { + is Direct -> { + // TODO [short links] this will have additional statuses for pending contact requests before they are accepted + if (contact.nextSendGrpInv) return null + if (!contact.active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null + if (!contact.sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null + if (contact.activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null + if (contact.activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null + return null + } + is Group -> { + if (groupInfo.membership.memberActive) { + when (groupChatScope) { + null -> { + if (groupInfo.membership.memberPending) { + return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) + } + if (groupInfo.membership.memberRole == GroupMemberRole.Observer) { + return generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) + } + return null + } + is GroupChatScopeInfo.MemberSupport -> + if (groupChatScope.groupMember_ != null) { + if ( + groupChatScope.groupMember_.versionRange.maxVersion < GROUP_KNOCKING_VERSION + && !groupChatScope.groupMember_.memberPending + ) { + return generalGetString(MR.strings.cant_send_message_member_has_old_version) to null + } + return null + } else { + return null + } + } + } else { + return when (groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null + GroupMemberStatus.MemGroupDeleted -> generalGetString(MR.strings.cant_send_message_group_deleted) to null + GroupMemberStatus.MemRemoved -> generalGetString(MR.strings.cant_send_message_mem_removed) to null + GroupMemberStatus.MemLeft -> generalGetString(MR.strings.cant_send_message_you_left) to null + else -> generalGetString(MR.strings.cant_send_message_generic) to null + } + } + } + is Local -> + return null + is ContactRequest -> + return generalGetString(MR.strings.cant_send_message_generic) to null + is ContactConnection -> + return generalGetString(MR.strings.cant_send_message_generic) to null + is InvalidJSON -> + return generalGetString(MR.strings.cant_send_message_generic) to null + } + } + + val sendMsgEnabled get() = userCantSendReason == null + + fun groupChatScope(): GroupChatScope? = when (this) { + is Group -> groupChatScope?.toChatScope() + else -> null + } + fun ntfsEnabled(ci: ChatItem): Boolean = ntfsEnabled(ci.meta.userMention) @@ -1465,7 +1594,13 @@ sealed class ChatInfo: SomeChat, NamedChat { is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active else -> false } - } + + val groupInfo_: GroupInfo? + get() = when (this) { + is Group -> groupInfo + else -> null + } +} @Serializable sealed class NetworkStatus { @@ -1519,17 +1654,6 @@ data class Contact( override val ready get() = activeConn?.connStatus == ConnStatus.Ready val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady val active get() = contactStatus == ContactStatus.Active - override val userCantSendReason: Pair? - get() { - // TODO [short links] this will have additional statuses for pending contact requests before they are accepted - if (nextSendGrpInv) return null - if (!active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null - if (!sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null - if (activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null - if (activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null - return null - } - override val sendMsgEnabled get() = userCantSendReason == null val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -1754,6 +1878,7 @@ data class GroupInfo ( override val updatedAt: Instant, val chatTs: Instant?, val uiThemes: ThemeModeOverrides? = null, + val membersRequireAttention: Int, val chatTags: List, val chatItemTTL: Long?, override val localAlias: String, @@ -1763,23 +1888,6 @@ data class GroupInfo ( override val apiId get() = groupId override val ready get() = membership.memberActive override val chatDeleted get() = false - override val userCantSendReason: Pair? get() = - if (membership.memberActive) { - if (membership.memberRole == GroupMemberRole.Observer) { - generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) - } else { - null - } - } else { - when (membership.memberStatus) { - GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null - GroupMemberStatus.MemGroupDeleted -> generalGetString(MR.strings.cant_send_message_group_deleted) to null - GroupMemberStatus.MemRemoved -> generalGetString(MR.strings.cant_send_message_mem_removed) to null - GroupMemberStatus.MemLeft -> generalGetString(MR.strings.cant_send_message_you_left) to null - else -> generalGetString(MR.strings.cant_send_message_generic) to null - } - } - override val sendMsgEnabled get() = userCantSendReason == null override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on @@ -1797,7 +1905,7 @@ data class GroupInfo ( get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent val canDelete: Boolean - get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent + get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrentOrPending val canAddMembers: Boolean get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive @@ -1832,6 +1940,7 @@ data class GroupInfo ( updatedAt = Clock.System.now(), chatTs = Clock.System.now(), uiThemes = null, + membersRequireAttention = 0, chatTags = emptyList(), localAlias = "", chatItemTTL = null @@ -1849,7 +1958,8 @@ data class GroupProfile ( val description: String? = null, override val image: String? = null, override val localAlias: String = "", - val groupPreferences: GroupPreferences? = null + val groupPreferences: GroupPreferences? = null, + val memberAdmission: GroupMemberAdmission? = null ): NamedChat { companion object { val sampleData = GroupProfile( @@ -1859,6 +1969,27 @@ data class GroupProfile ( } } +@Serializable +data class GroupMemberAdmission( + val review: MemberCriteria? = null, +) { + companion object { + val sampleData = GroupMemberAdmission( + review = null, + ) + } +} + +@Serializable +enum class MemberCriteria { + @SerialName("all") All; + + val text: String + get() = when(this) { + MemberCriteria.All -> generalGetString(MR.strings.member_criteria_all) + } +} + @Serializable data class BusinessChatInfo ( val chatType: BusinessChatType, @@ -1887,7 +2018,9 @@ data class GroupMember ( val memberProfile: LocalProfile, val memberContactId: Long? = null, val memberContactProfileId: Long, - var activeConn: Connection? = null + var activeConn: Connection? = null, + val supportChat: GroupSupportChat? = null, + val memberChatVRange: VersionRange ): NamedChat { val id: String get() = "#$groupId @$groupMemberId" val ready get() = activeConn?.connStatus == ConnStatus.Ready @@ -1946,6 +2079,7 @@ data class GroupMember ( GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false GroupMemberStatus.MemPendingApproval -> true + GroupMemberStatus.MemPendingReview -> true GroupMemberStatus.MemIntroduced -> false GroupMemberStatus.MemIntroInvited -> false GroupMemberStatus.MemAccepted -> false @@ -1963,6 +2097,7 @@ data class GroupMember ( GroupMemberStatus.MemUnknown -> false GroupMemberStatus.MemInvited -> false GroupMemberStatus.MemPendingApproval -> false + GroupMemberStatus.MemPendingReview -> false GroupMemberStatus.MemIntroduced -> true GroupMemberStatus.MemIntroInvited -> true GroupMemberStatus.MemAccepted -> true @@ -1972,6 +2107,15 @@ data class GroupMember ( GroupMemberStatus.MemCreator -> true } + val memberPending: Boolean get() = when (this.memberStatus) { + GroupMemberStatus.MemPendingApproval -> true + GroupMemberStatus.MemPendingReview -> true + else -> false + } + + val memberCurrentOrPending: Boolean get() = + memberCurrent || memberPending + fun canBeRemoved(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft @@ -1990,6 +2134,8 @@ data class GroupMember ( && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive } + val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange + val memberIncognito = memberProfile.profileId != memberContactProfileId companion object { @@ -2007,11 +2153,20 @@ data class GroupMember ( memberProfile = LocalProfile.sampleData, memberContactId = 1, memberContactProfileId = 1L, - activeConn = Connection.sampleData + activeConn = Connection.sampleData, + memberChatVRange = VersionRange(minVersion = 1, maxVersion = 15) ) } } +@Serializable +class GroupSupportChat ( + val chatTs: Instant, + val unread: Int, + val memberAttention: Int, + val mentions: Int +) + @Serializable data class GroupMemberSettings(val showMessages: Boolean) {} @@ -2068,6 +2223,7 @@ enum class GroupMemberStatus { @SerialName("unknown") MemUnknown, @SerialName("invited") MemInvited, @SerialName("pending_approval") MemPendingApproval, + @SerialName("pending_review") MemPendingReview, @SerialName("introduced") MemIntroduced, @SerialName("intro-inv") MemIntroInvited, @SerialName("accepted") MemAccepted, @@ -2084,6 +2240,7 @@ enum class GroupMemberStatus { MemUnknown -> generalGetString(MR.strings.group_member_status_unknown) MemInvited -> generalGetString(MR.strings.group_member_status_invited) MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval) + MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review) MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced) MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation) MemAccepted -> generalGetString(MR.strings.group_member_status_accepted) @@ -2101,6 +2258,7 @@ enum class GroupMemberStatus { MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short) MemInvited -> generalGetString(MR.strings.group_member_status_invited) MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval_short) + MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review_short) MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting) MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting) MemAccepted -> generalGetString(MR.strings.group_member_status_connecting) @@ -2155,8 +2313,6 @@ class NoteFolder( override val apiId get() = noteFolderId override val chatDeleted get() = false override val ready get() = true - override val userCantSendReason: Pair? = null - override val sendMsgEnabled get() = true override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice override val timedMessagesTTL: Int? get() = null @@ -2192,8 +2348,6 @@ class UserContactRequest ( override val apiId get() = contactRequestId override val chatDeleted get() = false override val ready get() = true - override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null - override val sendMsgEnabled get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -2232,8 +2386,6 @@ class PendingContactConnection( override val apiId get() = pccConnId override val chatDeleted get() = false override val ready get() = false - override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null - override val sendMsgEnabled get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -2451,11 +2603,16 @@ data class ChatItem ( is CIContent.RcvGroupFeature, is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { - is RcvGroupEvent.UserRole, is RcvGroupEvent.UserDeleted, is RcvGroupEvent.GroupDeleted, is RcvGroupEvent.MemberCreatedContact -> null + is RcvGroupEvent.UserRole, + is RcvGroupEvent.UserDeleted, + is RcvGroupEvent.GroupDeleted, + is RcvGroupEvent.MemberCreatedContact, + is RcvGroupEvent.NewMemberPendingReview -> + null else -> CIMergeCategory.RcvGroupEvent } is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) { - is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft -> null + is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null else -> CIMergeCategory.SndGroupEvent } else -> { @@ -2526,6 +2683,8 @@ data class ChatItem ( is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { is RcvGroupEvent.MemberAdded -> false is RcvGroupEvent.MemberConnected -> false + is RcvGroupEvent.MemberAccepted -> false + is RcvGroupEvent.UserAccepted -> false is RcvGroupEvent.MemberLeft -> false is RcvGroupEvent.MemberRole -> false is RcvGroupEvent.MemberBlocked -> false @@ -2537,6 +2696,7 @@ data class ChatItem ( is RcvGroupEvent.InvitedViaGroupLink -> false is RcvGroupEvent.MemberCreatedContact -> false is RcvGroupEvent.MemberProfileUpdated -> false + is RcvGroupEvent.NewMemberPendingReview -> true } is CIContent.SndGroupEventContent -> false is CIContent.RcvConnEventContent -> false @@ -2718,6 +2878,11 @@ data class ChatItem ( } } +sealed class SecondaryContextFilter { + class GroupChatScopeContext(val groupScopeInfo: GroupChatScopeInfo): SecondaryContextFilter() + class MsgContentTagContext(val contentTag: MsgContentTag): SecondaryContextFilter() +} + fun MutableState>.add(index: Int, elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(index, elem) } } @@ -2976,6 +3141,19 @@ sealed class CIStatus { @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() + // as in corresponds to SENT response from agent + fun isSent(): Boolean = when(this) { + is SndNew -> false + is SndSent -> true + is SndRcvd -> false + is SndErrorAuth -> true + is CISSndError -> true + is SndWarning -> true + is RcvNew -> false + is RcvRead -> false + is Invalid -> false + } + fun statusIcon( primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary, @@ -3015,6 +3193,13 @@ sealed class CIStatus { } } +fun shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus): Boolean = + when { + oldStatus is CIStatus.SndRcvd && newStatus !is CIStatus.SndRcvd -> true + oldStatus.isSent() && newStatus is CIStatus.SndNew -> true + else -> false + } + @Serializable sealed class SndError { @Serializable @SerialName("auth") class Auth: SndError() @@ -4153,6 +4338,8 @@ sealed class RcvDirectEvent() { sealed class RcvGroupEvent() { @Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() @Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent() + @Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() + @Serializable @SerialName("userAccepted") class UserAccepted(): RcvGroupEvent() @Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent() @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent() @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent() @@ -4164,10 +4351,13 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent() @Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent() @Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent() + @Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent() val text: String get() = when (this) { is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName) is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected) + is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName) + is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted) is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) is MemberBlocked -> if (blocked) { @@ -4183,6 +4373,7 @@ sealed class RcvGroupEvent() { is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link) is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact) is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile) + is NewMemberPendingReview -> generalGetString(MR.strings.rcv_group_event_new_member_pending_review) } private fun profileUpdatedText(from: Profile, to: Profile): String = @@ -4207,6 +4398,8 @@ sealed class SndGroupEvent() { @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent() + @Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() + @Serializable @SerialName("userPendingReview") class UserPendingReview(): SndGroupEvent() val text: String get() = when (this) { is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) @@ -4219,6 +4412,8 @@ sealed class SndGroupEvent() { is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left) is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated) + is MemberAccepted -> generalGetString(MR.strings.snd_group_event_member_accepted) + is UserPendingReview -> generalGetString(MR.strings.snd_group_event_user_pending_review) } } 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 979d79c839..0f65783152 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 @@ -58,6 +58,9 @@ typealias ChatCtrl = Long // version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) val CREATE_MEMBER_CONTACT_VERSION = 2 +// support group knocking (MsgScope) +val GROUP_KNOCKING_VERSION = 15 + enum class CallOnLockScreen { DISABLE, SHOW, @@ -163,6 +166,7 @@ class AppPreferences { val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) + val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } } val appSkippedUpdate = mkStrPreference(SHARED_PREFS_APP_SKIPPED_UPDATE, "") @@ -245,6 +249,7 @@ class AppPreferences { liveMessageAlertShown to false, showHiddenProfilesNotice to true, showMuteProfileAlert to true, + showReportsInSupportChatAlert to true, showDeleteConversationNotice to true, showDeleteContactNotice to true, ) @@ -415,6 +420,7 @@ class AppPreferences { private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" + private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase" @@ -882,8 +888,8 @@ object ChatController { return null } - suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair? { - val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search)) + suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair? { + val r = sendCmd(rh, CC.ApiGetChat(type, id, scope, contentTag, pagination, search)) if (r is API.Result && r.res is CR.ApiChat) return if (rh == null) r.res.chat to r.res.navInfo else r.res.chat.copy(remoteHostId = rh) to r.res.navInfo Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") val e = (r as? API.Error)?.err @@ -917,8 +923,8 @@ object ChatController { suspend fun apiReorderChatTags(rh: Long?, tagIds: List) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds)) - suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? { - val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages) + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? { + val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages) return processSendMessageCmd(rh, cmd) } @@ -969,27 +975,27 @@ object ChatController { return null } - suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? { - val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId)) + suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? { + val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, itemId)) if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItemInfo apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r) return null } - suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List, ttl: Int?): List? { - val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl) + suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List, ttl: Int?): List? { + val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl) return processSendMessageCmd(rh, cmd)?.map { it.chatItem } } - suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List): CR.ForwardPlan? { - val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds)) + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? { + val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r) return null } - suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? { - val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live)) + suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? { + val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, scope, itemId, updatedMessage, live)) when { r is API.Result && r.res is CR.ChatItemUpdated -> return r.res.chatItem r is API.Result && r.res is CR.ChatItemNotChanged -> return r.res.chatItem @@ -1011,8 +1017,8 @@ object ChatController { return null } - suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? { - val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, itemId, add, reaction)) + suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? { + val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, scope, itemId, add, reaction)) if (r is API.Result && r.res is CR.ChatItemReaction) return r.res.reaction.chatReaction.chatItem Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}") return null @@ -1026,8 +1032,8 @@ object ChatController { return null } - suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List, mode: CIDeleteMode): List? { - val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode)) + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List, mode: CIDeleteMode): List? { + val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, scope, itemIds, mode)) if (r is API.Result && r.res is CR.ChatItemsDeleted) return r.res.chatItemDeletions Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}") return null @@ -1564,6 +1570,16 @@ object ChatController { return null } + suspend fun apiAddMyAddressShortLink(rh: Long?): UserContactLinkRec? { + val userId = kotlin.runCatching { currentUserId("apiAddMyAddressShortLink") }.getOrElse { return null } + val r = sendCmd(rh, CC.ApiAddMyAddressShortLink(userId)) + if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddMyAddressShortLink", generalGetString(MR.strings.error_creating_address), r) + } + return null + } + suspend fun userAddressAutoAccept(rh: Long?, autoAccept: AutoAccept?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiAddressAutoAccept(userId, autoAccept)) @@ -1659,18 +1675,18 @@ object ChatController { return null } - suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean { - val r = sendCmd(rh, CC.ApiChatRead(type, id)) + suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): Boolean { + val r = sendCmd(rh, CC.ApiChatRead(type, id, scope)) if (r.result is CR.CmdOk) return true Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}") return false } - suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List): Boolean { - val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds)) - if (r.result is CR.CmdOk) return true + suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List): ChatInfo? { + val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, scope, itemIds)) + if (r is API.Result && r.res is CR.ItemsReadForChat) return r.res.chatInfo Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}") - return false + return null } suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean { @@ -1899,9 +1915,25 @@ object ChatController { } } - suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): List? { + suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair? { + val r = sendCmd(rh, CC.ApiAcceptMember(groupId, groupMemberId, memberRole)) + if (r is API.Result && r.res is CR.MemberAccepted) return r.res.groupInfo to r.res.member + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAcceptMember", generalGetString(MR.strings.error_accepting_member), r) + } + return null + } + + suspend fun apiDeleteMemberSupportChat(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { + val r = sendCmd(rh, CC.ApiDeleteMemberSupportChat(groupId, groupMemberId)) + if (r is API.Result && r.res is CR.MemberSupportChatDeleted) return r.res.groupInfo to r.res.member + apiErrorAlert("apiDeleteMemberSupportChat", generalGetString(MR.strings.error_deleting_member_support_chat), r) + return null + } + + suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): Pair>? { val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages)) - if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members + if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members if (!(networkErrorAlert(r))) { apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } @@ -1994,6 +2026,15 @@ object ChatController { return null } + suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): Pair? { + val r = sendCmd(rh, CC.ApiAddGroupShortLink(groupId)) + if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r) + } + return null + } + suspend fun apiCreateMemberContact(rh: Long?, groupId: Long, groupMemberId: Long): Contact? { val r = sendCmd(rh, CC.APICreateMemberContact(groupId, groupMemberId)) if (r is API.Result && r.res is CR.NewMemberContact) return r.res.contact @@ -2420,9 +2461,7 @@ object ChatController { } } withContext(Dispatchers.Main) { - if (cItem.isReport) { - chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) - } + chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) } } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { withContext(Dispatchers.Main) { @@ -2448,12 +2487,10 @@ object ChatController { val cItem = chatItem.chatItem if (!cItem.isDeletedContent && active(r.user)) { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + chatModel.chatsContext.upsertChatItem(rhId, cInfo, cItem) } withContext(Dispatchers.Main) { - if (cItem.isReport) { - chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) - } + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, cItem) } } } @@ -2465,9 +2502,7 @@ object ChatController { chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } withContext(Dispatchers.Main) { - if (r.reaction.chatReaction.chatItem.isReport) { - chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) - } + chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } } } @@ -2510,15 +2545,18 @@ object ChatController { } } withContext(Dispatchers.Main) { - if (cItem.isReport) { - if (toChatItem == null) { - chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem) - } else { - chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem) - } + if (toChatItem == null) { + chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem) + } else { + chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem) } } } + r.chatItemDeletions.lastOrNull()?.deletedChatItem?.chatInfo?.let { updatedChatInfo -> + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, updatedChatInfo) + } + } } is CR.GroupChatItemsDeleted -> { groupChatItemsDeleted(rhId, r) @@ -2575,6 +2613,13 @@ object ChatController { chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } } + is CR.MemberAcceptedByOther -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) + } + } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { withContext(Dispatchers.Main) { @@ -2592,6 +2637,7 @@ object ChatController { is CR.DeletedMember -> if (active(r.user)) { withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) if (r.withMessages) { chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) @@ -2607,6 +2653,7 @@ object ChatController { is CR.LeftMember -> if (active(r.user)) { withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } withContext(Dispatchers.Main) { @@ -2655,6 +2702,16 @@ object ChatController { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, r.groupInfo) } + if ( + chatModel.chatId.value == r.groupInfo.id + && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT) + && chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + ) { + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value = null + } + ModalManager.end.closeModals() + } } is CR.JoinedGroupMember -> if (active(r.user)) { @@ -3016,11 +3073,11 @@ object ChatController { if (activeUser(rh, user)) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem - withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) } withContext(Dispatchers.Main) { - if (cItem.isReport) { - chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) - } + chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) + } + withContext(Dispatchers.Main) { + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) } } } @@ -3032,7 +3089,7 @@ object ChatController { chatModel.users.addAll(users) return } - val cInfo = ChatInfo.Group(r.groupInfo) + val cInfo = ChatInfo.Group(r.groupInfo, groupChatScope = null) // TODO [knocking] get scope from items? withContext(Dispatchers.Main) { val chatsCtx = chatModel.chatsContext r.chatItemIDs.forEach { itemId -> @@ -3087,11 +3144,11 @@ object ChatController { if (!activeUser(rh, user)) { notify() } else { - val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) } + val createdChat = withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) + } withContext(Dispatchers.Main) { - if (cItem.content.msgContent is MsgContent.MCReport) { - chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) - } + chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem) } if (createdChat) { notify() @@ -3296,9 +3353,9 @@ sealed class CC { class ApiGetSettings(val settings: AppSettings): CC() class ApiGetChatTags(val userId: Long): CC() class ApiGetChats(val userId: Long): CC() - class ApiGetChat(val type: ChatType, val id: Long, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() - class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() - class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() + class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC() + class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() class ApiCreateChatTag(val tag: ChatTagData): CC() class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List): CC() class ApiDeleteChatTag(val tagId: Long): CC() @@ -3306,18 +3363,20 @@ sealed class CC { class ApiReorderChatTags(val tagIds: List): CC() class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List): CC() class ApiReportMessage(val groupId: Long, val chatItemId: Long, val reportReason: ReportReason, val reportText: String): CC() - class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC() - class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List, val mode: CIDeleteMode): CC() + class ApiUpdateChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC() + class ApiDeleteChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List, val mode: CIDeleteMode): CC() class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC() class ApiArchiveReceivedReports(val groupId: Long): CC() class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List, val mode: CIDeleteMode): CC() - class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() + class ApiChatItemReaction(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC() - class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List): CC() - class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List, val ttl: Int?): CC() + class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC() + class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, val ttl: Int?): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() + class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC() + class ApiDeleteMemberSupportChat(val groupId: Long, val groupMemberId: Long): CC() class ApiMembersRole(val groupId: Long, val memberIds: List, val memberRole: GroupMemberRole): CC() class ApiBlockMembersForAll(val groupId: Long, val memberIds: List, val blocked: Boolean): CC() class ApiRemoveMembers(val groupId: Long, val memberIds: List, val withMessages: Boolean): CC() @@ -3328,6 +3387,7 @@ sealed class CC { class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() + class ApiAddGroupShortLink(val groupId: Long): CC() class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() class APITestProtoServer(val userId: Long, val server: String): CC() @@ -3382,6 +3442,7 @@ sealed class CC { class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() + class ApiAddMyAddressShortLink(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC() class ApiGetCallInvitations: CC() @@ -3395,8 +3456,8 @@ sealed class CC { class ApiGetNetworkStatuses(): CC() class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() - class ApiChatRead(val type: ChatType, val id: Long): CC() - class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List): CC() + class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() + class ApiChatItemsRead(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() @@ -3468,16 +3529,16 @@ sealed class CC { } else { " content=${contentTag.name.lowercase()}" } - "/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") + "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search") } - is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" + is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId" is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) val ttlStr = if (ttl != null) "$ttl" else "default" - "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs" + "/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs" } is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}" - is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}" + is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}" is ApiDeleteChatTag -> "/_delete tag $tagId" is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}" is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}" @@ -3486,23 +3547,25 @@ sealed class CC { "/_create *$noteFolderId json $msgs" } is ApiReportMessage -> "/_report #$groupId $chatItemId reason=${json.encodeToString(reportReason).trim('"')} $reportText" - is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}" - is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}" + is ApiUpdateChatItem -> "/_update item ${chatRef(type, id, scope)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}" + is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id, scope)} ${itemIds.joinToString(",")} ${mode.deleteMode}" is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}" is ApiArchiveReceivedReports -> "/_archive reports #$groupId" is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}" - is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" + is ApiChatItemReaction -> "/_reaction ${chatRef(type, id, scope)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}" is ApiForwardChatItems -> { val ttlStr = if (ttl != null) "$ttl" else "default" - "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}" + "/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}" } is ApiPlanForwardChatItems -> { - "/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}" + "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" + is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}" + is ApiDeleteMemberSupportChat -> "/_delete member chat #$groupId $groupMemberId" is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}" is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}" is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}" @@ -3513,6 +3576,7 @@ sealed class CC { is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" + is ApiAddGroupShortLink -> "/_short link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" is APITestProtoServer -> "/_server test $userId $server" @@ -3526,13 +3590,13 @@ sealed class CC { is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" - is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}" + is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id, scope = null)} ${chatItemTTLStr(seconds)}" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}" is ReconnectServer -> "/reconnect $userId $server" is ReconnectAllServers -> "/reconnect" - is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" + is APISetChatSettings -> "/_settings ${chatRef(type, id, scope = null)} ${json.encodeToString(chatSettings)}" is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" @@ -3554,8 +3618,8 @@ sealed class CC { is APIConnectPlan -> "/_connect plan $userId $connLink" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" - is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" - is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" + is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}" + is ApiClearChat -> "/_clear chat ${chatRef(type, id, scope = null)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" @@ -3567,6 +3631,7 @@ sealed class CC { is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" + is ApiAddMyAddressShortLink -> "/_short_link_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" @@ -3580,9 +3645,9 @@ sealed class CC { is ApiEndCall -> "/_call end @${contact.apiId}" is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiGetNetworkStatuses -> "/_network_statuses" - is ApiChatRead -> "/_read chat ${chatRef(type, id)}" - is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}" - is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" + is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}" + is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}" + is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + " approved_relays=${onOff(userApprovedRelays)}" + @@ -3666,6 +3731,8 @@ sealed class CC { is ApiNewGroup -> "apiNewGroup" is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" + is ApiAcceptMember -> "apiAcceptMember" + is ApiDeleteMemberSupportChat -> "apiDeleteMemberSupportChat" is ApiMembersRole -> "apiMembersRole" is ApiBlockMembersForAll -> "apiBlockMembersForAll" is ApiRemoveMembers -> "apiRemoveMembers" @@ -3676,6 +3743,7 @@ sealed class CC { is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" + is ApiAddGroupShortLink -> "apiAddGroupShortLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" is APITestProtoServer -> "testProtoServer" @@ -3730,6 +3798,7 @@ sealed class CC { is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" + is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink" is ApiSetProfileAddress -> "apiSetProfileAddress" is ApiAddressAutoAccept -> "apiAddressAutoAccept" is ApiAcceptContact -> "apiAcceptContact" @@ -3802,7 +3871,13 @@ sealed class CC { private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) companion object { - fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" + fun chatRef(chatType: ChatType, id: Long, scope: GroupChatScope?) = when (scope) { + null -> "${chatType.type}${id}" + is GroupChatScope.MemberSupport -> when (scope.groupMemberId_) { + null -> "${chatType.type}${id}(_support)" + else -> "${chatType.type}${id}(_support:${scope.groupMemberId_})" + } + } } } @@ -5764,6 +5839,7 @@ sealed class CR { @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("itemsReadForChat") class ItemsReadForChat(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR() @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR() @@ -5812,6 +5888,9 @@ sealed class CR { @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() + @Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberSupportChatDeleted") class MemberSupportChatDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: GroupMember, val member: GroupMember): CR() @Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() @Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List, val toRole: GroupMemberRole): CR() @Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR() @@ -5942,6 +6021,7 @@ sealed class CR { is ContactAlreadyExists -> "contactAlreadyExists" is ContactDeleted -> "contactDeleted" is ContactDeletedByContact -> "contactDeletedByContact" + is ItemsReadForChat -> "itemsReadForChat" is ChatCleared -> "chatCleared" is UserProfileNoChange -> "userProfileNoChange" is UserProfileUpdated -> "userProfileUpdated" @@ -5987,6 +6067,9 @@ sealed class CR { is ReceivedGroupInvitation -> "receivedGroupInvitation" is GroupDeletedUser -> "groupDeletedUser" is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" + is MemberAccepted -> "memberAccepted" + is MemberSupportChatDeleted -> "memberSupportChatDeleted" + is MemberAcceptedByOther -> "memberAcceptedByOther" is MemberRole -> "memberRole" is MembersRoleUser -> "membersRoleUser" is MemberBlockedForAll -> "memberBlockedForAll" @@ -6110,6 +6193,7 @@ sealed class CR { is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) is ContactDeletedByContact -> withUser(user, json.encodeToString(contact)) + is ItemsReadForChat -> withUser(user, json.encodeToString(chatInfo)) is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) is UserProfileNoChange -> withUser(user, noDetails()) is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile)) @@ -6155,6 +6239,9 @@ sealed class CR { is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole") is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo)) is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") + is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberSupportChatDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member") + is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\nmember: $member") is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole") is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt index fca69d5398..19e40ab0a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt @@ -16,7 +16,7 @@ expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOut expect fun GrayU8.toImageBitmap(): ImageBitmap expect fun ImageBitmap.hasAlpha(): Boolean -expect fun ImageBitmap.addLogo(): ImageBitmap +expect fun ImageBitmap.addLogo(size: Float): ImageBitmap expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap expect fun isImage(uri: URI): Boolean 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 2a77d0a6dc..a8b77e8fdd 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 @@ -40,9 +40,8 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* -import chat.simplex.common.views.chat.group.ChatTTLSection +import chat.simplex.common.views.chat.group.ChatTTLOption import chat.simplex.common.views.chatlist.updateChatSettings -import chat.simplex.common.views.database.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.* @@ -617,7 +616,10 @@ fun ChatInfoLayout( } SectionDividerSpaced(maxBottomPadding = false) - ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) + SectionView { + ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + } SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) val conn = contact.activeConn @@ -1384,7 +1386,7 @@ private fun setChatTTL( private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState) { try { val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT) - val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return + val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, scope = null, contentTag = null, pagination) ?: return if (chat.chatItems.isEmpty()) { // replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview withContext(Dispatchers.Main) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index eabe9cb60a..07cdc065a7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -17,7 +17,7 @@ suspend fun apiLoadSingleMessage( apiId: Long, itemId: Long ): ChatItem? = coroutineScope { - val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null + val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null chat.chatItems.firstOrNull() } @@ -31,7 +31,7 @@ suspend fun apiLoadMessages( openAroundItemId: Long? = null, visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } ) = coroutineScope { - val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, pagination, search) ?: return@coroutineScope + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes /** When [openAroundItemId] is provided, chatId can be different too */ if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null) @@ -54,7 +54,7 @@ suspend fun processLoadedChat( when (pagination) { is ChatPagination.Initial -> { val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList() - if (chatsCtx.contentTag == null) { + if (chatsCtx.secondaryContextFilter == null) { // update main chats, not content tagged withContext(Dispatchers.Main) { val oldChat = chatModel.chatsContext.getChat(chat.id) @@ -68,7 +68,6 @@ suspend fun processLoadedChat( } } withContext(Dispatchers.Main) { - chatsCtx.chatItemStatuses.clear() chatsCtx.chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.id splits.value = newSplits 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 6d7cdcdebe..2ca0dcc35d 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 @@ -63,12 +63,23 @@ fun ChatView( onComposed: suspend (chatId: String) -> Unit ) { val showSearch = rememberSaveable { mutableStateOf(false) } + val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } - val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } + val activeChatInfo = remember { derivedStateOf { + var chatInfo = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo + if ( + chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chatInfo is ChatInfo.Group + ) { + val scopeInfo = chatsCtx.secondaryContextFilter.groupScopeInfo + chatInfo = chatInfo.copy(groupChatScope = scopeInfo) + } + chatInfo + } } val user = chatModel.currentUser.value val chatInfo = activeChatInfo.value - if (chatInfo == null || user == null) { + if (chat == null || chatInfo == null || user == null) { LaunchedEffect(Unit) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -99,7 +110,7 @@ fun ChatView( .distinctUntilChanged() .filterNotNull() .collect { chatId -> - if (chatsCtx.contentTag == null) { + if (chatsCtx.secondaryContextFilter == null) { markUnreadChatAsRead(chatId) } showSearch.value = false @@ -108,6 +119,18 @@ fun ChatView( } } } + if (chatsCtx.secondaryContextFilter == null && chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberPending) { + LaunchedEffect(Unit) { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(chatInfo.groupInfo, groupChatScope = scopeInfo) + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + } val view = LocalMultiplatformView() val chatRh = remoteHostId.value // We need to have real unreadCount value for displaying it inside top right button @@ -117,6 +140,7 @@ fun ChatView( chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } + val reportsCount = reportsCount(chatInfo.id) val clipboard = LocalClipboardManager.current CompositionLocalProvider( LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), @@ -133,7 +157,7 @@ fun ChatView( val sameText = searchText.value == value // showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it // (required on Android to have this check to prevent call to search with old text) - val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.contentTag == null + val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null val c = chatModel.getChat(chatInfo.id) if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { @@ -167,7 +191,7 @@ fun ChatView( ) } ComposeView( - chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, + rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }, focusRequester = focusRequester ) @@ -220,6 +244,7 @@ fun ChatView( rh = chatRh, fromChatType = chatInfo.chatType, fromChatId = chatInfo.apiId, + fromScope = chatInfo.groupChatScope(), chatItemIds = chatItemIds ) @@ -317,7 +342,7 @@ fun ChatView( } } }, - showGroupReports = { + showReports = { val info = activeChatInfo.value ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() @@ -328,6 +353,38 @@ fun ChatView( showGroupReportsView(staleChatId, scrollToItemId, info) } }, + showSupportChats = { + val info = activeChatInfo.value ?: return@ChatLayout + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + scope.launch { + if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + ModalManager.end.showCustomModal { close -> + MemberSupportView( + chatRh, + chat, + info.groupInfo, + scrollToItemId, + close + ) + } + } else if (info is ChatInfo.Group) { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(info.groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + } + } + }, showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> hideKeyboard(view) groupMembersJob.cancel() @@ -343,12 +400,12 @@ fun ChatView( setGroupMembers(chatRh, groupInfo, chatModel) if (!isActive) return@launch - if (chatsCtx.contentTag == null) { + if (chatsCtx.secondaryContextFilter == null) { ModalManager.end.closeModals() } ModalManager.end.showModalCloseable(true) { close -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) + GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, close, close) } } } @@ -379,6 +436,7 @@ fun ChatView( chatRh, type = chatInfo.chatType, id = chatInfo.apiId, + scope = chatInfo.groupChatScope(), itemIds = listOf(itemId), mode = mode ) @@ -397,14 +455,13 @@ fun ChatView( if (deletedItem.isActiveReport) { chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id) } + chatModel.chatsContext.updateChatInfo(chatRh, deleted.deletedChatItem.chatInfo) } withContext(Dispatchers.Main) { - if (deletedChatItem.isReport) { - if (toChatItem != null) { - chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) - } + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem) } } } @@ -512,6 +569,7 @@ fun ChatView( rh = chatRh, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), itemId = cItem.id, add = add, reaction = reaction @@ -521,16 +579,14 @@ fun ChatView( chatModel.chatsContext.updateChatItem(cInfo, updatedCI) } withContext(Dispatchers.Main) { - if (cItem.isReport) { - chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI) - } + chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI) } } } }, showItemDetails = { cInfo, cItem -> suspend fun loadChatItemInfo(): ChatItemInfo? = coroutineScope { - val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) + val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cInfo.groupChatScope(), cItem.id) if (ciInfo != null) { if (chatInfo is ChatInfo.Group) { setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) @@ -579,12 +635,16 @@ fun ChatView( withContext(Dispatchers.Main) { chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds) ntfManager.cancelNotificationsForChat(chatInfo.id) - chatModel.controller.apiChatItemsRead( + val updatedChatInfo = chatModel.controller.apiChatItemsRead( chatRh, chatInfo.chatType, chatInfo.apiId, + chatInfo.groupChatScope(), itemsIds ) + if (updatedChatInfo != null) { + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } } withContext(Dispatchers.Main) { chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id, itemsIds) @@ -599,7 +659,8 @@ fun ChatView( chatModel.controller.apiChatRead( chatRh, chatInfo.chatType, - chatInfo.apiId + chatInfo.apiId, + chatInfo.groupChatScope() ) } withContext(Dispatchers.Main) { @@ -682,7 +743,8 @@ fun ChatLayout( selectedChatItems: MutableState?>, back: () -> Unit, info: () -> Unit, - showGroupReports: () -> Unit, + showReports: () -> Unit, + showSupportChats: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -745,7 +807,7 @@ fun ChatLayout( sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { val composeViewHeight = remember { mutableStateOf(0.dp) } - Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.contentTag == null)) { + Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.secondaryContextFilter == null)) { val remoteHostId = remember { remoteHostId }.value val chatInfo = remember { chatInfo }.value val oneHandUI = remember { appPrefs.oneHandUI.state } @@ -773,6 +835,7 @@ fun ChatLayout( .padding(bottom = composeViewHeight.value) ) { GroupMentions( + chatsCtx = chatsCtx, rhId = remoteHostId, composeState = composeState, composeViewFocusRequester = composeViewFocusRequester, @@ -832,41 +895,76 @@ fun ChatLayout( } } val reportsCount = reportsCount(chatInfo?.id) + val supportUnreadCount = supportUnreadCount(chatInfo?.id) if (oneHandUI.value && chatBottomBar.value) { - if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0) { - ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports) + if ( + chatInfo is ChatInfo.Group + && chatsCtx.secondaryContextFilter == null + && (reportsCount > 0 || supportUnreadCount > 0) + ) { + SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = true, showReports, showSupportChats) } else { StatusBarBackground() } } else { NavigationBarBackground(true, oneHandUI.value, noAlpha = true) } - if (chatsCtx.contentTag == MsgContentTag.Report) { - if (oneHandUI.value) { - StatusBarBackground() - } - Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { - Box { - if (selectedChatItems.value == null) { - GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged) - } else { - SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) - } - } - } - } else { - Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { - Box { - if (selectedChatItems.value == null) { - if (chatInfo != null) { - ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + when (chatsCtx.secondaryContextFilter) { + is SecondaryContextFilter.GroupChatScopeContext -> { + when (chatsCtx.secondaryContextFilter.groupScopeInfo) { + is GroupChatScopeInfo.MemberSupport -> { + if (oneHandUI.value) { + StatusBarBackground() + } + Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged) + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) + } + } } - } else { - SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) } } - if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) { - ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports) + } + is SecondaryContextFilter.MsgContentTagContext -> { + when (chatsCtx.secondaryContextFilter.contentTag) { + MsgContentTag.Report -> { + if (oneHandUI.value) { + StatusBarBackground() + } + Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged) + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) + } + } + } + } + else -> TODO() + } + } + null -> { + Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { + Box { + if (selectedChatItems.value == null) { + if (chatInfo != null) { + ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value) + } + } + if ( + chatInfo is ChatInfo.Group + && (reportsCount > 0 || supportUnreadCount > 0) + && (!oneHandUI.value || !chatBottomBar.value) + ) { + SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = false, showReports, showSupportChats) + } } } } @@ -900,7 +998,7 @@ fun BoxScope.ChatInfoToolbar( showSearch.value = false } } - if (appPlatform.isAndroid && chatsCtx.contentTag == null) { + if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) { BackHandler(onBack = onBackClicked) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() @@ -1097,33 +1195,79 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo } @Composable -private fun ReportedCountToolbar( +private fun SupportChatsCountToolbar( + chatInfo: ChatInfo, reportsCount: Int, + supportUnreadCount: Int, withStatusBar: Boolean, - showGroupReports: () -> Unit + showReports: () -> Unit, + showSupportChats: () -> Unit ) { Box { val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp Row( Modifier - .fillMaxWidth() - .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) - .background(MaterialTheme.colors.background) - .clickable(onClick = showGroupReports) - .padding(top = statusBarPadding), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, ) { - Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) - Spacer(Modifier.width(4.dp)) - Text( - if (reportsCount == 1) { - stringResource(MR.strings.group_reports_active_one) - } else { - stringResource(MR.strings.group_reports_active).format(reportsCount) - }, - style = MaterialTheme.typography.button - ) + if ( + chatInfo is ChatInfo.Group + && chatInfo.groupInfo.canModerate + && reportsCount > 0 + ) { + Row( + Modifier + .fillMaxWidth() + .weight(1F) + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showReports) + .padding(top = statusBarPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) + Spacer(Modifier.width(4.dp)) + Text( + if (reportsCount == 1) { + stringResource(MR.strings.group_reports_active_one) + } else { + stringResource(MR.strings.group_reports_active).format(reportsCount) + }, + style = MaterialTheme.typography.button + ) + } + } + + if (supportUnreadCount > 0) { + Row( + Modifier + .fillMaxWidth() + .weight(1F) + .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding) + .background(MaterialTheme.colors.background) + .clickable(onClick = showSupportChats) + .padding(top = statusBarPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(4.dp)) + Text( + if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { + if (appPlatform.isAndroid) + stringResource(MR.strings.group_new_support_chats_short).format(supportUnreadCount) + else if (supportUnreadCount == 1) + stringResource(MR.strings.group_new_support_chat_one) + else + stringResource(MR.strings.group_new_support_chats).format(supportUnreadCount) + } else { + stringResource(MR.strings.group_new_support_messages).format(supportUnreadCount) + }, + style = MaterialTheme.typography.button + ) + } + } } Divider(Modifier.align(Alignment.BottomStart)) } @@ -1198,298 +1342,281 @@ fun BoxScope.ChatItemsList( val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } // not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state - if (chatsCtx != null) { - val mergedItems = remember { - derivedStateOf { - MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState) - } + val mergedItems = remember { + derivedStateOf { + MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState) } - val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } } - val reportsCount = reportsCount(chatInfo.id) - val topPaddingToContent = topPaddingToContent( - chatView = chatsCtx.contentTag == null, - additionalTopBar = chatsCtx.contentTag == null && reportsCount > 0 - ) - val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) - val numberOfBottomAppBars = numberOfBottomAppBars() + } + val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } } + val reportsCount = reportsCount(chatInfo.id) + val supportUnreadCount = supportUnreadCount(chatInfo.id) + val topPaddingToContent = topPaddingToContent( + chatView = chatsCtx.secondaryContextFilter == null, + additionalTopBar = chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 0) + ) + val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) + val numberOfBottomAppBars = numberOfBottomAppBars() - /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of - * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears - * */ - val maxHeightForList = rememberUpdatedState( - with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } - ) - val resetListState = remember { mutableStateOf(false) } - remember(chatModel.openAroundItemId.value) { - if (chatModel.openAroundItemId.value != null) { - closeSearch() - resetListState.value = !resetListState.value - } + /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of + * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears + * */ + val maxHeightForList = rememberUpdatedState( + with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() } + ) + val resetListState = remember { mutableStateOf(false) } + remember(chatModel.openAroundItemId.value) { + if (chatModel.openAroundItemId.value != null) { + closeSearch() + resetListState.value = !resetListState.value } - val highlightedItems = remember { mutableStateOf(setOf()) } - val hoveredItemId = remember { mutableStateOf(null as Long?) } - val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) { - val openAroundItemId = chatModel.openAroundItemId.value - val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() } - val reportsState = reportsListState - if (openAroundItemId != null) { - highlightedItems.value += openAroundItemId - chatModel.openAroundItemId.value = null - } - hoveredItemId.value = null - if (reportsState != null) { - reportsListState = null - reportsState - } else if (index <= 0 || !searchValueIsEmpty.value) { - LazyListState(0, 0) - } else { - LazyListState(index + 1, -maxHeightForList.value) - } - }) - SaveReportsStateOnDispose(chatsCtx, listState) - val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } - val loadingMoreItems = remember { mutableStateOf(false) } - val animatedScrollingInProgress = remember { mutableStateOf(false) } - val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } - LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) { - if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) - ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) + } + val highlightedItems = remember { mutableStateOf(setOf()) } + val hoveredItemId = remember { mutableStateOf(null as Long?) } + val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) { + val openAroundItemId = chatModel.openAroundItemId.value + val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() } + val reportsState = reportsListState + if (openAroundItemId != null) { + highlightedItems.value += openAroundItemId + chatModel.openAroundItemId.value = null } - PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> - if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false - loadingMoreItems.value = true - withContext(NonCancellable) { - try { - loadMessages(chatId, pagination) { - visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) - } - } finally { - loadingMoreItems.value = false + hoveredItemId.value = null + if (reportsState != null) { + reportsListState = null + reportsState + } else if (index <= 0 || !searchValueIsEmpty.value) { + LazyListState(0, 0) + } else { + LazyListState(index + 1, -maxHeightForList.value) + } + }) + SaveReportsStateOnDispose(chatsCtx, listState) + val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } + val loadingMoreItems = remember { mutableStateOf(false) } + val animatedScrollingInProgress = remember { mutableStateOf(false) } + val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } + LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) { + if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT) + ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect) + } + PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> + if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false + loadingMoreItems.value = true + withContext(NonCancellable) { + try { + loadMessages(chatId, pagination) { + visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value) } + } finally { + loadingMoreItems.value = false } - true } - val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) - val chatInfoUpdated = rememberUpdatedState(chatInfo) - val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { - // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling - if (chatsCtx.contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it } - scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) - } - val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } - if (chatsCtx.contentTag == null) { - LaunchedEffect(Unit) { - snapshotFlow { scrollToItemId.value }.filterNotNull().collect { - if (appPlatform.isAndroid) { - ModalManager.end.closeModals() - } - scrollToItem(it) - scrollToItemId.value = null + true + } + val remoteHostIdUpdated = rememberUpdatedState(remoteHostId) + val chatInfoUpdated = rememberUpdatedState(chatInfo) + val scope = rememberCoroutineScope() + val scrollToItem: (Long) -> Unit = remember { + scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) + } + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, scrollToItemId) } + if (chatsCtx.secondaryContextFilter == null) { + LaunchedEffect(Unit) { + snapshotFlow { scrollToItemId.value }.filterNotNull().collect { + if (appPlatform.isAndroid) { + ModalManager.end.closeModals() } + scrollToItem(it) + scrollToItemId.value = null } } - SmallScrollOnNewMessage(listState, reversedChatItems) - val finishedInitialComposition = remember { mutableStateOf(false) } - NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) + } + SmallScrollOnNewMessage(listState, reversedChatItems) + val finishedInitialComposition = remember { mutableStateOf(false) } + NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) - DisposableEffectOnGone( - whenGone = { - VideoPlayerHolder.releaseAll() - } - ) + DisposableEffectOnGone( + whenGone = { + VideoPlayerHolder.releaseAll() + } + ) - @Composable - fun ChatViewListItem( - itemAtZeroIndexInWholeList: Boolean, - range: State, - showAvatar: Boolean, - cItem: ChatItem, - itemSeparation: ItemSeparation, - previousItemSeparationLargeGap: Boolean, - revealed: State, - reveal: (Boolean) -> Unit + @Composable + fun ChatViewListItem( + itemAtZeroIndexInWholeList: Boolean, + range: State, + showAvatar: Boolean, + cItem: ChatItem, + itemSeparation: ItemSeparation, + previousItemSeparationLargeGap: Boolean, + revealed: State, + reveal: (Boolean) -> Unit + ) { + val itemScope = rememberCoroutineScope() + CompositionLocalProvider( + // Makes horizontal and vertical scrolling to coexist nicely. + // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view + LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { - val itemScope = rememberCoroutineScope() - CompositionLocalProvider( - // Makes horizontal and vertical scrolling to coexist nicely. - // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view - LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() - ) { - val provider = { - providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> + val provider = { + providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> + itemScope.launch { + listState.value.scrollToItem( + min(reversedChatItems.value.lastIndex, indexInReversed + 1), + -maxHeight.value + ) + } + } + } + + @Composable + fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State, fillMaxWidth: Boolean = true) { + tryOrShowError("${cItem.id}ChatItem", error = { + CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) + }) { + val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } + LaunchedEffect(Unit) { + snapshotFlow { highlighted.value } + .distinctUntilChanged() + .filter { it } + .collect { + delay(500) + highlightedItems.value = setOf() + } + } + ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, 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, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + } + } + + @Composable + fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { itemScope.launch { - listState.value.scrollToItem( - min(reversedChatItems.value.lastIndex, indexInReversed + 1), - -maxHeight.value - ) - } - } - } - - @Composable - fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State, fillMaxWidth: Boolean = true) { - tryOrShowError("${cItem.id}ChatItem", error = { - CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) - }) { - val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } } - LaunchedEffect(Unit) { - snapshotFlow { highlighted.value } - .distinctUntilChanged() - .filter { it } - .collect { - delay(500) - highlightedItems.value = setOf() - } - } - ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) - } - } - - @Composable - fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) { - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { - if (it == DismissValue.DismissedToStart) { - itemScope.launch { - if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local && !cItem.isReport) { - if (composeState.value.editing) { - composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) - } + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local && !cItem.isReport) { + if (composeState.value.editing) { + composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) + } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) } } } - false } - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = setOf(DismissDirection.EndToStart), - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val sent = cItem.chatDir.sent + false + } + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) + val sent = cItem.chatDir.sent - @Composable - fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) { - Box( - modifier = modifier.padding( - bottom = if (itemSeparation.largeGap) { - if (itemAtZeroIndexInWholeList) { - 8.dp - } else { - 4.dp - } - } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp - ), - contentAlignment = Alignment.CenterStart - ) { - content() - } + @Composable + fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) { + Box( + modifier = modifier.padding( + bottom = if (itemSeparation.largeGap) { + if (itemAtZeroIndexInWholeList) { + 8.dp + } else { + 4.dp + } + } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp + ), + contentAlignment = Alignment.CenterStart + ) { + content() } + } - @Composable - fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp { - val chatItemTail = remember { appPreferences.chatItemTail.state } - val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) - val tailRendered = style is ShapeStyle.Bubble && style.tailVisible + @Composable + fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) + val tailRendered = style is ShapeStyle.Bubble && style.tailVisible - return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp) - } + return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp) + } + + Box { + val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null + val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf + val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp) + val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() } + if (chatInfo is ChatInfo.Group) { + if (cItem.chatDir is CIDirection.GroupRcv) { + if (showAvatar) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .fillMaxWidth() + .then(swipeableModifier), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start + ) { + @Composable + fun MemberNameAndRole(range: State) { + Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) { + val member = cItem.chatDir.groupMember + val rangeValue = range.value + val (prevMember, memCount) = + if (rangeValue != null) { + chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value) + } else { + null to 1 + } + Text( + memberNames(member, prevMember, memCount), + Modifier + .padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF) + .weight(1f, false), + fontSize = 13.5.sp, + color = MaterialTheme.colors.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { + val chatItemTail = remember { appPreferences.chatItemTail.state } + val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) + val tailRendered = style is ShapeStyle.Bubble && style.tailVisible - Box { - val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null - val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf - val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp) - val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() } - if (chatInfo is ChatInfo.Group) { - if (cItem.chatDir is CIDirection.GroupRcv) { - if (showAvatar) { - Column( - Modifier - .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) - .fillMaxWidth() - .then(swipeableModifier), - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalAlignment = Alignment.Start - ) { - @Composable - fun MemberNameAndRole(range: State) { - Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) { - val member = cItem.chatDir.groupMember - val rangeValue = range.value - val (prevMember, memCount) = - if (rangeValue != null) { - chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value) - } else { - null to 1 - } Text( - memberNames(member, prevMember, memCount), - Modifier - .padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF) - .weight(1f, false), + member.memberRole.text, + Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp), fontSize = 13.5.sp, + fontWeight = FontWeight.Medium, color = MaterialTheme.colors.secondary, - overflow = TextOverflow.Ellipsis, maxLines = 1 ) - if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { - val chatItemTail = remember { appPreferences.chatItemTail.state } - val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true) - val tailRendered = style is ShapeStyle.Bubble && style.tailVisible - - Text( - member.memberRole.text, - Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp), - fontSize = 13.5.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colors.secondary, - maxLines = 1 - ) - } } } - - @Composable - fun Item() { - ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) { - androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedListItem(Modifier, cItem.id, selectedChatItems) - } - Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) { - val member = cItem.chatDir.groupMember - Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { - MemberImage(member) - } - Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) { - ChatItemViewShortHand(cItem, itemSeparation, range, false) - } - } - } - } - if (cItem.content.showMemberName) { - DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { - MemberNameAndRole(range) - Item() - } - } else { - Item() - } } - } else { - ChatItemBox { - AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { - SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + + @Composable + fun Item() { + ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) { + androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier, cItem.id, selectedChatItems) + } + Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) { + val member = cItem.chatDir.groupMember + Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { + MemberImage(member) + } + Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) { + ChatItemViewShortHand(cItem, itemSeparation, range, false) + } + } } - Row( - Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) - .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) - .then(swipeableOrSelectionModifier) - ) { - ChatItemViewShortHand(cItem, itemSeparation, range) + } + if (cItem.content.showMemberName) { + DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) { + MemberNameAndRole(range) + Item() } + } else { + Item() } } } else { @@ -1497,138 +1624,152 @@ fun BoxScope.ChatItemsList( AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } - Box( + Row( Modifier - .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp) + .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) - .then(if (selectionVisible) Modifier else swipeableModifier) + .then(swipeableOrSelectionModifier) ) { ChatItemViewShortHand(cItem, itemSeparation, range) } } } - } else { // direct message + } else { ChatItemBox { AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) } - Box( - Modifier.padding( - start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp, - end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false), - ) + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) - .then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier) + .then(if (selectionVisible) Modifier else swipeableModifier) ) { ChatItemViewShortHand(cItem, itemSeparation, range) } } } - if (selectionVisible) { - Box(Modifier.matchParentSize().clickable { - val checked = selectedChatItems.value?.contains(cItem.id) == true - selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) - }) + } else { // direct message + ChatItemBox { + AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + + Box( + Modifier.padding( + start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp, + end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false), + ) + .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) + .then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier) + ) { + ChatItemViewShortHand(cItem, itemSeparation, range) + } } } - } - if (itemSeparation.date != null) { - DateSeparator(itemSeparation.date) - } - ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) - } - } - LazyColumnWithScrollBar( - Modifier.align(Alignment.BottomCenter), - state = listState.value, - contentPadding = PaddingValues( - top = topPaddingToContent, - bottom = composeViewHeight.value - ), - reverseLayout = true, - additionalBarOffset = composeViewHeight, - additionalTopBar = rememberUpdatedState(chatsCtx.contentTag == null && reportsCount > 0), - chatBottomBar = remember { appPrefs.chatBottomBar.state } - ) { - val mergedItemsValue = mergedItems.value - itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> - val isLastItem = index == mergedItemsValue.items.lastIndex - val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null - val listItem = merged.newest() - val item = listItem.item - val range = if (merged is MergedItem.Grouped) { - merged.rangeInReversed.value - } else { - null - } - val showAvatar = shouldShowAvatar(item, listItem.nextItem) - val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } } - val itemSeparation: ItemSeparation - val prevItemSeparationLargeGap: Boolean - if (merged is MergedItem.Single || isRevealed.value) { - val prev = listItem.prevItem - itemSeparation = getItemSeparation(item, prev) - val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem - prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item) - } else { - itemSeparation = getItemSeparation(item, null) - prevItemSeparationLargeGap = false - } - ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) { - if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems) - } - - if (last != null) { - // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items - DateSeparator(last.meta.itemTs) - } - if (item.isRcvNew) { - val itemIds = when (merged) { - is MergedItem.Single -> listOf(merged.item.item.id) - is MergedItem.Grouped -> merged.items.map { it.item.id } - } - MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) - } - } - } - FloatingButtons( - chatsCtx, - reversedChatItems, - chatInfoUpdated, - topPaddingToContent, - topPaddingToContentPx, - loadingMoreItems, - loadingTopItems, - loadingBottomItems, - animatedScrollingInProgress, - mergedItems, - unreadCount, - maxHeight, - composeViewHeight, - searchValue, - markChatRead, - listState, - loadMessages - ) - FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState) - - LaunchedEffect(Unit) { - snapshotFlow { listState.value.isScrollInProgress } - .collect { - chatViewScrollState.value = it - } - } - LaunchedEffect(Unit) { - snapshotFlow { listState.value.isScrollInProgress } - .filter { !it } - .collect { - if (animatedScrollingInProgress.value) { - animatedScrollingInProgress.value = false + if (selectionVisible) { + Box(Modifier.matchParentSize().clickable { + val checked = selectedChatItems.value?.contains(cItem.id) == true + selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) + }) } } + } + if (itemSeparation.date != null) { + DateSeparator(itemSeparation.date) + } + ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap) } } + LazyColumnWithScrollBar( + Modifier.align(Alignment.BottomCenter), + state = listState.value, + contentPadding = PaddingValues( + top = topPaddingToContent, + bottom = composeViewHeight.value + ), + reverseLayout = true, + additionalBarOffset = composeViewHeight, + additionalTopBar = rememberUpdatedState(chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 0)), + chatBottomBar = remember { appPrefs.chatBottomBar.state } + ) { + val mergedItemsValue = mergedItems.value + itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged -> + val isLastItem = index == mergedItemsValue.items.lastIndex + val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null + val listItem = merged.newest() + val item = listItem.item + val range = if (merged is MergedItem.Grouped) { + merged.rangeInReversed.value + } else { + null + } + val showAvatar = shouldShowAvatar(item, listItem.nextItem) + val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } } + val itemSeparation: ItemSeparation + val prevItemSeparationLargeGap: Boolean + if (merged is MergedItem.Single || isRevealed.value) { + val prev = listItem.prevItem + itemSeparation = getItemSeparation(item, prev) + val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem + prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item) + } else { + itemSeparation = getItemSeparation(item, null) + prevItemSeparationLargeGap = false + } + ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) { + if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems) + } + + if (last != null) { + // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items + DateSeparator(last.meta.itemTs) + } + if (item.isRcvNew) { + val itemIds = when (merged) { + is MergedItem.Single -> listOf(merged.item.item.id) + is MergedItem.Grouped -> merged.items.map { it.item.id } + } + MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead) + } + } + } + FloatingButtons( + chatsCtx, + reversedChatItems, + chatInfoUpdated, + topPaddingToContent, + topPaddingToContentPx, + loadingMoreItems, + loadingTopItems, + loadingBottomItems, + animatedScrollingInProgress, + mergedItems, + unreadCount, + maxHeight, + composeViewHeight, + searchValue, + markChatRead, + listState, + loadMessages + ) + FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState) + + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .collect { + chatViewScrollState.value = it + } + } + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .filter { !it } + .collect { + if (animatedScrollingInProgress.value) { + animatedScrollingInProgress.value = false + } + } + } } private suspend fun loadLastItems(chatsCtx: ChatModel.ChatsContext, chatId: State, listState: State, loadItems: State Boolean>) { @@ -2234,6 +2375,15 @@ fun reportsCount(staleChatId: String?): Int { } } +@Composable +fun supportUnreadCount(staleChatId: String?): Int { + return if (staleChatId?.startsWith("#") != true) { + 0 + } else { + remember(staleChatId) { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId } } }.value?.supportUnreadCount ?: 0 + } +} + private fun reversedChatItemsStatic(chatsCtx: ChatModel.ChatsContext): List = chatsCtx.chatItems.value.asReversed() @@ -2315,7 +2465,8 @@ private fun findQuotedItemFromItem( rhId: State, chatInfo: State, scope: CoroutineScope, - scrollToItem: (Long) -> Unit + scrollToItem: (Long) -> Unit, + scrollToItemId: MutableState ): (Long) -> Unit = { itemId: Long -> scope.launch(Dispatchers.Default) { val item = apiLoadSingleMessage(chatsCtx, rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId) @@ -2327,7 +2478,11 @@ private fun findQuotedItemFromItem( chatModel.secondaryChatsContext.value?.updateChatItem(chatInfo.value, item) } if (item.quotedItem?.itemId != null) { - scrollToItem(item.quotedItem.itemId) + if (item.isReport && chatsCtx.secondaryContextFilter != null) { + scrollToItemId.value = item.quotedItem.itemId + } else { + scrollToItem(item.quotedItem.itemId) + } } else { showQuotedItemDoesNotExistAlert() } @@ -2499,6 +2654,7 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } } withContext(Dispatchers.Main) { for (di in deleted) { - if (di.deletedChatItem.chatItem.isReport) { - val toChatItem = di.toChatItem?.chatItem - if (toChatItem != null) { - chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) - } + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) } } } @@ -2559,16 +2716,17 @@ private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List + chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo) + } } withContext(Dispatchers.Main) { for (di in deleted) { - if (di.deletedChatItem.chatItem.isReport) { - val toChatItem = di.toChatItem?.chatItem - if (toChatItem != null) { - chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) - } else { - chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) - } + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) } } } @@ -2920,7 +3078,7 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chatsCtx = ChatModel.ChatsContext(contentTag = null), + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, @@ -2935,7 +3093,8 @@ fun PreviewChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, - showGroupReports = {}, + showReports = {}, + showSupportChats = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, @@ -2998,7 +3157,7 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chatsCtx = ChatModel.ChatsContext(contentTag = null), + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, @@ -3013,7 +3172,8 @@ fun PreviewGroupChatLayout() { selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, - showGroupReports = {}, + showReports = {}, + showSupportChats = {}, showMemberInfo = { _, _ -> }, loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt new file mode 100644 index 0000000000..3c3f99ad94 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt @@ -0,0 +1,126 @@ +package chat.simplex.common.views.chat + +import SectionItemView +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chat.group.removeMember +import chat.simplex.common.views.chat.group.removeMemberDialog +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun ComposeContextPendingMemberActionsView( + rhId: Long?, + groupInfo: GroupInfo, + member: GroupMember +) { + Column( + Modifier + .height(60.dp) + .background(MaterialTheme.colors.surface) + ) { + Divider() + + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + rejectMemberDialog(rhId, member, chatModel, close = { ModalManager.end.closeModal() }) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.reject_pending_member_button), color = Color.Red) + } + + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + acceptMemberDialog(rhId, groupInfo, member, close = { ModalManager.end.closeModal() }) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.accept_pending_member_button), color = MaterialTheme.colors.primary) + } + } + } +} + +fun rejectMemberDialog(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_pending_member_alert_title), + confirmText = generalGetString(MR.strings.reject_pending_member_button), + onConfirm = { + removeMember(rhId, member, chatModel, close) + }, + destructive = true, + ) +} + +fun acceptMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.accept_pending_member_alert_title), + text = generalGetString(MR.strings.accept_pending_member_alert_question), + buttons = { + Column { + // Accept as member + SectionItemView({ + AlertManager.shared.hideAlert() + acceptMember(rhId, groupInfo, member, GroupMemberRole.Member, close) + }) { + Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_member), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Accept as observer + SectionItemView({ + AlertManager.shared.hideAlert() + acceptMember(rhId, groupInfo, member, GroupMemberRole.Observer, close) + }) { + Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_observer), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private fun acceptMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, role: GroupMemberRole, close: (() -> Unit)?) { + withBGApi { + val r = chatModel.controller.apiAcceptMember(rhId, groupInfo.groupId, member.groupMemberId, role) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + close?.invoke() + } +} 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 894bcf3b37..ca6279fd88 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 @@ -333,7 +333,9 @@ suspend fun MutableState.processPickedMedia(uris: List, text: @Composable fun ComposeView( + rhId: Long?, chatModel: ChatModel, + chatsCtx: ChatModel.ChatsContext, chat: Chat, composeState: MutableState, attachmentOption: MutableState, @@ -466,6 +468,7 @@ fun ComposeView( rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), live = live, ttl = ttl, composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) @@ -473,7 +476,7 @@ fun ComposeView( if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> withContext(Dispatchers.Main) { - chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -498,15 +501,17 @@ fun ComposeView( rh = rhId, toChatType = chat.chatInfo.chatType, toChatId = chat.chatInfo.apiId, + toScope = chat.chatInfo.groupChatScope(), fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, + fromScope = fromChatInfo.groupChatScope(), itemIds = forwardedItem.map { it.id }, ttl = ttl ) withContext(Dispatchers.Main) { chatItems?.forEach { chatItem -> - chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem) + chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem) } } @@ -563,16 +568,21 @@ fun ComposeView( } } + fun showReportsInSupportChatAlert() { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.report_sent_alert_title), + text = generalGetString(MR.strings.report_sent_alert_msg_view_in_support_chat), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showReportsInSupportChatAlert.set(false) + }, + ) + } + suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List? { val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText) - if (cItems != null) { - withContext(Dispatchers.Main) { - cItems.forEach { chatItem -> - chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) - } - } - } - + if (chatModel.controller.appPrefs.showReportsInSupportChatAlert.get()) showReportsInSupportChatAlert() return cItems?.map { it.chatItem } } @@ -581,7 +591,7 @@ fun ComposeView( val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateContact(chat.remoteHostId, contact) + chatsCtx.updateContact(chat.remoteHostId, contact) } } } @@ -594,13 +604,14 @@ fun ComposeView( rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, + scope = cInfo.groupChatScope(), itemId = ei.meta.itemId, updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions), live = live ) if (updatedItem != null) { withContext(Dispatchers.Main) { - chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) } } return updatedItem?.chatItem @@ -891,7 +902,7 @@ fun ComposeView( fun editPrevMessage() { if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return - val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable } + val lastEditable = chatsCtx.chatItems.value.findLast { it.meta.editable } if (lastEditable != null) { composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) } @@ -1004,6 +1015,21 @@ fun ComposeView( val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) Column { + if ( + chat.chatInfo is ChatInfo.Group + && chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chatsCtx.secondaryContextFilter.groupScopeInfo is GroupChatScopeInfo.MemberSupport + && chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ != null + && chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_.memberPending + && composeState.value.contextItem == ComposeContextItem.NoContextItem + && composeState.value.preview == ComposePreview.NoPreview + ) { + ComposeContextPendingMemberActionsView( + rhId = rhId, + groupInfo = chat.chatInfo.groupInfo, + member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ + ) + } if (nextSendGrpInv.value) { ComposeContextInvitingContactMemberView() } @@ -1011,8 +1037,8 @@ fun ComposeView( if (ctx is ComposeContextItem.ReportedItem) { ReportReasonView(ctx.reason) } - val simplexLinkProhibited = hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) - val fileProhibited = composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files) + val simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files) val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice) if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { if (simplexLinkProhibited) { @@ -1041,7 +1067,10 @@ fun ComposeView( Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) { Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) + val isGroupAndProhibitedFiles = + chatsCtx.secondaryContextFilter == null + && chat.chatInfo is ChatInfo.Group + && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) val attachmentClicked = if (isGroupAndProhibitedFiles) { { AlertManager.shared.showAlertMsg( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index b9538bc691..ac722783a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -77,7 +77,7 @@ fun SelectedItemsButtonsToolbar( val forwardCountProhibited = remember { mutableStateOf(false) } Box { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty - ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) Row( Modifier .matchParentSize() @@ -101,21 +101,21 @@ fun SelectedItemsButtonsToolbar( ) } - IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value) { + IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value && chatsCtx.secondaryContextFilter == null) { Icon( painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), - tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + tint = if (!moderateEnabled.value || deleteCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.error ) } - IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) { + IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value && chatsCtx.secondaryContextFilter == null) { Icon( painterResource(MR.images.ic_forward), null, Modifier.size(22.dp), - tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + tint = if (!forwardEnabled.value || forwardCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index e670fae5ef..91f7af2b95 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -68,7 +68,7 @@ private fun VerifyCodeLayout( } } - QRCode(connectionCode, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF)) + QRCode(connectionCode, small = true, padding = PaddingValues(vertical = DEFAULT_PADDING_HALF)) Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Spacer(Modifier.weight(2f)) 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 10694d13bf..827af085ea 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 @@ -55,6 +55,16 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea GroupPreferencesView(chatModel, rhId, groupInfo.id, close) } }, + openMemberAdmission = { + ModalManager.end.showCustomModal { close -> + MemberAdmissionView( + chat.simplex.common.platform.chatModel, + rhId, + groupInfo.id, + close + ) + } + }, inviteMembers = { allowModifyMembers = false withLongRunningApi(slow = 120_000) { @@ -93,8 +103,9 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { .asSequence() .map { it.chatInfo } .filterIsInstance() + .filter { it.sendMsgEnabled } .map { it.contact } - .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + .filter { c -> !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) } .sortedBy { it.displayName.lowercase() } .toList() @@ -110,6 +121,7 @@ fun AddGroupMembersLayout( allowModifyMembers: Boolean, searchText: MutableState, openPreferences: () -> Unit, + openMemberAdmission: () -> Unit, inviteMembers: () -> Unit, clearSelection: () -> Unit, addContact: (Long) -> Unit, @@ -144,7 +156,7 @@ fun AddGroupMembersLayout( horizontalArrangement = Arrangement.Center ) { ChatInfoToolbarTitle( - ChatInfo.Group(groupInfo), + ChatInfo.Group(groupInfo, groupChatScope = null), imageSize = 60.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight ) @@ -165,6 +177,9 @@ fun AddGroupMembersLayout( } else { SectionView { if (creatingGroup) { + SectionItemView(openMemberAdmission) { + Text(stringResource(MR.strings.set_member_admission)) + } SectionItemView(openPreferences) { Text(stringResource(MR.strings.set_group_preferences)) } @@ -376,6 +391,7 @@ fun PreviewAddGroupMembersLayout() { allowModifyMembers = true, searchText = remember { mutableStateOf(TextFieldValue("")) }, openPreferences = {}, + openMemberAdmission = {}, inviteMembers = {}, clearSelection = {}, addContact = {}, 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 22956738e7..db6eff562e 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 @@ -74,6 +74,9 @@ fun ModalData.GroupChatInfoView( val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) } val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) } val scope = rememberCoroutineScope() + val activeSortedMembers = remember { chatModel.groupMembers }.value + .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } + .sortedByDescending { it.memberRole } GroupChatInfoLayout( chat, @@ -95,9 +98,7 @@ fun ModalData.GroupChatInfoView( setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems) }, - activeSortedMembers = remember { chatModel.groupMembers }.value - .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } - .sortedByDescending { it.memberRole }, + activeSortedMembers = activeSortedMembers, developerTools, onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, @@ -126,7 +127,7 @@ fun ModalData.GroupChatInfoView( } ModalManager.end.showModalCloseable(true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) { + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, closeCurrent) { closeCurrent() close() } @@ -140,6 +141,17 @@ fun ModalData.GroupChatInfoView( addOrEditWelcomeMessage = { ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) } }, + openMemberSupport = { + ModalManager.end.showCustomModal { close -> + MemberSupportView( + rhId, + chat, + groupInfo, + scrollToItemId, + close + ) + } + }, openPreferences = { ModalManager.end.showCustomModal { close -> GroupPreferencesView( @@ -317,6 +329,40 @@ fun AddGroupMembersButton( ) } +@Composable +fun UserSupportChatButton( + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState +) { + val scope = rememberCoroutineScope() + + SettingsActionItemWithContent( + painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), + stringResource(MR.strings.button_support_chat), + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary), + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary + ) + } + } +} + @Composable fun ModalData.GroupChatInfoLayout( chat: Chat, @@ -337,6 +383,7 @@ fun ModalData.GroupChatInfoLayout( showMemberInfo: (GroupMember) -> Unit, editGroupProfile: () -> Unit, addOrEditWelcomeMessage: () -> Unit, + openMemberSupport: () -> Unit, openPreferences: () -> Unit, deleteGroup: () -> Unit, clearChat: () -> Unit, @@ -422,6 +469,40 @@ fun ModalData.GroupChatInfoLayout( SectionSpacer() + var anyTopSectionRowShow = false + SectionView { + if (groupInfo.canAddMembers && groupInfo.businessChat == null) { + anyTopSectionRowShow = true + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } + } + if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + anyTopSectionRowShow = true + MemberSupportButton(chat, openMemberSupport) + } + if (groupInfo.canModerate) { + anyTopSectionRowShow = true + GroupReportsButton(chat) { + scope.launch { + showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo) + } + } + } + if ( + groupInfo.membership.memberActive && + (groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null) + ) { + anyTopSectionRowShow = true + UserSupportChatButton(chat, groupInfo, scrollToItemId) + } + } + if (anyTopSectionRowShow) { + SectionDividerSpaced(maxBottomPadding = false) + } + SectionView { if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { EditGroupProfileButton(editGroupProfile) @@ -431,19 +512,17 @@ fun ModalData.GroupChatInfoLayout( } val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences GroupPreferencesButton(prefsTitleId, openPreferences) - if (groupInfo.canModerate) { - GroupReportsButton { - scope.launch { - showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo) - } - } - } + } + val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs + SectionTextFooter(stringResource(footerId)) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + + SectionView { if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { SendReceiptsOptionDisabled() } - WallpaperButton { ModalManager.end.showModal { val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } @@ -453,23 +532,13 @@ fun ModalData.GroupChatInfoLayout( } } } + ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) } - val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs - SectionTextFooter(stringResource(footerId)) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - - ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems) SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true) SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { if (groupInfo.canAddMembers) { - if (groupInfo.businessChat == null) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) - } else { - GroupLinkButton(manageGroupLink) - } - } val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary val addMembersTitleId = when (groupInfo.businessChat?.chatType) { @@ -481,7 +550,7 @@ fun ModalData.GroupChatInfoLayout( } if (activeSortedMembers.size > 8) { SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { - SearchRowView(searchText) + MemberListSearchRowView(searchText) } } SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { @@ -527,7 +596,7 @@ fun ModalData.GroupChatInfoLayout( val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat DeleteGroupButton(titleId, deleteGroup) } - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat LeaveGroupButton(titleId, leaveGroup) } @@ -631,17 +700,14 @@ private fun SelectedItemsCounterToolbarSetter( } @Composable -fun ChatTTLSection(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { +fun ChatTTLOption(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) { Box { - SectionView { - TtlOptions( - chatItemTTL, - enabled = remember { derivedStateOf { !deletingItems.value } }, - onSelected = setChatItemTTL, - default = chatModel.chatItemTTL - ) - SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) - } + TtlOptions( + chatItemTTL, + enabled = remember { derivedStateOf { !deletingItems.value } }, + onSelected = setChatItemTTL, + default = chatModel.chatItemTTL + ) if (deletingItems.value) { Box(Modifier.matchParentSize()) { ProgressIndicator() @@ -683,6 +749,23 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { } } +@Composable +private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) { + SettingsActionItemWithContent( + painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), + stringResource(MR.strings.member_support), + click = onClick, + iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary + ) + } + } +} + @Composable private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) { SettingsActionItem( @@ -693,12 +776,20 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) } @Composable -private fun GroupReportsButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_flag), +private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) { + SettingsActionItemWithContent( + painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), stringResource(MR.strings.group_reports_member_reports), - click = onClick - ) + click = onClick, + iconColor = (if (chat.chatStats.reportsCount > 0) Color.Red else MaterialTheme.colors.secondary) + ) { + if (chat.chatStats.reportsCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.chatStats.reportsCount), + backgroundColor = Color.Red + ) + } + } } @Composable @@ -820,7 +911,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr } @Composable -private fun MemberVerifiedShield() { +fun MemberVerifiedShield() { Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary) } @@ -941,7 +1032,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) { } @Composable -private fun SearchRowView( +fun MemberListSearchRowView( searchText: MutableState = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } ) { Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) { @@ -964,16 +1055,18 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel) fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { withBGApi { - val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) - if (updatedMembers != null) { + val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) + if (r != null) { + val (updatedGroupInfo, updatedMembers) = r withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo) updatedMembers.forEach { updatedMember -> - chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember) + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) } } withContext(Dispatchers.Main) { updatedMembers.forEach { updatedMember -> - chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember) + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) } } onSuccess() @@ -1016,7 +1109,18 @@ fun PreviewGroupChatInfoLayout() { selectedItems = remember { mutableStateOf(null) }, appBar = remember { mutableStateOf(null) }, scrollToItemId = remember { mutableStateOf(null) }, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) } + addMembers = {}, + showMemberInfo = {}, + editGroupProfile = {}, + addOrEditWelcomeMessage = {}, + openMemberSupport = {}, + openPreferences = {}, + deleteGroup = {}, + clearChat = {}, + leaveGroup = {}, + manageGroupLink = {}, + onSearchClicked = {}, + deletingItems = remember { mutableStateOf(true) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 6e1b9a731d..30c16db6a4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -49,6 +49,18 @@ fun GroupLinkView( creatingLink = false } } + fun addShortLink() { + creatingLink = true + withBGApi { + val link = chatModel.controller.apiAddGroupShortLink(rhId, groupInfo.groupId) + if (link != null) { + groupLink = link.first + groupLinkMemberRole.value = link.second + onGroupLinkUpdated?.invoke(link) + } + creatingLink = false + } + } LaunchedEffect(Unit) { if (groupLink == null && !creatingLink) { createLink() @@ -60,6 +72,7 @@ fun GroupLinkView( groupLinkMemberRole, creatingLink, createLink = ::createLink, + addShortLink = ::addShortLink, updateLink = { val role = groupLinkMemberRole.value if (role != null) { @@ -105,6 +118,7 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, + addShortLink: () -> Unit, updateLink: () -> Unit, deleteLink: () -> Unit, creatingGroup: Boolean = false, @@ -182,12 +196,26 @@ fun GroupLinkLayout( ) } } + if (groupLink.connShortLink == null && appPreferences.privacyShortLinks.get()) { + AddShortLinkButton(addShortLink) + } } } SectionBottomSpacer() } } +@Composable +private fun AddShortLinkButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.add_short_link), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState, enabled: Boolean = true) { Row( 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 285c96165c..e56bc36562 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 @@ -45,6 +45,7 @@ fun GroupMemberInfoView( rhId: Long?, groupInfo: GroupInfo, member: GroupMember, + scrollToItemId: MutableState, connectionStats: ConnectionStats?, connectionCode: String?, chatModel: ChatModel, @@ -79,6 +80,7 @@ fun GroupMemberInfoView( rhId = rhId, groupInfo, member, + scrollToItemId, connStats, newRole, developerTools, @@ -243,32 +245,34 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c text = generalGetString(messageId), confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { - withBGApi { - val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) - if (removedMembers != null) { - withContext(Dispatchers.Main) { - removedMembers.forEach { removedMember -> - chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember) - } - } - withContext(Dispatchers.Main) { - removedMembers.forEach { removedMember -> - chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember) - } - } - } - close?.invoke() - } + removeMember(rhId, member, chatModel, close) }, destructive = true, ) } +fun removeMember(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) { + withBGApi { + val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) + if (r != null) { + val (updatedGroupInfo, removedMembers) = r + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo) + removedMembers.forEach { removedMember -> + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember) + } + } + } + close?.invoke() + } +} + @Composable fun GroupMemberInfoLayout( rhId: Long?, groupInfo: GroupInfo, member: GroupMember, + scrollToItemId: MutableState, connStats: MutableState, newRole: MutableState, developerTools: Boolean, @@ -299,6 +303,29 @@ fun GroupMemberInfoLayout( } } + @Composable + fun SupportChatButton() { + val scope = rememberCoroutineScope() + + SettingsActionItem( + painterResource(MR.images.ic_flag), + stringResource(MR.strings.button_support_chat_member), + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + iconColor = MaterialTheme.colors.secondary, + ) + } + @Composable fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) @@ -413,6 +440,12 @@ fun GroupMemberInfoLayout( if (member.memberActive) { SectionView { + if ( + groupInfo.membership.memberRole >= GroupMemberRole.Moderator && + (member.memberRole < GroupMemberRole.Moderator || member.supportChat != null) + ) { + SupportChatButton() + } if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -878,6 +911,7 @@ fun PreviewGroupMemberInfoLayout() { rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, + scrollToItemId = remember { mutableStateOf(null) }, connStats = remember { mutableStateOf(null) }, newRole = remember { mutableStateOf(GroupMemberRole.Member) }, developerTools = false, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt index 2c4d4b16a8..62f1a4337c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt @@ -44,7 +44,7 @@ fun SelectedItemsMembersToolbar( ) { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty Box(Modifier.alpha(0f)) { - ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) } Row( Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt index 91f4f5173c..aa737a02d3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt @@ -35,6 +35,7 @@ private val MAX_PICKER_HEIGHT = (PICKER_ROW_SIZE * 4) + (MEMBER_ROW_AVATAR_SIZE @Composable fun GroupMentions( + chatsCtx: ChatModel.ChatsContext, rhId: Long?, composeState: MutableState, composeViewFocusRequester: FocusRequester?, @@ -48,12 +49,31 @@ fun GroupMentions( val mentionName = remember { mutableStateOf("") } val mentionRange = remember { mutableStateOf(null) } val mentionMemberId = remember { mutableStateOf(null) } + + fun contextMemberFilter(member: GroupMember): Boolean = + when (chatsCtx.secondaryContextFilter) { + null -> true + is SecondaryContextFilter.GroupChatScopeContext -> + when (chatsCtx.secondaryContextFilter.groupScopeInfo) { + is GroupChatScopeInfo.MemberSupport -> { + val scopeMember = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ + if (scopeMember != null) { + member.memberRole >= GroupMemberRole.Moderator || member.groupMemberId == scopeMember.groupMemberId + } else { + member.memberRole >= GroupMemberRole.Moderator + } + } + } + is SecondaryContextFilter.MsgContentTagContext -> false + } + val filteredMembers = remember { derivedStateOf { val members = chatModel.groupMembers.value .filter { val status = it.memberStatus status != GroupMemberStatus.MemLeft && status != GroupMemberStatus.MemRemoved && status != GroupMemberStatus.MemInvited + && contextMemberFilter(it) } .sortedByDescending { it.memberRole } 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 12c5b65769..b8db5969a1 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,7 +17,9 @@ import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.model.* import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* private val featureRoles: List> = listOf( @@ -71,6 +73,16 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> preferences = currentPreferences }, savePrefs = ::savePrefs, + openMemberAdmission = { + ModalManager.end.showCustomModal { close -> + MemberAdmissionView( + chatModel, + rhId, + chatId, + close + ) + } + } ) } } @@ -83,10 +95,15 @@ private fun GroupPreferencesLayout( applyPrefs: (FullGroupPreferences) -> Unit, reset: () -> Unit, savePrefs: () -> Unit, + openMemberAdmission: () -> Unit, ) { ColumnWithScrollBar { val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences AppBarTitle(stringResource(titleId)) + if (groupInfo.businessChat == null) { + MemberAdmissionButton(openMemberAdmission) + SectionDividerSpaced(maxBottomPadding = false) + } val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) } val onTTLUpdated = { ttl: Int? -> applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl))) @@ -156,6 +173,15 @@ private fun GroupPreferencesLayout( } } +@Composable +private fun MemberAdmissionButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_toggle_on), + stringResource(MR.strings.member_admission), + click = onClick + ) +} + @Composable private fun FeatureSection( feature: GroupFeature, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt index 1eeeb99c93..2cc2402c0a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt @@ -15,7 +15,15 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.* @Composable -private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State, scrollToItemId: MutableState) { +private fun GroupReportsView( + reportsChatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState, + close: () -> Unit +) { + KeyChangeEffect(chatModel.chatId.value) { + close() + } ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {}) } @@ -53,7 +61,7 @@ fun GroupReportsAppBar( } @Composable -private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { +fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { LaunchedEffect(Unit) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() @@ -69,13 +77,13 @@ private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { } suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: MutableState, chatInfo: ChatInfo) { - val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report) + val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(MsgContentTag.Report)) openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo) ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> ModalView({}, showAppBar = false) { val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { - GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId) + GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId, close) } else { LaunchedEffect(Unit) { close() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt new file mode 100644 index 0000000000..48171bfeb7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -0,0 +1,152 @@ +package chat.simplex.common.views.chat.group + +import InfoRow +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) { + val groupInfo = remember { derivedStateOf { + val ch = m.getChat(chatId) + val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo + if (g == null || ch.remoteHostId != rhId) null else g + }} + val gInfo = groupInfo.value ?: return + var admission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.groupProfile.memberAdmission) } + var currentAdmission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(admission) } + + fun saveAdmission(afterSave: () -> Unit = {}) { + withBGApi { + val gp = gInfo.groupProfile.copy(memberAdmission = admission) + val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) + if (g != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, g) + currentAdmission = admission + } + } + afterSave() + } + } + ModalView( + close = { + if (admission == currentAdmission) close() + else showUnsavedChangesAlert({ saveAdmission(close) }, close) + }, + ) { + MemberAdmissionLayout( + admission, + currentAdmission, + gInfo, + applyAdmission = { admsn -> + admission = admsn + }, + reset = { + admission = currentAdmission + }, + saveAdmission = ::saveAdmission, + ) + } +} + +@Composable +private fun MemberAdmissionLayout( + admission: GroupMemberAdmission?, + currentAdmission: GroupMemberAdmission?, + groupInfo: GroupInfo, + applyAdmission: (GroupMemberAdmission) -> Unit, + reset: () -> Unit, + saveAdmission: () -> Unit, +) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.member_admission)) + val review = remember(admission) { mutableStateOf(admission?.review) } + AdmissionSection(MR.strings.admission_stage_review, MR.strings.admission_stage_review_descr, review, groupInfo) { criteria -> + if (admission != null) { + applyAdmission(admission.copy(review = criteria)) + } else { + applyAdmission(GroupMemberAdmission(review = criteria)) + } + } + if (groupInfo.isOwner) { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + ResetSaveButtons( + reset = reset, + save = saveAdmission, + disabled = admission == currentAdmission + ) + } + SectionBottomSpacer() + } +} + +private val memberCriterias: List> = listOf( + null to generalGetString(MR.strings.member_criteria_off), + MemberCriteria.All to generalGetString(MR.strings.member_criteria_all) +) + +@Composable +private fun AdmissionSection( + admissionStageStrId: StringResource, + admissionStageDescrStrId: StringResource, + memberCriteria: State, + groupInfo: GroupInfo, + onSelected: (MemberCriteria?) -> Unit +) { + SectionView { + if (groupInfo.isOwner) { + ExposedDropDownSettingRow( + generalGetString(admissionStageStrId), + memberCriterias, + memberCriteria, + onSelected = { value -> + onSelected(value) + } + ) + } else { + InfoRow( + stringResource(admissionStageStrId), + memberCriteria.value?.text ?: generalGetString(MR.strings.member_criteria_off) + ) + } + } + SectionTextFooter(stringResource( admissionStageDescrStrId)) +} + +@Composable +private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) { + SectionView { + SectionItemView(reset, disabled = disabled) { + Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView(save, disabled = disabled) { + Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + } +} + +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.save_admission_question), + confirmText = generalGetString(MR.strings.save_and_notify_group_members), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt new file mode 100644 index 0000000000..99e2e3198e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -0,0 +1,145 @@ +package chat.simplex.common.views.chat.group + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +private fun MemberSupportChatView( + chatInfo: ChatInfo, + memberSupportChatsCtx: ChatModel.ChatsContext, + staleChatId: State, + scrollToItemId: MutableState +) { + KeyChangeEffect(chatModel.chatId.value) { + ModalManager.end.closeModals() + } + if (appPlatform.isAndroid) { + DisposableEffect(Unit) { + onDispose { + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatInfo.id } + if ( + memberSupportChatsCtx.isUserSupportChat + && chat?.chatInfo?.groupInfo_?.membership?.memberPending == true + ) { + withBGApi { + chatModel.chatId.value = null + } + } + } + } + } + ChatView(memberSupportChatsCtx, staleChatId, scrollToItemId, onComposed = {}) +} + +@Composable +fun MemberSupportChatAppBar( + chatsCtx: ChatModel.ChatsContext, + scopeMember_: GroupMember?, + close: () -> Unit, + onSearchValueChanged: (String) -> Unit +) { + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + val showSearch = rememberSaveable { mutableStateOf(false) } + val onBackClicked = { + if (!showSearch.value) { + close() + } else { + onSearchValueChanged("") + showSearch.value = false + } + } + BackHandler(onBack = onBackClicked) + if (scopeMember_ != null) { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + title = { MemberSupportChatToolbarTitle(scopeMember_) }, + onTitleClick = null, + onTop = !oneHandUI.value, + showSearch = showSearch.value, + onSearchValueChanged = onSearchValueChanged, + buttons = { + IconButton({ showSearch.value = true }) { + Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + ) + } else { + DefaultAppBar( + navigationButton = { NavigationButtonBack(onBackClicked) }, + fixedTitleText = stringResource(MR.strings.support_chat), + onTitleClick = null, + onTop = !oneHandUI.value, + showSearch = showSearch.value, + onSearchValueChanged = onSearchValueChanged, + buttons = { + IconButton({ showSearch.value = true }) { + Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) + } + } + ) + } + ItemsReload(chatsCtx) +} + +@Composable +fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + MemberProfileImage(size = imageSize * fontSizeSqrtMultiplier, member, iconColor) + Column( + Modifier.padding(start = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (member.verified) { + MemberVerifiedShield() + } + Text( + member.displayName, fontWeight = FontWeight.SemiBold, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + } + if (member.fullName != "" && member.fullName != member.displayName && member.localAlias.isEmpty()) { + Text( + member.fullName, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +suspend fun showMemberSupportChatView(staleChatId: State, scrollToItemId: MutableState, chatInfo: ChatInfo, scopeInfo: GroupChatScopeInfo) { + val memberSupportChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.GroupChatScopeContext(scopeInfo)) + openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo) + ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> + ModalView({}, showAppBar = false) { + if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) { + MemberSupportChatView(chatInfo, memberSupportChatsCtx, staleChatId, scrollToItemId) + } else { + LaunchedEffect(Unit) { + close() + } + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt new file mode 100644 index 0000000000..298a545c8c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -0,0 +1,302 @@ +package chat.simplex.common.views.chat.group + +import SectionBottomSpacer +import SectionItemView +import SectionItemViewLongClickable +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.* + +@Composable +fun ModalData.MemberSupportView( + rhId: Long?, + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState, + close: () -> Unit +) { + KeyChangeEffect(chatModel.chatId.value) { + ModalManager.end.closeModals() + } + LaunchedEffect(Unit) { + setGroupMembers(rhId, groupInfo, chatModel) + } + ModalView( + close = close, + endButtons = { RefreshMembersButton(rhId, groupInfo) } + ) { + MemberSupportViewLayout( + chat, + groupInfo, + scrollToItemId + ) + } +} + +@Composable +fun RefreshMembersButton( + rhId: Long?, + groupInfo: GroupInfo +) { + IconButton( + onClick = { + withBGApi { + setGroupMembers(rhId, groupInfo, chatModel) + } + } + ) { + Icon( + painterResource(MR.images.ic_refresh), + contentDescription = null, + tint = MaterialTheme.colors.primary + ) + } +} + +@Composable +private fun ModalData.MemberSupportViewLayout( + chat: Chat, + groupInfo: GroupInfo, + scrollToItemId: MutableState +) { + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + val scope = rememberCoroutineScope() + + val membersWithChats = remember { chatModel.groupMembers }.value + .filter { it.supportChat != null && it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } + .sortedWith( + compareByDescending { it.memberPending } + .thenByDescending { (it.supportChat?.mentions ?: 0) > 0 } + .thenByDescending { (it.supportChat?.memberAttention ?: 0) > 0 } + .thenByDescending { (it.supportChat?.unread ?: 0) > 0 } + .thenByDescending { it.supportChat?.chatTs } + ) + + val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } } + val filteredmembersWithChats = remember(membersWithChats) { + derivedStateOf { + val s = searchText.value.text.trim().lowercase() + if (s.isEmpty()) membersWithChats else membersWithChats.filter { m -> m.anyNameContains(s) } + } + } + + LazyColumnWithScrollBar( + contentPadding = + PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp else topPaddingToContent(false) + ) + ) { + item { + AppBarTitle(stringResource(MR.strings.member_support)) + } + + if (membersWithChats.isEmpty()) { + item { + Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } + } + } else { + item { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) + } + } + items(filteredmembersWithChats.value, key = { it.groupMemberId }) { member -> + Divider() + val showMenu = remember { mutableStateOf(false) } + SectionItemViewLongClickable( + click = { + val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member) + val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo) + scope.launch { + showMemberSupportChatView( + chatModel.chatId, + scrollToItemId = scrollToItemId, + supportChatInfo, + scopeInfo + ) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu) + SupportChatRow(member) + } + } + } + item { + Divider() + SectionBottomSpacer() + } + } + } +} + +@Composable +fun SupportChatRow(member: GroupMember) { + fun memberStatus(): String { + return if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_disabled) + } else if (member.activeConn?.connInactive == true) { + generalGetString(MR.strings.member_info_member_inactive) + } else if (member.memberPending) { + member.memberStatus.text + } else { + member.memberRole.text + } + } + + @Composable + fun SupportChatUnreadIndicator(supportChat: GroupSupportChat) { + Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) { + if (supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0) { + val unreadBadgeColor = when { + supportChat.mentions > 0 || supportChat.memberAttention > 0 -> MaterialTheme.colors.primaryVariant + else -> MaterialTheme.colors.secondary + } + if (supportChat.mentions == 1 && supportChat.unread == 1) { + Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(unreadBadgeColor, shape = CircleShape), contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_alternate_email), + contentDescription = generalGetString(MR.strings.notifications), + tint = Color.White, + modifier = Modifier.size(9.sp.toDp()) + ) + } + } else { + if (supportChat.mentions > 0 && supportChat.unread > 1) { + Icon( + painterResource(MR.images.ic_alternate_email), + contentDescription = generalGetString(MR.strings.notifications), + tint = unreadBadgeColor, + modifier = Modifier.size(12.sp.toDp()).offset(y = 2.sp.toDp()) + ) + } + + UnreadBadge( + text = unreadCountStr(supportChat.unread), + backgroundColor = unreadBadgeColor, + yOffset = 2.dp + ) + } + } + } + } + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + Modifier.weight(1f).padding(top = MEMBER_ROW_VERTICAL_PADDING, end = DEFAULT_PADDING, bottom = MEMBER_ROW_VERTICAL_PADDING), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + if (member.verified) { + MemberVerifiedShield() + } + Text( + member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (member.memberIncognito) Indigo else Color.Unspecified + ) + } + + Text( + memberStatus(), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + Row { + if (member.memberPending) { + Icon( + painterResource(MR.images.ic_flag_filled), + contentDescription = null, + Modifier.padding(end = 3.dp).size(16.dp), + tint = MaterialTheme.colors.primaryVariant + ) + } + if (member.supportChat != null) { + SupportChatUnreadIndicator(member.supportChat) + } + } + } +} + +@Composable +private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState) { + DefaultDropdownMenu(showMenu) { + if (member.memberPending) { + ItemAction(stringResource(MR.strings.accept_pending_member_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = { + acceptMemberDialog(rhId, groupInfo, member) + showMenu.value = false + }) + } else { + ItemAction(stringResource(MR.strings.delete_member_support_chat_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { + deleteMemberSupportChatDialog(rhId, groupInfo, member) + showMenu.value = false + }) + } + } +} + +fun deleteMemberSupportChatDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.delete_member_support_chat_alert_title), + confirmText = generalGetString(MR.strings.delete_member_support_chat_button), + onConfirm = { + deleteMemberSupportChat(rhId, groupInfo, member) + }, + destructive = true, + ) +} + +private fun deleteMemberSupportChat(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) { + withBGApi { + val r = chatModel.controller.apiDeleteMemberSupportChat(rhId, groupInfo.groupId, member.groupMemberId) + if (r != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second) + chatModel.chatsContext.updateGroup(rhId, r.first) + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 2e789df7bc..6e938aa5c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -86,6 +86,7 @@ fun ChatItemView( joinGroup: (Long, () -> Unit) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, + scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, @@ -271,6 +272,7 @@ fun ChatItemView( } } + // improvement could be to track "forwarded from" scope and open it @Composable fun GoToItemButton(alignStart: Boolean, parentActivated: State) { val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId @@ -324,7 +326,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem) + FramedItemView(chatsCtx, cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem) } fun deleteMessageQuestionText(): String { @@ -635,6 +637,15 @@ fun ChatItemView( CIEventView(eventItemViewText(reversedChatItems)) } + @Composable fun PendingReviewEventItemView() { + Text( + buildAnnotatedString { + withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text) } + }, + Modifier.padding(horizontal = 6.dp, vertical = 6.dp) + ) + } + @Composable fun DeletedItem() { MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) @@ -711,12 +722,16 @@ fun ChatItemView( is CIContent.RcvGroupEventContent -> { when (c.rcvGroupEvent) { is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) + is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView() else -> EventItemView() } MsgContentItemDropdownMenu() } is CIContent.SndGroupEventContent -> { - EventItemView() + when (c.sndGroupEvent) { + is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView() + else -> EventItemView() + } MsgContentItemDropdownMenu() } is CIContent.RcvConnEventContent -> { @@ -1422,7 +1437,7 @@ fun PreviewChatItemView( chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello") ) { ChatItemView( - chatsCtx = ChatModel.ChatsContext(contentTag = null), + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, ChatInfo.Direct.sampleData, chatItem, @@ -1444,6 +1459,7 @@ fun PreviewChatItemView( joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToItemId = remember { mutableStateOf(null) }, scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, @@ -1472,7 +1488,7 @@ fun PreviewChatItemView( fun PreviewChatItemViewDeletedContent() { SimpleXTheme { ChatItemView( - chatsCtx = ChatModel.ChatsContext(contentTag = null), + chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, ChatInfo.Direct.sampleData, ChatItem.getDeletedContentSampleData(), @@ -1494,6 +1510,7 @@ fun PreviewChatItemViewDeletedContent() { joinGroup = { _, _ -> }, acceptCall = { _ -> }, scrollToItem = {}, + scrollToItemId = remember { mutableStateOf(null) }, scrollToQuotedItemFromItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index fd8a32af64..b2beba29e4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -27,6 +27,7 @@ import kotlin.math.ceil @Composable fun FramedItemView( + chatsCtx: ChatModel.ChatsContext, chatInfo: ChatInfo, ci: ChatItem, uriHandler: UriHandler? = null, @@ -39,6 +40,7 @@ fun FramedItemView( receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, + scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { val sent = ci.chatDir.sent @@ -253,7 +255,11 @@ fun FramedItemView( onLongClick = { showMenu.value = true }, onClick = { if (ci.quotedItem.itemId != null) { - scrollToItem(ci.quotedItem.itemId) + if (ci.isReport && chatsCtx.secondaryContextFilter != null) { + scrollToItemId.value = ci.quotedItem.itemId + } else { + scrollToItem(ci.quotedItem.itemId) + } } else { scrollToQuotedItemFromItem(ci.id) } 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 958b794bd7..52b4059eef 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 @@ -236,7 +236,6 @@ suspend fun openChat( suspend fun openLoadedChat(chat: Chat) { withContext(Dispatchers.Main) { - chatModel.chatsContext.chatItemStatuses.clear() chatModel.chatsContext.chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id chatModel.chatsContext.chatState.clear() @@ -305,7 +304,7 @@ fun GroupMenuItems( } } GroupMemberStatus.MemAccepted -> { - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { @@ -327,7 +326,7 @@ fun GroupMenuItems( } } ClearChatAction(chat, showMenu) - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { @@ -615,7 +614,8 @@ fun markChatRead(c: Chat) { chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, - chat.chatInfo.apiId + chat.chatInfo.apiId, + chat.chatInfo.groupChatScope() ) chat = chatModel.getChat(chat.id) ?: return@withApi } @@ -651,7 +651,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { if (success) { withContext(Dispatchers.Main) { chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) - chatModel.chatsContext.updateChatTagReadNoContentTag(chat, wasUnread) + chatModel.chatsContext.updateChatTagReadInPrimaryContext(chat, wasUnread) } } } @@ -886,7 +886,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch ChatInfo.Direct(contact.copy(chatSettings = chatSettings)) } is ChatInfo.Group -> with(chatInfo) { - ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) + ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings), groupChatScope = null) } else -> null } @@ -914,7 +914,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch val updatedChat = chatModel.getChat(chatInfo.id) if (updatedChat != null) { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread) + chatModel.chatsContext.updateChatTagReadInPrimaryContext(updatedChat, wasUnread) } } val current = currentState?.value 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 93d512507a..65a9d175dd 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 @@ -241,6 +241,8 @@ fun ChatPreviewView( GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected)) GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) + GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval -> + Text(stringResource(MR.strings.reviewed_by_admins), color = MaterialTheme.colors.secondary) else -> {} } else -> {} @@ -363,7 +365,11 @@ fun ChatPreviewView( if (progressByTimeout) { progressView() } else if (chat.chatStats.reportsCount > 0) { - GroupReportsIcon() + FlagIcon(color = MaterialTheme.colors.error) + } else if (chat.supportUnreadCount > 0) { + FlagIcon(color = MaterialTheme.colors.primary) + } else if (chat.chatInfo.groupInfo_?.membership?.memberPending == true) { + FlagIcon(color = MaterialTheme.colors.secondary) } else { IncognitoIcon(chat.chatInfo.incognito) } @@ -465,17 +471,10 @@ fun ChatPreviewView( ) } } else { - Text( - if (n > 0) unreadCountStr(n) else "", - color = Color.White, - fontSize = 10.sp, - style = TextStyle(textAlign = TextAlign.Center), - modifier = Modifier - .offset(y = 3.sp.toDp()) - .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 2.sp.toDp()) - .padding(vertical = 1.sp.toDp()) + UnreadBadge( + text = if (n > 0) unreadCountStr(n) else "", + backgroundColor = if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, + yOffset = 3.dp ) } } @@ -548,11 +547,11 @@ fun IncognitoIcon(incognito: Boolean) { } @Composable -fun GroupReportsIcon() { +fun FlagIcon(color: Color) { Icon( painterResource(MR.images.ic_flag), contentDescription = null, - tint = MaterialTheme.colors.error, + tint = color, modifier = Modifier .size(21.sp.toDp()) .offset(x = 2.sp.toDp()) @@ -567,6 +566,26 @@ private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, g stringResource(MR.strings.group_preview_you_are_invited) } +@Composable +fun UnreadBadge( + text: String, + backgroundColor: Color, + yOffset: Dp? = null +) { + Text( + text, + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = yOffset ?: 0.dp) + .background(backgroundColor, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) +} + @Composable fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) 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 185ec3925f..13351a2111 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 @@ -634,7 +634,7 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) { } @Composable -fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { +fun BoxScope.userUnreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { Text( if (unreadCount > 0) unreadCountStr(unreadCount) else "", color = Color.White, 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 3d913cf957..2db40f770d 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 @@ -45,7 +45,6 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo) chatModel.chatsContext.chatItems.clearAndNotify() - chatModel.chatsContext.chatItemStatuses.clear() chatModel.chatId.value = groupInfo.id } setGroupMembers(rhId, groupInfo, chatModel) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index bacb5ab802..74c9a55ecf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -31,6 +31,7 @@ fun SimpleXCreatedLinkQRCode( ) { QRCode( connLink.simplexChatUri(short), + small = short && connLink.connShortLink != null, modifier, padding, tintColor, @@ -50,6 +51,7 @@ fun SimpleXLinkQRCode( ) { QRCode( simplexChatLink(connReq), + small = connReq.count() < 200, modifier, padding, tintColor, @@ -61,6 +63,7 @@ fun SimpleXLinkQRCode( @Composable fun QRCode( connReq: String, + small: Boolean = false, modifier: Modifier = Modifier, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF), tintColor: Color = Color(0xff062d56), @@ -68,9 +71,11 @@ fun QRCode( onShare: (() -> Unit)? = null, ) { val scope = rememberCoroutineScope() + val logoSize = if (small) 0.21f else 0.16f + val errorLevel = if (small) QrCode.ErrorLevel.M else QrCode.ErrorLevel.L val qr = remember(connReq, tintColor, withLogo) { - qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) - .let { if (withLogo) it.addLogo() else it } + qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) + .let { if (withLogo) it.addLogo(logoSize) else it } } Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Image( @@ -79,12 +84,13 @@ fun QRCode( Modifier .padding(padding) .widthIn(max = 400.dp) + .fillMaxWidth(if (small) 0.63f else 1f) .aspectRatio(1f) .then(modifier) .clickable { scope.launch { - val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) - .let { if (withLogo) it.addLogo() else it } + val image = qrCodeBitmap(connReq, 1024, errorLevel).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) + .let { if (withLogo) it.addLogo(logoSize) else it } val file = saveTempImageUncompressed(image, true) if (file != null) { shareFile("", CryptoFile.plain(file.absolutePath)) @@ -96,8 +102,8 @@ fun QRCode( } } -fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap { - val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate() +fun qrCodeBitmap(content: String, size: Int = 1024, errorLevel: QrCode.ErrorLevel): ImageBitmap { + val qrCode = QrCodeEncoder().addAutomatic(content).setError(errorLevel).fixate() /** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */ val numModules = QrCode.totalModules(qrCode.version) // Hide border on light themes to make it fit to the same place as camera in QRCodeScanner. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 8c7c2d8416..d77c4bc7f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -43,6 +43,7 @@ fun UserAddressView( KeyChangeEffect(user.value?.remoteHostId, user.value?.userId) { close() } + fun setProfileAddress(on: Boolean) { progressIndicator = true withBGApi { @@ -81,6 +82,17 @@ fun UserAddressView( } } + fun addShortLink() { + withBGApi { + progressIndicator = true + val userAddress = chatModel.controller.apiAddMyAddressShortLink(user.value?.remoteHostId) + if (userAddress != null) { + chatModel.userAddress.value = userAddress + } + progressIndicator = false + } + } + LaunchedEffect(autoCreateAddress) { if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() @@ -95,6 +107,7 @@ fun UserAddressView( userAddress = userAddress.value, shareViaProfile, createAddress = { createAddress() }, + addShortLink = { addShortLink() }, learnMore = { ModalManager.start.showModal { UserAddressLearnMore() @@ -169,6 +182,7 @@ private fun UserAddressLayout( userAddress: UserContactLinkRec?, shareViaProfile: MutableState, createAddress: () -> Unit, + addShortLink: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, sendEmail: (UserContactLinkRec) -> Unit, @@ -211,6 +225,9 @@ private fun UserAddressLayout( // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) + if (userAddress.connLinkContact.connShortLink == null && appPreferences.privacyShortLinks.get()) { + AddShortLinkButton(addShortLink) + } if (autoAcceptState.value.business) { SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) @@ -248,6 +265,17 @@ private fun CreateAddressButton(onClick: () -> Unit) { ) } +@Composable +private fun AddShortLinkButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.add_short_link), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } @@ -559,6 +587,7 @@ fun PreviewUserAddressLayoutNoAddress() { user = User.sampleData, userAddress = null, createAddress = {}, + addShortLink = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, @@ -592,6 +621,7 @@ fun PreviewUserAddressLayoutAddressCreated() { user = User.sampleData, userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)), createAddress = {}, + addShortLink = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt index bebc96a28c..8626fd3143 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -189,7 +189,7 @@ fun CustomServer( if (valid.value) { SectionDividerSpaced() SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { - QRCode(serverAddress.value) + QRCode(serverAddress.value, small = true) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 4bb2244785..ab6973d141 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -30,7 +30,7 @@ أضِف خوادم مُعدة مسبقًا أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! - الوصول إلى الخوادم عبر وكيل SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تفعيل هذا الخيار. + الوصول إلى الخوادم عبر وسيط SOCKS على المنفذ %d؟ يجب بدء تشغيل الوسيط قبل تفعيل هذا الخيار. أضِف خادم إعدادات الشبكة المتقدمة سيبقى جميع أعضاء المجموعة على اتصال. @@ -486,7 +486,7 @@ سيتم استلام الصورة عندما يكتمل جهة اتصالك من رفعِها. اعرض رمز QR في مكالمة الفيديو، أو شارك الرابط.]]> ثبّت SimpleX Chat لطرفية - إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها. + إذا أكّدت، فستتمكن خوادم المُراسلة من رؤية عنوان IP الخاص بك ومزود خدمتك - أي الخوادم التي تتصل بها. إخفاء: إذا أدخلت رمز المرور هذا عند فتح التطبيق، فستتم إزالة جميع بيانات التطبيق نهائيًا! استيراد قاعدة بيانات الدردشة؟ @@ -826,7 +826,7 @@ الإشعارات الدورية مُعطَّلة صورة ملف التعريف الإشعارات خاصة - يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. + يُرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. يُرجى تحديث التطبيق والتواصل مع المطورين. دليل المستخدم.]]> غيّر ملفات تعريف الدردشة @@ -843,7 +843,7 @@ إزالة إزالة عبارة المرور من Keystore؟ الرجاء إدخال عبارة المرور الحالية الصحيحة. - يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. + يُرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها. منع مكالمات الصوت/الفيديو. منع حذف الرسائل التي لا رجعة فيها. استلام الملفات غير معتمد حتى الآن @@ -908,7 +908,7 @@ رُفضت المكالمة حماية شاشة التطبيق أُزيلت - يرجى تذكرها أو تخزينها بأمان - لا توجد طريقة لاستعادة كلمة المرور المفقودة! + يُرجى تذكرها أو تخزينها بأمان - لا توجد طريقة لاستعادة كلمة المرور المفقودة! معاينة من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح يتم استلام الرسائل… @@ -918,7 +918,7 @@ سحب وصول الملف سحب وصول الملف؟ رٌفض الإذن! - يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. + يُرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. العنصر النائب لصورة ملف التعريف رمز QR صفّر @@ -1025,7 +1025,7 @@ إيقاف الدردشة؟ عرض حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد: - وكيل SOCKS + وسيط SOCKS تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits. إيقاف عرض المعاينة @@ -1060,10 +1060,10 @@ إيقاف الملف التوقف عن إرسال الملف؟ عنوان SimpleX - استخدم مضيفي .onion إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]> + استخدم مضيفي .onion إلى "لا" إذا كان وسيط SOCKS لا يدعمها.]]> مشاركة مع جهات الاتصال إيقاف التشغيل؟ - إعدادات وكيل SOCKS + إعدادات وسيط SOCKS إيقاف التشغيل السماعة قيد التشغيل أرسل @@ -1108,7 +1108,7 @@ عزل النقل هذه السلسلة ليست رابط اتصال! هذه الإعدادات لملف تعريفك الحالي - يمكن تجاوزها في إعدادات الاتصال و المجموعة. + يمكن تجاوزها في إعدادات الاتصال والمجموعة. انتهت مهلة اتصال TCP لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC). فقدنا القراد الثاني! ✅ @@ -1160,9 +1160,9 @@ ستكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا! ستكون متصلاً عندما يتم قبول طلب اتصالك، يُرجى الانتظار أو التحقق لاحقًا! تستخدم خوادم SimpleX Chat. - استخدم وكيل SOCKS + استخدم وسيط SOCKS استخدم مضيفي onion. - استخدام وكيل SOCKS؟ + استخدام وسيط SOCKS؟ عندما تكون متاحة ستبقى جهات اتصالك متصلة. لا نقوم بتخزين أي من جهات اتصالك أو رسائلك (بمجرد تسليمها) على الخوادم. @@ -1276,8 +1276,7 @@ عبر رابط عنوان الاتصال ما لم يحذف جهة الاتصال الاتصال أو استُخدم هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. \nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء رابط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت. - سيتم إرسال ملف تعريف الدردشة الخاص بك -\nإلى جهة اتصالك + سيتم إرسال ملف تعريف دردشتك\nإلى جهة اتصالك إلغاء الإخفاء ملفك التعريفي العشوائي ستستمر في استلام المكالمات والإشعارات من الملفات التعريفية المكتومة عندما تكون نشطة. @@ -1290,7 +1289,7 @@ رسالة صوتية رسالة صوتية… أنت مدعو إلى المجموعة - لا يمكنك إرسال رسائل! + أنت المراقب! تحتاج إلى السماح لجهة اتصالك بإرسال رسائل صوتية لتتمكن من إرسالها. أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s). الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات.]]> @@ -1340,8 +1339,7 @@ %s، %s و %d أعضاء آخرين متصلون %s، %s و %s متصل سيتم تعمية قاعدة البيانات وتخزين عبارة المرور في الإعدادات. - يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي. -\nيمكنك تغييره لاحقا. + يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي.\nيمكنك تغييره لاحقًا. سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في الإعدادات. هل تريد إزالة عبارة المرور من الإعدادات؟ استخدم عبارة مرور عشوائية @@ -1351,9 +1349,9 @@ افتح مجلد قاعدة البيانات سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق. عبارة المرور مخزنة في الإعدادات كنص عادي. - يُرجى الملاحظة: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> + يُرجى الملاحظة: يتم توصيل مُرحلات الرسائل والملفات عبر وسيط SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]> عَمِّ الملفات المحلية - عَمِّ الملفات والوسائط المخزنة + عمِّ الملفات والوسائط المخزنة تطبيق سطح المكتب الجديد! 6 لغات واجهة جديدة يُعمِّي الملفات المحلية الجديدة (باستثناء مقاطع الفيديو). @@ -1608,11 +1606,11 @@ أنهيّ المكالمة متصفح الويب الافتراضي مطلوب للمكالمات. يُرجى تضبيط المتصفح الافتراضي في النظام، ومشاركة المزيد من المعلومات مع المطورين. حدث خطأ أثناء فتح المتصفح - أرشفة و رفع + أرشف وأرفع يمكن للمُدراء حظر عضو للجميع. ترحيل بيانات التطبيق جارِ أرشفة قاعدة البيانات - جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تشفيرها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة. + جميع جهات الاتصال، المحادثات والملفات الخاصة بك سيتم تعميتها بأمان ورفعها على شكل أجزاء إلى موجهات XFTP المُعدة. طبّق يُرجى ملاحظة: استخدام نفس قاعدة البيانات على جهازين سيؤدي إلى كسر فك تعمية الرسائل من اتصالاتك، كحماية أمنية.]]> تحذير: سيتم حذف الأرشيف.]]> @@ -1694,7 +1692,7 @@ رحّل من جهاز آخر على الجهاز الجديد و امسح رمز QR ضوئيًا.]]> تحذير: بدء الدردشة على أجهزة متعددة غير مدعوم وسيؤدي إلى فشل تسليم الرسائل إتصال شبكة - محوّلة + مُحوّلة خلوي إيثرنت سلكية لا إتصال شبكة @@ -1712,7 +1710,7 @@ مفعّل لـ المالكون الملفات والوسائط غير مسموح بها - محوّلة + مُحوّلة حوّل الرسالة… تلقي التزامن لا يستطيع المُستلم/ون معرفة مَن أرسل هذه الرسالة. @@ -1722,7 +1720,7 @@ السماعة سماعة الأذن سماعات الرأس - محوّلة مِن + مُحوّلة مِن حُفظت نزّل حوّل @@ -1744,10 +1742,8 @@ تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. خطأ في خادم الوجهة: %1$s خطأ: %1$s - خادم التحويل: %1$s -\nخطأ في الخادم الوجهة: %2$s - خادم التحويل: %1$s -\nخطأ: %2$s + خادم التحويل: %1$s\nخطأ في خادم الوجهة: %2$s + خادم التحويل: %1$s\nخطأ: %2$s تحذير تسليم الرسالة مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها. نعم @@ -1772,7 +1768,7 @@ استخدم دائمًا التوجيه الخاص. الملفات مطلقًا - سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وكيل SOCKS). + سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وسيط SOCKS). أرسل الرسائل مباشرة عندما يكون عنوان IP محميًا ولا يدعم الخادم الوجهة لديك التوجيه الخاص. خوادم غير معروفة خوادم غير معروفة! @@ -1847,13 +1843,13 @@ أرسلت الإجمالي الحجم الملفات المرفوعة - يُرجى المحاولة لاحقا. + يُرجى المحاولة لاحقًا. خطأ في التوجيه الخاص عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s. إصدار الخادم غير متوافق مع تطبيقك: %1$s. العضو غير نشط - رسالة محوّلة - لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المُدير. + رسالة مُحوّلة + لا يوجد اتصال مباشر حتى الآن، الرسالة مُحوّلة بواسطة المُدير. امسح / ألصِق الرابط خوادم SMP المهيأة خوادم SMP أخرى @@ -1868,7 +1864,7 @@ ثُبّت بنجاح افتح مكان الملف يُرجى إعادة تشغيل التطبيق. - تذكر لاحقا + تذكر لاحقًا تخطي هذه النسخة أُلغيت تنزيل التحديث مُعطَّل @@ -1910,7 +1906,7 @@ مؤمن أرسل الأخطاء أُرسلت مباشرةً - مُرسَل عبر الوكيل + مُرسَل عبر الوسيط مشترك أخطاء الاشتراك رفع الأخطاء @@ -1957,11 +1953,11 @@ لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. قرّب - حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا. + حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقًا. عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s. إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s. - فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا. + فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقًا. إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. مطفي قوي @@ -2011,7 +2007,7 @@ خوادم الوسائط والملفات خوادم الرسائل متابعة - وكيل SOCKS + وسيط SOCKS يمكنك ترحيل قاعدة البيانات المُصدرة. يمكنك حفظ الأرشيف المُصدر. حالة الاتصال والخوادم. @@ -2046,19 +2042,19 @@ لم يتم تنزيل %1$d ملف/ات. نزّل شارك ملف التعريف - استخدم بيانات اعتماد الوكيل المختلفة لكل اتصال. + استخدم بيانات اعتماد الوسيط المختلفة لكل اتصال. اسم المستخدم قد يتم إرسال بيانات اعتمادك غير مُعمَّاة. - خطأ في حفظ الوكيل + خطأ في حفظ الوسيط إزالة الأرشيف؟ وضع النظام سيتم إزالة أرشيف قاعدة البيانات المرفوعة نهائيًا من الخوادم. - استخدم بيانات اعتماد الوكيل المختلفة لكل ملف تعريف. + استخدم بيانات اعتماد الوسيط المختلفة لكل ملف تعريف. استخدم بيانات اعتماد عشوائية قاعدة بيانات الدردشة حُذف %1$d ملف/ات. لا يزال يتم تنزيل %1$d ملف/ات. - لا تستخدم بيانات الاعتماد مع الوكيل. + لا تستخدم بيانات الاعتماد مع الوسيط. خطأ في تحويل الرسائل خطأ في تبديل ملف التعريف حدد ملف تعريف الدردشة @@ -2069,12 +2065,12 @@ حوّل الرسائل… تحويل الرسائل بدون ملفات؟ جارِ حفظ %1$s رسالة - تأكد من صحة تضبيط الوكيل. + تأكد من صحة تضبيط الوسيط. %1$d خطأ في ملف آخر. حُذفت الرسائل بعد تحديدها. لا يوجد شيء لتحويله! كلمة المرور - استيثاق الوكيل + استيثاق الوسيط سيتم حذف الرسائل - لا يمكن التراجع عن هذا! الصوت مكتوم حدث خطأ أثناء تهيئة WebView. تأكد من تثبيت WebView وأن بنيته المدعومة هي arm64.\nالخطأ: %s @@ -2376,4 +2372,53 @@ إيقاف التشغيل الخوادم المُعدة مسبقًا جميع الخوادم + خطأ في قبول العضو + دردشة واحدة مع عضو + %d دردشة/ات + %d دردشات مع الأعضاء + %d رسائل + أُرسِل البلاغ للمشرفين + يمكنك عرض تقاريرك في \"دردش مع المُدراء\". + اقبل كمراقب + حُذفت جهة الاتصال + عُطِّلت جهة الاتصال + جهة الاتصال غير جاهزة + لا يمكنك إرسال الرسائل! + غير متزامن + قبلت %1$s + قبِلك + لقد قبلت هذا العضو. + الرجاء الانتظار ريثما يراجع مشرفو المجموعة طلبك للانضمام إليها. + دردش مع المُدراء + راجع الأعضاء + غير مفعّل + اقبل + عضو جديد يريد الانضمام للمجموعة. + الكل + دخول العضو + راجع الأعضاء قبل القبول (الطرق). + حدث خطأ أثناء حذف الدردشة مع العضو. + لا يمكن إرسال الرسائل + رُفض طلب الانضمام + غادرت + حُذفت المجموعة + أُزيل من المجموعة + حفظ إعدادات القبول؟ + قيد المراجعة + مراجعة + دردش مع عضو + حدّد دخول العضو + دردش مع المُدراء + احذف الدردشة + حذف الدردشة مع العضو؟ + ارفض + اقبل كعضو + اقبل العضو + سينضم العضو إلى المجموعة، هل تقبل العضو؟ + رفض العضو؟ + دردشات مع الأعضاء + رُجع من قِبل المُدراء + لا دردشات مع الأعضاء + العضو لديه نُسخة قديمة + أضف رابط قصير diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 6726009a5f..7af75e64e8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -158,6 +158,8 @@ Error loading details Error adding member(s) Error joining group + Error accepting member + Error deleting chat with member Cannot receive file Sender cancelled file transfer. Unknown servers! @@ -464,6 +466,10 @@ 1 report %d reports Member reports + %d messages + %d chats with members + 1 chat with a member + %d chat(s) Share message… @@ -486,7 +492,6 @@ Decoding error The image cannot be decoded. Please, try a different image or contact developers. The video cannot be decoded. Please, try a different video or contact developers. - you are observer Files and media prohibited! Only group owners can enable files and media. Send direct message to connect @@ -505,6 +510,8 @@ Report violation: only group moderators will see it. Report content: only group moderators will see it. Report other: only group moderators will see it. + Report sent to moderators + You can view your reports in Chat with admins. You can\'t send messages! contact not ready @@ -518,6 +525,9 @@ removed from group you left can\'t send messages + you are observer + reviewed by admins + member has old version Image @@ -1048,6 +1058,7 @@ Address settings Business address Add your team members to the conversations. + Add short link Continue @@ -1063,6 +1074,7 @@ Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. Edit image Delete image + Save admission settings? Save preferences? Save and notify contact Save and notify contacts @@ -1582,6 +1594,8 @@ invited %1$s connected + accepted %1$s + accepted you left changed role of %s to %s blocked %s @@ -1593,6 +1607,7 @@ updated group profile invited via your group link connected directly + New member wants to join the group. you changed role of %s to %s you changed role for yourself to %s you blocked %s @@ -1600,6 +1615,8 @@ you removed %1$s you left group profile updated + you accepted this member + Please wait for group moderators to review your request to join the group. %s connected %s and %s connected @@ -1659,6 +1676,8 @@ invited pending approval pending + pending review + review connecting (introduced) connecting (introduction invitation) connecting (accepted) @@ -1729,6 +1748,7 @@ Receipts are disabled This group has over %1$d members, delivery receipts are not sent. Invite + Chat with admins FOR CONSOLE @@ -1765,7 +1785,7 @@ Remove member? Remove members? Remove member - + Chat with member Send direct message Member will be removed from group - this cannot be undone! Members will be removed from group - this cannot be undone! @@ -2042,6 +2062,7 @@ Contact preferences Group preferences Set group preferences + Set member admission Your preferences Disappearing messages Direct messages @@ -2159,6 +2180,29 @@ owners Enabled for + + Member admission + Review members + Review members before admitting ("knocking"). + off + all + + + Chats with members + No chats with members + Delete chat + Delete chat with member? + + + Chat with admins + Reject + Reject member? + Accept + Accept member + Member will join the group, accept member? + Accept as member + Accept as observer + What\'s new New in %s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 0e179fadd4..30376c43b7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -1248,7 +1248,7 @@ Trucada pendent So silenciat Missatges omesos - El hash del missatge anterior és diferent.\" + El hash del missatge anterior és diferent. L\'identificador del missatge següent és incorrecte (menor o igual a l\'anterior).\nPot passar per algun error o quan la connexió està compromesa. Informeu-ho als desenvolupadors. Pot passar quan tu o el teu contacte feu servir la còpia de seguretat de la base de dades antiga. @@ -1716,7 +1716,7 @@ La imatge no es pot descodificar. Si us plau, proveu amb una imatge diferent o contacteu amb els desenvolupadors. El vídeo no es pot descodificar. Si us plau, prova amb un vídeo diferent o contacta amb els desenvolupadors. ets observador - No pots enviar missatges! + ets observador(a) Poseu-vos en contacte amb l\'administrador del grup. Només els propietaris del grup poden activar fitxers i mitjans. Desant %1$s missatges @@ -2353,4 +2353,53 @@ Apagat Feu servir el port TCP 443 només per a servidors predefinits. Servidors predefinits + Error en acceptar membre + %d missatges + Informe enviat a la moderació + desactivat + Acceptar + Acceptar com a membre + Acceptar membre + El(la) membre s\'unirà al grup, l\'accepteu? + revisat per l\'administració + no es poden enviar missatges + contacte eliminat + contacte desactivat + el contacte no està a punt + el grup s\'ha suprimit + no sincronitzat + eliminat(da) del grup + sol·licitud d\'unió rebutjada + No podeu enviar missatges! + Podeu veure els vostres informes al xat amb administració. + Heu sortit + %1$s acceptat(da) + us ha acceptat + pendient de revisió + Establir l\'admissió de membres + Admissió de membre + Xat amb admins + No hi ha xats amb membres + Acceptar com a observador + Desar la configuració d\'admissió? + Error en suprimir el xat amb membre + %d xat(s) + el(la) membre té una versió antiga + Un(a) nou(va) membre vol unir-se al grup. + Si us plau, espereu que els moderadors del grup revisin la vostra sol·licitud per unir-vos-hi. + heu acceptat aquest(a) membre + per revisar + Xats amb admins + Xats amb membre + Eliminar xat + Rebutjar + Rebutjar membre? + Revisar membres + Revisar membres abans d\'admetre (trucar a la porta). + un xat amb un(a) membre + Xats amb membres + Suprimir el xat amb membre? + %d xats amb membres + tots(es) + Afegir enllaç curt diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index 439b9df18d..fdc0266f2d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -937,7 +937,7 @@ pozorovatel Zpráva bude smazána pro všechny členy. Zpráva bude pro všechny členy označena jako moderovaná. - Nemůžete posílat zprávy! + jste pozorovatel Chyba aktualizace odkazu skupiny Počáteční role Systém @@ -2380,4 +2380,53 @@ Vypnut Přednastavené servery Použít TCP port 443 jen pro přednastavené servery. + Chyba přijmutí člena + %d chat(y) + %d chat se členy + %d zpráv + 1 chat se členem + Uložit nastavení vstupného? + Chat s adminy + Nastavit přijímání členů + Přihlášení člena + Schválit členy + Schválit členy před přijetím (zaklepat). + Chat se členy + Žádné chaty se členy + Chat s adminy + Přijmout + Přijmout člena + schválen adminy + Přidat krátký odkaz + přijat %1$s + Vás přijal + schválení + Odstranit chat se členem? + Odmítnout + odešli jste + Chat se členem + Prosím, počkejte až moderátoři skupiny vaši žádost o připojení ke skupině schválí. + přijali jste tohoto člen + Hlášení odesláno moderátorům + kontakt smazán + kontakt vypnut + kontakt nepřipraven + nesynchronizováno + žádost o přihlášení zamítnuta + Nemůžete posílat zprávy! + Můžete zobrazit své hlášení v Chatu s adminy. + nemůže posílat zprávy + skupina smazána + člen má starou verzi + odstraněn ze skupiny + Nový člen chce připojit do skupiny. + čeká na posouzení + vypnuto + vše + Odstranit chat + Člen se připojí ke skupině, přijmout? + Přijmout jako člena + Přijmout jako pozorovatele + Odmítnout člena? + Chyba odstranění chatu se členem 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 6646720c5c..13b0bc95b6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -1014,7 +1014,7 @@ Moderieren Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet. Sie sind Beobachter - Sie können keine Nachrichten versenden! + Sie sind Beobachter Beobachter Anfängliche Rolle Nachricht des Mitglieds löschen\? @@ -1465,7 +1465,7 @@ Profil erstellen %s und %s Ihrer Gruppe beitreten? - %1$s.]]> + %1$s.]]> Das ist Ihr eigener Einmal-Link! %d Nachrichten als gelöscht markiert Gruppe besteht bereits! @@ -2257,7 +2257,7 @@ Adress- oder Einmal-Link? App-Symbolleiste Verpixeln - nur mit einem Kontakt genutzt werden - teilen Sie in nur persönlich oder über einen beliebigen Messenger.]]> + nur mit einem Kontakt genutzt werden - teilen Sie ihn nur persönlich oder über einen beliebigen Messenger.]]> %s.]]> %s.]]> Die Nutzungsbedingungen wurden akzeptiert am: %s @@ -2462,4 +2462,53 @@ Aus TCP-Port 443 nur für voreingestellte Server verwenden. Voreingestellte Server + %d Chats mit Mitgliedern + %d Chat(s) + Meldung wurde an die Moderatoren gesendet + Sie haben dieses Mitglied übernommen + Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\"). + Überprüfung der Mitglieder + alle + Aus + Als Beobachter übernehmen + Mitglied übernehmen + Chats mit Mitgliedern + Chat mit Administratoren + Keine Chats mit Mitgliedern + Entfernen + hat Sie übernommen + Chat mit einem Mitglied + %d Nachrichten + Ein Mitglied wird der Gruppe beitreten. Übernehmen? + Ein neues Mitglied will der Gruppe beitreten. + Überprüfung + Von Administratoren überprüft + Aufnahme von Mitgliedern festlegen + Speichern der Aufnahme-Einstellungen? + Sie können Ihre Meldungen im Chat mit den Administratoren sehen. + Chat mit Administratoren + %1$s übernommen + Als Mitglied übernehmen + Fehler beim Übernehmen des Mitglieds + Aufnahme von Mitgliedern + Ausstehende Überprüfung + Chat mit einem Mitglied + Übernehmen + Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. + Gruppe wird gelöscht + Beitrittsanfrage abgelehnt + Von der Gruppe entfernt + Sie haben die Gruppe verlassen + Kontakt deaktiviert + Nicht synchronisiert + Fehler beim Löschen des Chats mit dem Mitglied + Kontakt nicht bereit + Sie können keine Nachrichten senden! + Chat löschen + Chat mit dem Mitglied löschen? + Mitglied ablehnen? + Es können keine Nachrichten gesendet werden + Kontakt gelöscht + Das Mitglied hat eine alte App-Version + Verkürzten Link hinzufügen 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 5667c42d2d..ae782eb09c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -855,7 +855,7 @@ Ya tienes un perfil con este nombre mostrado. Por favor, selecciona otro nombre. Abrir en aplicación móvil.]]> ponerte en contacto con los desarrolladores de SimpleX Chat para consultas y para recibir actualizaciones.]]> - ¡No puedes enviar mensajes! + eres observador Puedes usar la sintaxis markdown para dar formato a tus mensajes: Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos. El contacto debe estar en línea para completar la conexión. @@ -1046,7 +1046,7 @@ %d minutos Introduce código Inmediatamente - Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay forma de recuperar un código perdido! + Por favor, recuerda y guarda el código de acceso en un lugar seguro. ¡No hay manera de recuperar un código perdido! ¡Bloqueo SimpleX no activado! Puedes activar el Bloqueo SimpleX a través de Configuración. Confirma código @@ -2387,4 +2387,53 @@ Todos los servidores Servidores predefinidos No + Admisión miembro + Aceptar como observador + Error al aceptar el miembro + ¿Guardar configuración? + Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. + has aceptado al miembro + pendiente de revisión + por revisar + Chat con administradores + Chat con miembro + revisado por los administradores + %1$s aceptado + desactivado + Admisión del miembro + el miembro usa una versión antigua + Sin chats con miembros + Error al eliminar el chat con el miembro + %d chats con miembros + %d mensajes + un chat con miembro + %d chat(s) + Informe enviado a los moderadores + no se pueden enviar mensajes + contacto eliminado + contacto desactivado + el contacto no está listo + el grupo ha sido eliminado + no sincronizado + expulsado del grupo + petición para unirse rechazada + ¡No puedes enviar mensajes! + Puedes ver tus informes en Chat con administradores + has salido + te ha aceptado + Un miembro nuevo desea unirse al grupo. + todos + Chats con miembros + Chat con administradores + Eliminar chat + ¿Eliminar el chat con el miembro? + Rechazar + ¿Rechazar al miembro? + Revisar miembros + Revisar miembros antes de admitir + Aceptar + Aceptar miembro + Aceptar como miembro + El miembro se unirá al grupo, ¿aceptas al miembro? + Añadir enlace corto 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 c8897c4063..ec91c3040a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -21,7 +21,7 @@ A SimpleXről Kiemelőszín fogadott hívás - Hozzáférés a kiszolgálókhoz SOCKS-proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. + Hozzáférés a kiszolgálókhoz SOCKS proxyn a következő porton keresztül: %d? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. Elfogadás Elfogadás gombra fent, majd: @@ -45,7 +45,7 @@ Az Android Keystore-t a jelmondat biztonságos tárolására használják – lehetővé teszi az értesítési szolgáltatás működését. Érvénytelen az üzenet hasítóértéke Háttér - Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS-proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének elküldése közvetlen kapcsolatot használnak.]]> + Megjegyzés: az üzenet- és fájltovábbító kiszolgálók SOCKS proxyn keresztül kapcsolódnak. A hívások és a hivatkozások előnézetének elküldése közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése Az adatbázis előkészítése sikertelen A partnereivel kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára. @@ -350,7 +350,7 @@ Az adatbázis titkosítási jelmondata frissülni fog és a beállításokban lesz tárolva. Adatbázis-azonosító Adatbázis-azonosító: %d - Adatbázis-azonosítók és átvitel-izolációs beállítások. + Adatbázis-azonosítók és átvitelelkülönítési beállítások. Az adatbázis titkosítási jelmondata frissülni fog és a Keystore-ban lesz tárolva. Az adatbázis titkosítva lesz, a jelmondat pedig a beállításokban lesz tárolva. Kiszolgáló törlése @@ -366,7 +366,7 @@ %dmp Kézbesítési jelentések! Az eszközön 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 + Titkosításvisszafejtési hiba Eltűnik: %s szerkesztve Törlés @@ -532,7 +532,7 @@ Fájlok és médiatartalmak KONZOLHOZ Nem sikerült a titkosítást újraegyeztetni. - Hiba történt a felhasználó-profil törlésekor + Hiba történt a felhasználóprofil törlésekor Csoporttag általi javítás nem támogatott Adja meg az üdvözlőüzenetet… Titkosított adatbázis @@ -676,7 +676,7 @@ Csevegési profil létrehozása Védett a kéretlen tartalommal szemben Hordozható eszközök leválasztása - Különböző nevek, profilképek és átvitel-izoláció. + Különböző nevek, profilképek és átvitelelkülönítés. Elutasítás esetén a feladó NEM kap értesítést. Szerepkörválasztó kibontása A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! @@ -716,7 +716,7 @@ Hamarosan további fejlesztések érkeznek! A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben. Helytelen biztonsági kód! - Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta. Új számítógép-alkalmazás! Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör) meghívta őt: %1$s @@ -734,7 +734,7 @@ soha (új)]]> Győződjön meg arról, hogy a megadott SMP-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. - Az onion-kiszolgálók nem lesznek használva. + Az onion kiszolgálók nem lesznek használva. perc Tudjon meg többet Új meghívási kérés @@ -762,7 +762,7 @@ Az üzenetek végleges törlése le van tiltva ebben a csevegésben. Max 40 másodperc, azonnal fogadható. inkognitó a kapcsolattartási címhivatkozáson keresztül - Onion-kiszolgálók szükségesek a kapcsolódáshoz.\nMegjegyzés: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz. + Onion kiszolgálók szükségesek a kapcsolódáshoz.\nMegjegyzés: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz. Olasz kezelőfelület Nincsenek háttérhívások Üzenetek @@ -802,7 +802,7 @@ Megjelölés olvasatlanként Továbbiak Bejelentkezés hitelesítőadatokkal - érvénytelen üzenet-formátum + érvénytelen üzenetformátum Csatlakozás Az értesítések az alkalmazás elindításáig nem fognak működni kikapcsolva` @@ -810,10 +810,10 @@ %s ajánlotta Csoport elhagyása %s összes üzenete meg fog jelenni! - Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült visszafejteni az üzenetet, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült. + Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.\n3. A kapcsolat sérült. megfigyelő inkognitó a csoporthivatkozáson keresztül - Onion-kiszolgálók használata, ha azok rendelkezésre állnak. + Onion kiszolgálók használata, ha azok rendelkezésre állnak. Barátok meghívása Menük és figyelmeztetések Tagok meghívása @@ -1026,7 +1026,7 @@ frissítette a csoport profilját SIMPLEX CHAT TÁMOGATÁSA SimpleX Chat szolgáltatás - Nem lehet üzeneteket küldeni! + Ön megfigyelő %s hitelesítve Jelszó a megjelenítéshez Adatvédelem és biztonság @@ -1076,7 +1076,7 @@ KEZELŐFELÜLET SZÍNEI Adja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. Másodlagos szín - SOCKS-PROXY + SOCKS PROXY Mentés Újraindítás SMP-kiszolgálók @@ -1173,7 +1173,7 @@ indítás… Leállítás elküldve - SOCKS-proxy használata + SOCKS proxy használata Élő üzenet küldése Újraértelmezett adatvédelem Hangüzenet… @@ -1186,11 +1186,11 @@ Koppintson a Mentés és a partner értesítése Elutasított hívás - SOCKS-proxybeállítások + SOCKS proxybeállítások QR-kód Titkosítás újraegyeztetése Eltávolítás - Onion-kiszolgálók használata + Onion kiszolgálók használata Felfedés Zárolási mód Fájl visszavonása @@ -1203,9 +1203,9 @@ Ön eltávolította őt: %1$s Jelmondat mentése és a csevegés megnyitása Menti a beállításokat? - Nincsenek felhasználó-azonosítók. + Nincsenek felhasználói azonosítók. A közvetlen üzenetek küldése a tagok között le van tiltva. - SOCKS-proxy használata? + SOCKS proxy használata? Hangszóró kikapcsolva hét Megjelenítés @@ -1258,8 +1258,8 @@ Fogadott üzenetbuborék színe Csak a partnere tudja az üzeneteket véglegesen törölni (Ön csak törlésre jelölheti meg azokat). (24 óra) Az önmegsemmisítő-jelkód módosult! - SimpleX Chat-kiszolgálók használatban. - SimpleX Chat-kiszolgálók használata? + SimpleX Chat kiszolgálók használatban. + SimpleX Chat kiszolgálók használata? Csevegési profil felfedése Videók és fájlok legfeljebb 1GB méretig TCP-kapcsolat időtúllépése @@ -1337,7 +1337,7 @@ A csatlakozás már folyamatban van a csoporthoz ezen a hivatkozáson keresztül. Ön meghívást kapott a csoportba A partnere a jelenleg megengedett maximális méretű (%1$s) fájlnál nagyobbat küldött. - A partnerei és az üzenetek (kézbesítés után) nem a SimpleX-kiszolgálókon vannak tárolva. + A partnerei és az üzenetek (kézbesítés után) nem a SimpleX kiszolgálókon vannak tárolva. Üzenetek formázása a szövegbe szúrt speciális karakterekkel: Megnyitás az alkalmazásban gombra.]]> A csevegési profilja el lesz küldve\na partnere számára @@ -1345,27 +1345,27 @@ %1$s nevű csoporthoz.]]> Amikor az alkalmazás fut Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva - Átvitel-izoláció + Átvitelelkülönítés Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később! A hangüzenetek küldése le van tiltva. Alkalmazás akkumulátor-használata / Korlátlan módot az alkalmazás beállításaiban.]]> Biztonságos kvantumbiztos protokollon keresztül. - - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni üzenet-eltűnési időkorlát.\n- előzmények szerkesztése. + - legfeljebb 5 perc hosszúságú hangüzenetek.\n- egyéni időkorlát beállítása az üzenetek eltűnéséhez.\n- előzmények szerkesztése. Társítás számítógéppel menüt a hordozható eszköz alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s Akkor lesz kapcsolódva, amikor a partnerének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. - Onion-kiszolgálók használata opciót „Nemre”, ha a SOCKS-proxy nem támogatja őket.]]> + Onion kiszolgálók használata opciót „Nemre”, ha a SOCKS proxy nem támogatja őket.]]> Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat Önhöz. Létrehozás később - A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját. + A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX kiszolgálók nem láthatják a profilját. Ön a következőre módosította %s szerepkörét: „%s” Csoportmeghívó elutasítva - Adatainak védelme érdekében a SimpleX külön üzenet-azonosítókat használ minden egyes kapcsolatához. + Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához. (a megosztáshoz a partnerével) Csoportmeghívó elküldve - Frissíti az átvitel-izoláció módját? - Átvitel-izoláció + Frissíti az átvitelelkülönítési módot? + Átvitelelkülönítés Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak. A csevegési adatbázis nem titkosított – állítson be egy jelmondatot annak védelméhez. Közvetlen internetkapcsolat használata? @@ -1395,7 +1395,7 @@ a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet a friss hírekről.]]> Nem kötelező üdvözlőüzenettel. Ismeretlen adatbázishiba: %s - Elrejtheti vagy lenémíthatja a felhasználó-profiljait – koppintson (vagy számítógép-alkalmazásban kattintson) hosszan a profilra a felugró menühöz. + Elrejtheti vagy lenémíthatja a felhasználóprofiljait – koppintson (vagy számítógép-alkalmazásban kattintson) hosszan a profilra a felugró menühöz. Inkognitóra váltás kapcsolódáskor. Megoszthat egy hivatkozást vagy QR-kódot – így bárki csatlakozhat a csoporthoz. Ha a csoportot Ön később törli, akkor nem fogja elveszíteni annak tagjait. Ön csatlakozott ehhez a csoporthoz @@ -1435,7 +1435,7 @@ A kézbesítési jelentések le vannak tiltva %d csoportban Néhány nem végzetes hiba történt az importáláskor: Köszönet a felhasználóknak a Weblate-en való közreműködésért! - A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet. + A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét. Beállítás a rendszer-hitelesítés helyett. A fogadási cím egy másik kiszolgálóra fog módosulni. A cím módosítása a feladó online állapotba kerülése után fejeződik be. A csevegés megállítása a csevegési adatbázis exportálásához, importálásához vagy törléséhez. A csevegés megállításakor nem tud üzeneteket fogadni és küldeni. @@ -1444,7 +1444,7 @@ Jelmondat mentése a beállításokban Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem lesznek elküldve. A második jelölés, amit kihagytunk! ✅ - A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát. + A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók el lesznek távolítva. A kézbesítési jelentések engedélyezve vannak %d csoportban @@ -1483,7 +1483,7 @@ Hiba történt az üzenet megjelenítésekor Láthatóvá teheti a SimpleXbeli partnerei számára a „Beállításokban”. Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára. - A beolvasott QR-kód nem egy SimpleX-QR-kód-hivatkozás. + A beolvasott QR-kód nem egy SimpleX-hivatkozás. A beillesztett szöveg nem egy SimpleX-hivatkozás. A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. Elindítja a csevegést? @@ -1664,8 +1664,8 @@ Ez a csevegés végpontok közötti titkosítással védett. Átköltöztetési párbeszédablak megnyitása Ez a csevegés végpontok közötti kvantumbiztos titkosítással védett. - végpontok közötti titkosítással, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]> - végpontok közötti kvantumbiztos titkosítással, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.]]> + végpontok közötti titkosítással, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.]]> + végpontok közötti kvantumbiztos titkosítással, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.]]> Hiba történt az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel. Engedélyezés a beállításokban @@ -1749,14 +1749,14 @@ 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. Üzenet-útválasztási tartalék PRIVÁT ÜZENET-ÚTVÁLASZTÁS - Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett. + Használjon privát útválasztást az ismeretlen kiszolgálókkal, ha az IP-cím nem védett. NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. - Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára. FÁJLOK IP-cím védelme - Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS-proxy engedélyezve van). + Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). Ismeretlen kiszolgálók! - Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára:\n%1$s. + Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára:\n%1$s. Összes színmód Fekete Színmód @@ -1993,7 +1993,7 @@ Védi az IP-címét és a kapcsolatait. Könnyen elérhető eszköztár Üzenetkiszolgálók - SOCKS-proxy + SOCKS proxy Néhány fájl nem lett exportálva Az exportált adatbázist átköltöztetheti. Mentés és újrakapcsolódás @@ -2072,9 +2072,9 @@ Hang/Videó váltása hívás közben. Csevegési profilváltás az egyszer használható meghívókhoz. Továbbfejlesztett biztonság ✅ - A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva. + A SimpleX-protokollokat a Trail of Bits auditálta. Hiba történt a kiszolgálók mentésekor - Nincsenek üzenet-kiszolgálók. + Nincsenek üzenetkiszolgálók. Nincsenek üzenetfogadási kiszolgálók. Nincsenek média- és fájlkiszolgálók. A(z) %s nevű csevegési profilhoz: @@ -2109,7 +2109,7 @@ Kiszolgálóüzemeltetők Hálózatüzemeltetők Az alkalmazás úgy védi az adatait, hogy minden egyes beszélgetéshez más-más üzemeltetőt használ. - Például, ha a partnere egy SimpleX Chat-kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux-kiszolgálón keresztül fogja azokat kézbesíteni. + Például, ha a partnere egy SimpleX Chat kiszolgálón keresztül fogadja az üzeneteket, akkor az Ön alkalmazása egy Flux kiszolgálón keresztül fogja azokat kézbesíteni. Jelölje ki a használni kívánt hálózatüzemeltetőket. Felülvizsgálat később A kiszolgálókat a „Hálózat és kiszolgálók” menüben konfigurálhatja. @@ -2354,4 +2354,54 @@ Kikapcsolva Előre beállított kiszolgálók A 443-as TCP-port használata kizárólag az előre beállított kiszolgálokhoz. + Hiba a tag befogadásakor + %d csevegés a tagokkal + %d üzenet + 1 csevegés egy taggal + %d csevegés + A jelentés el lett küldve a moderátoroknak + A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben. + függőben lévő áttekintés + áttekintés + Csevegés az adminisztrátorokkal + Csevegés a tagokkal + Tagbefogadás + Nincsenek csevegések a tagokkal + Tagok áttekintése + Tagok áttekintése a befogadás előtt (kopogtatás). + Csevegés az adminisztrátorokkal + A tag csatlakozni akar a csoporthoz, befogadja a tagot? + Eltávolítás + Befogadás + Tag befogadása + összes + Csevegés a taggal + Új tag szeretne csatlakozni a csoporthoz. + kikapcsolva + Befogadás megfigyelőként + Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérelmét. + befogadta Önt + Tagbefogadás beállítása + Elmenti a befogadási beállításokat? + Ön befogadta ezt a tagot + Befogadás tagként + befogadta őt: %1$s + áttekintve a moderátorok által + nem lehet üzeneteket küldeni + partner letiltva + csoport törölve + eltávolítva a csoportból + csatlakozási kérelem elutasítva + Ön elhagyta a csoportot + a tag régi verziót használ + Hiba a taggal való csevegés törlésekor + Ön nem tud üzeneteket küldeni! + a kapcsolat nem áll készen + nincs szinkronizálva + Törli a taggal való csevegést? + partner törölve + Csevegés törlése + Elutasítás + Elutasítja a tagot? + Rövid hivatkozás hozzáadása 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 a269149e99..a9819b32d0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -434,7 +434,7 @@ Panggilan berlangsung Menghubungkan panggilan Pesan yang terlewati - Hash dari pesan sebelumnya berbeda.\" + Hash dari pesan sebelumnya berbeda. Privasi & keamanan Enkripsi berkas lokal Terima gambar otomatis @@ -2143,7 +2143,7 @@ Frase sandi salah! Anda telah meminta koneksi melalui alamat ini! Hentikan obrolan - Anda tidak dapat kirim pesan! + Anda adalah pengamat Pesan suara (%1$s) Lihat kode keamanan Anda perlu mengizinkan kontak mengirim pesan suara agar dapat mengirimkannya. @@ -2359,4 +2359,53 @@ Gunakan tautan singkat (BETA) Tautan koneksi tidak didukung Tautan singkat + gagal mengirim pesan + tinjau + sedang ditinjau + Penerimaan anggota + Mengobrol dengan anggota + Tidak ada obrolan dengan anggota + Terima sebagai anggota + Terima sebagai pengamat + Ada kesalahan saat menerima anggota + 1 obrolan dengan anggota + %d obrolan + %d mengobrol dengan anggota + %d pesan + Anda dapat meninjau laporan anda di Obrolan dengan pengurus. + grup telah dihapus + Silakan tunggu pengurus grup untuk meninjau permintaanmu untuk bergabung ke grup + ditinjau oleh pengurus + Tambah tautan singkat + menerimamu + diterima %1$s + Atur penerimaan anggota + Tinjau anggota + Mengobrol dengan pengurus + Hapus obrolan + Hapus obrolan dengan anggota? + luring + Tolak + Tinjau anggota sebelum menerima (mengetuk) + Mengobrol dengan pengurus + semua + Ada kesalahan saat menghapus obrolan dengan anggota + Laporan telah dikirim ke pengurus + Anda tidak dapat mengirim pesan! + kontak telah dihapus + kontak tidak siap + kontak telah dinonaktifkan + tidak diselaraskan + permintaan bergabung ditolak + anggota menggunakan versi lama + dihapus dari grup + Anda keluar + Simpan pengaturan penerimaan? + Anggota baru ingin bergabung ke grup. + kamu menerima anggota ini + Mengobrol dengan anggota + Terima + Terima anggota + Anggota akan bergabung ke grup, terima? + Tolak anggota? 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 6c086835ea..1dbb0617af 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -940,7 +940,7 @@ Errore nell\'aggiornamento del link del gruppo osservatore Contatta l\'amministratore del gruppo. - Non puoi inviare messaggi! + sei un osservatore Sistema Aggiungi messaggio di benvenuto Messaggio di benvenuto @@ -2391,4 +2391,53 @@ Off Server preimpostati Usa la porta TCP 443 solo per i server preimpostati. + 1 chat con un membro + %d chat + %d chat con membri + %d messaggi + Salvare le impostazioni di ammissione? + ha accettato %1$s + ti ha accettato/a + Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. + hai accettato questo membro + revisiona + Ammissione del membro + Nessuna chat con membri + off + Revisiona i membri + Revisiona i membri prima di ammetterli (bussare). + Accetta + Chat con amministratori + Rimuovi + Il membro entrerà nel gruppo, accettarlo? + revisionato dagli amministratori + Accetta membro + Chatta con gli amministratori + Accetta come osservatore + Un nuovo membro vuole entrare nel gruppo. + tutti + Chatta con il membro + Chat con membri + Errore di accettazione del membro + Accetta come membro + Imposta l\'ammissione dei membri + Segnalazione inviata ai moderatori + in attesa di revisione + Puoi vedere le tue segnalazioni nella chat con gli amministratori. + Non puoi inviare messaggi! + contatto non pronto + contatto eliminato + contatto disattivato + non sincronizzato + richiesta di entrare rifiutata + impossibile inviare messaggi + il gruppo è eliminato + il membro ha una versione vecchia + rimosso dal gruppo + sei uscito/a + Eliminare la chat con il membro? + Rifiutare il membro? + Elimina chat + Errore di eliminazione della chat con il membro + Aggiungi link breve diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 21c04b8473..abb9f8e473 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -2008,4 +2008,8 @@ リストを追加 すべて ワンタイムリンクを生成 + %d 件を選択中 + グループのパフォーマンス向上 + 選択 + プライバシーとセキュリティの向上 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 d07cb6db39..ff90fb7783 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -936,7 +936,7 @@ Fout bij bijwerken van groep link Initiële rol Waarnemer - Je kunt geen berichten versturen! + jij bent waarnemer je bent waarnemer Systeem Audio en video oproepen @@ -1059,7 +1059,7 @@ Toegangscode ingesteld! Systeem Decodering fout - De hash van het vorige bericht is anders.\" + De hash van het vorige bericht is anders. %1$d berichten overgeslagen. Het kan gebeuren wanneer u of de ander een oude database back-up gebruikt. Meld het alsjeblieft aan de ontwikkelaars. @@ -2384,4 +2384,57 @@ Serveroperators configureren Privacybeleid en gebruiksvoorwaarden. Privéchats, groepen en uw contacten zijn niet toegankelijk voor serverbeheerders. + contact verwijderd + Chat met beheerders + Chats met leden + lid heeft oude versie + Controleer de leden voordat u ze toelaat (knocking). + Gebruik TCP-poort 443 alleen voor vooraf ingestelde servers. + Fout bij het accepteren van lid + 1 chat met een lid + %d chat(s) + %d chats met leden + %d berichten + geaccepteerde %1$s + beoordeling + Chat met lid + Accepteren als lid + Accepteren als waarnemer + beoordeeld door beheerders + Korte link toevoegen + Nieuw lid wil zich bij de groep aansluiten. + Chat met beheerders + Chat verwijderen + Chat met lid verwijderen? + Geen chats met leden + Afwijzen + Lid afwijzen? + kan geen berichten versturen + verwijderd uit de groep + je bent weggegaan + Uit + alle + uit + Leden beoordelen + Fout bij het verwijderen van chat met lid + Rapport verzonden naar moderators + Je kunt geen berichten versturen! + U kunt uw rapporten bekijken in Chat met beheerders. + contact uitgeschakeld + contact niet klaar + groep is verwijderd + niet gesynchroniseerd + verzoek tot toetreding afgewezen + Alle servers + Vooraf ingestelde servers + Toegangsinstellingen opslaan? + heb je geaccepteerd + Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. + je hebt dit lid geaccepteerd + in afwachting van beoordeling + Toegang voor leden instellen + Toelating van leden + Accepteer + Lid accepteren + Lid zal toetreden tot de groep, lid accepteren? 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 a0cce488af..0c82bc4a33 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 @@ -87,7 +87,7 @@ Aceitar solicitações de contato automaticamente Aparência O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis. - Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. + Uma conexão TCP separada (e credencial SOCKS) será usada para cada contato e membro do grupo. \nAtenção: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar. Bom para bateria. O aplicativo procura por mensagens a cada 10 minutos. Você pode perder chamadas ou mensagens urgentes.]]> chamda encerrada %1$s @@ -739,7 +739,7 @@ Chamada perdida Salvar senha na Keystore Seu banco de dados de bate-papo não está criptografado - defina uma senha para protegê-lo. - convite para o grupo%1$s + convite para o grupo %1$s Você esta usando um perfil anônimo para este grupo - para evitar compartilhar seu perfil principal, convidar contatos não é permitido Confirmação de migração inválida Migrações: %s @@ -1445,7 +1445,7 @@ Modo anônimo simplificado Desktop encontrado Aleatório - Migração do banco de dados em progresso. + Migração do banco de dados em progresso. \nIsso pode levar alguns minutos. %1$d mensagens moderadas por %2$s %d mensagens bloqueadas diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index 92572516b9..9cf3e3ac6a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -104,7 +104,7 @@ Arhivează și încarcă Bază de date de arhivare Se creează un link de arhivare - Creează un link de invitare unic. + Creează un link de invitație unic. Acceptă automat imagini Apel audio Apel audio @@ -116,7 +116,7 @@ Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile. Autentificare Autentificare eșuată - Codul de acces curent + Codul de acces actual Autentificare indisponibilă Atașează Înapoi @@ -139,12 +139,12 @@ Acceptă automat cererile de contactare personalizat În prezent dimensiunea maximă pentru fișiere este %1$s. - Creează adresă SimpleX + Creează o adresă SimpleX Acceptare automată Creează-ți profilul Apeluri audio și video Audio pornit - Frază de acces curentă… + Frază de acces actuală… Creează link pentru grup Creează link creator @@ -155,11 +155,11 @@ Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate. Apelurile audio/video sunt interzise. (actual) - Poză de profil eliminată + fotografia de profil a fost eliminată Temă întunecată eliminat Întunecată - Adresă de contact eliminată + adresa de contact eliminată Personalizează și distribuie teme colorate. Teme personalizate Repetă descărcarea @@ -170,174 +170,174 @@ Elimină Elimină Elimină membru - Elimini membrul? - Resetează la implicit - Resetează culoarea - Resetează culorile + Eliminați membrul? + Resetați la valorile implicite + Resetați culoarea + Resetați culorile Elimină imagine Repetă încărcarea apel respins - Elimini fraza de acces din Keystore? - Elimini parola din reglări? + Eliminați fraza de acces din Keystore? + Eliminați fraza de acces din setări? Repetă cererea de conectare? Necesar Reîncearcă - Primește fișiere în siguranță + Primiți fișiere în siguranță Grupuri mai sigure - Restabilește + Restaurați Reîmprospătează - Revoci fișierul? - Revocă - Renegociezi cifrarea? - Resetează + Revocați fișierul? + Revocați + Renegociați criptarea? + Resetare Respinge - Salvează parola din reglări - Salvează și actualizează profilul grupului + Salvați fraza de acces în setări + Salvați și actualizați profilul grupului Repetă cererea de alăturare? - Repornește conversația + Reporniți conversația salvat - Salvat de la %s + salvat de la %s Salvează Salvat Salvat de la Renegociază - Salvează servere - Servere WebRTC ICE salvate vor fi eliminate. - Salvează parola profilului - Salvează fraza de acces și deschide conversația + Salvați serverele + Serverele ICE WebRTC salvate vor fi eliminate. + Salvați parola profilului + Salvați fraza de acces și deschideți chatul %s și %s - Renegociază cifrarea - Salvează și notifică contactul - Salvează și notifică contactele - Salvează și notifică membrii grupului - Rulează când aplicația este pornită + Renegociați criptarea + Salvați și notificați contactul + Salvați și notificați contactele + Salvați și notificați membrii grupului + Rulează când aplicația este deschisă Răspunde - Revocă fișierul + Revocați fișierul Salvează - Salvezi reglările? - Salvezi preferințe? + Salvați setările? + Salvați preferințele? Repornire - Restabilește copia de rezervă a bazei de date - Restabilești copia de rezervă a bazei de date? - Eroare la restabilirea bazei de date + Restaurați copia de rezervă a bazei de date + Restaurați copia de rezervă a bazei de date? + Eroare de restaurare a bazei de date %1$s eliminat %s și %s conectați Rol Salvează Arată Respinge - Salvezi servere? + Salvați serverele? Apel respins Mesaj salvat - Repornește aplicația pentru a crea un nou profil - Salvează fraza de acces în Keystore - Salvează profilul grupului + Reporniți aplicația pentru a crea un nou profil de chat. + Salvați fraza de acces în Keystore + Salvați profilul grupului Repetă - Trimite previzualizări ale link-ului - Setează frază de acces + Trimiteți previzualizări de linkuri + Setați frază de acces Distribuie adresă Trimis la Secundar Mesaj trimis - Setează preferințele grupului + Setați preferințele grupului trimis - Adresa serverului este incompatibilă cu reglările rețelei. - Versiunea serverului este incompatibilă cu reglările rețelei. - Trimițând prin - trimiterea de fișiere nu este acceptată încă - Scanează codul de securitate din aplicația contactului tău - Selectează contacte + Adresa serverului este incompatibilă cu setările de rețea. + Versiunea serverului este incompatibilă cu setările de rețea. + Trimitere prin + trimiterea de fișiere nu este încă acceptată + Scanați codul de securitate din aplicația persoanei de contact. + Selectați contactele Autodistrugere Răspuns trimis Distribuie media… Distribuie mesaj… Arată lista conversațiilor într-o fereastră nouă Arată consola într-o fereastră nouă - Setează fraza de acces a bazei de date - Setează fraza de acces a bazei de date - setează adresă de contact nouă + Configurați fraza de acces la baza de date + Setați parola bazei de date + setați adresă de contact nouă %s (actual) - Cod de sesiune + Codul sesiunii Expeditorul a anulat transferul de fișiere. - Serverul necesită autorizație pentru a crea cozi, verifică parola + Serverul necesită autorizare pentru a crea cozi, verificați parola Distribuie trimitere eșuată - Caută sau lipește link SimpleX - Setează numele de contact - Salvezi mesajul de bun venit? - Evaluare de securitate - Scanează cod QR de pe desktop + Căutați sau lipiți linkul SimpleX + Setați numele contactului + Salvați mesajul de bun venit? + Evaluarea securității + Scanați codul QR de pe desktop Caută - Trimite un mesaj live - se va actualiza pentru destinatar(i) în timp ce îl tastezi - Distribuie fișier - Trimite până la ultimele 100 de mesaje membrilor noi. - Bara de căutare acceptă link-uri de invitație. + Trimiteți un mesaj live - acesta se va actualiza pentru destinatar(i) pe măsură ce îl tastați + Distribuie link + Trimiteți până la 100 de mesaje recente noilor membri. + Bara de căutare acceptă linkuri de invitație. secunde - Scanează de pe mobil + Scanare de pe mobil Mesaj trimis - Scanează cod QR + Scanați codul QR Trimite - Trimite întrebări și idei + Trimiteți întrebări și idei Arată opțiuni dezvoltator sec - Mesajele trimise vor fi șterse după timpul setat. - Serverul necesită autorizație pentru a încărca, verifică parola + Mesajele trimise vor fi șterse după ora setată. + Serverul necesită autorizare pentru încărcare, verificați parola Arată contact și mesaje Distribuie fișier… - Setează numele de contact… + Setați numele contactului… Trimite mesaj - Trimite mesaj temporar - (scanează sau lipește din clipboard) - Arată cod QR - Test server eșuat! + Trimiteți un mesaj care dispare + (scanați sau lipiți din clipboard) + Arată codul QR + Testul serverului a eșuat! Arată: - Arată erori interne + Afișați erorile interne secret - REGLĂRI + SETĂRI %s conectat - setează imagine de profil - Trimis către: %s + setați o nouă poză de profil + Trimis la: %s SERVERE - Trimite mesaj live + Trimiteți mesaj live %s descărcat - Distribui adresa cu contactele? - Arată previzualizare - trimite mesaj direct - Trimite mesaj direct pentru a te conecta - Selectează - Trimiterea de fișiere va fi oprită. + Partajați adresa cu contactele? + Arată previzualizarea + trimiteți un mesaj direct + Trimiteți mesaj direct pentru a vă conecta + Selectați + Trimiterea fișierului va fi oprită. Trimite - Reglări - Scanează cod + Setări + Scanați codul Cod de securitate - Trimite-ne email - Scanează codul QR al serverului - Distribuie contactelor + Trimiteți-ne un e-mail + Scanați codul QR al serverului + Distribuie persoanelor de contact Arată - cod de securitate schimbat + codul de securitate a fost schimbat Arată ultimul mesaj - Trimite mesaj direct - Setează tema implicită + Trimiteți un mesaj direct + Setați tema implicită SimpleX - SimpleX nu poate rula în fundal. Vei primi notificările doar când aplicația rulează. + SimpleX nu poate rula în fundal. Vei primi notificări doar atunci când aplicația rulează. Serviciu SimpleX Chat Adresă SimpleX Închide Link-uri SimpleX - Link-urile SimpleX sunt interzise în acest grup. - Securitatea SimpleX Chat a fost verificată de Trail of Bits. + Link-urile SimpleX sunt interzise. + Securitatea chatului SimpleX a fost auditată de Trail of Bits. Mesaje SimpleX Chat Apeluri SimpleX Chat Adresă SimpleX simplexmq: v%s (%2s) Link-uri SimpleX nepermise Echipa SimpleX - Închizi? + Oprire? Adresă de contact SimpleX Link pentru grup SimpleX Link-uri SimpleX Invitație unică SimpleX - Siglă SimpleX + Logo SimpleX Grupuri mici (max 20) Mod incognito simplificat Pătrat, cerc, sau orice între. @@ -361,13 +361,13 @@ Aplică pentru Începe o nouă conversație Stea pe GitHub - Cifrare standard de la un capăt la altul + criptare standard end-to-end Pornește periodic Mereu Folosește mereu rutare privată. Toate contactele vor rămâne conectate. Actualizarea profilului va fi trimisă contactelor tale. pornire… - %s secunde + %s secundă(e) Începi conversația? Reglări avansate Adresă desktop rea @@ -378,7 +378,7 @@ Schimbă adresa de primire Conversația este oprită. Dacă ai folosit deja această bază de date pe alt dispozitiv, ar trebui să o transferi înapoi înainte de a porni conversația. APELURI - ai schimbat rolul pentru tine la %s + v-ați schimbat rolul în %s Capacitate depășită - destinatarul nu a primit mesajele trimise anterior. Schimbă codul de acces autodistructibil Conversația este oprită @@ -391,11 +391,10 @@ Conectare incognito Contactul deja există Schimbă codul de acces - Poți porni Blocare SimpleX din Reglări. + Puteți activa SimpleX Lock din Setări. Conversații Alege un fișier - Contactul tău trebuie să fie online pentru a se completa conexiunea. -\nPoți anula această conexiune și elimina contactul (și poți încerca mai târziu cu un nou link). + Contactul dvs. trebuie să fie online pentru ca conexiunea să se finalizeze.\nPuteți anula această conexiune și elimina contactul (și încerca mai târziu cu un link nou). eroare apel Apelurile tale Schimbă @@ -407,7 +406,7 @@ Te conectezi cu %1$s? Anulează previzualizarea imaginii Discută cu dezvoltatorii - Trebuie să introduci fraza de acces de fiecare dată când aplicația pornește - nu este stocată pe dispozitiv. + Trebuie să introduceți o frază de acces de fiecare dată când pornește aplicația - nu este stocată pe dispozitiv. blocat Preferințe conversație Mod întunecat @@ -418,12 +417,12 @@ Conectat la desktop Anulează migrarea Celular - Nu poți trimite mesaje! + ești observator Schimbi adresa de primire? Contactul nu este conectat încă! Conectare prin link - Poți vedea linkul de invitație din nou în detaliile conexiunii. - Profilurile tale de conversație + Puteți vedea din nou linkul de invitație în detaliile conexiunii. + Profilurile tale de chat Crează profil de conversație apel terminat %1$s Crează @@ -438,18 +437,18 @@ Baza de date a conversației ștearsă Conversația rulează Baza ta de date a conversațiilor - Baza de date a conversațiilor tale nu este cifrată - pune o parolă pentru a o proteja. - Parola de cifrare a bazei de date va fi actualizată și stocată în reglări. - ai schimbat rolul %s la %s + Baza de date a conversațiilor tale nu este criptată - setează o parolă pentru a o proteja. + Parola pentru criptarea bazei de date va fi actualizată și stocată în setări. + ați schimbat rolul %s în %s Nu se pot invita contactele! - Te-ai alăturat grupului + Te-ai alăturat acestui grup Nu se poate invita contactul! se conectează (acceptat) Creat la: %s Blochează Blochează membru Conectare directă? - Profilul tău de conversație va fi trimis membrilor grupului + Profilul tău de chat va fi trimis membrilor grupului Întunecat Culori mod întunecat Și tu și contactul tău puteți face apeluri. @@ -457,40 +456,39 @@ Conectat la mobil Eroare copiere Te conectezi prin link? - Nu ai putut fi verificat(ă); te rog încearcă din nou. + Nu ați putut fi verificat; vă rugăm să încercați din nou. Copiază Anulează mesajul live Conectare prin link / cod QR Contactele tale vor rămâne conectate. - Parola de cifrare a bazei de date va fi actualizată. + Parola pentru criptarea bazei de date va fi actualizată. Contactele tale pot permite ștergerea totală a mesajelor. - Trebuie să permiți contactului tău să trimită mesaje vocale pentru a le putea trimite. + Trebuie să îi permiți contactului tău să trimită mesaje vocale pentru a le putea trimite. Versiunede bază: v%s Creează o adresă pentru a permite oamenilor să se conecteze cu tine. %s blocat - ai schimbat adresa - ai schimbat adresa pentru %s + ați schimbat adresa + ați schimbat adresa pentru %s se schimbă adresa… se schimbă adresa pentru %s… Și tu și contactul tău puteți trimite mesaje temporare. Nu se pot primi fișiere Poate fi dezactivat din reglări – notificările vor fi afișate dacă aplicația este în funcțiune.]]> Verifică mesajele noi la fiecare 10 minute timp de până la 1 minut - Nu ai conversații + Nu aveți conversații Contactul și toate mesajele vor fi șterse - acest lucru nu poate fi anulat! Copiat în clipboard Camera - Ai invitat un contact - Profilul tău de conversație va fi trimis -\ncontactului tău + Ați invitat un contact + Profilul tău de chat va fi trimis\ncontactului tău Contribuie Profil conversație aldin - Poți folosi markdown pentru a formata mesaje: + Puteți folosi marcarea pentru a formata mesajele: se apelează… Schimbă modul de autodistrugere Baza de date a conversației importată - Trebuie să folosești cea mai recentă versiune a bazei de date a conversațiilor DOAR pe un singur dispozitiv, altfel se poate să nu mai primești mesajele de la unele contacte. + Trebuie să utilizați cea mai recentă versiune a bazei de date de chat DOAR pe un singur dispozitiv, altfel este posibil să nu mai primiți mesaje de la unele contacte. Nu se poate accesa Keystore pentru a salva parola bazei de date Conversație migrată! Continuă @@ -500,11 +498,11 @@ Contact ascuns: Nume contact Crează adresă - Bază de date cifrată! + Bază de date criptată! Schimbi fraza de acces a bazei de date? Conversația este oprită - Poți porni discuția din Reglările aplicației / Baza de date sau repornind aplicația. - ai ieșit + Puteți porni discuția din Setările aplicației / Baza de date sau repornind aplicația. + ați ieșit Blochezi membrul? Blocat de admin Și tu și contactul tău puteți adăuga reacții la mesaje. @@ -512,14 +510,14 @@ blocat blocat de admin Nu se poate inițializa baza de date - Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă suportată în prezent (%1$s). + Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă acceptată în prezent (%1$s). anulează previzualizarea link-ului Verifică adresa serverului și încearcă din nou. - Tu îți controlezi conversația! + Tu îți controlezi chatul! Apel deja terminat! Apel terminat Blochează pentru toți - Ai cerut deja conexiunea prin această adresă! + Ați solicitat deja conexiunea prin această adresă! Te conectezi la tine? Verifică conexiunea la internet și încearcă din nou Culori conversație @@ -528,7 +526,7 @@ Negru Blochezi membrul pentru toți? Și tu și contactul tău puteți șterge ireversibil mesajele trimise. (24 de ore) - Folosește mai multă baterie! + Folosește mai multă baterie! \nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile. Nu se poate trimite mesajul Ștergeți @@ -676,10 +674,10 @@ Contactul %1$s a schimbat la %2$s Contactul este șters. Controlează-ți rețeaua - Parola pentru cifrarea bazei de date va fi actualizată și stocată în Keystore. - Fraza de acces a bazei de date + Parola pentru criptarea bazei de date va fi actualizată și stocată în Keystore. + Parola bazei de date Servere XFTP configurate - Baza de date este cifrată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare. + Baza de date este criptată folosind o parolă aleatorie. Trebuie schimbată înainte de exportare. apel conectare Contact șters! @@ -702,9 +700,8 @@ Eroare bază de date Contactele pot marca mesajele pentru ștergere; tu le vei putea vedea. Downgrade al bazei de date - Migrarea bazei de date este în proces. -\nPoate dura câteva minute. - Fraza de acces a bazei de date și export + Migrarea bazei de date este în curs. \nPoate dura câteva minute. + Parola bazei de date și export Toate profilurile Profil actual Confirmi ștergerea contactului? @@ -713,24 +710,24 @@ Conectat Corectează numele la %s? Continuă - Baza de date este cifrată folosind o parolă aleatorie; o poți schimba. + Baza de date este criptată folosind o parolă aleatorie, o poți schimba. Nu se pot trimite mesaje membrului grupului Se conectează Servere conectate Nu se poate apela contactul Se conectează la contact, așteaptă sau verifică mai târziu! Reglări avansate - Reglări - Reglările tale - apel vocal cifrat e2e - apel video cifrat e2e - APARAT + Setări + Setările tale + apel vocal criptat e2e + apel video criptat e2e + DISPOZITIV EXPERIMENTAL - Cifrează + Criptează erori la descifrare TU - necifrat e2e - cifrat e2e + nicio criptare e2e + criptat e2e Apel video primit Dezactivează Permiți apeluri? @@ -739,58 +736,58 @@ Apeluri îmbunătățite Apel audio primit Termină apelul - marcate şterse + marcajele şterse %d mesaje marcate șterse moderat de %s %1$d alte erori de fișier. %1$s mesaje netransmise - ai distribuit un link de unică folosință - ai distribuit un link ascuns de unică folosință - prin link pentru grup - ascuns prin link pentru grup + ai distribuit un link uic + ai distribuit linkul unic în mod incognito + prin link de grup + incognito prin link de grup Prin browser - Reclamă + Spam Conținut inadecvat Încălcă normelor comunitare Profil inadecvat Alt motiv Eroare la salvarea serverelor SMP - Asigurați-vă că adresele serverelor XFTP sunt în format corect, pe randuri separate și nu sunt duplicate. + Asigurați-vă că adresele serverelor XFTP sunt în format corect, separate pe linii și nu sunt duplicate. Eroare la încărcarea serverlor XFTP Eroare la actualizarea configurației de rețea - Eșec la încărcarea conversațiilor + Conversațiile nu au putut fi încărcate Vă rugăm să actualizați aplicația și contactați dezvoltatorii. Eroare la crearea profilului! %d mesaje blocate Eroare de renegociere a criptării invitat să se conecteze - ascuns prin link pentru adresa de contact + incognito prin linkul adresei de contact printr-un link de unică folosință 1 raport 1 an - înaintat - eroare de afișare a mesajului - eroare de afișat conținutul + redirecționat + eroare la afișarea mesajului + eroare la afișarea conținutului criptare cap-coadă cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> criptare cap-coadă rezistentă la algoritmi cuantici cu secretizare înaintată perfecta, repudiere si recuperare în caz de spargere.]]> - Această conversație este protejată prin criptare cap-coadă. - Link întreg - ascuns printr-un link de unică folosință + Acest chat este protejat prin criptare end-to-end. + Link complet + incognito printr-un link de unică folosință a + b prin %1$s Eroare la încărcarea serverlor SMP %1$d fișier(e) șterse. raport arhivat de %s invitație acceptată - Asigurați-vă că adresele serverelor SMP sunt în format corect, pe randuri separate și nu sunt duplicate. + Asigurați-vă că adresele serverelor SMP sunt în format corect, separate pe linii și nu sunt duplicate. Eroare la salvarea serverelor XFTP %1$d fișier(e) încă în descărcare. %1$d fișier(e) a eșuat să se descărcarce. %1$d fișier(e) nu au fost descărcate. - prin link pentru adresa de contact - Deschiderea link-ului în browser poate reduce confidențialitatea și securitatea conexiunii. Link-urile SimpleX de neîncredere vor fi roșii. + prin linkul adresei de contact + Deschiderea link-ului în browser poate reduce confidențialitatea și securitatea conexiunii. Link-urile SimpleX de nesigure vor fi roșii. solicitat să se conecteze - Eșec la încărcarea conversației + Conversația nu a putut fi încărcată Doar tu și moderatorii vedeți asta Doar expeditorul şi moderatorii văd asta raport arhivat @@ -798,11 +795,1613 @@ primirea de fișiere nu este acceptată încă tu format mesaj necunoscut - format mesaj invalid + format de mesaj nevalid LIVE moderat conversație nevalidă date nevalide - Această conversație este protejată prin criptare cap-coadă, rezistentă la algoritmi cuantici. + Acest chat este protejat prin criptare end-to-end rezistentă la detecții cuantice. Notițe private - + Eroare la citirea parolei bazei de date + Versiunea bazei de date este incompatibilă + Invitația la grup nu mai este validă, a fost eliminată de expeditor. + Alăturare la grup în curs + Părăsiți grupul + Eroare la ștergerea link-ului grupului + Eroare la trimiterea invitației + dezactivat + Membrii pot trimite mesaje vocale. + Filtrați conversațiile necitite și favorite. + Migrare în curs + Managementul rețelei + Finalizează migrarea pe un alt dispozitiv. + Incompatibil! + Cum să utilizați serverele dvs. + Importați baza de date de chat? + Șterge raport + Redirecționați mesajele… + Ștergeți doar conversația + Păstrați invitația neutilizată? + Introduceți numele dumneavoastră: + Acordați permisiunea (permisiunile) de a efectua apeluri + Notificări și baterie + Deschide + Activați confirmarea de primire pentru grupuri? + PENTRU CONSOLĂ + Moderat la + Remediați conexiunea + Multiple profile de chat + Ascuns + Introducere parolă + Redirecționare server: %1$s\nEroare:%2$s + Fișierul nu a fost găsit - cel mai probabil fișierul a fost șters sau anulat. + Adresă de server invalidă! + Dezactivat + Fișier: %s + Eroare la crearea linkului de grup + Dispare la + dezactivat + direct + Fișiere și media + Alăturați-vă grupului? + k + Se deschide baza de date… + Cale de fișier nevalidă + Nume afișat duplicat! + Moderare + Marchează ca necitit + Eroare la redirecționarea mesajelor + Sau scanați codul QR + Gazdă + Cum se folosește marcarea + Imun la spam + Baza de date va fi criptată. + Grup inactiv + Bună dimineaţa! + Fișierele și conținutul media sunt interzise. + moderatori + Max 40 de secunde, primit instantaneu. + Configurație îmbunătățită a serverului + Mesaj de bun venit pentru grup + Reducerea suplimentară a consumului de baterie + Alăturați-vă conversațiilor de grup + Livrarea mesajelor îmbunătățită + Interfață lituaniană + Faceți conversațiile să arate diferit! + Menționați membrii 👋 + Ajutați administratorii să modereze grupurile lor. + Doar un singur dispozitiv poate funcționa în același timp + Grupul exită deja! + Se descarcă arhiva + Finalizați migrarea + Eroare la verificarea parolei: + Eroare la importarea bazei de date de conversații + Mesaje + Eroare la exportarea bazei de date de conversații + Nicio informație, încercați să reîncărcați + Activează jurnalele + Autentificarea dispozitivului este dezactivată. Se dezactivează SimpleX Lock. + Invitația a expirat! + Grup + Invitați la grup + Deconectați telefoanele mobile + Nume afișat nevalid! + Eroare la schimbarea profilului! + Fără servere media și de fișiere. + Nu există servere de mesaje. + Nu există servere pentru rutarea mesajelor private. + Nu există servere pentru a primi mesaje. + Pentru profilul de chat %s: + Nu există servere pentru a primi fișiere. + Eroare la conectarea la serverul de redirecționare %1$s. Vă rugăm să încercați mai târziu. + Versiunea serverului de destinație %1$s este incompatibilă cu serverul de redirecționare %2$s. + Notificările instantanee sunt dezactivate! + Dezactivare notificări + Migrați de pe un alt dispozitiv + Eroare la ștergerea bazei de date de conversații + Actualizarea bazei de date + este necesară renegocierea criptării + membru + Șterge conversația? + eroare + Link nevalid + Fișierul va fi primit după ce persoana de contact va finaliza încărcarea. + Interfață italiană + Desktopul este ocupat + Eroare la reconectarea serverului + Eroare la adăugarea membrului/membrilor + Eroare la alăturarea grupului + Ascunde + Listă + Nicio conversație necitită + Nicio conversație în lista %s. + Nimic selectat + deschis + AJUTOR + Doar contactul tău poate trimite mesaje care dispar. + Se importă arhiva + Migrare dispozitiv + nu + oprit` + %ds + Remediați criptarea după restaurarea copiilor de rezervă. + Șterge sau moderează pana la 200 de mesaje. + Primirea mesajelor + Eroare la resetarea statisticilor + Eroare la crearea mesajului + Redirecționați mesajul… + Eroare + Importă + Opțiuni de conectate desktop + Eroare la crearea raportului + Adresa de destinație a serverului %1$s este incompatibilă cu setările de redirecționare ale serverului %2$s . + Eroare la trimiterea mesajului + Eroare la schimbarea adresei + Eroare la sincronizarea conexiunii + Eroare + Deconectați-vă + Execuția funcției durează prea mult: %1$d secunde: %2$s + Ascunde + Introduceți parola + Serviciu de notificare + Redirecționare server: %1$s\nEroare server destinatar: %2$s + Fișierul este blocat de operatorul serverului:\n%1$s. + Editați + Info + Redirecționat de la + Șterge %d mesaje de la membri? + Rapoarte de membri + Fișierele și conținutul media sunt interzise! + Fișierul nu a fost găsit + Fișier + Operatori de rețea + Draft de mesaje + Parola a fost schimbată! + Activare (păstrați suprascrierile grupului) + Dezactivare (păstrare suprascrieri grup) + Activează pentru toate grupurile + Versiunea bazei de date este mai nouă decât app, dar nu există migrare ulterioară pentru: %s + Alăturați-vă anonim + Deschide + Pentru rutare privată + Activați funcția TCP keep-alive + Mut cand este inactiv! + Teme noi de chat + Descentralizarea rețelei + altul + Doar 10 imagini pot fi trimise simultan + Mesajul este prea mare! + Imaginea va fi primită când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! + Șterge mesajele din conversație de pe dispozitiv. + Dacă alegeți să respingeți, expeditorul NU va fi notificat. + Cum se folosește + Marcare în mesaje + Eroare la salvarea proxy-ului + Dacă confirmi, serverele de mesagerie vor putea vedea adresa ta IP, iar furnizorul tău - la ce servere te conectezi. + Cum ajută la confidențialitate + mesaj duplicat + Invitați prietenii + Să vorbim în chat -ul simplex + Salut!\nConectează-te cu mine prin SimpleX Chat: %s + Pentru social media + Ascundeți profilul + Numele afișat nu poate conține spații albe. + Nume nevalid! + Eroare la deschiderea browserului + Cum funcționează + Doar dispozitivele client stochează profiluri de utilizator, contacte, grupuri și mesaje. + Instalat cu succes + Dezactivează + Opțiuni dezvoltator + Notificările vor înceta să funcționeze până când nu redeschideți aplicația. + Eroare la salvarea setărilor + Șterge mesaje după + %d sec + Email + Eroare la salvarea parolei utilizatorului + Eroare la salvarea bazei de date + Conectați aplicații mobile și desktop! 🔗 + Dispozitive + Fișierul exportat nu există + Sau distribuiți în siguranță acest link pentru fișier + Statusul mesajului + Niciun telefon conectat + Numai tu poți șterge ireversibil mesajele (contactul tău le poate marca pentru ștergere). (24 de ore) + Doar contactul tău poate trimite mesaje vocale. + Găsește conversații mai rapid + Luminos + Nu + Deschide linkul web? + MESAJE ȘI FIȘIERE + moderator + Rol inițial + Doar proprietarii grupului pot modifica preferințele grupului. + Introdu mesajul de bun venit… + Doar contactul tău poate adăuga reacții la mesaje. + Erori de ștergere + Dezactivați ștergerea mesajelor + Păstrează conversația + Cod QR nevalid + Cod de securitate incorect! + Dispare la: %s + Conectați-vă folosind credențialele dvs. + Deschide conversația + Eroare la keychain + Imediat + %d minute + Probleme de rețea - mesajul a expirat după multe încercări de a -l trimite. + Nicio informație despre livrare + Mesajul poate fi transmis mai târziu dacă membrul devine activ. + editat + Se încarcă conversațiile… + Nicio conversație filtrată + Server nou + Asigurați -vă că adresele Serverului WebRTC ICE sunt în format corect, separate pe linii și nu sunt duplicate. + Rutarea mesajelor + Editați imaginea + Deschideți setările + Acordare în setări + Instant + Cum afectează bateria + Activare (păstrați suprascrierile) + Activați codul de autodistrugere + MEMBRU + Operator de rețea + Desktop-uri conectate + Eroare la acceptarea membrului + Mesaj text + mesaj nou + Ascunde contactul și mesajul + Cerere de contact nouă + Deschide consola conversației + Autentificarea dispozitivului nu este activată. Puteți activa SimpleX Lock din Setări, după ce activați autentificarea dispozitivului. + Deschideți freastra de migrare + Niciun mesaj + Pentru mine + Eroare server fișiere: %1$s + Pentru toți + Mesajele vor fi marcate pentru ștergere. Destinatarul (destinatarii) va (vor) putea dezvălui aceste mesaje. + Descărcare + Fișierul va fi șters de pe servere. + Alăturați-vă ca %s + Nicio conversație selectată + Încărcarea fișierului + Eroare la salvarea fișierului + Șterge fără notificare + Deconectat + Remediați conexiunea? + Mesaj care dispare + Mai mult + Pentru fiecare server vor fi utilizate acreditări noi SOCKS. + Sau partajați în privat + Microfon + Acordare permisiuni + Cască + Închide + Criptați fișierele locale + Blochează după + Mod de blocare + Cod de acces nou + Caracteristici experimentale + Pentru a deschide chatul este necesară o parolă pentru baza de date. + Introduceți parola… + Downgrade și deschide chatul + Vor fi afișate mesaje de la acești membri! + nici unul + Eroare la eliminarea membrului + indirect (%1$s) + Remedierea nu este acceptată de membrul grupului + Operator + Faceți profilul privat! + Deschideți contiții + Nu mai afișa + Permite să aibă multe conexiuni anonime fără date partajate între ele într -un singur profil de chat. + Luminos + Mesajele care dispar sunt interzise în această conversație. + Numai tu poți trimite mesaje care dispar. + %d lună + Mai multe îmbunătățiri vor veni în curând! + Mai multe îmbunătățiri vor veni în curând! + Livrarea mesajelor îmbunătățită + Conexiune mai fiabilă. + luni + Forma mesaj personalizabilă. + Versiunea pentru desktop nu este compatibilă. Asigurați-vă că utilizați aceeași versiune pe ambele dispozitive. + Statistici detaliate + Descărcat + Eroare la reconectarea serverelor + Ectivat pentru + %d evenimente de grup + %d conversații cu membri + activat + Conexiunea a atins limita de mesaje nelivrate, este posibil ca persoana de contact să fie offline. + Dezactivați SimpleX Lock + Eroare la salvarea setărilor + Deconectați desktopul? + Nu activați + Eroare la primirea fișierului + Eroare la ștergerea solicitării de contact + Dezactivați ștergerea automată a mesajelor? + Nu trimiteți istoricul noilor membri. + renegocierea criptării este permisă pentru %s + Mesaje care dispar + Eroare la ștergerea grupului + %d rapoarte + Eroare + Eroare la anularea modificării adresei + Eroare la ștergerea conexiunii de contact în așteptare + criptare agreată pentru %s + Introduceți mesajul de bun venit… (opțional) + Criptați fișierele stocate și media + Eroare la acceptarea condițiilor + Eroare la activarea confirmărilor de livrare! + Eroare fișier + Eroare la pornirea conversației + Eroare la setarea adresei + Eroare la actualizarea linkului de grup + Eroare la actualizarea confidențialității utilizatorului + Fișier + Imagine salvată în Galerie + Exportați baza de date + apel ratat + Căști + Activați autodistrugerea + Dezactivare primire confirmare? + Activați confirmarea de primire? + Nume nou afișat: + Instrumente pentru dezvoltatori + FIȘIERE + Deschide linkuri din lista de chat + Forma mesajului + Importați baza de date + Arhivă nouă a bazei de date + Arhivă veche a bazei de date + Deschideți folderul bazei de date + implicit (%s) + Activați ștergerea automată a mesajelor? + Notificările vor fi livrate doar până când aplicația se oprește! + Baza de date va fi criptată, iar parola va fi stocată în Keystore. + Bază de date criptată + migrare diferită în aplicație/baza de date: %s / %s + Confirmare de migrare nevalidă + Membrul %1$s schimbat la %2$s + Niciun contact de adăugat + Extindeți selecția de roluri + Invitați să discutați + Nou rol de membru + Invitați membrii + Editați profilul grupului + Grupul va fi șters pentru dvs. - această acțiune nu poate fi anulată! + Membrul va fi eliminat din grup - acest lucru nu poate fi anulat! + Membrii vor fi eliminați din chat - acest lucru nu poate fi anulat! + Membrii vor fi eliminați din grup - acest lucru nu poate fi anulat! + Mesajele de la %s vor fi afișate! + inactiv + Eroare la blocarea membrului pentru toți + Profilul grupului este stocat pe dispozitivele membrilor, nu pe servere. + Eroare la salvarea profilului de grup + Textul condițiilor actuale nu a putut fi încărcat, puteți consulta condițiile prin intermediul acestui link: + Eroare la actualizarea serverului + Serverul operator + Ștergeți profilul de chat pentru + Introduceți parola în căutare + Incognito + Modul incognito vă protejează confidențialitatea utilizând un nou profil aleatoriu pentru fiecare contact. + Numai date despre profilul local + Potrivire + Bună ziua! + Mesaje directe + Mesaje care dispar + Doar contactul dvs. poate șterge ireversibil mesajele (le puteți marca pentru ștergere). (24 de ore) + Ștergerea ireversibilă a mesajelor este interzisă în acest chat. + Numai tu poți trimite mesaje vocale. + Reacțiile la mesaje sunt interzise în această conversație. + Numai tu poți efectua apeluri. + Doar contactul tău poate efectua apeluri. + %dh + %d ore + Mesajele directe între membri sunt interzise. + Mesajele directe între membri sunt interzise în această conversație. + Membrii pot trimite link-uri SimpleX. + %d saptamana + oferit %s + oferit %s: %2s + Șterge conversația + Ștergeți conversația cu membrul? + Membru se va alătura grupului, acceptați membru? + Ascundeți ecranul aplicației în aplicațiile recente. + Nume diferite, avatare și izolare transport. + Interfață franceză + Draft de mesaje + În sfârșit, le avem! 🚀 + Conectare mai rapidă și mesaje mai de încredere. + Interfață în japoneză și portugheză + Interfață de utilizator maghiară și turcă + Redirecționați și salvați mesajele + Activează Flux în setările Rețea și servere pentru o mai bună confidențialitate a metadatelor. + pentru o mai bună confidențialitate a metadatelor. + Redirecționați până la 20 de mesaje simultan. + Nu rata mesajele importante. + Organizați conversațiie în liste + Trimitere mai rapidă a mesajelor. + Ștergere mai rapidă a grupurilor. + ore + Activați + Telefoane conectate + Introduceți numele acestui dispozitiv… + Eroare + Versiune incompatibilă + Nou dispozitiv mobil + Descoperibil prin rețeaua locală + Descoperiți prin intermediul rețelei locale + Deschideți portul în firewall + Desktopul este inactiv + Desktopul a fost deconectat + Desktopul are codul de invitație greșit + Descărcarea a eșuat + Importul a eșuat + Introduceți parola + Eroare la descărcarea arhivei + Migrați către un alt dispozitiv + duplicate + Mesaje trimise + Șters + Fișiere descărcate + Erori de descărcare + Găsește această permisiune în setările Android și activeaz-o manual. + Dimensiunea fontului + Dacă nu vă puteți întâlni în persoană, arătați codul QR într-un apel video sau distribuiți linkul. + Cod QR nevalid + Cod de acces incorect + Redirecționați %1$s mesaj/mesaje? + Redirecționarea serverului %1$s nu s-a putut conecta la serverul de destinație %2$s. Vă rugăm să încercați mai târziu. + Ascunde: + Imaginea va fi primită după ce persoana de contact o va termina de încărcat. + Versiunea serverului de redirecționare este incompatibilă cu setările de rețea: %1$s. + Redirecționați mesajele fără fișiere? + Link de conexiune nevalid + Invitația la grup a expirat + Eroare la crearea listei de conversație + Eroare la încărcarea listelor de conversații + Eroare la actualizarea listei de chat + Notificări instant + Fără parolă pentru aplicație + Favorite + Fișier salvat + Fișierul va fi primit când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! + niciun detaliu + Deschide cu %s + Link invitație unică + Șterge lista? + Marchează ca verificat + Activează pentru toți + grup șters + criptare ok + invitat + Mut + ajutor + niciun text + Membrii pot trimite mesaje directe. + - opțional, notificați contactele șterse.\n- nume de profil cu spații.\n- și multe altele! + Activează în chat-urile directe (BETA)! + Navigare îmbunătățită în chat + Noile acreditări SOCKS vor fi utilizate de fiecare dată când porniți aplicația. + Vă protejează adresa și conexiunile IP. + Mod luminos + Sursa mesajelor rămâne privată. + Mesaje live + Asigurați-vă că configurația proxy este corectă. + Cel mai probabil acest contact a șters conexiunea cu dvs. + Avertizare de livrare a mesajelor + Membrul va fi eliminat din chat - acest lucru nu poate fi anulat! + Mesaje primite + Noua aplicație pentru desktop! + Nu s-au găsit conversații + Fără istoric + Niciun contact selectat + Previzualizare notificări + Fără apeluri în fundal + Nu + Note + Fără servicii în fundal + Gazdele Onion vor fi folosite atunci când sunt disponibile. + Numai tu poți adăuga reacții la mesaje. + Încă nu există conexiune directă, mesajul a fost redirecționat de administrator. + - Deschide chatul la primul mesaj necitit.\n- Sări la mesajele citate. + Oprit + Deschideți setările serverului + criptare ok pentru %s + profilul grupului a fost actualizat + proprietar + Șterge conversația + ieșit + Remedierea nu este suportată de contact + Erori + Deschideți grupul + Mesajele vor fi șterse - acest lucru nu poate fi anulat! + Eroare la criptarea bazei de date + Fișiere + Păstrează-ți conexiunile + Membru inactiv + Doar 10 videoclipuri pot fi trimise simultan + Imagine trimisă + mesaj + Favorit + Marchează ca citit + Mut + Nume listă... + Numele listei și emoji ar trebui să fie diferite pentru toate listele. + Află mai multe + Invită + Eroare la inițializarea WebView. Asigurați-vă că aveți WebView instalat și că arhitectura sa suportată este arm64.\nEroare: %s + Faceți o conexiune privată + Deschideți SimpleX Chat pentru a accepta apelul + Activați blocarea + Se poate întâmpla atunci când tu sau conexiunea ta ați folosit backupul vechi al bazei de date. + Cod de acces + Mod incognito + Conexiune de rețea + Statusul rețelei + Asigurați-vă că fișierul are sintaxa YAML corectă. Exportați tema pentru a avea un exemplu de structură de fișiere tematice. + Grupuri incognito + Noua experiență de chat 🎉 + Noi opțiuni media + Primiți notificări când sunteți menționați. + Migrare completă + Altul + Migrați aici + Numele complet: + Ajutor pentru marcare + Dacă introduceți această parolă la deschiderea aplicației, toate datele aplicației vor fi șterse ireversibil! + Eroare la modificarea setării + invitație la grup %1$s + Confidențialitate și securitate îmbunătățite + Moderarea grupului + Fișierul a fost șters sau linkul este invalid + Pentru a continua, chatul trebuie oprit. + Extinde + Parola bazei de date este diferită de cea salvată în Keystore. + observator + Ascunde + Eroare la încărcarea detaliilor + Eroare la ștergerea profilului de utilizator + Descărcare + NU trimiteți mesaje direct, chiar dacă serverul dvs. sau serverul de destinație nu acceptă rutare privată. + Se redirecționează %1$s mesaje + grupul este șters + nesincronizat + Din Galerie + Imagine + (stocat doar de membrii grupului) + Mesaj nou + Link complet + Păstrează + Sau arătați acest cod + Eroare la schimbarea profilului + Nicio persoană de contact filtrată + Servere media și fișiere + Alte servere XFTP + NU utilizați rutare privată. + Activează apelurile de pe ecranul de blocare prin Setări. + Dezactivare (păstrare suprascrieri) + Dacă introduceți parola de autodistrugere în timp ce deschideți aplicația: + Mediu + Părăsiți conversația + Fără conversații cu membrii + Confirmare de primire a mesajelor! + minute + Adaugă un mobil + Creează o listă + Șterge + Instalați SimpleX Chat pentru terminal + Eroare la salvarea serverelor ICE + CULORILE INTERFEȚEI + %d fișier(e) cu dimensiunea totală de %s + Fișiere și media + invitat %1$s + Membrii pot șterge ireversibil mesajele trimise. (24 de ore) + Membrii pot trimite mesaje ce dispar. + Mesajele directe între membri sunt interzise în acest grup. + Mesajele care dispar sunt interzise. + %dw + Desktop găsit + Se descarcă detaliile linkului + Eroare internă + Sau importați fișierul de arhivă + Sau lipiți linkul arhivei + Eroare la inițializarea WebView. Actualizați sistemul la noua versiune. Vă rugăm să contactați dezvoltatorii.\nEroare: %s + Grupul va fi șters pentru toți membrii - această acțiune nu poate fi anulată! + Nume local + Depanați livrarea + Eroare la schimbarea rolului + Reacții la mesaje + expirat + Descărcați %s (%s) + Instalați update + Nu creați adresa + De exemplu, dacă persoana de contact primește mesaje prin intermediul unui server SimpleX Chat, aplicația le va livra prin intermediul unui server Flux. + Deschideți Setări Safari / Site-uri web / Microfon, apoi alegeți Permiteți pentru localhost. + Apel ratat + Renegocierea criptării a eșuat. + Oprit + Dezactivați confirmări pentru grupuri? + Dezactivați pentru toate grupurile + Dezactivați pentru tot + Deshideți link-ul + Baza de date va fi criptată, iar parola va fi stocată în setări. + Eroare: %s + Părăsiți grupul? + Grupul nu a fost găsit! + Eroare la crearea contactului membrului + Doar proprietarii chatului pot schimba preferințele. + Stare fișier + Starea fișierului: %s + Statusul mesajului: %s + Mesajul prea mare + Reparare + Deschideți modificările + Eroare la adăugarea serverului + Meniuri și alerte + pornit + oprit + activat pentru contact + activat pentru tine + Profiluri de chat ascunse + Acum administratorii pot:\n- șterge mesajele membrilor.\n- dezactiva membrii (rol de observator) + Reacții la mesaje + Rapid și fără așteptare până când expeditorul este online! + Descoperiți și alăturați-vă grupurilor + Chiar și atunci când este dezactivat în conversație. + - livrare mai stabilă a mesajelor.\n- grupuri mai bune.\n- și multe altele! + Face ca un mesaj să dispară + Migrați către un alt dispozitiv prin codul QR. + Șterge până la 20 de mesaje simultan. + Alăturați-vă grupului dvs.? + Fără conexiune la rețea + Notificări instant! + Deschide setările aplicației + %d secunde + Activați SimpleX Lock + Eroare la afișarea notificării, contactați dezvoltatorii. + Eroare de livrare a mesajelor + Eroare server destinație: %1$s + Eroare: %1$s + Pentru toți moderatorii + Redirecționat + Istoric + Ca răspuns la + Mesaj redirecționat + Grupuri + Mesajele au fost șterse după ce le -ați selectat. + Fără conversații + Nimic de redirecționat! + %d conversație(i) + %d mesaje + Eroare de decodare + Fișierele și conținutul media nu sunt permise + Mesaj + membrul are o versiune veche + Renegocierea criptării în desfășurare. + Reparare + Fișier mare! + Mesajele din această conversație nu vor fi niciodată șterse. + Notificări + Doar proprietarii grupului pot activa mesajele vocale. + Editați + Mesaj live! + Mut pentru toate + OK + Previzualizare imagine din link + Conversație nouă + Link invitație unică + Servere mesaje + Alte servere SMP + Introduceți serverul manual + Servere ICE (unul pe linie) + Rețea și servere + Nu utilizați acreditările cu proxy. + Se descarcă actualizarea aplicației, nu închideți aplicația + Oprit + Deschideți locația fișierului + Ieșire fără salvare + Parola profilului ascunsă + italic + Cum funcționează SimpleX + Fără identificatori de utilizator. + Se poate întâmpla când:\n1. Mesajele au expirat în clientul care trimite după 2 zile sau pe server după 30 de zile.\n2. Decriptarea mesajelor a eșuat, deoarece tu sau contactul dvs. ați folosit o copie de rezervă a bazei de date vechi.\n3. Conexiunea a fost compromisă. + Eroare la exportarea bazei de date de conversații + Eroare la oprirea conversației + niciodată + Niciun fișier primit sau trimis + Frază de acces nouă… + Criptați baza de date? + Introduceți parola corectă. + Alăturați-vă grupului? + Migrații: %s + Alătură -te + Părăsiți + Părăsiți conversația? + ieșit + Invitat prin link-ul dvs. de grup + Un membru nou vrea să se alăture grupului. + criptare agreată + renegocierea criptării este permisă + renegocierea criptării este necesară pentru %s + Invită + Moderat la: %s + Numele complet al grupului: + Preferințele grupului + Ștergerea ireversibilă a mesajelor este interzisă. + Membrii pot adăuga reacții la mesaje. + Membrii pot trimite fișiere și media. + Reacțiile la mesaje sunt interzise. + %d oră + %dm + %d min + %d luni + %dmth + Istoricul nu este trimis noilor membri. + Membrii pot raporta mesaje către moderatori. + proprietari + Admiterea membrilor + oprit + Link-urile grupului + Ștergerea ireversibilă a mesajelor + Măriți dimensiunea fontului. + Deconectați-vă + Deconectat din motivul: %s + Eroare la ștergerea bazei de date + Eroare la încărcarea arhivei + Detalii + alte erori + Eroare la ștergerea conversației cu membrul + Link-ul grupului + Remediați conexiunea? + %d săptămâni + Redirecţionează + Mesajul va fi șters - acesta nu poate fi anulat! + Mesajul va fi marcat pentru ștergere. Destinatarul (destinatarii) va (vor) putea dezvălui acest mesaj. + Doar proprietarii grupului pot activa fișiere și media. + Imagine + Activează accesul la cameră + Cum să + Ignoră + Informații despre coada de mesaje + Complet descentralizat – vizibil doar membrilor. + Introduceți numele grupului: + Umple + Descărcați noi versiuni de pe GitHub. + Dacă ați primit linkul de invitație la SimpleX Chat, îl puteți deschide în browser: + Link nevalid! + Nu + Invitați membrii + Nou în %s + Gazdele Onion vor fi necesare pentru conectare.\nRețineți: nu vă veți putea conecta la servere fără adresa .onion. + Nu se vor folosi gazde Onion. + Exportă tema + Importați tema + Eroare la importul temei + Eroare la acceptarea solicitării de contact + Descărcați fișierul + Eroare la ștergerea contactului + Eroare la crearea adresei + Eroare la ștergerea notelor private + Eroare la salvarea serverelor + Erori în configurarea serverelor. + Adresa serverului de redirecționare este incompatibilă cu setările rețelei: %1$s. + Nu există servere pentru a trimite fișiere. + Mod de rutare a mesajelor + Niciodată + Întoarce camera + Link nevalid + în așteptarea aprobării + Notificări periodice + Notificările periodice sunt dezactivate! + Lipiți linkul arhivei + Se pregătește descărcarea + Lipește + Vă rugăm să rugați persoana de contact să activeze apelurile. + Rutare privată + Confidențialitatea redefinită + Vă rugăm să verificați conexiunea la rețea cu %1$s și să încercați din nou. + Rutare mesaje private 🚀 + Bara de instrumente de chat accesibilă + Redă din lista de conversații. + Vă rugăm să încercați mai târziu. + Citeşte mai mult + Vă rugăm să verificați dacă linkul SimpleX este corect. + Vă rugăm să contactați administratorul grupului. + Se pregătește încărcarea + Acces refuzat! + Vă rugăm să reduceți dimensiunea mesajului și să îl trimiteți din nou. + În așteptare + peer-to-peer + Vă rugăm să reporniți aplicația. + Mesaje primite + Lipește linkul pentru conectare! + Interziceți reacțiile la mesaje. + Interzicerea trimiterii de linkuri SimpleX + Lipește linkul primit pentru a te conecta cu persoana de contact… + Te rugăm să-i ceri persoanei tale de contact să activeze trimiterea de mesaje vocale. + Cod QR + portul %d + Periodic + Servere presetate + Vă rugăm să o rețineți sau să o păstrați în siguranță - nu există nicio modalitate de a recupera o parolă pierdută! + Confidențialitate și securitate + Protejați ecranul aplicației + Imagini de profil + Vă rugăm să stocați în siguranță fraza de acces, NU veți putea accesa chatul dacă o pierdeți. + Interziceți trimiterea de mesaje vocale. + Apeluri cu funcție picture-in-picture + Interfață persană + Proxiat + Conexiuni de profil și server + răspuns primit… + Politica de confidențialitate și condițiile de utilizare. + RUTAREA MESAJELOR PRIVATE + Vă rugăm să păstrați parola în siguranță, NU o veți putea schimba dacă o pierdeți. + Parolă negăsită în Keystore, vă rugăm să o introduceți manual. Acest lucru s-ar fi putut întâmpla dacă ați restaurat datele aplicației folosind un instrument de backup. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. + Parola din Keystore nu poate fi citită. Acest lucru s-ar fi putut întâmpla după o actualizare de sistem incompatibilă cu aplicația. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. + Fost membru %1$s + în așteptare + recenzie în așteptare + Primit la: %s + Expirare protocol + Număr de PING-uri + Parolă de profil + Interziceți reacțiile la mesaje. + Interzice ștergerea ireversibilă a mesajelor. + Interziceți trimiterea de mesaje directe către membri. + Interziceți trimiterea de mesaje care dispar. + Interziceți raportarea mesajelor către moderatori. + Interziceți trimiterea de mesaje vocale. + Păstrează ultima schiță a mesajului, cu atașamente. + Nume de fișiere private + Criptare rezistentă cuantic + Confidențialitate pentru clienții dumneavoastră. + Nume de fișiere media private. + Aleatoriu + Vă rugăm să raportați dezvoltatorilor:\n%s\n\nSe recomandă repornirea aplicației. + Vă rugăm să confirmați că setările de rețea sunt corecte pentru acest dispozitiv. + Este necesară o parolă + Parolă + Servere presetate + Lipiți adresa desktopului + Este posibil ca amprenta certificatului din adresa serverului să fie incorectă. + Interval PING-uri + Expirare protocol per KB + Notificări private + Actualizarea profilului va fi trimisă contactelor tale. + Interziceți apelurile audio/video. + Nume profil: + Autentificare proxy + Parolă de afișat + Vă rugăm să reduceți dimensiunea mesajului sau să eliminați fișierul media și să îl trimiteți din nou. + Protejați adresa IP + mesaj primit + Note private + Bara de instrumente de chat accesibilă + Temă de profil + Lipiți linkul primit + Parola nu a fost modificată! + Parolă setată! + Parola din Keystore nu poate fi citită, vă rugăm să o introduceți manual. Acest lucru s-ar fi putut întâmpla după o actualizare de sistem incompatibilă cu aplicația. Dacă nu este cazul, vă rugăm să contactați dezvoltatorii. + Bare de instrumente accesibile ale aplicațiilor + Vă rugăm să așteptați ca moderatorii grupului să vă examineze cererea de alăturare. + Interziceți trimiterea de mesaje care dispar. + Interziceți trimiterea de fișiere și fișiere media. + Vă rugăm să raportați dezvoltatorilor:\n%s + Servere proxy + Apel în așteptare + Vă rugăm să raportați dezvoltatorilor. + Vă rugăm să introduceți parola anterioară după restaurarea copiei de rezervă a bazei de date. Această acțiune nu poate fi anulată. + criptare e2e rezistentă cuantic + Chitanțele sunt dezactivate + Primit la + Interfață poloneză + Protejați-vă adresa IP de retransmiterile de mesagerie alese de contactele dvs.\nActivați în setările *Rețea și servere*. + Mesaj primit + Vă rugăm să așteptați în timp ce fișierul este încărcat de pe mobilul conectat. + imagine de profil + substituent pentru imaginea de profil + Lipiți linkul + Adresa prestabilită a serverului + Server presetat + Evaluează aplicația + Port + Conversațiile private, grupurile și contactele dvs. nu sunt accesibile operatorilor serverului. + Parola aleatorie este stocată în setări ca text simplu.\nO puteți modifica ulterior. + Vă rugăm să introduceți parola curentă corectă. + Previzualizare + Verificați dacă dispozitivul mobil și desktopul sunt conectate la aceeași rețea locală și dacă firewall-ul desktopului permite conexiunea.\nVă rugăm să comunicați orice alte probleme dezvoltatorilor. + Servere conectate anterior + În așteptare + am primit confirmarea… + Protejează-ți profilurile de chat cu o parolă! + Vă rugăm să verificați dacă ați folosit linkul corect sau cereți contactului dumneavoastră să vă trimită altul. + Eroare de rutare privată + respins + Răspuns primit + Destinatarul(ii) nu pot vedea de la cine este acest mesaj. + Trimiterea confirmărilor este dezactivată pentru %d contacte + primit, interzis + Reamintește mai târziu + Server adăugat la operatorul %s. + Setați mesajul afișat noilor membri! + Setați-l în locul autentificării sistemului. + Setați parola pentru export + Raportați conținutul: doar moderatorii grupului îl vor vedea. + Adresa serverului + Primirea fișierului va fi oprită. + Resetați la tema aplicației + Înregistrare actualizată la: %s + Trimiteți un mesaj pentru a activa apelurile. + Resetare + Rapoarte + informații despre coada serverului: %1$s\n\nultimul mesaj primit: %2$s + Salvați și reconectați + Raportați altceva: doar moderatorii grupului îl vor vedea. + căutare + Setați numele chatului… + Serverul de retransmisie este utilizat doar dacă este necesar. O altă parte poate observa adresa dvs. IP. + Resetați toate statisticile? + Resetați toate sugestiile + Trimiteți mesaje direct atunci când adresa IP este protejată și serverul dvs. sau serverul de destinație nu acceptă rutare privată. + Parola de autodistrugere activată! + Setați parola + Examinați condițiile + Raport + Adresa de primire va fi schimbată la un alt server. Schimbarea adresei se va finaliza după ce expeditorul se conectează online. + Salvați setările pentru admitere? + Revizuiți mai târziu + Trimiterea confirmărilor este dezactivată pentru %d grupuri + recenzie + Operatorul serverului s-a schimbat. + Scala + Utilizare redusă a bateriei + Mesaje trimise + Primit total + Primiți erori + Reconectați serverul pentru a forța livrarea mesajului. Consumă trafic suplimentar. + Resetați toate statisticile + Trimis prin proxy + Trimis direct + revizuit de administratori + Operatori de server + Serverul de retransmisie protejează adresa IP, dar poate observa durata apelului. + PORNIȚI CHATUL + te-a eliminat + %s la %s + Protocolul serverului a fost modificat. + Recepție simultană + Resetați la tema utilizatorului + Setați admiterea membrilor + Respinge + Respingeți membrul? + Destinatarii văd actualizările pe măsură ce le tastați. + Istoric recent și bot de directoare îmbunătățit. + Setați expirarea mesajului în chat-uri. + Trimiterea confirmărilor de livrare va fi activată pentru toate contactele. + Trimiterea confirmărilor de livrare va fi activată pentru toate contactele din toate profilurile de chat vizibile. + Securizat + Selectat %d + Trimiterea confirmărilor este activată pentru %d contacte + Informații despre servere + Reconectați serverele? + Statisticile serverelor vor fi resetate - această acțiune nu poate fi anulată! + Reconectați toate serverele + Reconectați serverul? + Trimis total + Preferințele de chat selectate interzic acest mesaj. + Trimiterea confirmărilor este activată pentru %d grupuri + Parolă de autodistrugere + Trimiteți rapoarte private + Trimiteți mesaje direct atunci când serverul dvs. sau serverul de destinație nu acceptă rutare privată. + eliminat din grup + cererea de înscriere a fost respinsă + Selectați profilul de chat + Salvați setările de acceptare automată + Salvați lista + TRIMITEȚI CONFIRMĂRI DE LIVRARE LA + Parola de autodistrugere a fost schimbată! + Înregistrare actualizată la + Selectați operatorii de rețea pe care îi veți utiliza. + Telefoane mobile la distanță + Trimiteți confirmări de primire + Setați 1 zi + Parolă de autodistrugere + Reconectați toate serverele conectate pentru a forța livrarea mesajelor. Aceasta utilizează trafic suplimentar. + Se primesc mesaje… + Motivul raportării? + respins + Raport: %s + Se salvează %1$s mesaje + Raport trimis moderatorilor + Raportați spam: numai moderatorii grupului îl vor vedea. + Raportați profilul membrului: numai moderatorii grupului îl vor vedea. + Raportați încălcarea: numai moderatorii grupului o vor vedea. + Înregistrați mesajul vocal + Scanare / Lipire link + Server + Reporniți aplicația pentru a utiliza baza de date de chat importată. + Eliminați membri? + Primirea prin intermediul + Raportarea mesajelor este interzisă în acest grup. + Examinați membrii + Verificați membrii înainte de a-i primi (a bate la ușă). + Eliminați arhiva? + Trimiteți erorile + Reconectați-vă + Selectați + Adresa serverului este incompatibilă cu setările de rețea: %1$s. + Coadă securizată + Este posibil ca expeditorul să fi șters cererea de conectare. + Versiunea serverului este incompatibilă cu aplicația dvs.: %1$s. + Deblocare + Încărcarea a eșuat + Aplicația se poate închide după 1 minut în fundal. + Activează sunetul + Această setare se aplică mesajelor din profilul tău de chat actual + Folosiți servere SimpleX Chat? + Proxy SOCKS + Nume de utilizator + SimpleX Chat și Flux au ajuns la un acord pentru a include în aplicație servere operate de Flux. + Aplicația preia mesaje noi periodic — consumă doar câteva procente din baterie pe zi. Aplicația nu folosește notificări push — datele de pe dispozitivul tău nu sunt trimise către servere. + Blocare SimpleX + Cheie greșită sau adresă necunoscută a fragmentului de fișier - cel mai probabil fișierul este șters. + Prea multe videoclipuri! + Videoclip trimis + Opriți partajarea + Servere ICE WebRTC + Actualizați și deschideți chatul + Fișiere încărcate + Folosește profilul curent + Folosește noul profil incognito + Vizualizarea s-a blocat + Funcție lentă + Blocare SimpleX + Deblocați pentru toți + Deblocați membrii pentru toate? + Actualizare + Până la 100 de mesaje recente sunt trimise noilor membri. + Ce este nou + Mulțumim utilizatorilor – contribuiți prin Weblate! + Mulțumim utilizatorilor – contribuiți prin Weblate! + Cu un consum redus de energie al bateriei. + Cu un consum redus de energie al bateriei. + Începând de la %s.\nToate datele sunt păstrate private pe dispozitivul dvs. + Abonat/ă + Erori de încărcare + Apel video + Pentru a vă conecta prin link + Mesajele vocale nu sunt permise + Neprotejat + Da + Actualizare + Aveți deja un profil de chat cu același nume afișat. Vă rugăm să alegeți un alt nume. + Acest nume afișat este invalid. Vă rugăm să alegeți un alt nume. + Mesajele vor fi șterse pentru toți membrii. + Folosiți serverul + Sistem + Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact. + Ești conectat la serverul folosit pentru a primi mesaje de la acest contact. + Mulțumim utilizatorilor – contribuiți prin Weblate! + Poți să încerci încă o dată. + Ești deja conectat la %1$s. + Cheie greșită sau conexiune necunoscută - cel mai probabil această conexiune este ștearsă. + Video + Mesaj vocal (%1$s) + Trimite + Comutare listă de chat: + Pentru a primi notificări, vă rugăm să introduceți parola bazei de date + Se așteaptă videoclipul + Mesaje nelivrate + Testul a eșuat la pasul %s. + Opriți fișierul + trimitere neautorizată + Bun venit, %1$s! + Atingeți pentru a începe o conversație nouă + Folosește acreditări proxy diferite pentru fiecare conexiune. + Adresă SimpleX sau link unic? + Pentru a vă proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare dintre contactele dvs. + Ești invitat(ă) în grup + Pentru a primi + Utilizare pentru fișiere + Pentru a trimite + Temă + Utilizați aplicația cu o singură mână. + Videoclipul va fi primit după ce persoana de contact termină de încărcat. + Videoclipul va fi recepționat când contactul dvs. va fi online, vă rugăm să așteptați sau să verificați mai târziu! + video + Utilizarea serverelor de chat SimpleX. + Folosiți proxy SOCKS? + Browserul web implicit este necesar pentru apeluri. Vă rugăm să configurați browserul implicit în sistem și să partajați mai multe informații cu dezvoltatorii. + Afișează starea mesajului + Port TCP pentru mesagerie + Descărcarea actualizării a fost anulată + Omiteți invitarea membrilor + Începând de la %s. + Se așteaptă conectarea mobilului: + Server XFTP + Această acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele dvs. vor fi pierdute iremediabil. + Sistem + Deblocați membrul? + Activează sunetul + Mesaje vocale + Statistici + Opriți conversația? + necunoscut + Puteți ascunde sau dezactiva un profil de utilizator - țineți apăsat pentru meniu. + Ați permis + Distribuie linkul unic unui prieten + Mesaje vocale + Mesaj vocal… + Setări proxy SOCKS + profil actualizat + Mod Blocare SimpleX + Mesajele vor fi marcate ca moderate pentru toți membrii. + necitit + Bun venit! + Atingeți pentru conectare + Mesajele vocale sunt interzise! + Ați acceptat conexiunea + Distribuiți adresa public + Distribuie acest link cu invitație unică + Pentru a verifica criptarea end-to-end cu persoana de contact, comparați (sau scanați) codul de pe dispozitivele dvs. + Utilizare pentru conexiuni noi + Utilizați proxy SOCKS + Opriți partajarea adresei? + Când aplicația rulează + Aplicația vă protejează confidențialitatea utilizând operatori diferiți în fiecare conversație. + Vizualizați condițiile + Afișează doar contactul + Deblocare + Porniți + Oprește conversația + Acest mesaj a fost șters sau nu a fost încă primit. + Mesajul va fi marcat ca moderat pentru toți membrii. + Stop + Opriți primirea fișierului? + Se așteaptă fișierul + Această acțiune nu poate fi anulată - mesajele trimise și primite în acest chat anterior celei selectate vor fi șterse. + Vizualizați codul de securitate + Atingeți pentru a lipi linkul + Textul pe care l-ai lipit nu este un link SimpleX. + Stabil + Puteți configura operatorii în setările Rețea și servere. + Actualizare + apel video (necriptat e2e) + ID-ul următorului mesaj este incorect (mai mic sau egal cu precedentul).\nSe poate întâmpla din cauza unei erori sau când conexiunea este compromisă. + Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru serverele de fișiere. + Aplicația va solicita confirmarea descărcărilor de pe servere de fișiere necunoscute (cu excepția .onion sau când este activat proxy-ul SOCKS). + Sistem + Acestea pot fi ignorate în setările de contact și de grup. + ASISTENȚĂ CHAT SIMPLEX + PROXY SOCKS + TEME + În timpul importului au apărut câteva erori non-fatale: + Atingeți pentru a vă alătura + Atenție: este posibil să pierdeți unele date! + Folosiți serverele + Reafișează profilul de chat + Titlu + Transparenţă + Videoclipuri și fișiere de până la 1 GB + Comută modul incognito la conectare. + Formă imagine de profil + Schimbă profilul de chat pentru invitații unice. + Puteți activa mai târziu prin Setări + săptămâni + Acesta este link-ul tău unic! + Servere necunoscute! + Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru aceste retransmiteri XFTP:\n%1$s. + Actualizați parola bazei de date + Opriți trimiterea fișierului? + lovitură + Viitorul mesageriei + Pentru a efectua apeluri, permiteți utilizarea microfonului. Încheiați apelul și încercați să sunați din nou. + Când sunt activați mai mulți operatori, niciunul dintre ei nu are metadate pentru a afla cine comunică cu cine. + Video pornit + Aceste setări sunt pentru profilul tău actual + Da + Urmă + Utilizare de pe desktop + Oprește chatul pentru a exporta, importa sau șterge baza de date a chatului. Nu vei putea primi și trimite mesaje cât timp chatul este oprit. + Unele fișiere nu au fost exportate + Parola este stocată în setări ca text simplu. + deblocat %s + status necunoscut + tu: %1$s + Mesaj de bun venit + Deblocați membrul + Deblocați membrul pentru toate? + Comutator + Rolul va fi schimbat în %s. Toți participanții la chat vor fi notificați. + Rolul va fi schimbat în %s. Toți membrii grupului vor fi notificați. + Folosește %s + Site web + %s Servere + Actualizați setările de rețea? + Atingeți pentru a activa profilul. + Sistem + Mulțumim utilizatorilor – contribuiți prin Weblate! + Verificați securitatea conexiunii + Izolarea transportului + Pentru a proteja fusul orar, fișierele imagine/voce utilizează UTC. + - mesaje vocale de până la 5 minute.\n- timp personalizat de dispariție.\n- istoricul modificărilor. + Pentru a ascunde mesajele nedorite. + Prin protocol securizat de rezistență cuantică. + Cu fișiere și suporturi media criptate. + Va fi activat în chat-urile directe! + Al doilea operator presetat din aplicație! + Vizualizați condițiile actualizate + Verificați conexiunea + Le poți activa ulterior prin setările de Confidențialitate și Securitate ale aplicației. + Verificați codul pe mobil + Numele acestui dispozitiv + Acest dispozitiv + Deconectare + Pentru a permite unei aplicații mobile să se conecteze la desktop, deschideți acest port în firewall, dacă îl aveți activat. + Verificați codul cu desktopul + Această funcție nu este încă compatibilă. Încercați următoarea versiune. + Vă conectați deja prin intermediul acestei legături unice! + Atenție: inițierea chatului pe mai multe dispozitive nu este acceptată și va cauza erori de livrare a mesajelor. + Verificați parola bazei de date + Ethernet prin cablu + Abonamente ignorate + Mărime + Poți menționa până la %1$s membri per mesaj! + Blocarea SimpleX este activată + Verificați codul de securitate + Criptarea funcționează și noul acord de criptare nu este necesar. Poate duce la erori de conexiune! + Expirare conexiune TCP + Afișează + Afișează informații pentru + Mesajul va fi șters pentru toți membrii. + Vă mulțumim pentru instalarea SimpleX Chat! + Distribuiți profilul + Eroare necunoscută + Sesiuni de transport + Când IP-ul este ascuns + Sunet dezactivat + Reafișează profilul + Accent imagine de fundal + Arhiva bazei de date încărcată va fi eliminată definitiv de pe servere. + WiFi + Stop + Mesaje ignorate + Parola va fi stocată în setări ca text simplu după ce o modificați sau reporniți aplicația. + Conexiune TCP + Erori de abonare + Atingeți pentru a vă conecta incognito + Imaginea nu poate fi decodificată. Vă rugăm să încercați o altă imagine sau să contactați dezvoltatorii. + Serverele pentru fișierele noi din profilul tău de chat actual + Acest grup nu mai există. + Se încearcă conectarea la serverul folosit pentru a primi mesaje de la acest contact (eroare: %1$s). + Actualizarea setărilor va reconecta clientul la toate serverele. + Acest șir de caractere nu este un link! + S-a atins timpul de expirare la conectarea la desktop + Pentru a te proteja împotriva înlocuirii linkului tău, poți compara codurile de securitate ale contactelor. + Pentru a vă proteja informațiile, activați SimpleX Lock.\nVi se va solicita să finalizați autentificarea înainte ca această funcție să fie activată. + Dacă nu cumva contactul dvs. a șters conexiunea sau acest link era deja utilizat, ar putea fi o eroare - vă rugăm să o raportați.\nPentru a vă conecta, rugați contactul dvs. să creeze un alt link de conexiune și verificați dacă aveți o conexiune stabilă la rețea. + Când partajezi un profil incognito cu cineva, acest profil va fi folosit pentru grupurile în care te invită. + Mesaj vocal + La conectarea apelurilor audio și video. + Când oamenii solicită conectarea, poți accepta sau respinge solicitarea. + Acest grup are peste %1$d membri, confirmările de livrare nu sunt trimise. + Comutați între audio și video în timpul apelului. + Deconectați desktopul? + Se așteaptă desktopul… + Prea multe imagini! + Încărcați fișierul + Atingeți butonul + Atingeți pentru a scana + (pentru a partaja cu persoana de contact) + Pentru a începe o nouă conversație + Video + Distribuiți linkul unic + Acest link nu este un link de conectare valid! + Acest cod QR nu este un link! + Link scurt + Server XFTP + Actualizați modul de izolare a transportului? + Folosește rutare privată cu servere necunoscute. + Folosește rutare privată cu servere necunoscute atunci când adresa IP nu este protejată. + Folosiți portul TCP %1$s atunci când nu este specificat niciun port. + Nu stocăm niciunul dintre contactele sau mesajele dvs. (odată livrate) pe servere. + prin releu + Soft + Puternic + A doua ticăitură pe care am ratat-o! ✅ + Mulțumim utilizatorilor – contribuiți prin Weblate! + Afișează procentul + Serverele pentru conexiuni noi ale profilului dvs. de chat actual + Afișează apeluri API lente + Pentru a fi notificat despre noile lansări, activează verificarea periodică pentru versiunile stabile sau beta. + Folosește o parolă aleatorie + Această acțiune nu poate fi anulată - toate fișierele și fișierele media primite și trimise vor fi șterse. Imaginile cu rezoluție mică vor rămâne. + Îl poți modifica în setările Aspect. + ați acceptat acest membru + Istoric vizibil + Te alături deja grupului prin intermediul acestui link. + Server SMP + Servere necunoscute + Folosește aplicația în timpul apelului. + Numele dispozitivului va fi partajat cu clientul mobil conectat. + Încărcat + Folosiți portul TCP 443 doar pentru serverele presetate. + Omiteți această versiune + Îl poți crea mai târziu + Îl poți face vizibil pentru contactele tale SimpleX prin Setări. + Platforma de mesagerie și aplicații care vă protejează confidențialitatea și securitatea. + Profilul este partajat doar cu contactele tale. + Pentru a dezvălui profilul ascuns, introduceți o parolă completă într-un câmp de căutare din pagina Profilurile dvs. de chat. + Puteți configura serverele prin intermediul setărilor. + apel video + Video dezactivat + Parolă greșită pentru baza de date + Eroare necunoscută în baza de date: %s + l-ai blocat pe %s + Rolul va fi schimbat în %s. Membrul va primi o nouă invitație. + Mesaj de bun venit + Mesajul de bun venit este prea lung + Imagine de fundal + Mod sistem + da + Mesajele vocale sunt interzise în acest chat. + Cu mesaj opțional de bun venit. + Suportă Bluetooth și alte îmbunătățiri. + Aceasta este adresa ta SimpleX! + Se încarcă arhiva + %s a fost încărcat + Verificați parola + Blocarea SimpleX nu este activă! + Autentificare de sistem + Baza de date nu funcționează corect. Atingeți pentru a afla mai multe. + Raportul va fi arhivat pentru dvs. + Atingeți Creare adresă SimpleX din meniu pentru a o crea mai târziu. + Acest text este disponibil în setări + ești invitat în grup + Se așteaptă imaginea + Se așteaptă imaginea + Se așteaptă videoclipul + Eroare temporară de fișier + Eliminare favorit + Mențiuni necitite + Conexiunea pe care ați acceptat-o va fi anulată! + Contactul cu care ai distribuit acest link NU se va putea conecta! + vrea să se conecteze cu tine! + Distribuiți adresa SimpleX pe rețelele de socializare. + Pentru a se conecta, contactul dvs. poate scana codul QR sau poate folosi linkul din aplicație. + Puteți seta numele conexiunii, pentru a vă aminti cu cine a fost partajat linkul. + Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice serviciu de mesagerie. + Codul pe care l-ați scanat nu este un cod QR SimpleX link. + Server de testare + Servere de testare + Folosește acreditări aleatorii + Actualizare disponibilă: %s + Folosește chatul + Hash-ul mesajului anterior este diferit. + Această acțiune nu poate fi anulată - mesajele trimise și primite anterior celei selectate vor fi șterse. Poate dura câteva minute. + Fraza de acces greșită! + Încercarea de modificare a parolei bazei de date nu a fost finalizată. + Ești invitat(ă) în grup. Alătură-te pentru a te conecta cu membrii grupului. + profil de grup actualizat + Condiții actualizate + Utilizați pentru mesaje + Mesajele vocale sunt interzise. + Protocoalele SimpleX analizate de Trail of Bits. + Acest link a fost folosit cu un alt dispozitiv mobil, vă rugăm să creați un link nou pe desktop. + Verificați conexiunile + Poți să încerci încă o dată. + Nu ești conectat la aceste servere. Rutarea privată este utilizată pentru a livra mesaje către ele. + Se oprește chatul + Total + ești observator + Videoclipul nu poate fi decodificat. Vă rugăm să încercați un alt videoclip sau să contactați dezvoltatorii. + Puteți copia și micșora dimensiunea mesajului pentru a-l trimite. + aștept răspunsul… + aștept confirmarea… + Actualizați aplicația automat + Folosește acreditări proxy diferite pentru fiecare profil. + Folosești conexiune directă la internet? + Poți trimite mesaje către %1$s din contactele arhivate. + Pentru a vă proteja adresa IP, rutarea privată utilizează serverele SMP pentru a livra mesaje. + Izolarea transportului + Folosește gazde .onion + Când este disponibil + Link canal SimpleX + Spam + Acest link necesită o versiune mai nouă a aplicației. Vă rugăm să actualizați aplicația sau să solicitați persoanei de contact să vă trimită un link compatibil. + Legătură de conexiune neacceptată + Utilizați linkuri scurte (BETA) + Utilizați portul web + %s.]]> + %s]]> + Arhivare raport + Chatul va fi șters pentru tine - această acțiune nu poate fi anulată! + Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> + Ghidul utilizatorului.]]> + V-ați alăturat acestui grup. Se conectează la membrul invitat al grupului. + Profilul tău va fi trimis persoanei de contact de la care ai primit acest link. + Te vei conecta cu toți membrii grupului. + Toate mesajele noi de la acești membri vor fi ascunse! + Profilul tău aleatoriu + (acest dispozitiv v%s)]]> + %s are o versiune neacceptată. Asigurați-vă că utilizați aceeași versiune pe ambele dispozitive]]> + %1$s.]]> + Conexiuni active + Discuții de afaceri + l-ai blocat pe %s + Bucăți șterse + Serverele tale ICE + ai eliminat %1$s + Preferințele dumneavoastră + Adăugați o listă + Totul + Puteți vedea în continuare conversația cu %1$s în lista de chaturi. + Condiții acceptate + %s.]]> + %1$d eroare(i) de fișier:\n%2$s + Conexiune blocată + Conexiunea este blocată de operatorul serverului:\n%1$s. + criptate end-to-end, cu securitate post-cuantică în mesajele directe.]]> + Bare de instrumente ale aplicației + Securitate mai bună ✅ + Toate chat-urile vor fi eliminate din lista %s, iar lista va fi ștearsă. + Conexiunea dvs. a fost mutată la %s, dar a apărut o eroare neașteptată la redirecționarea către profil. + Puteți migra baza de date exportată. + Serverele tale XFTP + Utilizați gazdele .onion la Nu dacă proxy-ul SOCKS nu le acceptă.]]> + Profilul tău de chat va fi trimis membrilor chatului + Vei primi în continuare apeluri și notificări de la profilurile dezactivate atunci când acestea sunt active. + Întreabă + %s.]]> + Schimbați ștergerea automată a mesajelor? + Adresă sau link unic? + Sesiune de aplicație + Contactele dvs. + Este posibil ca datele dumneavoastră de autentificare să fie trimise necriptate. + Adresa firmei + Adaugă membrii echipei tale la conversații. + Verifică mesajele la fiecare 10 minute + %s, acceptați condițiile de utilizare.]]> + Confirmați autentificarea dvs + Schimbați profilurile de chat + Contacte + Conexiunea necesită renegocierea criptării. + Adresa ta SimpleX + Creați un link unic + Profilul tău actual + Aplicația rulează întotdeauna în fundal + acceptat %1$s + %s.]]> + Condițiile vor fi acceptate pe: %s. + Servere media și de fișiere adăugate + Încețoșare + - conectare la serviciul de directoare (BETA)!\n- confirmări de livrare (până la 20 de membri).\n- mai rapid și mai stabil. + Arhivează contactele pentru a discuta mai târziu. + %1$s.]]> + Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. + Deschideți în aplicația mobilă, apoi atingeți Conectați-vă în aplicație.]]> + Adăugați un link scurt + Configurați operatorii serverului + Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. + Faceți clic pe butonul de informații de lângă câmpul de adresă pentru a permite utilizarea microfonului. + Colţ + Puteți salva arhiva exportată. + Vei înceta să mai primești mesaje din acest chat. Istoricul chatului va fi păstrat. + Vei înceta să mai primești mesaje de la acest grup. Istoricul chatului va fi păstrat. + Încercați să invitați persoana de contact cu care ați partajat un profil incognito în grupul în care utilizați profilul principal + Poți partaja un link sau un cod QR - oricine se va putea alătura grupului. Nu vei pierde membrii grupului dacă îl ștergi ulterior. + Blochezi membrii pentru tot? + Chat + %s.]]> + Condiții acceptate pe: %s. + Condițiile vor fi acceptate automat pentru operatorii activați pe: %s. + %s.]]> + %s.]]> + %s.]]> + Condiții de utilizare + Permite raportarea mesajelor către moderatori. + Conversează cu membrii + Discută cu administratorii + Accepta + Acceptă membru + Date mai bune pentru mesaje. + Performanță mai bună a grupurilor + Confidențialitate și securitate mai bune + %s este inactiv]]> + %s lipsește]]> + %1$s!]]> + Recunoscut + Erori de recunoaștere + Vi se va solicita să vă autentificați când porniți sau reluați aplicația după 30 de secunde în fundal. + conecta cu dezvoltatorii SimpleX Chat pentru a pune întrebări și pentru a primi actualizări.]]> + Adaugă prieteni + Adăugați membri în echipă + Chatul există deja! + Deschideți în aplicația mobilă.]]> + Continuă + %s a fost deconectat]]> + Despre operatori + %s este ocupat]]> + Nu puteți trimite mesaje! + ați ieșit + nu pot trimite mesaje + contact șters + contact dezactivat + contactul nu este gata + Profilul, contactele și mesajele livrate sunt stocate pe dispozitiv. + Serverele tale ICE + Adaugă în listă + Modificați lista + te-a acceptat + Puteți să vă împărtășiți adresa ca link sau cod QR - oricine se poate conecta la dvs. + Accepta + Confidențialitatea dumneavoastră + Acceptați condițiile + Măriți + %1$s.]]> + %1$s.]]> + Bucăți încărcate + Arhivați toate rapoartele? + Arhivați %d rapoarte? + Arhivare raport? + Dispozitive Xiaomi: vă rugăm să activați Pornirea automată în setările de sistem pentru ca notificările să funcționeze.]]> + Arhivă + Pictogramă de context + Puteți vizualiza rapoartele dvs. în Chat cu administratorii. + Schimbați ordinea + Vei fi conectat când dispozitivul contactului tău va fi online, te rugăm să aștepți sau să verifici mai târziu! + scana codul QR în apelul video sau contactul dvs. poate partaja un link de invitație.]]> + Profilul tău %1$s va fi distribuit. + Nu îți vei pierde contactele dacă ulterior îți ștergi adresa. + Securitatea conexiunii + Serverele tale SMP + Toate serverele + Profilul tău este stocat pe dispozitiv și este partajat doar cu contactele tale. Serverele SimpleX nu pot vedea profilul tău. + Baza ta de date actuală de chat va fi ȘTERSĂ și ÎNLOCUITĂ cu cea importată.\nAceastă acțiune nu poate fi anulată - profilul, contactele, mesajele și fișierele tale vor fi pierdute iremediabil. + Discută cu administratorii + Poți partaja această adresă cu contactele tale pentru a le permite să se conecteze cu %s. + Discută cu emmbrul + Cererea de conectare va fi trimisă acestui membru al grupului. + Servere de mesaje adăugate + %s cu motivul: %s]]> + %s a fost deconectat]]> + (nou)]]> + Serverul dumneavoastră + Trebuie să îi permiți contactului tău să te sune pentru a-l putea suna. + Adresa serverului dvs. + Serverele dumneavoastră + Vei fi conectat la grup atunci când dispozitivul gazdei grupului va fi online, te rugăm să aștepți sau să verifici mai târziu! + Tu decizi cine se poate conecta. + Vei fi conectat când cererea ta de conectare va fi acceptată, te rugăm să aștepți sau să verifici mai târziu! + Acceptați ca observator + O experiență mai bună pentru utilizator + 1 chat cu un membru + Toate rapoartele vor fi arhivate pentru dvs. + repozitoriul nostru GitHub.]]> + Permiteți în următoarea fereastră de dialog să primească notificări instantaneu.]]> + Setări adresă + Arhivare rapoarte + Acceptă ca membru + totul + Întreprinderi + Prin utilizarea SimpleX Chat, sunteți de acord să:\n- trimiteți doar conținut legal în grupuri publice.\n- respectați ceilalți utilizatori – fără spam. + cu un singur contact - partajați personal sau prin orice mesagerie.]]> + Nu trebuie să utilizați aceeași bază de date pe două dispozitive. + arătați codul QR în apelul video sau distribuiți linkul.]]> + Utilizare de pe desktop în aplicația mobilă și scanați codul QR.]]> + SimpleX rulează în fundal în loc să utilizeze notificări push.]]> + Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> + Bucăți descărcate + Chatul va fi șters pentru toți membrii - această acțiune nu poate fi anulată! + Conexiunea nu este gata. + Conținutul încalcă condițiile de utilizare + Folosești un profil incognito pentru acest grup - pentru a preveni partajarea profilului principal, invitarea contactelor nu este permisă. + Ai trimis o invitație la grup + Ați respins invitația de grup + Scanare cod QR.]]> + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index 97742f82a8..189bd7e07e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -1019,7 +1019,7 @@ Сообщение будет удалено для всех членов группы. Сообщение будет помечено как удаленное для всех членов группы. Пожалуйста, свяжитесь с админом группы. - Вы не можете отправлять сообщения! + Вы \"читатель\" только чтение сообщений читатель Роль при вступлении @@ -1132,7 +1132,7 @@ Ошибка расшифровки Блокировка SimpleX не включена! Ошибка хэш сообщения - Хэш предыдущего сообщения отличается\" + Хэш предыдущего сообщения отличается. Подтвердить код Неправильный код Заблокировать через @@ -2467,4 +2467,59 @@ Частные разговоры, группы и Ваши контакты недоступны для операторов серверов. Настроить операторов серверов Политика конфиденциальности и условия использования. + все + Принять + Участник хочет присоединиться к группе. Принять? + группа удалена + удален из группы + %d чата(ов) + контакт не готов + контакт удален + не синхронизирован + запрос на вступление отклонён + Новый участник хочет присоединиться к группе. + Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. + ожидает одобрения + Отклонить + Отклонить участника? + Ошибка при удалении чата с членом группы + Полная ссылка + Ошибка вступления члена группы + Ссылка не поддерживается + Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку. + %d сообщений + Вы можете найти Ваши жалобы в Чате с админами. + Чат с админами + Чат с членом группы + выключено + Одобрять членов + Чаты с членами группы + Приём членов в группу + Одобрять членов для вступления в группу. + Нет чатов с членами группы + Принять как читателя + Принять в группу + Принять члена + одобрен админами + Жалоба отправлена модераторам + Вы вышли + нельзя отправлять + %d чатов с членами группы + контакт выключен + член имеет старую версию + Вы не можете отправлять сообщения! + Короткая ссылка + Сохранить настройки вступления? + Вы приняли этого члена + рассмотрение + Установить вступление в группу + Удалить чат с членом группы? + Удалить разговор + принят %1$s + Чат с админами + Вы приняты + 1 чат с членом группы + SimpleX ссылка канала + Короткие ссылки (БЕТА) + добавить короткую ссылку diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 257daec596..5920c311ac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -14,7 +14,7 @@ Görüntülü ara Gizlilik ve güvenlik Gizli - Konuşmalar + Sohbetler Bağlantı ile bağlan Sohbet profillerin Konuşma tercihleri @@ -288,7 +288,7 @@ Üyeyi çıkar Kaldır ARAMALAR - KONUŞMALAR + SOHBETLER SEN SOHBET VERİTABANI Kaldır @@ -301,7 +301,7 @@ sen: %1$s kaldırıldı ÜYE - Grup üyeleri kendiliğinden yok olan mesajlar gönderebilir. + Üyeler kendiliğinden yok olan mesajlar gönderebilir. Kendiliğinden yok olan mesaj gönderimini engelle. Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver. Kişilerinin sesli mesaj göndermesine izin ver. @@ -647,11 +647,11 @@ Grup adını gir: Grup tam adı: Dosya ve medya - Grup üyeleri doğrudan mesaj gönderebilir. + Üyeler doğrudan mesaj gönderebilir. Üyeler, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) - Grup üyeleri sesli mesaj gönderebilirler. + Üyeler sesli mesaj gönderebilirler. Dosya ve medya yasaklanmıştır. - Grup üyeleri dosya ve medya paylaşabilir. + Üyeler dosya ve medya paylaşabilir. Grup bağlantıları Konuşmada devre dışı bırakıldığında bile Çabuk ve göndericinin çevrim içi olmasını beklemeden! @@ -1732,7 +1732,7 @@ SimpleX bağlantılarına izin verilmiyor Dosyalar ve medyaya izin verilmiyor Sesli mesajlara izin verilmiyor - Grup üyeleri SimpleX bağlantıları gönderebilir. + Üyeler SimpleX bağlantıları gönderebilir. iletildi Ağ bağlantısı SimpleX bağlantıları göndermesine izin ver. @@ -2223,4 +2223,87 @@ Onar Bağlantı onarılsın mı? Sunucuyu güncellerken hata oluştu + Mesaj sunucuları yok. + Sunucu yapılandırmasında hatalar. + Yeni sunucu + yöneticiler + Bahsedildiğinizde haberdar olun. + Uygunsuz içerik + Uygunsuz profil + Liste adı... + Gizliliğe nasıl yardım ediyor + Liste adı ve emoji tüm listeler için farklı olmalıdır. + Herkesi sustur + Ağ merkezsizleştirme + Daha iyi meta veri gizliliği için Ağ ve sunucular ayarlarında Flux\'u etkinleştirin. + Mesaj silme özelliğini devre dışı bırak + Bu sohbetteki mesajlar asla silinmeyecektir. + Üyeyi kabul ederken hata oluştu + Liste + %d üyelerle sohbetler + 1 üye ile sohbet + %d sohbet(ler) + Tam bağlantı + Ağ operatörleri + Yeni üye gruba katılmak istiyor. + Yöneticilerle sohbetler + Üye ile sohbet + Gizli yönlendirme için + Üye kabulü + Kabul Et + Yöneticilerle sohbetler + Üyeyi kabul et + Mesaj yok + Gruplar + Sohbete davet et + Sohbetten çık + Üye sohbetten çıkarılacaktır - bu geri alınamaz! + Ağ operatörü + Mevcut koşullar metni yüklenemedi, koşulları bu bağlantı üzerinden inceleyebilirsiniz: + %s listesinde sohbet yok. + Hayır + %1$s kabul edildi + seni kabul etti + Koşullar şu tarihte kabul edilecektir: %s. + hepsi + Üyelerle sohbetler + Arkaplan servisi yok + Kabul Et + SimpleX Chat\'i kullanarak şunları kabul etmiş olursunuz:\n- genel gruplarda sadece yasal içerik göndermeyi.\n- diğer kullanıcılara saygı göstermeyi - spam yapmamayı. + Sunucu operatörlerini yapılandırma + Sohbetten çıkılsın mı? + yönetici + Bütün sunucular + Şunlar için sohbet profilini sil + Önemli mesajları kaçırmayın. + Bu üyelerden gelen mesajlar gösterilecektir! + Kabul edilen koşullar: %s. + %s , kullanım koşullarını kabul edin.]]> + Üyeler mesajları moderatörlere bildirebilir. + Üye ile birlikte sohbet silinsin mi? + Üyeli sohbetler yok + Üye olarak kabul et + Üye ve sohbet silinirken hata oluştu + Sohbetler yok + Sohbetler bulunamadı + %d mesajlar + Üye raporları + grup silindi + üyenin eski uygulama sürümü var + Şifreleme yeniden anlaşması devam ediyor. + Bağlantı şifreleme yeniden anlaşması gerektirir. + Geliştirilmiş sohbet navigasyonu + Üyelerden bahsedin 👋 + Gözlemci olarak kabul et + Üyeler sohbetten çıkarılacaktır - bu geri alınamaz! + kişi devre dışı bırakıldı + Sohbeti sil + Yöneticilerin gruplarını yönetmelerine yardım edin. + kişi hazır değil + kişi silindi + Üyeler gruptan çıkarılacaktır - bu geri alınamaz! + Üye gruba katılacak, üye kabul edilsin mi? + Özel mesaj yönlendirmesi için sunucu yok. + mesajlar gönderilemiyor + Medya ve dosya sunucuları yok. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 549cb01b63..011eec76fc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -329,7 +329,7 @@ Торкніться, щоб розпочати новий чат Чат із розробниками У вас немає чатів - Ви не можете відправляти повідомлення! + ви спостерігач Будь ласка, зв\'яжіться з адміністратором групи. Файл буде отримано, коли ваш контакт буде в мережі, будь ласка, зачекайте або перевірте пізніше! Відео відправлене @@ -459,7 +459,7 @@ Створити без зашифрування e2e контакт має зашифрування e2e - Хеш попереднього повідомлення інший.\" + Хеш попереднього повідомлення інший. Підтвердити пароль Новий пароль Перезапустити @@ -2388,4 +2388,53 @@ Ні Типові сервери Використовуйте TCP порт 443 лише для попередньо налаштованих серверів. + контакт не готовий + Помилка при прийомі учасника + Чат з учасником + Прийом учасника + усі + Чати з учасниками + Чат з адміністраторами + контакт вимкнено + Чат з адміністраторами + Прийняти + 1 чат з учасником + %d чат(ів) + %d чатів з учасниками + %d повідомлень + не можна надсилати + контакт видалено + групу видалено + прийнято %1$s + прийняв(ла) вас + Прийняти як учасника + Прийняти як спостерігача + Прийняти учасника + Видалити чат + Видалити чат з учасником? + Помилка видалення чату з учасником + Встановити прийом учасників + Зберегти налаштування прийому? + Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи. + ви прийняли цього учасника + Схвалювати учасників + вимкнено + Схвалювати учасників для вступу до групи. + Додати коротке посилання + не синхронізовано + схвалено адміністраторами + очікує на схвалення + перегляд + учасник використовує застарілу версію + видалено з групи + Повідомлення надіслано модераторам + запит на приєднання відхилено + Ви не можете надсилати повідомлення! + Ви можете переглянути свої звіти у чаті з адміністраторами. + Новий учасник хоче приєднатися до групи. + Немає чатів з учасниками + Учасник приєднається до групи, прийняти учасника? + Відхилити + Відхилити учасника? + ви вийшли 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 28a5f6f50d..f71a61f895 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 @@ -929,7 +929,7 @@ 观察员 你是观察者 更新群链接错误 - 你无法发送消息! + 你是观察员 初始角色 请联系群管理员。 系统 @@ -2375,4 +2375,53 @@ 关闭 预设服务器 仅预设服务器使用 TCP 协议 443 端口。 + 接受成员出错 + 举报已发送至 moderators + %d 个聊天 + 和成员的 %d 个聊天 + %d 条消息 + 接受了 %1$s + 接受了你 + 你接受了该成员 + 新成员要加入本群。 + 审核 + 待审核 + 全部 + 成员准入 + 关闭 + 删除 + 接受 + 和成员聊天 + 和管理员聊天 + 没有和成员的聊天 + 接受为成员 + 接受成员 + 成员将加入本群,接受成员吗? + 由管理员审核 + 设置成员入群准许 + 和成员聊天 + 和管理员聊天 + 准许入群前审核成员(knocking)。 + 请等待群的 moderator 审核你加入该群的请求。 + 审核成员 + 保存入群设置? + 你可以在和管理员和聊天中查看你的举报。 + 接受为观察员 + 和一名成员的一个聊天 + 无法发送消息 + 你离开了 + 删除和成员的聊天出错 + 你无法发送消息! + 禁用了联系人 + 群被删除了 + 从群被删除了 + 加入请求被拒绝 + 删除聊天 + 删除和成员的聊天吗? + 未同步 + 成员有旧版本 + 删除了联系人 + 联系人未就绪 + 拒绝成员? + 添加短链接 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 8ae414de00..0922ed5d55 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -5,7 +5,7 @@ 關於 SimpleX 接受 接受 - 1天 + 1 天 1 個月 接受 關於 SimpleX Chat @@ -23,7 +23,7 @@ 全名: 使用更多電量!程式始終在背景中運行 – 通知會立即顯示。]]> 對電量友善。程式每10分鐘檢查一次訊息。你可能會錯過電話或警急訊息。]]> - 回應通話請求 + 回應通話 清除 允許向群組內的成員傳送私訊。 %d 秒 @@ -31,32 +31,32 @@ 自動接受聯絡人請求 允許傳送自動銷毀的訊息。 - 無法初始化數據庫 - 一直開啟 + 無法初始化資料庫 + 總是開啟 關閉 SimpleX 鎖定 啟用 SimpleX 鎖定 複製 回覆 分享 - 附件 + 附加 和開發人員對話 - 你的對話 + 聊天 分享檔案… - 分享訊息 + 分享訊息… 取消圖片預覽 允許使用語音訊息? 取消 取消實況訊息 - 選擇檔案 + 檔案 相機 - 從圖片庫選擇圖片 - 接受匿名聊天模式 + 從圖片庫 + 接受匿名 所有訊息記錄會刪除 - 這不能還原!這些訊息只會在你裝置中刪除。 清除 - 要清除對話記錄? + 清除聊天? 取消連結預覽 清除 - 清除對話記錄 + 清除聊天 分享一次性連結 傳送問題和想法給開發者 如何使用 @@ -81,30 +81,30 @@ 通話已經結束了! 關閉語音 開啟語音 - 通話已經取消了 + 通話已結束 通話中 開啟喇叭 通話 - 樣式 + 外觀 語音通話(沒有端對端加密) 語音通話 - 語音 & 視訊通話 + 語音和視訊通話 視訊通話 粗體 自動接收圖片 備份應用程式資料 應用程式圖示 - 已匯入對話數據庫 + 已匯入對話資料庫 Android 金鑰庫是用於安全地儲存密碼 - 確保通知推送服務的運作。 請注意:如果你忘記了密碼你將不能再次復原或修改密碼。]]> 當你重新啟動應用程式或修改密碼後, Android 金鑰庫將用來安全地儲存密碼 - 將允許接收訊息通知。 - 聊天室已停止運作 + 聊天已停止 只有這個群組的負責人才能修改群組內的設定。 - 修改 - 新增新的個人檔案 + 變更 + 新增個人檔案 所有對話和對話記錄會刪除 - 這不能還原! - 匿名聊天模式 - 經常 + 匿名 + 總是 開啟 你的設定 允許你的聯絡人傳送語音訊息。 @@ -115,13 +115,13 @@ 管理員可以建立加入群組的連結。 使用二維碼掃描以新增伺服器。 聊天室運行中 - 對話數據庫 - 聊天室已停止運作 + 聊天資料庫 + 聊天已停止 停止 - 已刪除數據庫的對話內容 - 修改數據庫密碼? + 已刪除資料庫的對話內容 + 修改資料庫密碼? 確定要退出群組? - 退出 + 離開 無法邀請聯絡人! 已連接 退出 @@ -133,11 +133,11 @@ 群組全名: 對話設定 關閉 - + 自動銷毀訊息 - 群組內的成員可以私訊群組內的成員。 + 成員可以傳送私訊。 %d 分鐘 - %d 月 + %d 個月 通話結束 %1$s 取消檔案預覽 無法接收檔案 @@ -146,8 +146,8 @@ 每個聊天室的設定。]]> 返回 省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。 - 匿名聊天模式 - 預設值 (%s) + 匿名模式 + 預設(%s) 群組設定 聯絡人設定 分享媒體… @@ -175,9 +175,9 @@ 你的設定 新增到另一個裝置上 檢查輸入的伺服器地址,然後再試一次。 - 終端機對話 + 聊天終端機 於 Github 給個星星 - 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護您的隱私。 + 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護你的隱私。 這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。 只有你的聯絡人允許的情況下,才允許自動銷毀訊息。 允許你的聯絡人傳送自動銷毀的訊息。 @@ -187,8 +187,8 @@ 允許傳送語音訊息。 多久後刪除 群組內所有成員會保持連接。 - 自訂顏色 - 已移除 + 輔色 + 已被審查 SimpleX 群組連結 私人檔案名稱 加入成員(s)時出錯 @@ -205,9 +205,9 @@ 未知的訊息格式 無效的訊息格式 - 實況 - 無效對話 - 無效數據 + 直播 + 無效的聊天 + 無效的數據 連接 %1$d 已建立連接 已邀請連接 @@ -222,7 +222,7 @@ 傳送訊息時出錯 聯絡人已存在 你已經連接到 %1$s。 - 使用個人檔案(預設值)或使用連接(測試版)。 + 按聊天個人檔案(預設)或按連接(測試版)。 減少電量使用 更多改進即將推出! 意大利語言界面 @@ -233,7 +233,7 @@ 透過一次性連結連接? 加入群組? 你的個人檔案將傳送給你接收此連結的聯絡人。 - 你將加入此連結內的群組並且連接到此群組成為群組內的成員。 + 你將連接至此群組內的所有成員。 連接 錯誤 連接中 @@ -241,12 +241,12 @@ 嘗試連接至用於接收此聯絡人訊息的伺服器 (錯誤:%1$s)。 正在嘗試連接到用於接收此聯絡人訊息伺服器。 已刪除 - 已標記為刪除 - 在瀏覽器中開啟連結可能會有私隱疑慮和不確定性。不受 SimpleX 信任的連結會顯示紅色。 + 已標記為已刪除 + 在瀏覽器中開啟連結可能會有隱私疑慮和不確定性。不受 SimpleX 信任的連結會顯示紅色。 儲存 SMP 伺服器時出錯 請確保 SMP 伺服器連結是正確的格式,每行也分隔且不重複。 更新網路配置時出錯 - 聯絡人載入失敗 + 聊天載入失敗 多個聯絡人載入失敗 請更新應用程式或聯絡開發人員。 連接超時 @@ -275,20 +275,20 @@ 即時通知 即時通知! 已禁用即時通知功能! - 數據庫目前沒有正常運作。點擊查看更多 + 資料庫目前沒有正常運作。點擊查看更多 SimpleX Chat 通話來電 通知服務 顯示預覽 通知預覽 如果你解鎖程式三十秒後在後台再次啟動或返回應用程式,你會需要進行多一次認證。 - 已解鎖 + 解鎖 使用你的憑據登入 使用終端機開啟對話 傳送訊息時出錯 大概是你的聯絡人已經刪除了和你的對話並且已經沒有和你有連接。 只為我刪除 - 為所有人刪除 - 已修改 + 為所有人 + 已編輯 已傳送 未經授權傳送 傳送失敗 @@ -320,8 +320,8 @@ 安全佇列 刪除佇列 斷開連接 - 禁用電量優化 為了 SimpleX 在下一個對話中。否則,將禁用通知功能。]]> - 在接收通知之前,請你輸入數據庫的密碼 + 允許它以接收實時通知。]]> + 在接收通知之前,請你輸入資料庫的密碼 應用程式會定期取得新的訊息 — 它每天會消耗百分之幾的電量。應用程式將不使用推送通知 — 你裝置中的數據不會傳送至伺服器。 SimpleX Chat 服務 正在接收訊息… @@ -332,7 +332,7 @@ 顯示聯絡人名稱和訊息內容 隱藏聯絡人名稱和訊息內容 隱藏聯絡人: - 有新的訊息 + 新訊息 已連接 SimpleX 鎖定 為了保護你的個人訊息,開啟 SimpleX 鎖定。 @@ -340,7 +340,7 @@ 開啟 已開啟SimpleX 鎖定 你的裝置沒有啟用螢幕鎖定。你可以通過設定內啟用螢幕鎖定,當你啟用後就可以使用 SimpleX 鎖定。 - 修改 + 編輯 刪除 展露 刪除訊息? @@ -355,11 +355,11 @@ 下載檔案需要傳送者上線的時候才能下載檔案,請等待對方上線! 語音訊息 傳送實況的訊息 - 實況訊息! + 直播訊息! 傳送實況訊息 - 這會即時顯示你在輸入中的文字 (掃描或使用剪貼薄貼上) 權限被拒絕! - SimpleX 有一個後台通知服務 – 它每天使用百分之幾的電量。]]> + SimpleX 在背景運作而不是使用推送通知。]]> 定期通知 每十分鐘會檢查一次訊息,最快可設為每分鐘檢查一次 訊息文字 @@ -386,10 +386,10 @@ 驗證安全碼 傳送訊息 刪除個人檔案時出錯 - 停止對話 + 停止聊天 圖片不能解碼,嘗試其他圖片或聯絡開發人員。 檔案 - 大型檔案 + 大型檔案! 等待檔案中 確定 重設 @@ -397,7 +397,7 @@ 沒有詳細資料 一次性邀請連結 已複製至你的剪貼薄 - 透過連結 / 二維碼去連接 + 透過連結/QR 圖碼連接 掃描二維碼 (僅由群組成員儲存) 想和你對話! @@ -411,17 +411,17 @@ 使用 .onion 主機 Onion 主機不會啟用。 連接 - 刪除聯絡地址 + 刪除地址 顏色 如何使用你的伺服器 使用連結連接 掃描二維碼。]]> 設定 這個二維碼不是一個連結! - 當可行的時候 + 當可用時 核心版本:v%s simplexmq: v%s (%2s) - 建立聯絡地址 + 建立地址 編輯圖片 斜體 已拒絕通話 @@ -446,17 +446,17 @@ 標記為未讀 你的聯絡人需要上線才能連接成功。 \n你可以取消此連接和刪除此聯絡人(或者你可以稍後使用新的連結再試一次)。 - 二維碼 + QR 圖碼 個人檔案頭像 預覧連結圖片 SimpleX 地址 幫助 SimpleX 團隊 電郵 - 查看更多 + 更多 顯示二維碼 - 無效的二維碼 - 無效連結! + 無效的 QR 圖碼 + 無效的連結! 這個連結不是一個有效的連接連結! 已傳送連接請求 當群組的建立人上線,你便會成功連接至群組,請耐心等待! @@ -476,10 +476,10 @@ 安全碼 Markdown 幫助 在訊息中使用 Markdown 語法 - 人手輸入伺服器地址 + 手動輸入伺服器 使用伺服器 用於新的連接 - 無效的伺服器地址 + 無效的伺服器地址! 此伺服器用於你目前的個人檔案 使用 SimpleX Chat 伺服器? 你的 SMP 伺服器 @@ -491,7 +491,7 @@ 需要 Onion 主機會在可用時啟用。 - 刪除聯絡地址? + 刪除地址? 你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。 儲存並通知你的聯絡人 儲存並通知你的多個聯絡人 @@ -501,12 +501,12 @@ 如何使用 Markdown 語法 你可以使用 Markdown 語法以更清楚標明訊息: 刪除線 - 你錯過了電話 + 已錯過通話 接收到回應… 連接中… - 通話完結 + 已結束 連接 - 網路 & 伺服器 + 網路和伺服器 進階設定 使用 SOCKS 代理伺服器 儲存並通知群組內的聯絡人 @@ -533,7 +533,7 @@ %s 並未驗證 你的 SimpleX 聯絡地址 你的個人檔案 - 數據庫密碼及匯出 + 資料庫密碼和匯出 傳送電郵 SimpleX 鎖定 SMP 伺服器 @@ -545,30 +545,29 @@ 有一些伺服器測試失敗: 掃描伺服器的二維碼 你的伺服器 - 連接時需要使用 Onion 主機。 -\n請注意:如果沒有 .onion 地址,您將無法連接到伺服器。 - 對話檔案 + 連接時需要使用 Onion 主機。 \n請注意:如果沒有 .onion 地址,你將無法連接到伺服器。 + 聊天個人檔案 透過群組連結 透過群組連結使用匿名聊天模式 一個使用了匿名聊天模式的人透過連結加入了群組 透過使用一次性連結匿名聊天模式連接 即時 定期的 - 關閉 - 你錯過了通話 + 停用 + 已錯過通話 開啟 開啟 SimpleX Chat 以接受通話 已拒絕通話 顯示 連接通話中 - 檔案及媒體檔案 + 檔案和媒體 重新啟動應用程式以建立新的對話檔案。 刪除所有檔案及媒體檔案? 刪除所有你的個人對話資料的檔案 %d 檔案(s) 的總共大小為 %s 訊息 永不 - 沒有接收到或傳送到的檔案 + 沒有已接收或已傳送的檔案 目前的密碼… 加密 修改設定時出錯 @@ -584,11 +583,11 @@ 已修改你的身份為 %s 連接中(已接受) 退出 - 負責人 + 擁有者 已移除 完成 連接中 - 創建人 + 建立者 新的成員身份 %1$s 位成員 修改群組內的設定 @@ -599,9 +598,9 @@ 成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。 成員的身份會修改為 "%s"。該成員將接收到新的邀請。 網路狀態 - 重置為預設值 + 重設為預設值 當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。 - 黑暗 + 深色 為所有人刪除 語音訊息 已提供 %s: %2s @@ -613,7 +612,7 @@ 訊息草稿 保留最後一則帶附件的訊息草稿。 傳輸隔離 - 匯入對話數據庫? + 匯入對話資料庫? 請放置你的密碼於安全的地方,如果你遺失了密碼將不可能再次存取它。 金鑰庫錯誤 沒有聯絡人可以選擇 @@ -621,15 +620,15 @@ 刪除連結 完全去中心化 - 只有成員能看到。 禁止傳送自動銷毀的訊息。 - %d 個小時 + %d 小時 更新內容 帶有可選擇的歡迎訊息。 - 刪除數據庫時出錯 + 刪除資料庫時出錯 匯入 - %s 秒(s) - 加密數據庫時出錯 + %s 秒 + 加密資料庫時出錯 在金鑰庫儲存密碼 - 還原數據庫的備份 + 還原資料庫的備份 群組為不活躍狀態 邀請連結過時! 刪除群組? @@ -641,25 +640,25 @@ 語音訊息於這個聊天室是禁用的。 允許你的聯絡人可以完全刪除訊息。 沒有用戶識別符。 - 新一代的私密訊息平台 + 未來的訊息平台 去中心化的 - 人們只能在你分享了連結後,才能和你連接。 - 重新定義私隱 + 你決定誰可以連接。 + 重新定義隱私 建立你的個人檔案 這是如何運作 - 你可以之後透過設定修改。 + 它如何影響電池 私下連接 任何人都可以託管伺服器。 - 無視 + 忽略 語音通話來電 貼上你收到的連結 端對端加密 沒有端對端加密 關閉喇叭 訊息和檔案 - 私隱 & 安全性 + 隱私和安全性 主題 - 法語界面 + 法語介面 端對端加密的語音通話 拒絕 錯誤的訊息 ID @@ -670,32 +669,32 @@ 幫助 設定 幫助 SIMPLEX CHAT - 對話 + 聊天 開發者工具 SOCKS 代理伺服器 - 重新啟動應用程式以匯入對話數據庫。 + 重新啟動應用程式以匯入對話資料庫。 刪除所有檔案 啟用自動銷毀訊息? 刪除訊息 在多久後刪除訊息 請輸入正確的目前密碼。 - 已受加密的數據庫密碼是使用隨機性的文字,你可以修改它。 - 數據庫將加密。 - 數據庫將加密並且密碼會儲存於金鑰庫。 - 數據庫錯誤 - 不能讀取金鑰庫以儲存數據庫密碼 + 已受加密的資料庫密碼是使用隨機性的文字,你可以修改它。 + 資料庫將加密。 + 資料庫將加密並且密碼會儲存於金鑰庫。 + 資料庫錯誤 + 不能讀取金鑰庫以儲存資料庫密碼 錯誤:%s 檔案:%s - 需要數據庫的密碼以開啟對話。 + 需要資料庫的密碼以開啟對話。 輸入密碼… 輸入正確的密碼。 開啟對話 儲存密碼和開啟對話 - 你未完成修改數據庫密碼的程序。 - 密碼不存在於金鑰庫,請手動輸入它,有這種情況你可能是使用了備份用的工具。如果不是請聯絡開發人員。 + 你未完成修改資料庫密碼的程序。 + Keystore 中找不到密碼,請手動輸入它。如果你使用了備份工具還原應用程式的資料,則可能會發生這種情況。如果不是這種情況,請聯絡開發人員。 還原 - 還原數據庫的備份? - 還原數據庫時出錯 + 還原資料庫的備份? + 還原資料庫時出錯 加入 確定要加入群組? 加入匿名聊天模式 @@ -713,7 +712,7 @@ 刪除群組邀請連結時出錯 為終端機 本機名稱 - 數據庫 ID + 資料庫 ID 身份 傳送私人訊息 成員將被移除於此群組 - 這不能還原! @@ -734,17 +733,17 @@ 更新網路設定? 更新 更新設定會將客戶端重新連接到所有的伺服器。 - 刪除對話資料? - 刪除對話資料給 + 刪除聊天個人檔案? + 刪除聊天個人檔案給 檔案和伺服器連接 只有本機檔案 你的隨機個人檔案 系統 - 明亮 + 淺色 重設顏色 關閉 已接收,已禁用 - 設定為1天 + 設定為 1 天 聯絡人可以標記訊息為已刪除;你仍可以看到那些訊息。 禁止傳送語音訊息。 只有你的聯絡人可以傳送自動銷毀的訊息。 @@ -752,26 +751,26 @@ 不可逆地刪除訊息於這個聊天室內是禁用的。 只有你可以傳送語音訊息。 私訊群組內的成員於這個群組內是禁用的。 - 群組內的成員可以不可逆地刪除訊息。(24小時) + 成員可以不可逆地刪除訊息。(24 小時) 語音訊息 改善伺服器配置 當你切換至最近應用程式版面時,無法預覽程式畫面。 運行對話 - 數據庫密碼 - 匯出數據庫 - 匯入數據庫 - 新的數據庫存檔 - 舊的數據庫存檔 - 刪除數據庫 + 資料庫密碼 + 匯出資料庫 + 匯入資料庫 + 新的資料庫封存 + 舊的資料庫封存 + 刪除資料庫 開啟對話時出錯 停止對話? 設定密碼以匯出 停止對話時出錯 - 已受加密的數據庫是使用一個隨機性的文字。請在修改前將它匯出。 - 匯出數據庫時出錯 - 匯入數據庫時出錯 - 受加密的數據庫密碼會再次更新。 - 加密數據庫? + 已受加密的資料庫是使用一個隨機性的文字。請在修改前將它匯出。 + 匯出資料庫時出錯 + 匯入資料庫時出錯 + 受加密的資料庫密碼會再次更新。 + 加密資料庫? 邀請至群組 %1$s 邀請成員 群組找不到! @@ -779,18 +778,18 @@ 已刪除群組 群組已經刪除 已邀請 - 已透過連結邀請了你進群組 + 已透過你的群組連結邀請 聯絡人允許 %ds 私人通知 GitHub內查看更多。]]> 視訊通話來電 - 掛斷電話來電 + 掛斷 點對點 對話已經過端對端加密 對話沒有經過端對端加密 - 數據庫已加密! - 已加密數據庫 + 資料庫已加密! + 已加密資料庫 群組資料已經更新 成員 你:%1$s @@ -808,8 +807,8 @@ 禁止傳送自動銷毀的訊息。 禁止不可逆的訊息刪除。 禁止傳送語音訊息。 - 群組內的成員可以傳送自動銷毀的訊息。 - 自動銷毀訊息於這個群組內是禁用的。 + 成員可以傳送自動銷毀的訊息。 + 自動銷毀訊息已停用。 已提供 %s 儲存群組檔案時出錯 主題 @@ -823,9 +822,9 @@ 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。(24小時) 只有你的聯絡人可以傳送語音訊息。 禁止私訊群組內的成員。 - 不可逆地刪除訊息於這個群組內是禁用的。 - 群組內的成員可以傳送語音訊息。 - 語音訊息於這個群組內是禁用的。 + 不可逆地刪除訊息已停用。 + 成員可以傳送語音訊息。 + 語音訊息已停用。 %d 個月 %dm %dmth @@ -847,36 +846,32 @@ 驗證你與聯絡人的安全碼。 感謝用戶 - 使用 Weblate 的翻譯貢獻! 正在修改聯絡地址為 %s … - 受加密的數據庫密碼會再次更新和儲存於金鑰庫。 + 受加密的資料庫密碼會再次更新和儲存於金鑰庫。 SimpleX 是怎樣運作 - 當發生: -\n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。 -\n2. 訊息解密失敗,因為你或你的聯絡人用了舊的數據庫備份 -\n3. 連接被破壞。 - 兩層的端對端加密。]]> + 當發生: \n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。 \n2. 訊息解密失敗,因為你或你的聯絡人用了舊的資料庫備份 \n3. 連接被破壞。 + 只有客戶端裝置儲存個人檔案、聯絡人、群組,和訊息。 請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。 - 停止聊天室以匯出對話,匯入或刪除對話數據庫。當聊天室停止後你將不能接收或傳送訊息。 + 停止聊天室以匯出對話,匯入或刪除對話資料庫。當聊天室停止後你將不能接收或傳送訊息。 你正在使用匿名聊天模式進入此群組 - 為了避免分享你的真實個人檔案,邀請聯絡人是不允許的。 你傳送了一個群組連結 你移除了 %1$s - 你退出了群組 + 你已退出 你修改了聯絡地址為 %s 連接中(邀請介紹階段) - 你目前的對話數據庫會刪除並且以你匯入的對話數據庫頂替上。 -\n這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 + 你目前的對話資料庫會刪除並且以你匯入的對話資料庫頂替上。 \n這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 透過聯絡人的邀請連結連接 透過一次性連結連接 傳輸隔離 更新傳輸隔離模式? - 為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 IDs 資料, SimpleX 平台有自家佇列的標識符,這對於你的每個聯絡人也是獨一無二的。 + 為了保護你的隱私,SimpleX 對你的每個聯絡人使用不同的 ID。 當應用程式是運行中 透過設定啟用於上鎖畫面顯示來電通知。 這操作不能還原 - 你目前的個人檔案,聯絡人,訊息和檔案將不可逆地遺失。 - 你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。 + 你必須在裝置上使用最新版本的對話資料庫,否則你可能會停止接收某些聯絡人的訊息。 這操作不能還原 - 所有已經接收和傳送的檔案和媒體檔案將刪除。低解析度圖片將保留。 這設置適用於你目前的個人檔案 這操作無法撤銷 - 早於所選擇的時間發送和接收的訊息將被刪除。這可能需要幾分鐘的時間。 - 更新數據庫密碼 + 更新資料庫密碼 當每次啟動應用程式後你會需要輸入密碼 - 這不是儲存於你的個人裝置上。 你被邀請加入至群組 你將停止接收來自此群組的訊息。群組內的記錄會保留。 @@ -894,7 +889,7 @@ 開啟視訊 翻轉相機 待確認通話 - 你的私隱 + 你的隱私 你的通話 經由分程傳遞連接 在上鎖畫面顯示來電通知: @@ -903,25 +898,25 @@ 你錯過了多個訊息 實驗性功能 - 你的對話數據庫並未加密 - 設置密碼保護它。 - 數據庫密碼與儲存在金鑰庫中的密碼不同。 + 你的對話資料庫並未加密 - 設置密碼保護它。 + 資料庫密碼與儲存在金鑰庫中的密碼不同。 未知的錯誤 - 還原數據庫備份後請輸入舊密碼。這個操作是不能撤銷的! - 你可以透過 應用程式的設定或透過數據庫 去重新啟動應用程式來開啟對話。 + 還原資料庫備份後請輸入舊密碼。這個操作是不能撤銷的! + 你可以透過 應用程式的設定或透過資料庫 去重新啟動應用程式來開啟對話。 你已經被邀请加入至群組。加入後可與群組內的成員對話。 你已加入至群組 已確認聯絡人 - 你的對話數據庫 - 刪除對話資料? - 錯誤的數據庫密碼 - 未知的數據庫錯誤:%s + 你的對話資料庫 + 刪除聊天個人檔案? + 錯誤的資料庫密碼 + 未知的資料庫錯誤:%s 密碼錯誤! 你已經加入至群組。正在連接至群組內的成員。 這群組已經不存在。 更新群組檔案 你修改了 %s 的身份為 %s 連接中(介紹階段) - 使用對話 + 使用聊天 透過轉送 關閉視訊 你修改了自己的身份為 %s @@ -929,11 +924,11 @@ 由 %s 移除 將為所有成員刪除該訊息。 刪除成員訊息? - 主持 + 審查 該訊息將對所有成員標記為已移除。 - 你不能傳送訊息! - 你是觀察者 - 觀察者 + 你是觀察員 + 你是觀察員 + 觀察員 更新群組連接時出錯 請聯絡群組管理員。 初始角色 @@ -965,7 +960,7 @@ 儲存用戶密碼時出錯 更新用戶隱私時出錯 中繼伺服器僅在必要時使用。 另一方可以觀察到你的 IP 地址。 - 輸入密碼以搜尋! + 在搜尋中輸入密碼 群組歡迎訊息 隱藏的對話資料 不再顯示 @@ -980,35 +975,35 @@ 當靜音配置檔案處於活動狀態時,你仍會接收來自靜音配置檔案的通話和通知。 取消隱藏 影片將在你的聯絡人在線時接收,請你等等或者稍後再檢查! - 確認數據庫更新 - 數據庫版本不相容 - 數據庫降級 - 數據庫升級 + 確認資料庫更新 + 資料庫版本不相容 + 資料庫降級 + 資料庫升級 無效的遷移確認 - 數據庫現行版本比應用程式新,但是無法降級遷出:%s - 在應用程式/數據庫的不同遷移:%s/%s + 資料庫現行版本比應用程式新,但是無法降級遷出:%s + 在應用程式/資料庫的不同遷移:%s/%s 遷移:%s 警告:你可能會遺失一些數據! - 圖片將在你的聯絡人完成上傳後接收。 - 檔案將在你的聯絡人完成上傳後接收。 + 圖片將在你的聯絡人完成上載後接收。 + 檔案將在你的聯絡人完成上載後接收。 實驗性 升級和開啟對話 顯示開發者選項 - 刪除資料 + 刪除個人檔案 取消隱藏聊天資料 取消隱藏個人檔案 - 刪除對話資料 + 刪除聊天個人檔案 詢問以接收影片 同一時間只能傳送十段影片 太多影片! 影片 已傳送影片 等待影片中 - 影片將在你的聯絡人完成上傳後接收。 + 影片將在你的聯絡人完成上載後接收。 等待影片中 隱藏: 顯示: - 數據庫 IDs 和傳輸隔離選項。 + 資料庫 IDs 和傳輸隔離選項。 降級和開啟對話 個人檔案密碼 儲存 XFTP 伺服器時出錯 @@ -1016,7 +1011,7 @@ 加載 SMP 伺服器時出錯 加載 XFTP 伺服器時出錯 建立檔案 - 伺服器需要認證後才能上傳,檢查密碼 + 伺服器需要認證後才能上載,檢查密碼 對比檔案 刪除檔案 下載檔案 @@ -1060,7 +1055,7 @@ 停止 停止接收檔案? 錯誤的訊息 ID - 當你或你的連結在用舊的數據庫備份時會發生。 + 當你或你的連結在用舊的資料庫備份時會發生。 上一則訊息的雜奏則是不同的。 應用程式密碼 迅速以及不用等待發送者在線! @@ -1082,7 +1077,7 @@ " \n在 v5.1中可用" 允許你的聯絡人與你進行通話。 - 上傳檔案 + 上載檔案 XFTP 伺服器 系統認證 你未能通過認證;請再試一次。 @@ -1090,14 +1085,14 @@ 系統 此ID的下一則訊息是錯誤(小於或等於上一則的)。 \n當一些錯誤出現或你的連結被破壞時會發生。 - %1$d 訊息解密失敗。 + %1$d 條訊息解密失敗。 使用SOCKS 代理伺服器 你的 XFTP 伺服器 %1$d 條訊息已跳過。 影片和檔案和最大上限為1gb 影片 - 呈交 - 開啟數據庫中… + 提交 + 開啟資料庫中… 查看更多 SimpleX 聯絡地址 一次性連結 @@ -1130,15 +1125,15 @@ 已傳送訊息 標題 關於 SimpleX 的聯絡地址 - 額外的強調色 + 額外的輔色 外加的輔助 - 聯絡地址 - 後台 + 地址 + 背景 建立一個聯絡地址讓其他用戶與你連接。 自定義主題 黑暗主題 設定聯絡地址時出錯 - 開啟多個個人檔案 + 變更聊天個人檔案 內查看更多關於用戶指南的資料。]]> 自毁 啟用自毀密碼 @@ -1152,7 +1147,7 @@ 如果你在開啟應用程式時輸入此密碼,所有應用程式資料將被不可逆轉地刪除! 啟用自毀 應用程式密碼已替換為自毀密碼。 - 已建立一個具有所提供名稱的空白對話檔案,而應用程式將照常開啟。 + 已建立一個具有所提供名稱的空白聊天個人檔案,而應用程式將照常開啟。 已刪除所有的應用程式數據。 設定密碼 你可以與聯絡人分享此地址,讓他們使用 %s 進行連接。 @@ -1164,10 +1159,10 @@ 當有人向你發出連接請求,你可以接受或拒絕請求。 你的聯絡人會保持連接。 你的所有聯絡人會保持連接。更新了的個人檔案將傳送給你的聯絡人。 - 將聯絡地址新增到你的個人檔案中,以便你的聯絡人與其他人分享你的聯絡地址。更新了的個人檔案將傳送給你的聯絡人。 + 新增地址至你的個人檔案,以便你的聯絡人可以與其他人分享。更新了的個人檔案將傳送給你的聯絡人。 讓我們在 SimpleX Chat中聊天 確保檔案具有正確的 YAML 語法。匯出主題以獲得主題檔案結構的範例。 - 訊息互動 + 訊息反應 允許你的聯絡人新增訊息互動。 星期 @@ -1178,10 +1173,10 @@ 自定義時間 傳送 自動銷毀訊息 - 已移除在:%s + 已移除於:%s (目前的) - 允許訊息互動。 - 更佳的訊息 + 允許訊息反應。 + 更好的訊息 自定義和分享顏色主題 自定義主題 小時 @@ -1189,19 +1184,19 @@ 分鐘 個月 - 自定義 + 自訂 已接收在 已刪除在 - 已傳送在 - 數據庫 ID:%d + 已傳送於 + 資料庫 ID:%d 已移除在 - 1分鐘 - 30秒 - 5分鐘 - 已刪除在:%s + 1 分鐘 + 30 秒 + 5 分鐘 + 已刪除於:%s 禁止訊息互動。 - 群組內的成員可以新增訊息互動。 - 訊息互動於這個群組內是禁用的。 + 成員可以新增訊息反應。 + 訊息反應已停用。 加載詳細資料時出錯 傳送自動銷毀訊息 %s (目前的) @@ -1216,35 +1211,35 @@ 所有數據會在輸入後刪除。 沒有文字 紀錄已更新在 - 已傳送在:%s - 已接收在:%s + 已傳送於:%s + 已接收於:%s 紀錄已更新在:%s 只有你可以新增訊息互動。 銷毀於 - 訊息互動 + 訊息反應 感謝用戶-透過 Weblate 做出貢獻! 你和你的聯絡人可以新增訊息互動。 - 導入過程中出現了一些非致命錯誤 - 你可以到綜端機對話以獲取更多詳細資訊。 + 匯入過程中出現了一些非致命錯誤: 只有你的聯絡人允許的情況下,才允許訊息互動。 - 長達5分鐘的語音訊息。 \n- 自定義銷毀時間。 \n- 編輯紀錄。 搜尋 - 已關閉 + 關閉 確認來自未知伺服器的檔案。 超出額度 - 收件人未收到先前傳送的訊息 應用程式資料轉移 - 應用 - 請在轉移之前確認你還記得數據庫密碼 + 套用 + 請在轉移之前確認你還記得資料庫密碼 被管理員封鎖 進階設定 封鎖群組成員 活躍連接 中止 - 和其他 %d 事件 + 和其他 %d 個事件 封鎖成員? - 6種全新的介面語言 - 藍芽 + 6 種全新的介面語言 + 藍牙 %2$s 審核了 %1$d 條訊息 已封鎖 將停止地址更改。將使用舊聯絡地址。 @@ -1254,12 +1249,12 @@ 封鎖 應用程式主題 管理員 - 模糊以增強隱私 + 模糊以增強隱私。 所有成員 管理員可以為所有人封鎖一名成員 無法傳送訊息 為所有成員封鎖此成員? - + 黑色 中止更改地址? 無法傳送訊息給群組成員 所有顏色模式 @@ -1267,14 +1262,14 @@ 應用程式密碼 應用程式 聊天顏色 - 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將數據庫傳輸回來。 + 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將資料庫傳輸回來。 即將推出! 軟體更新以下載 儲存聯絡人以便稍後聊天 相機 選擇一個檔案 - 存檔並上傳 - 你的所有聯絡人、對話和檔案將被安全加密並切塊上傳到你設定的 XFTP 中繼 + 封存並上載 + 你的所有聯絡人、對話和檔案將被安全加密並切塊上載到你設定的 XFTP 中繼 正在儲存資料庫 取消遷移 與 %s 協調加密中… @@ -1284,12 +1279,12 @@ 所有訊息都將被刪除 - 這無法復原 請注意:訊息和檔案中繼通過 SOCKS 代理連接。通話和傳送連預覽使用直接連接。]]> 封鎖全部 - 改進群組功能 + 更好的群組 行動網路 封鎖成員 - 警告:此存檔將被刪除。]]> + 警告:此封存將被刪除。]]> 清除私密筆記? - 添加聯絡人 + 新增聯絡人 總是 協調加密中… 允許傳送檔案和媒體 @@ -1303,23 +1298,23 @@ 作者 已封鎖 被管理員封鎖 - 額外的強調色2 + 額外的輔色 2 阿拉伯語、保加利亞語、芬蘭語、希伯來語、泰國語和烏克蘭語——感謝使用者們與Weblate 已加入群組! 確認網路設定 嘗試 已確認 確認錯誤 - 完成 + 已完成 區塊已刪除 - 區塊已上傳 + 區塊已上載 區塊已下載 添加聯絡人: 來創建新的邀請連結,或通過你收到的連結進行連接。]]> 建立群組: 建立新的群組。]]> 錯誤的桌面地址 已轉移聊天 從另一部設備轉移 並掃描QR code。]]> - 請注意: 作為安全保護措施,在兩部設備上使用同一數據庫會破壞解密來自你聯絡人的訊息。]]> + 請注意: 作為安全保護措施,在兩部設備上使用同一資料庫會破壞解密來自你聯絡人的訊息。]]> 確定刪除聯絡人? 檢查更新 無法與聯絡人通話 @@ -1327,7 +1322,7 @@ 無法與群組成員通話 應用程式將為新的本機檔案(影片除外)加密。 檢查你的網路連接並重試 - 所有配置文件 + 所有個人檔案 已設定的 SMP 伺服器 聊天主題 通話 @@ -1343,23 +1338,23 @@ Webview 初始化失敗。更新你的系統到新版本。請聯繫開發者。 \n錯誤:%s 已刪除聯絡人 - %d 個群事件 + %d 個群組事件 訊息太大 訊息傳送警告 錯誤:%1$s 開發者選項 與 %s 的加密需要重協商 %s 不活躍]]> - 最喜歡 + 最愛 訊息成功送達! 檔案和媒體 連線停止 %s的連接不穩定]]> 聯絡人 適合 - 群組成員可以傳送檔案和媒體。 - 連結行動裝置 - 此群組禁止檔案和媒體 + 成員可以傳送檔案和媒體。 + 已連結行動裝置 + 檔案和媒體已停用。 結束通話 刪除 %d 條訊息嗎? 訊息草稿 @@ -1373,11 +1368,11 @@ 啟用(保留組覆蓋) 淺色 淺色模式 - 群組成員可傳送 SimpleX 連結。 + 成員可以傳送 SimpleX 連結。 深色 詳情 訊息接收 - 無效連結 + 無效的連結 %d 條訊息被標記為刪除 %d 條訊息已攔截 轉發伺服器地址不相容網路設定:%1$s。 @@ -1391,7 +1386,7 @@ 如果成員變得活躍,可能會在之後傳送訊息。 刪除了聯絡人! 聯絡人將被刪除 - 無法復原此操作 - + 已停用 安裝成功 建立 @@ -1409,7 +1404,7 @@ 新的聊天主題 連線和伺服器狀態 控制你的網路 - 從GitHub下載最新版本。 + 從 GitHub 下載最新版本。 啟用 新的聊天體驗 🎉 新的媒體選項 @@ -1418,11 +1413,11 @@ 連接到桌面 連線終止 連接到桌面 - 上傳存檔出錯 - 文件被刪除或鏈接無效 - 導入失敗 - 頭戴式耳機 - 耳機 + 上載封存出錯 + 檔案已被刪除或連結無效 + 匯入失敗 + 耳機 + 聽筒 對所有聯絡人關閉 深色模式 啟用已讀回條時出錯! @@ -1439,7 +1434,7 @@ 檔案錯誤 備用訊息路由 如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。 - 建立個人資料 + 建立個人檔案 從另一台裝置轉移 成員姓名從 %1$s 改為了 %2$s 同意加密 @@ -1450,7 +1445,7 @@ \n- 還有更多! 匈牙利語和土耳其語用戶界面 轉移完成 - 從此裝置刪除數據庫 + 從此裝置刪除資料庫 傳送 下載 轉發 @@ -1460,7 +1455,7 @@ 轉發訊息… 建立連結中… 保留 - 聯絡人姓名從 %1$s 改為了 %2$s + 聯絡人名稱從 %1$s 變更為 %2$s 新訊息 邀請 停用(保留覆蓋) @@ -1487,7 +1482,7 @@ 轉發伺服器:%1$s \n錯誤:%2$s 轉發伺服器 %1$s 連結目標伺服器 %2$s 失敗。請稍後嘗試。 - 導入存檔中 + 正在匯入封存 改進訊息傳送 安裝更新 它保護你的 IP 位址和連線。 @@ -1495,13 +1490,13 @@ %s 的版本。請檢察兩台裝置安裝的是否版本相同]]> 更可靠的網路連接 發現和加入群組 - 設備 + 裝置 新行動裝置 保存設定出錯 導出的檔案不存在 導出資料庫時出錯 - 確認上傳 - 正在建立存檔連結 + 確認上載 + 正在建立封存連結 加密OK 將顯示來自 %s 的訊息! 送達回執! @@ -1512,12 +1507,12 @@ 加密重協商失敗 將更新資料庫密碼並儲存在設定中。 使用隨機身分建立群組 - 加入速度更快、訊息更可靠。 + 更快的加入速度,訊息更可靠。 匿名群組 - 連接行動裝置 + 連結行動裝置 回復 和 %1$s 連接? - 在桌面應用裡建立新的帳號。💻 + 在桌面應用程式建立新的個人檔案。💻 輸入此裝置名稱… 已連結到行動裝置 可通過局域網發現 @@ -1525,10 +1520,10 @@ 嚴重錯誤 內部錯誤 驗證密碼短語出錯: - 顯示名稱無效! + 無效的顯示名稱! 無效的檔案路徑 過濾未讀和收藏的聊天記錄。 - 斷開連結 + 斷開連接 斷開桌面連結? 中止地址更改時出錯 顯示通知出錯,請聯繫開發者。 @@ -1550,12 +1545,12 @@ 已關閉送達回執! %s 斷開連接]]> %s 未找到]]> - 存檔下載中 + 封存下載中 轉移中 下載失敗 為所有組啟用 需要重協商加密 - 關閉 + 已停用 修復連結 修復聯絡人不支援的問題 修復 @@ -1571,14 +1566,14 @@ 顯示內容出錯 顯示訊息出錯 錯誤 - 無後台通話 - 建立聊天資料 + 無背景通話 + 建立聊天個人檔案 已同意 %s 的加密 允許重新協商與 %s 的加密 與 %s 的加密OK 桌面設備 - 連接桌面選項 - 連接桌面 + 已連結桌面選項 + 已連結桌面 直接連線中 邀請 建立群組 @@ -1621,7 +1616,7 @@ 已下載的檔案 下載出錯 功能執行所花費的時間過長:%1$d 秒:%2$s - 無效名稱 + 無效的名稱! 正確名字為 %s? 複製錯誤 正在連接到桌面 @@ -1634,14 +1629,14 @@ 通過連結連接? 加入你的群組嗎? 轉移到此處 - 無效連結 + 無效的連結 在另一部設備上完成轉移 - 下載存檔錯誤 + 下載封存錯誤 轉移到另一部裝置 必須停止聊天才能繼續。 保留對話 不通知刪除 - 無效的QR code + 無效的 QR 圖碼 保留未使用的邀請嗎? 停用回執? 啟用回執? @@ -1651,7 +1646,7 @@ 為儲存的檔案和媒體加密 連接到你自己? 完成轉移 - 目前配置文件 + 目前個人檔案 檔案 重連伺服器出錯 重連伺服器出錯 @@ -1662,14 +1657,14 @@ 資料庫將被加密,密碼將儲存在設定中 刪除成員的 %d 條訊息嗎? 成員非活躍 - 輸入訊息 + 訊息 訊息將被標記為刪除。收信人可以揭示這些訊息。 連接 訊息 停用 下載更新中,請不要關閉應用 下載 %s(%s) - 從不 + 永不 中等 媒體和檔案伺服器 訊息伺服器 @@ -1684,7 +1679,7 @@ 為群組停用回執? 過往的成員 %1$s 私密訊息路由 🚀 - 貼上存檔連結 + 貼上封存連結 從桌面使用並掃描QR code。]]> 無傳送資訊 或者顯示此碼 @@ -1694,11 +1689,11 @@ 請稍後再試。 沒有過濾的聯絡人 打開檔案位置 - 打開 + 開啟 私密筆記 波斯語用戶界面 從聊天列表播放。 - 正在準備上傳 + 正在準備上載 擁有者 不相容! 請將它報告給開發者: @@ -1708,21 +1703,21 @@ 從已連接行動裝置加載檔案時請稍候片刻 或者掃描QR code 禁止傳送 SimpleX 連結 - 沒有選擇聊天 + 沒有已選擇的聊天 打開設定 這人資料主題 個人資料圖片 同一時刻只有一台裝置可工作 保護 IP 地址 其他 - 無網路連接 + 沒有網路連接 無歷史記錄 無過濾聊天 請將它報告給開發者: \n%s 打開應用程式設定 開啟轉移畫面 - 通知將停止,直到您重啟應用程式 + 通知將停止,直到你重啟應用程式 禁止傳送檔案和媒體。 打開群組 只有群組所有者才能啟用檔案和媒體。 @@ -1740,7 +1735,7 @@ 請檢查行動裝置和桌面設備連接到的是同一個本地網絡,且桌面防火牆允許連接。 \n請和開發者分享任何其他問題。 在防火牆中打開端口 - 或貼上存檔連結 + 或貼上封存連結 正在準備下載 只刪除對話 貼上連結 @@ -1754,7 +1749,7 @@ 私密路由出錯 尚無直接連接,訊息由管理員轉發。 什麼也沒選中 - 打開 + 開啟 私密路由 打開資料庫文件夾 私密訊息路由 @@ -1766,28 +1761,318 @@ 接收到的訊息 接收錯誤 重新連接所有已連接的伺服器來強制傳送訊息。這會使用額外流量。 - 重連伺服器強制傳送訊息。這會使用額外流量。 + 重新連接伺服器以強制傳送訊息。這會使用額外流量。 並行接收 收件人看不到這條訊息來自誰。 刪除了資料圖片 - 可使用的聊天工具箱 - 可存取的聊天工具欄 + 單手模式的應用程式工具列 + 單手模式的聊天工具列 最近歷史和改進的目錄機器人。 每 KB 協議超時 - 保護您的真實 IP 地址。不讓你聯絡人選擇的訊息中繼看到它。 -\n在*網絡&伺服器*設定中開啓。 - 隨機密碼以明文形式儲存在設定中。 -\n您可以稍後更改。 + 保護你的真實 IP 地址,以不讓你的聯絡人所選擇的訊息中繼看到它。 \n在*網路和伺服器*設定中啟用。 + 隨機密碼以明文形式儲存在設定中。 \n你可以稍後更改。 傳送回條已禁用 代理伺服器 - 重連伺服器? + 重新連接伺服器? 抗量子端到端加密 收到的回覆 - 重連所有伺服器 - 重連 + 重新連接所有伺服器 + 重新連接 代理 隨機 - 更新 + 重新整理 接收總計 稍後提醒 + 新增朋友 + 營運者伺服器 + 關閉? + 此裝置 + 審核員 + %1$d 個檔案下載失敗。 + 聊天已存在! + 沒有訊息伺服器。 + %1$d 個檔案錯誤:\n%2$s + 設定聊天名稱… + 靜音全部 + 刪除 + + + 關閉 + 此裝置名稱 + 關於營運者 + 預設(%s) + 聯絡人 + 你的聯絡人 + 聊天資料庫 + 詢問 + + 重設顏色 + 已傳送回覆 + 審核員 + 拒絕 + 重設 + 已傳送總數 + 清單 + 設定 + 清單名稱… + 單手模式的聊天工具列 + %1$d 個檔案仍然正在下載。 + %1$s 條訊息未被轉發 + %1$d 個檔案已刪除。 + 下載 + 筆記 + 尾部 + 重新啟動 + 應用程式工具列 + 伺服器地址 + 營運者 + 你已退出 + 顯示百分比 + 伺服器 + 接受 + + %s:%s + 聊天 + 你的伺服器 + 縮放 + 模糊 + %1$d 個檔案未被下載。 + 1 年 + 接受 + 已訂閱 + SimpleX 頻道連結 + %s、%s,和 %d 名成員 + 待處理 + 解除連結 + 最愛 + 給所有審核員 + 舉報 + 濫發 + 搜尋 + 影片 + 取消最愛 + + 喇叭 + 解除封鎖 + 網站 + 透明度 + 重複 + 編輯 + 重新同步 + 選擇 + 使用者名稱 + 密碼 + 更新 + 繼續 + 已拒絕 + 系統 + 關閉 + 全部 + 已受保護 + 全部 + 商業 + 角落 + 群組 + 報告 + 統計 + 比例 + 重試 + 穩定 + 審核 + 未知 + 關閉 + 已儲存 + 封存 + 已拒絕 + 大小 + WiFi + 修復 + 已儲存 + 濫發 + 已上載 + 未受保護 + 沒有私密訊息路由伺服器。 + 沒有訊息接收伺服器。 + 沒有媒體和檔案伺服器。 + 其他 %1$d 個檔案錯誤。 + 已新增媒體和檔案伺服器 + %s 已封存報告 + 沒有檔案傳送伺服器。 + 沒有檔案接收伺服器。 + 封存報告 + 此聊天受到抗量子端對端加密保護。 + 使用目前個人檔案 + 使用新的匿名個人檔案 + 更好的通話 + 地址設定 + 已新增訊息伺服器 + 你分享了一個無效的檔案路徑。請將此問題報告給應用程式開發者。 + 如果沒有 Tor 或 VPN,你的 IP 位址將對以下 XFTP 中繼可見:\n%1$s。 + 檢視已崩潰 + 新增短連結 + 接受了 %1$s + 接受了你 + 新增團隊成員 + 接受成員 + 新增清單 + SimpleX 無法在背景執行。你只有在應用程式執行時才會收到通知。 + 應用程式工作階段 + 已接受邀請 + 此顯示名稱無效。請選擇另一個名稱。 + 新增至清單 + 地址還是一次性連結? + 應用程式總是在背景執行 + 已接受的條款 + 接受為成員 + 接受為觀察員 + 全部伺服器 + 允許向審核員舉報訊息。 + 封存全部報告? + 封存 %d 份報告? + 封存報告? + 封存報告 + 1 個舉報 + 無背景服務 + 短連結 + 已封存的報告 + 只有你和審核員能夠檢視 + 只有傳送者和審核員能夠檢視 + 已儲存自 %s + 此聊天受到端對端加密保護。 + 另一個原因 + 不當的個人檔案 + 不當的內容 + 違反社群準則 + 接受條款 + 重複連接請求? + 移除封存? + 已直接傳送 + 傳送錯誤 + 連接安全性 + 變更排序 + 訊息形狀 + 驗證連接 + 視訊通話 + 刪除舉報 + 使用 %s + 重新同步加密? + 已儲存自 + 連接被封鎖 + 建立清單 + 轉發訊息… + 建立一次性連結 + 網路營運者 + 解除連結桌上電腦? + 重複上載 + 設定資料庫密碼 + %s 已上載 + 已更新個人檔案 + 開啟連結 + 設定資料庫密碼 + 工作階段代碼 + 已傳送訊息 + 正在停止聊天 + 伺服器資訊 + %d 條訊息 + %d 個聊天 + 設定密碼短語 + 待審核 + 未知伺服器! + 伺服器營運者 + 已解除封鎖 %s + 安全碼已變更 + 未知狀態 + 傳送回條 + 移除成員? + 重新同步加密 + 使用伺服器 + TCP 連接 + 用於傳送 + SimpleX 連結 + 審核成員 + 刪除聊天 + 拒絕成員? + 重新啟動聊天 + %s 已下載 + 重複下載 + 重複匯入 + 上載失敗 + 驗證密碼短語 + 已選取 %d + SOCKS 代理 + 解除封鎖成員 + 使用隨機密碼 + %s 已連接 + 已儲存訊息 + 更安全的群組 + XFTP 伺服器 + 變更清單 + 新伺服器 + 儲存清單 + 刪除清單? + 解除封鎖成員? + 網路營運者 + 系統模式 + 驗證連接 + 開始聊天? + %s 在 %s + 移除成員? + 移除成員 + 移除圖片 + 傳送私人訊息 + 聯絡人已刪除 + 未同步 + 聯絡人已停用 + 稍後審核 + 分享個人檔案 + 完整連結 + 已上載檔案 + 上載錯誤 + 正在上載封存 + 開始聊天 + SMP 伺服器 + 傳輸工作階段 + 修復連接? + 總計 + 未知伺服器 + 舉報理由? + %s 和 %s + 減少電量使用。 + 你的個人檔案 %1$s 將會被分享。 + 你無法傳送訊息! + 減少電量使用。 + 你已解除封鎖 %s + 你已接受此成員 + 停用刪除訊息 + 商業地址 + 變更自動燒毀訊息? + 與成員聊天 + 與管理員們聊天 + 刪除聊天 + 刪除聊天? + 與成員們聊天 + 商業聊天 + 更好的安全性 ✅ + 與管理員們聊天 + 無法傳送訊息 + 可自訂訊息形狀。 + 更好的使用體驗 + 內容違反使用條款 + 停用自動刪除訊息? + 刪除或審查最多 200 條訊息。 + 於網路和伺服器設定中啟用 Flux 以獲得更好的元資料隱私。 + 配置伺服器營運者 + 使用條款 + 更好的隱私和安全性 + 你可以再試一次。 + 每 10 分鐘檢查訊息 + 聯絡人未就緒 + 你可以在外觀設定中變更它。 + 從你的裝置刪除聊天訊息。 + 更好的群組性能 + 你已封鎖 %s + 連接未就緒。 + 更好的訊息日期。 + 為了更好的元資料隱私。 + 你可以再試一次。 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt index 53f3301507..d0ba082adf 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -133,9 +133,9 @@ actual fun ImageBitmap.hasAlpha(): Boolean { return false } -actual fun ImageBitmap.addLogo(): ImageBitmap { - val radius = (width * 0.16f).toInt() - val logoSize = (width * 0.24).toInt() +actual fun ImageBitmap.addLogo(size: Float): ImageBitmap { + val radius = (width * size).toInt() + val logoSize = (width * size * 1.5).toInt() val logo: BufferedImage = MR.images.icon_foreground_common.image val original = toAwtImage() val withLogo = BufferedImage(width, height, original.type) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt index 3855835ab6..52e845b422 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -63,7 +63,7 @@ actual fun UserPickerUsersSection( ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) if (u.unreadCount > 0 && !user.activeUser) { - unreadBadge(u.unreadCount, user.showNtfs, true) + userUnreadBadge(u.unreadCount, user.showNtfs, true) } } diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml index 9f9b2d6d20..2254c6d5bf 100644 --- a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml +++ b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml @@ -52,7 +52,7 @@ https://github.com/simplex-chat/simplex-chat/issues https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat https://simplex.chat/connect-team - https://github.com/simplex-chat/simplex-chat#help-us-with-donations + https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index aa4c7a7470..1b427e075c 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.3.6 -android.version_code=295 +android.version_name=6.4-beta.3 +android.version_code=297 android.bundle=false -desktop.version_name=6.3.6 -desktop.version_code=106 +desktop.version_name=6.4-beta.3 +desktop.version_code=107 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 faaccbd2bf..aa2374f919 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -79,7 +79,7 @@ crDirectoryEvent_ = \case CEvtJoinedGroupMember {groupInfo, member = m} | pending m -> Just $ DEPendingMember groupInfo m | otherwise -> Nothing - CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t _ -> Nothing CEvtMemberRole {groupInfo, member, toRole} diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 4517ee9c5b..13510b4d2b 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -167,7 +167,7 @@ acceptMemberHook when (useMemberFilter img $ rejectNames a) checkName pure $ if - | useMemberFilter img (passCaptcha a) -> (GAPending, GRMember) + | useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) | otherwise -> (GAAccepted, memberRole) where @@ -494,7 +494,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName [] -> textMsg "" : _ -> textMsg img : _ -> MCImage "" $ ImageData img - sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just gmId) [(quotedId, MCText noticeText), (Nothing, mc)] + sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(quotedId, MCText noticeText), (Nothing, mc)] gmId = groupMemberId' m approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO () @@ -503,9 +503,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_ gmId = groupMemberId' m sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case - Right CRJoinedGroupMember {} -> do + Right CRMemberAccepted {member} -> do atomically $ TM.delete gmId $ pendingCaptchas env - logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g + if memberStatus member == GSMemPendingReview + then logInfo $ "Member " <> viewName displayName <> " accepted and pending review, group " <> tshow groupId <> ":" <> viewGroupName g + else logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g r -> logError $ "unexpected accept member response: " <> tshow r dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO () @@ -516,7 +518,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just PendingCaptcha {captchaText, sentAt, attempts} | ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1 | matchCaptchaStr captchaText msgText -> do - sendComposedMessages_ cc (SRGroup groupId $ Just $ groupMemberId' m) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] + sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just $ groupMemberId' m)) [(Just ciId, MCText $ "Correct, you joined the group " <> n)] approvePendingMember a g m | attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts | otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts @@ -526,7 +528,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName a = groupMemberAcceptance g rejectPendingMember rjctNotice = do let gmId = groupMemberId' m - sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice] + sendComposedMessages cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [MCText rjctNotice] sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case Right (CRUserDeletedMembers _ _ (_ : _) _) -> do atomically $ TM.delete gmId $ pendingCaptchas env @@ -711,7 +713,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName case mRole_ of Nothing -> getGroupLinkRole cc user g >>= \case - Just (_, CCLink gLink _, mRole) -> do + Just (_, CCLink gLink _, _, mRole) -> do let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n mRole @@ -1043,7 +1045,7 @@ vr :: ChatController -> VersionRangeChat vr ChatController {config = ChatConfig {chatVRange}} = chatVRange {-# INLINE vr #-} -getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupMemberRole)) +getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupLinkId, GroupMemberRole)) getGroupLinkRole cc user gInfo = withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo diff --git a/cabal.project b/cabal.project index f406b9820e..d8c7147be6 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: 3d62a383d5dcae6529d6d866233857182bcb4d47 + tag: d9500125300f89bd29377705e7a0e43d2d111707 source-repository-package type: git diff --git a/docs/rfcs/2025-03-07-group-knocking.md b/docs/rfcs/2025-03-07-group-knocking.md new file mode 100644 index 0000000000..fedbbfaf72 --- /dev/null +++ b/docs/rfcs/2025-03-07-group-knocking.md @@ -0,0 +1,38 @@ +# Group knocking + +## Problem + +In v6.3 release we added an option to "manually" approve members before introducing to group, based on decision made via `acceptMember` chat hook. Currently it's not supported in UI clients, and only used for directory service bot captcha challenge. + +The goal of next improvement is to let: +- admins screen members before admitting to group, while not limiting communication with joining member to a single admin (and also removing the need for admin to be a highly available host of the group link); +- and group owners set up other arbitrary automated challenges or entry rules, while still being able to advertise groups in directory service. + +## Solution + +Group link host (further host), knowing group requires admin approval, would initially only introduce member pending approval to admins. Admins can connect with member for screening, meanwhile host would be forwarding messages as usual between connecting members. As a result of screening, pending member can either be removed, or approved by admins. + +Upon acceptance, for further member connections to not depend on availability of admins, host should not only forward acceptance message, but also introduce remaining members to now accepted joining member. Respectively, admins' clients should not introduce members for approved members who are not their invitees. + +For group owners to be able to set up alternative automated challenges, these are some possible alternatives: +- We could add a new role `Approver`, so that instead of adding all \[possibly human\] admins, host would initially introduce only approvers. +- It could be an orthogonal to role member setting (would require protocol extension). +- List of "approver" member IDs could be communicated to host client. + +### Implementation details draft + +Host needs to have knowledge whether to automatically accept, or only introduce admins/approvers. + +```sql +ALTER TABLE group_profiles ADD COLUMN approval TEXT; -- comma separated member IDs; null - automatic introduction + +-- or + +ALTER TABLE group_profiles ADD COLUMN approval INTEGER; -- if based on `Approver` role +``` + +Alternatively, a different extension of protocol could be done in order to communicate group approval rule from owner to host outside of group profile (special messages). + +Admins/approvers need to have separate conversation per pending member, requires adding scope to chat items. + +Host to have specific processing of forwarded `XGrpLinkAcpt` - continue introduction of remaining members. diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 6cc7aac011..ffd5e13337 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -37,13 +37,27 @@ cp *imple*.desktop usr/share/applications/ cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo 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 + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi if [ ! -f ../runtime-x86_64 ]; then - wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 chmod +x ../runtime-x86_64 fi -../appimagetool-x86_64.AppImage --runtime-file ../runtime-x86_64 . +# Determenistic build + +export SOURCE_DATE_EPOCH=1704067200 + +# Delete redundant jar file and modify cfg +rm -f ./usr/lib/app/*skiko-awt-runtime-linux* +sed -i -e '/skiko-awt-runtime-linux/d' ./usr/lib/app/simplex.cfg + +# Set all files to fixed time +find . -exec touch -d "@$SOURCE_DATE_EPOCH" {} + + +../appimagetool-x86_64.AppImage --verbose --no-appstream --runtime-file ../runtime-x86_64 . mv *imple*.AppImage ../../ + +# Just a safeguard +strip-nondeterminism ../../*imple*.AppImage diff --git a/scripts/desktop/make-deb-linux.sh b/scripts/desktop/make-deb-linux.sh new file mode 100755 index 0000000000..6f9de8a1e6 --- /dev/null +++ b/scripts/desktop/make-deb-linux.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +scripts/desktop/build-lib-linux.sh +cd apps/multiplatform +./gradlew packageDeb + +# Workaround for skiko library +# +# Compose Multiplatform depends on skiko library, that +# handles all of the window managment and graphics drawing. +# +# This skiko library comes with two jar's: +# - platform-agnostic "skiko-awt" +# - and platform-specific "skiko-awt-runtime" +# +# In case of Linux, second jar is called "skiko-awt-runtime-linux-x64". +# Essentially, this jar has the Linux .so library called "libskiko-linux-x64.so" +# that is being unpacked to runtime libs. +# +# Since the jar is nothing more than a zip archive, extracting library +# from "skiko-awt-runtime-linux-x64" modifies it's timestamps +# with current time, which in changes it's hash, which in turn +# makes the whole build unreproducible. +# +# It seems to be there is no way to handle this extraction in our code and +# https://docs.gradle.org/current/userguide/working_with_files.html#sec:reproducible_archives +# unfortunately doesn't solve the issue. +# +# Instead, just modify the deb, removing the redundant skiko library. +# +# Also, it seems this is related to: +# https://youtrack.jetbrains.com/issue/CMP-1971/createDistributable-produces-duplicated-skiko-awt.jar-and-skiko-awt-runtime-windows-x64.jar + +export SOURCE_DATE_EPOCH=1704067200 + +dpkg-deb -R ./release/main/deb/simplex*.deb ./extracted + +rm -f ./extracted/opt/*imple*/lib/app/*skiko-awt-runtime-linux* +sed -i -e '/skiko-awt-runtime-linux/d' ./extracted/opt/*imple*/lib/app/simplex.cfg +find ./extracted/ -exec touch -d "@$SOURCE_DATE_EPOCH" {} + + +dpkg-deb --build --root-owner-group --uniform-compression ./extracted ./release/main/deb/simplex_amd64.deb + +strip-nondeterminism ./release/main/deb/simplex_amd64.deb diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index dae1c9255d..a798fd6fc9 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -10,11 +10,13 @@ vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linu mkdir $vlc_dir || exit 0 +vlc_tag='v3.0.21-1' +vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-linux-x86_64.appimage" cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl --tlsv1.2 https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage +curl --tlsv1.2 "${vlc_url}" -L -o appimage chmod +x appimage ./appimage --appimage-extract cp -r squashfs-root/usr/lib/* $vlc_dir diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh index 288b5a1a7d..4db2983f67 100755 --- a/scripts/desktop/prepare-vlc-mac.sh +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -9,7 +9,9 @@ if [ "$ARCH" == "arm64" ]; then else vlc_arch=intel64 fi -vlc_version=3.0.19 + +vlc_tag='v3.0.21-1' +vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-macos-${ARCH}.zip" function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" @@ -23,7 +25,7 @@ mkdir -p $vlc_dir/vlc || exit 0 cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl --tlsv1.2 https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc +curl --tlsv1.2 "${vlc_url}" -L -o vlc unzip -oqq vlc install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen cd VLC.app/Contents/MacOS/lib diff --git a/scripts/desktop/prepare-vlc-windows.sh b/scripts/desktop/prepare-vlc-windows.sh index 7d2a71a952..4e65528ca0 100644 --- a/scripts/desktop/prepare-vlc-windows.sh +++ b/scripts/desktop/prepare-vlc-windows.sh @@ -10,10 +10,13 @@ vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/wind rm -rf $vlc_dir mkdir -p $vlc_dir/vlc || exit 0 +vlc_tag='v3.0.21-1' +vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-win-x86_64.zip" + cd /tmp mkdir tmp 2>/dev/null || true cd tmp -curl --tlsv1.2 https://irltoolkit.mm.fcix.net/videolan-ftp/vlc/3.0.18/win64/vlc-3.0.18-win64.zip -L -o vlc +curl --tlsv1.2 "${vlc_url}" -L -o vlc $WINDIR\\System32\\tar.exe -xf vlc cd vlc-* # Setting the same date as the date that will be on the file after extraction from JAR to make VLC cache checker happy diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index b15e382207..e56ab82d2b 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,33 @@ + + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1-5:

+
    +
  • fixes
  • +
  • fixes mentions with trailing punctuation (e.g., hello @name!).
  • +
  • recognizes domain names as links (e.g., simplex.chat).
  • +
  • forward compatibility with "knocking" (a feature for group admins to review and to chat with the new members prior to admitting them to groups, it will be released in 6.4)
  • +
  • support for connecting via short connection links.
  • +
  • fix related to backward/forward compatibility of the app in some rare cases.
  • +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 84f9d0db34..b5a092de0f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."3d62a383d5dcae6529d6d866233857182bcb4d47" = "133xm8jkim7agd6drwm3lbx1z7v8nf4l3asrm46ag3n2q201yfxc"; + "https://github.com/simplex-chat/simplexmq.git"."d9500125300f89bd29377705e7a0e43d2d111707" = "17yyy991279g49l9s4p0691dxrsb1fhkf4v180kmf5mbd9wz1rsf"; "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/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh index 408d1d4a53..f66edb7043 100755 --- a/scripts/simplex-chat-reproduce-builds.sh +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -6,9 +6,14 @@ TAG="$1" tempdir="$(mktemp -d)" init_dir="$PWD" +ghc='9.6.3' + repo_name="simplex-chat" repo="https://github.com/simplex-chat/${repo_name}" +image_name='sx-local' +container_name='sx-builder' + cabal_local='ignore-project: False package direct-sqlcipher flags: +openssl' @@ -16,105 +21,151 @@ package direct-sqlcipher export DOCKER_BUILDKIT=1 cleanup() { - docker exec -t builder sh -c 'rm -rf ./dist-newstyle' 2>/dev/null || : - rm -rf -- "$tempdir" - docker rm --force builder 2>/dev/null || : - docker image rm local 2>/dev/null || : - cd "$init_dir" + docker exec -t "${container_name}" sh -c 'rm -rf ./dist-newstyle ./apps' 2>/dev/null || : + rm -rf -- "${tempdir}" + docker rm --force "${container_name}" 2>/dev/null || : + docker image rm "${image_name}" 2>/dev/null || : + cd "${init_dir}" } trap 'cleanup' EXIT INT -mkdir -p "$init_dir/$TAG-$repo_name/from-source" "$init_dir/$TAG-$repo_name/prebuilt" +mkdir -p "${init_dir}/${TAG}-${repo_name}/from-source" "${init_dir}/${TAG}-${repo_name}/prebuilt" -git -C "$tempdir" clone "$repo.git" &&\ - cd "$tempdir/${repo_name}" &&\ - git checkout "$TAG" +git -C "${tempdir}" clone "${repo}.git" &&\ + cd "${tempdir}/${repo_name}" &&\ + git checkout "${TAG}" -for os in 22.04 24.04; do - os_url="$(printf '%s' "$os" | tr '.' '_')" +for os in '22.04' '24.04'; do + os_url="$(printf '%s' "${os}" | tr '.' '_')" + + cli_name="simplex-chat-ubuntu-${os_url}-x86-64" + deb_name="simplex-desktop-ubuntu-${os_url}-x86_64.deb" + appimage_name="simplex-desktop-x86_64.AppImage" # Build image docker build \ --no-cache \ - --build-arg TAG=${os} \ - --build-arg GHC=9.6.3 \ - -f "$tempdir/${repo_name}/Dockerfile.build" \ - -t local \ + --build-arg TAG="${os}" \ + --build-arg GHC="${ghc}" \ + -f "${tempdir}/${repo_name}/Dockerfile.build" \ + -t "${image_name}" \ . - printf '%s' "$cabal_local" > "$tempdir/${repo_name}/cabal.project.local" + printf '%s' "${cabal_local}" > "${tempdir}/${repo_name}/cabal.project.local" # Run container in background docker run -t -d \ - --name builder \ - -v "$tempdir/${repo_name}:/project" \ - local + --name "${container_name}" \ + --device /dev/fuse \ + --cap-add SYS_ADMIN \ + --security-opt apparmor:unconfined \ + -v "${tempdir}/${repo_name}:/project" \ + "${image_name}" + # Consistent permissions docker exec \ - -t \ - builder \ - sh -c 'cabal clean && cabal update && cabal build -j --enable-tests && mkdir -p /out && for i in simplex-chat; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && chmod +x "$bin" && mv "$bin" /out/; done && strip /out/simplex-chat' + -t "${container_name}" \ + sh -c 'find /project -type d -exec chmod 755 {} \; ; find /project -type f -perm /111 -exec chmod 755 {} \; ; find /project -type f ! -perm /111 -exec chmod 644 {} \;' + # CLI + docker exec \ + -t "${container_name}" \ + sh -c 'cabal clean && cabal update && cabal build -j && mkdir -p /out && for i in simplex-chat; do bin=$(find /project/dist-newstyle -name "$i" -type f -executable) && chmod +x "$bin" && mv "$bin" /out/; done && strip /out/simplex-chat' + + # Copy CLI docker cp \ - builder:/out/simplex-chat \ - "$init_dir/$TAG-$repo_name/from-source/simplex-chat-ubuntu-${os_url}-x86-64" + "${container_name}":/out/simplex-chat \ + "${init_dir}/${TAG}-${repo_name}/from-source/${cli_name}" - # Download prebuilt postgresql binary + # Download prebuilt CLI binary curl -L \ - --output-dir "$init_dir/$TAG-$repo_name/prebuilt/" \ - -O \ - "$repo/releases/download/${TAG}/simplex-chat-ubuntu-${os_url}-x86-64" + --output-dir "${init_dir}/${TAG}-${repo_name}/prebuilt/" \ + -O "${repo}/releases/download/${TAG}/${cli_name}" + + # Desktop: deb + docker exec \ + -t "${container_name}" \ + sh -c './scripts/desktop/make-deb-linux.sh' + + # Copy deb + docker cp \ + "${container_name}":/project/apps/multiplatform/release/main/deb/simplex_amd64.deb \ + "${init_dir}/${TAG}-${repo_name}/from-source/${deb_name}" + + # Download prebuilt deb package + curl -L \ + --output-dir "${init_dir}/${TAG}-${repo_name}/prebuilt/" \ + -O "${repo}/releases/download/${TAG}/${deb_name}" + + # Desktop: appimage. Build only on 22.04 + case "$os" in + 22.04) + # Appimage + docker exec \ + -t "${container_name}" \ + sh -c './scripts/desktop/make-appimage-linux.sh && mv ./apps/multiplatform/release/main/*imple*.AppImage ./apps/multiplatform/release/main/simplex.appimage' + + # Copy appimage + docker cp \ + "${container_name}":/project/apps/multiplatform/release/main/simplex.appimage \ + "${init_dir}/${TAG}-${repo_name}/from-source/${appimage_name}" + + # Download prebuilt appimage binary + curl -L \ + --output-dir "${init_dir}/${TAG}-${repo_name}/prebuilt/" \ + -O "${repo}/releases/download/${TAG}/${appimage_name}" + ;; + esac # Important! Remove dist-newstyle for the next interation docker exec \ - -t \ - builder \ - sh -c 'rm -rf ./dist-newstyle' + -t "${container_name}" \ + sh -c 'rm -rf ./dist-newstyle ./apps/multiplatform' # Also restore git to previous state git reset --hard && git clean -dfx # Stop containers, delete images - docker stop builder - docker rm --force builder - docker image rm local + docker stop "${container_name}" + docker rm --force "${container_name}" + docker image rm "${image_name}" done # Cleanup -rm -rf -- "$tempdir" -cd "$init_dir" +rm -rf -- "${tempdir}" +cd "${init_dir}" # Final stage: compare hashes # Path to binaries -path_bin="$init_dir/$TAG-$repo_name" +path_bin="${init_dir}/${TAG}-${repo_name}" # Assume everything is okay for now bad=0 # Check hashes for all binaries -for file in "$path_bin"/from-source/*; do +for file in "${path_bin}"/from-source/*; do # Extract binary name - app="$(basename $file)" + app="$(basename ${file})" # Compute hash for compiled binary - compiled=$(sha256sum "$path_bin/from-source/$app" | awk '{print $1}') + compiled=$(sha256sum "${path_bin}/from-source/${app}" | awk '{print $1}') # Compute hash for prebuilt binary - prebuilt=$(sha256sum "$path_bin/prebuilt/$app" | awk '{print $1}') + prebuilt=$(sha256sum "${path_bin}/prebuilt/${app}" | awk '{print $1}') # Compare - if [ "$compiled" != "$prebuilt" ]; then + if [ "${compiled}" != "${prebuilt}" ]; then # If hashes doesn't match, set bad... bad=1 # ... and print affected binary - printf "%s - sha256sum hash doesn't match\n" "$app" + printf "%s - sha256sum hash doesn't match\n" "${app}" fi done # If everything is still okay, compute checksums file -if [ "$bad" = 0 ]; then - sha256sum "$path_bin"/from-source/* | sed -e "s|$PWD/||g" -e 's|from-source/||g' -e "s|-$repo_name||g" > "$path_bin/_sha256sums" +if [ "${bad}" = 0 ]; then + sha256sum "${path_bin}"/from-source/* | sed -e "s|$PWD/||g" -e 's|from-source/||g' -e "s|-$repo_name||g" > "${path_bin}/_sha256sums" - printf 'Checksums computed - %s\n' "$path_bin/_sha256sums" + printf 'Checksums computed - %s\n' "${path_bin}/_sha256sums" fi diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 96b2f941c7..5d74fbaa95 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.3.6.0 +version: 6.4.0.4 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -105,6 +105,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20241220_initial Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission + Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope else exposed-modules: Simplex.Chat.Archive @@ -236,6 +237,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission + Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope other-modules: Paths_simplex_chat hs-source-dirs: @@ -254,6 +256,7 @@ library , constraints >=0.12 && <0.14 , containers ==0.6.* , crypton ==0.34.* + , crypton-x509 ==1.7.* , data-default ==0.7.* , directory ==1.3.* , email-validate ==2.3.* @@ -269,7 +272,6 @@ library , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* - , scientific ==0.3.7.* , simple-logger ==0.1.* , simplexmq >=6.3 , socks ==0.6.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6b554d29c4..e14275b75c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -44,6 +44,7 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 73a2970c61..9b92c8b800 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -95,7 +95,7 @@ deleteMessage cc ct chatItemId = do r -> putStrLn $ "unexpected delete message response: " <> show r contactRef :: Contact -> ChatRef -contactRef = ChatRef CTDirect . contactId' +contactRef ct = ChatRef CTDirect (contactId' ct) Nothing printLog :: ChatController -> ChatLogLevel -> String -> IO () printLog cc level s diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 02e2e121b3..92fc6ed19f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -87,7 +87,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) -import Simplex.Messaging.Transport (TLS, simplexMQVersion) +import Simplex.Messaging.Transport (TLS, TransportPeer (..), simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>)) import Simplex.RemoteControl.Client @@ -360,16 +360,20 @@ data ChatCommand | APIAddMember GroupId ContactId GroupMemberRole | APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter} | APIAcceptMember GroupId GroupMemberId GroupMemberRole + | APIDeleteMemberSupportChat GroupId GroupMemberId | APIMembersRole GroupId (NonEmpty GroupMemberId) GroupMemberRole | APIBlockMembersForAll GroupId (NonEmpty GroupMemberId) Bool | APIRemoveMembers {groupId :: GroupId, groupMemberIds :: Set GroupMemberId, withMessages :: Bool} | APILeaveGroup GroupId | APIListMembers GroupId + -- | APIDeleteGroupConversations GroupId (NonEmpty GroupConversationId) + -- | APIArchiveGroupConversations GroupId (NonEmpty GroupConversationId) | APIUpdateGroupProfile GroupId GroupProfile | APICreateGroupLink GroupId GroupMemberRole CreateShortLink | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId + | APIAddGroupShortLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} | GetUserProtoServers AProtocolType @@ -458,6 +462,7 @@ data ChatCommand | DeleteMyAddress | APIShowMyAddress UserId | ShowMyAddress + | APIAddMyAddressShortLink UserId | APISetProfileAddress UserId Bool | SetProfileAddress Bool | APIAddressAutoAccept UserId (Maybe AutoAccept) @@ -467,7 +472,7 @@ data ChatCommand | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} - | SendMessage ChatName Text + | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | SendLiveMessage ChatName Text | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text} @@ -481,6 +486,7 @@ data ChatCommand | NewGroup IncognitoEnabled GroupProfile | AddMember GroupName ContactName GroupMemberRole | JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter} + | AcceptMember GroupName ContactName GroupMemberRole | MemberRole GroupName ContactName GroupMemberRole | BlockForAll GroupName ContactName Bool | RemoveMembers {groupName :: GroupName, members :: Set ContactName, withMessages :: Bool} @@ -488,6 +494,7 @@ data ChatCommand | DeleteGroup GroupName | ClearGroup GroupName | ListMembers GroupName + | ListMemberSupportChats GroupName | APIListGroups UserId (Maybe ContactId) (Maybe String) | ListGroups (Maybe ContactName) (Maybe String) | UpdateGroupNames GroupName GroupProfile @@ -523,6 +530,7 @@ data ChatCommand | SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed) | SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled | SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole) + | SetGroupMemberAdmissionReview GroupName (Maybe MemberCriteria) | SetUserTimedMessages Bool -- UserId (not used in UI) | SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled) | SetGroupTimedMessages GroupName (Maybe Int) @@ -626,9 +634,9 @@ data ChatResponse | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} | CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} - | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} + | CEvtGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} - | CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} + | CEvtGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactRatchetSyncStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberRatchetSyncStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} @@ -648,6 +656,9 @@ data ChatResponse | CRWelcome {user :: User} | CRGroupCreated {user :: User, groupInfo :: GroupInfo} | CRGroupMembers {user :: User, group :: Group} + | CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]} + -- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]} + -- | CRGroupConversationsDeleted {user :: User, groupInfo :: GroupInfo, deletedGroupConversations :: [GroupConversation]} | CRContactsList {user :: User, contacts :: [Contact]} | CRUserContactLink {user :: User, contactLink :: UserContactLink} | CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink} @@ -669,6 +680,7 @@ data ChatResponse | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} + | CRItemsReadForChat {user :: User, chatInfo :: AChatInfo} | CRContactDeleted {user :: User, contact :: Contact} | CRChatCleared {user :: User, chatInfo :: AChatInfo} | CRUserContactLinkCreated {user :: User, connLinkContact :: CreatedLinkContact} @@ -694,6 +706,8 @@ data ChatResponse | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} | CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRMemberAccepted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} + | CRMemberSupportChatDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole} | CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool} | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} @@ -723,7 +737,7 @@ data ChatResponse | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} #endif - | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} + | CRDebugLocks {chatLockName :: Maybe Text, chatEntityLocks :: Map Text Text, agentLocks :: AgentLocks} | CRAgentSubsTotal {user :: User, subsTotal :: SMPServerSubs, hasSession :: Bool} | CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} @@ -802,6 +816,7 @@ data ChatEvent | CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CEvtJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- there is the same command response | CEvtJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} + | CEvtMemberAcceptedByOther {user :: User, groupInfo :: GroupInfo, acceptingMember :: GroupMember, member :: GroupMember} | CEvtMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole} | CEvtMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool} | CEvtConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact} @@ -828,7 +843,7 @@ data ChatEvent | CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo} | CEvtRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} | CEvtRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} - | CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} + | CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} | CEvtRemoteCtrlStopped {rcsState :: RemoteCtrlSessionState, rcStopReason :: RemoteCtrlStopReason} | CEvtContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CEvtContactDisabled {user :: User, contact :: Contact} @@ -841,6 +856,7 @@ data ChatEvent | CEvtChatErrors {chatErrors :: [ChatError]} | CEvtTimedAction {action :: String, durationMilliseconds :: Int64} | CEvtTerminalEvent TerminalEvent + | CEvtCustomChatEvent {user_ :: Maybe User, response :: Text} deriving (Show) data TerminalEvent @@ -899,16 +915,15 @@ logEventToFile = \case _ -> False _ -> False --- (Maybe GroupMemberId) can later be changed to GroupSndScope = GSSAll | GSSAdmins | GSSMember GroupMemberId data SendRef = SRDirect ContactId - | SRGroup GroupId (Maybe GroupMemberId) + | SRGroup GroupId (Maybe GroupChatScope) deriving (Eq, Show) sendToChatRef :: SendRef -> ChatRef sendToChatRef = \case - SRDirect cId -> ChatRef CTDirect cId - SRGroup gId _ -> ChatRef CTGroup gId + SRDirect cId -> ChatRef CTDirect cId Nothing + SRGroup gId scope -> ChatRef CTGroup gId scope data ChatPagination = CPLast Int @@ -1400,7 +1415,7 @@ data RemoteCtrlSession { remoteCtrlId_ :: Maybe RemoteCtrlId, ctrlDeviceName :: Text, rcsClient :: RCCtrlClient, - tls :: TLS, + tls :: TLS 'TClient, sessionCode :: Text, rcsWaitSession :: Async (), rcsWaitConfirmation :: TMVar (Either RCErrorType (RCCtrlSession, RCCtrlPairing)) @@ -1408,7 +1423,7 @@ data RemoteCtrlSession | RCSessionConnected { remoteCtrlId :: RemoteCtrlId, rcsClient :: RCCtrlClient, - tls :: TLS, + tls :: TLS 'TClient, rcsSession :: RCCtrlSession, http2Server :: Async (), remoteOutputQ :: TBQueue (Either ChatError ChatEvent) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 8c475b111b..78c2d1cc4f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -27,6 +27,7 @@ import Control.Monad.Reader import qualified Data.Aeson as J import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.Attoparsec.Combinator as A import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -87,6 +88,7 @@ import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, m import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Agent.Store.Interface (execSQL) import Simplex.Messaging.Agent.Store.Shared (upMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -196,7 +198,7 @@ startChatController mainApp enableSndFiles = do startExpireCIThread user setExpireCIFlag user True where - shouldExpireChats = + shouldExpireChats = fmap (fromRight False) $ runExceptT $ withStore' $ \db -> do ttl <- getChatItemTTL db user ttlCount <- getChatTTLCount db user @@ -500,17 +502,17 @@ processChatCommand' vr = \case (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) pure $ CRApiChats user previews - APIGetChat (ChatRef cType cId) contentFilter pagination search -> withUser $ \user -> case cType of + APIGetChat (ChatRef cType cId scope_) contentFilter pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do - when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" + when (isJust contentFilter) $ throwCmdError "content filter not supported" (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId pagination search) pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do - (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId contentFilter pagination search) + (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search) pure $ CRApiChat user (AChat SCTGroup groupChat) navInfo CTLocal -> do - when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" + when (isJust contentFilter) $ throwCmdError "content filter not supported" (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) pure $ CRApiChat user (AChat SCTLocal localChat) navInfo CTContactRequest -> throwCmdError "not implemented" @@ -531,29 +533,30 @@ processChatCommand' vr = \case getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) -> - Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId) + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId Nothing) fwdItemId) Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> - Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) + -- TODO [knocking] getAChatItem doesn't differentiate how to read based on scope - it should, instead of using group filter + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId Nothing) fwdItemId) _ -> pure Nothing APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of SRDirect chatId -> do mapM_ assertNoMentions cms withContactLock "sendMessage" chatId $ sendContactContentMessages user chatId live itemTTL (L.map composedMessageReq cms) - SRGroup chatId directMemId_ -> + SRGroup chatId gsScope -> withGroupLock "sendMessage" chatId $ do (gInfo, cmrs) <- withFastStore $ \db -> do g <- getGroupInfo db vr user chatId (g,) <$> mapM (composedMessageReqMentions db user g) cms - sendGroupContentMessages user gInfo directMemId_ live itemTTL cmrs + sendGroupContentMessages user gInfo gsScope live itemTTL cmrs APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do _ <- createChatTag db user emoji text CRChatTags user <$> getUserChatTags db user - APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> case cType of + APISetChatTags (ChatRef cType chatId scope) tagIds -> withUser $ \user -> case cType of CTDirect -> withFastStore' $ \db -> do updateDirectChatTags db chatId (maybe [] L.toList tagIds) CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId - CTGroup -> withFastStore' $ \db -> do + CTGroup | isNothing scope -> withFastStore' $ \db -> do updateGroupChatTags db chatId (maybe [] L.toList tagIds) CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId _ -> throwCmdError "not supported" @@ -571,26 +574,18 @@ processChatCommand' vr = \case createNoteFolderContentItems user folderId (L.map composedMessageReq cms) APIReportMessage gId reportedItemId reportReason reportText -> withUser $ \user -> withGroupLock "reportMessage" gId $ do - (gInfo, ms) <- - withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user gId - (gInfo,) <$> liftIO (getGroupModerators db vr user gInfo) - let ms' = filter compatibleModerator ms - mc = MCReport reportText reportReason + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + let mc = MCReport reportText reportReason cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} - when (null ms') $ throwChatError $ CECommandError "no moderators support receiving reports" - let numFileInvs = length $ filter memberCurrent ms' - sendGroupContentMessages_ user gInfo Nothing ms' numFileInvs False Nothing [composedMessageReq cm] - where - compatibleModerator GroupMember {activeConn, memberChatVRange} = - maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= contentReportsVersion + -- TODO [knocking] reports sent to support scope may be wrong + sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False Nothing [composedMessageReq cm] ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage processChatCommand $ APIReportMessage gId reportedItemId reportReason "" - APIUpdateChatItem (ChatRef cType chatId) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of + APIUpdateChatItem (ChatRef cType chatId scope) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do - unless (null mentions) $ throwChatError $ CECommandError "mentions are not supported in this chat" + unless (null mentions) $ throwCmdError "mentions are not supported in this chat" ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ cci <- withFastStore $ \db -> getDirectCIWithReactions db user ct itemId @@ -601,37 +596,41 @@ processChatCommand' vr = \case let changed = mc /= oldMC if changed || fromMaybe False itemLive then do - (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XMsgUpdate itemSharedMId mc M.empty (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + let event = XMsgUpdate itemSharedMId mc M.empty (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) Nothing + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct event ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) let edited = itemLive /= Just True updateDirectChatItem' db user contactId ci (CISndMsgContent mc) edited live Nothing $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + startUpdatedTimedItemThread user (ChatRef CTDirect contactId Nothing) ci ci' pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> withGroupLock "updateChatItem" chatId $ do - Group gInfo@GroupInfo {groupId, membership} ms <- withFastStore $ \db -> getGroup db vr user chatId - assertUserGroupRole gInfo GRAuthor + gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId + when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor let (_, ft_) = msgContentTexts mc if prohibitedSimplexLinks gInfo membership ft_ then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) else do + -- TODO [knocking] check chat item scope? cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of (CISndMsgContent oldMC, Just itemSharedMId, True) -> do + (chatScopeInfo, recipients) <- getGroupRecipients vr user gInfo scope groupKnockingVersion let changed = mc /= oldMC if changed || fromMaybe False itemLive then do ciMentions <- withFastStore $ \db -> getCIMentions db user gInfo ft_ mentions - let mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions - -- TODO [knocking] send separately to pending approval member - SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + let msgScope = toMsgScope gInfo <$> chatScopeInfo + mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions + event = XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) msgScope + SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients event ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ @@ -639,13 +638,13 @@ processChatCommand' vr = \case let edited = itemLive /= Just True ci' <- updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId updateGroupCIMentions db gInfo ci' ciMentions - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') - else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + startUpdatedTimedItemThread user (ChatRef CTGroup groupId scope) ci ci' + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo) ci') + else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo) ci) _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTLocal -> do - unless (null mentions) $ throwChatError $ CECommandError "mentions are not supported in this chat" + unless (null mentions) $ throwCmdError "mentions are not supported in this chat" (nf@NoteFolder {noteFolderId}, cci) <- withFastStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId case cci of CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent oldMC} @@ -658,7 +657,7 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> throwCmdError "not supported" CTContactConnection -> throwCmdError "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of + APIDeleteChatItem (ChatRef cType chatId scope) itemIds mode -> withUser $ \user -> case cType of CTDirect -> withContactLock "deleteChatItem" chatId $ do (ct, items) <- getCommandDirectChatItems user chatId itemIds deletions <- case mode of @@ -677,18 +676,22 @@ processChatCommand' vr = \case pure $ CRChatItemsDeleted user deletions True False CTGroup -> withGroupLock "deleteChatItem" chatId $ do (gInfo, items) <- getCommandGroupChatItems user chatId itemIds + -- TODO [knocking] check scope for all items? deletions <- case mode of - CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime - CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternal -> do + chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + deleteGroupCIs user gInfo chatScopeInfo items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> do + chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + markGroupCIsDeleted user gInfo chatScopeInfo items Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + (chatScopeInfo, recipients) <- getGroupRecipients vr user gInfo scope groupKnockingVersion assertDeletable items assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier let msgIds = itemsMsgIds items events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds - -- TODO [knocking] validate: only current members or only single pending approval member - mapM_ (sendGroupMessages user gInfo ms) events - delGroupChatItems user gInfo items False + mapM_ (sendGroupMessages user gInfo Nothing recipients) events + delGroupChatItems user gInfo chatScopeInfo items False pure $ CRChatItemsDeleted user deletions True False CTLocal -> do (nf, items) <- getCommandLocalChatItems user chatId itemIds @@ -711,8 +714,10 @@ processChatCommand' vr = \case itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds + -- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?) ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo - deletions <- delGroupChatItemsForMembers user gInfo ms items + let recipients = filter memberCurrent ms + deletions <- delGroupChatItemsForMembers user gInfo Nothing recipients items pure $ CRChatItemsDeleted user deletions True False APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do g <- getGroupInfo db vr user gId @@ -721,27 +726,30 @@ processChatCommand' vr = \case pure $ CRGroupChatItemsDeleted user g ciIds True (Just $ membership g) APIDeleteReceivedReports gId itemIds mode -> withUser $ \user -> withGroupLock "deleteReports" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds - unless (all isRcvReport items) $ throwChatError $ CECommandError "some items are not received reports" + unless (all isRcvReport items) $ throwCmdError "some items are not received reports" + -- TODO [knocking] scope can be different for each item if reports are from different members + -- TODO (currently we pass Nothing as scope which is wrong) deletions <- case mode of - CIDMInternal -> deleteGroupCIs user gInfo items Nothing =<< liftIO getCurrentTime - CIDMInternalMark -> markGroupCIsDeleted user gInfo items Nothing =<< liftIO getCurrentTime + CIDMInternal -> deleteGroupCIs user gInfo Nothing items Nothing =<< liftIO getCurrentTime + CIDMInternalMark -> markGroupCIsDeleted user gInfo Nothing items Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do ms <- withFastStore' $ \db -> getGroupModerators db vr user gInfo - delGroupChatItemsForMembers user gInfo ms items + let recipients = filter memberCurrent ms + delGroupChatItemsForMembers user gInfo Nothing recipients items pure $ CRChatItemsDeleted user deletions True False where isRcvReport = \case CChatItem _ ChatItem {content = CIRcvMsgContent (MCReport {})} -> True _ -> False - APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of + APIChatItemReaction (ChatRef cType chatId scope) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> withContactLock "chatItemReaction" chatId $ withFastStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do unless (featureAllowed SCFReactions forUser ct) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions) unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") + throwCmdError "reaction not allowed - chat item has no content" rs <- withFastStore' $ \db -> getDirectReactions db ct itemSharedMId True checkReactionAllowed rs (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add @@ -752,46 +760,48 @@ processChatCommand' vr = \case let ci' = CChatItem md ci {reactions} r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + _ -> throwCmdError "reaction not possible - no shared item ID" CTGroup -> withGroupLock "chatItemReaction" chatId $ do - (Group g@GroupInfo {membership} ms, CChatItem md ci) <- withFastStore $ \db -> do - gr@(Group g _) <- getGroup db vr user chatId - (gr,) <$> getGroupCIWithReactions db user g itemId + -- TODO [knocking] check chat item scope? + (g@GroupInfo {membership}, CChatItem md ci) <- withFastStore $ \db -> do + g <- getGroupInfo db vr user chatId + (g,) <$> getGroupCIWithReactions db user g itemId + (chatScopeInfo, recipients) <- getGroupRecipients vr user g scope groupKnockingVersion case ci of ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} -> do unless (groupFeatureAllowed SGFReactions g) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions) unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") + throwCmdError "reaction not allowed - chat item has no content" let GroupMember {memberId = itemMemberId} = chatItemMember g ci rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs - -- TODO [knocking] send separately to pending approval member - SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + SndMessage {msgId} <- sendGroupMessage user g scope recipients (XMsgReact itemSharedMId (Just itemMemberId) reaction add) createdAt <- liftIO getCurrentTime reactions <- withFastStore' $ \db -> do setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction + r = ACIReaction SCTGroup SMDSnd (GroupChat g chatScopeInfo) $ CIReaction CIGroupSnd ci' createdAt reaction pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + _ -> throwCmdError "invalid reaction" CTLocal -> throwCmdError "not supported" CTContactRequest -> throwCmdError "not supported" CTContactConnection -> throwCmdError "not supported" where checkReactionAllowed rs = do when ((reaction `elem` rs) == add) $ - throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") + throwCmdError $ "reaction already " <> if add then "added" else "removed" when (add && length rs >= maxMsgReactions) $ - throwChatError (CECommandError "too many reactions") + throwCmdError "too many reactions" APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do memberReactions <- withStore $ \db -> do CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction pure $ CRReactionMembers user memberReactions - APIPlanForwardChatItems (ChatRef fromCType fromChatId) itemIds -> withUser $ \user -> case fromCType of + -- TODO [knocking] forward from scope? + APIPlanForwardChatItems (ChatRef fromCType fromChatId _scope) itemIds -> withUser $ \user -> case fromCType of CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds @@ -837,7 +847,8 @@ processChatCommand' vr = \case MCFile t -> t /= "" MCReport {} -> True MCUnknown {} -> True - APIForwardChatItems toChat@(ChatRef toCType toChatId) fromChat@(ChatRef fromCType fromChatId) itemIds itemTTL -> withUser $ \user -> case toCType of + -- TODO [knocking] forward from / to scope + APIForwardChatItems toChat@(ChatRef toCType toChatId toScope) fromChat@(ChatRef fromCType fromChatId _fromScope) itemIds itemTTL -> withUser $ \user -> case toCType of CTDirect -> do cmrs <- prepareForward user case L.nonEmpty cmrs of @@ -851,7 +862,7 @@ processChatCommand' vr = \case Just cmrs' -> withGroupLock "forwardChatItem, to group" toChatId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user toChatId - sendGroupContentMessages user gInfo Nothing False itemTTL cmrs' + sendGroupContentMessages user gInfo toScope False itemTTL cmrs' Nothing -> pure $ CRNewChatItems user [] CTLocal -> do cmrs <- prepareForward user @@ -878,6 +889,7 @@ processChatCommand' vr = \case forwardName Contact {profile = LocalProfile {displayName, localAlias}} | localAlias /= "" = localAlias | otherwise = displayName + -- TODO [knocking] from scope? CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do (gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items @@ -903,8 +915,8 @@ processChatCommand' vr = \case ciComposeMsgReq (CChatItem _ ci) (mc', file) = let ciff = forwardCIFF ci Nothing in (composedMessage file mc', ciff, msgContentTexts mc', M.empty) - CTContactRequest -> throwChatError $ CECommandError "not supported" - CTContactConnection -> throwChatError $ CECommandError "not supported" + CTContactRequest -> throwCmdError "not supported" + CTContactConnection -> throwCmdError "not supported" where prepareMsgReq :: CChatItem c -> CM (Maybe (MsgContent, Maybe CryptoFile)) prepareMsgReq (CChatItem _ ci) = forwardMsgContent ci $>>= forwardContent ci @@ -977,7 +989,7 @@ processChatCommand' vr = \case pure $ prefix <> formattedDate <> ext APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId - APIChatRead chatRef@(ChatRef cType chatId) -> withUser $ \_ -> case cType of + APIChatRead chatRef@(ChatRef cType chatId scope) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId ts <- liftIO getCurrentTime @@ -988,11 +1000,14 @@ processChatCommand' vr = \case forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user CTGroup -> do - user <- withFastStore $ \db -> getUserByGroupId db chatId + (user, gInfo) <- withFastStore $ \db -> do + user <- getUserByGroupId db chatId + gInfo <- getGroupInfo db vr user chatId + pure (user, gInfo) ts <- liftIO getCurrentTime timedItems <- withFastStore' $ \db -> do timedItems <- getGroupUnreadTimedItems db user chatId - updateGroupChatItemsRead db user chatId + updateGroupChatItemsRead db user gInfo scope setGroupChatItemsDeleteAt db user chatId timedItems ts forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user @@ -1002,31 +1017,40 @@ processChatCommand' vr = \case ok user CTContactRequest -> throwCmdError "not supported" CTContactConnection -> throwCmdError "not supported" - APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of + APIChatItemsRead chatRef@(ChatRef cType chatId scope) itemIds -> withUser $ \_ -> case cType of CTDirect -> do - user <- withFastStore $ \db -> getUserByContactId db chatId + (user, ct) <- withFastStore $ \db -> do + user <- getUserByContactId db chatId + ct <- getContact db vr user chatId + pure (user, ct) timedItems <- withFastStore' $ \db -> do timedItems <- updateDirectChatItemsReadList db user chatId itemIds setDirectChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt - ok user + pure $ CRItemsReadForChat user (AChatInfo SCTDirect $ DirectChat ct) CTGroup -> do - user <- withFastStore $ \db -> getUserByGroupId db chatId - timedItems <- withFastStore' $ \db -> do - timedItems <- updateGroupChatItemsReadList db user chatId itemIds - setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + (user, gInfo) <- withFastStore $ \db -> do + user <- getUserByGroupId db chatId + gInfo <- getGroupInfo db vr user chatId + pure (user, gInfo) + chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + (timedItems, gInfo') <- withFastStore $ \db -> do + (timedItems, gInfo') <- updateGroupChatItemsReadList db vr user gInfo chatScopeInfo itemIds + timedItems' <- liftIO $ setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime + pure (timedItems', gInfo') forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt - ok user + pure $ CRItemsReadForChat user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing) CTLocal -> throwCmdError "not supported" CTContactRequest -> throwCmdError "not supported" CTContactConnection -> throwCmdError "not supported" - APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of + APIChatUnread (ChatRef cType chatId scope) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do ct <- getContact db vr user chatId liftIO $ updateContactUnreadChat db user ct unreadChat ok user - CTGroup -> do + -- TODO [knocking] set support chat as unread? + CTGroup | isNothing scope -> do withFastStore $ \db -> do Group {groupInfo} <- getGroup db vr user chatId liftIO $ updateGroupUnreadChat db user groupInfo unreadChat @@ -1037,7 +1061,7 @@ processChatCommand' vr = \case liftIO $ updateNoteFolderUnreadChat db user nf unreadChat ok user _ -> throwCmdError "not supported" - APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of + APIDeleteChat cRef@(ChatRef cType chatId scope) cdm -> withUser $ \user@User {userId} -> case cType of CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct @@ -1076,7 +1100,7 @@ processChatCommand' vr = \case deleteAgentConnectionAsync acId withFastStore' $ \db -> deletePendingContactConnection db userId chatId pure $ CRContactConnectionDeleted user conn - CTGroup -> do + CTGroup | isNothing scope -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user chatId let GroupMember {memberRole = membershipMemRole} = membership let isOwner = membershipMemRole == GROwner @@ -1086,8 +1110,8 @@ processChatCommand' vr = \case withGroupLock "deleteChat group" chatId . procCmd $ do deleteCIFiles user filesInfo let doSendDel = memberActive membership && isOwner - -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) - when doSendDel . void $ sendGroupMessage' user gInfo members XGrpDel + recipients = filter memberCurrentOrPending members + when doSendDel . void $ sendGroupMessage' user gInfo recipients XGrpDel deleteGroupLinkIfExists user gInfo deleteMembersConnections' user members doSendDel updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure () @@ -1096,23 +1120,22 @@ processChatCommand' vr = \case withFastStore' $ \db -> deleteGroupMembers db user gInfo withFastStore' $ \db -> deleteGroup db user gInfo pure $ CRGroupDeletedUser user gInfo - CTLocal -> throwCmdError "not supported" - CTContactRequest -> throwCmdError "not supported" - APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of + _ -> throwCmdError "not supported" + APIClearChat (ChatRef cType chatId scope) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct deleteCIFiles user filesInfo withFastStore' $ \db -> deleteContactCIs db user ct pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) - CTGroup -> do + CTGroup | isNothing scope -> do gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo deleteCIFiles user filesInfo 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) + pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo Nothing) CTLocal -> do nf <- withFastStore $ \db -> getNoteFolder db user chatId filesInfo <- withFastStore' $ \db -> getNoteFolderFileInfo db user nf @@ -1120,8 +1143,7 @@ processChatCommand' vr = \case withFastStore' $ \db -> deleteNoteFolderFiles db userId nf withFastStore' $ \db -> deleteNoteFolderCIs db user nf pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) - CTContactConnection -> throwCmdError "not supported" - CTContactRequest -> throwCmdError "not supported" + _ -> throwCmdError "not supported" APIAcceptContact incognito connReqId -> withUser $ \_ -> do userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId withUserContactLock "acceptContact" userContactLinkId $ do @@ -1168,7 +1190,7 @@ processChatCommand' vr = \case forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] ok user - else throwCmdError ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) + else throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFCalls) SendCallInvitation cName callType -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName processChatCommand $ APISendCallInvitation contactId callType @@ -1181,7 +1203,7 @@ processChatCommand' vr = \case timed_ <- contactCITimed ct updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) + startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId) pure Nothing _ -> throwChatError . CECallState $ callStateTag callState APISendCallOffer contactId WebRTCCallOffer {callType, rtcSession} -> @@ -1271,13 +1293,13 @@ processChatCommand' vr = \case pure user' when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes} ok user' - APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of + APISetChatUIThemes (ChatRef cType chatId scope) uiThemes -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do ct <- getContact db vr user chatId liftIO $ setContactUIThemes db user ct uiThemes ok user - CTGroup -> do + CTGroup | isNothing scope -> do withFastStore $ \db -> do g <- getGroupInfo db vr user chatId liftIO $ setGroupUIThemes db user g uiThemes @@ -1312,9 +1334,9 @@ processChatCommand' vr = \case SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) case L.nonEmpty userServers_ of - Nothing -> throwChatError $ CECommandError "no servers" + Nothing -> throwCmdError "no servers" Just userServers -> case srvs of - [] -> throwChatError $ CECommandError "no servers" + [] -> throwCmdError "no servers" _ -> do srvs' <- mapM aUserServer srvs processChatCommand $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers @@ -1322,7 +1344,7 @@ processChatCommand' vr = \case aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of Just Refl -> pure $ AUS SDBNew $ newUserServer srv - Nothing -> throwChatError $ CECommandError $ "incorrect server protocol: " <> B.unpack (strEncode srv) + Nothing -> throwCmdError $ "incorrect server protocol: " <> B.unpack (strEncode srv) APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> @@ -1362,7 +1384,7 @@ processChatCommand' vr = \case CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do errors <- validateAllUsersServers userId $ L.toList userServers - unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) + unless (null errors) $ throwCmdError $ "user servers validation error(s): " <> show errors uss <- withFastStore $ \db -> do ts <- liftIO getCurrentTime mapM (setUserServers db user ts) userServers @@ -1396,7 +1418,7 @@ processChatCommand' vr = \case currentTs <- liftIO getCurrentTime acceptConditions db condId opIds currentTs CRServerOperatorConditions <$> getServerOperators db - APISetChatTTL userId (ChatRef cType chatId) newTTL_ -> + APISetChatTTL userId (ChatRef cType chatId scope) newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatTTL" $ do (oldTTL_, globalTTL, ttlCount) <- withStore' $ \db -> (,,) <$> getSetChatTTL db <*> getChatItemTTL db user <*> getChatTTLCount db user @@ -1410,25 +1432,26 @@ processChatCommand' vr = \case where getSetChatTTL db = case cType of CTDirect -> getDirectChatTTL db chatId <* setDirectChatTTL db chatId newTTL_ - CTGroup -> getGroupChatTTL db chatId <* setGroupChatTTL db chatId newTTL_ + CTGroup | isNothing scope -> getGroupChatTTL db chatId <* setGroupChatTTL db chatId newTTL_ _ -> pure Nothing expireChat user globalTTL = do currentTs <- liftIO getCurrentTime case cType of CTDirect -> expireContactChatItems user vr globalTTL chatId - CTGroup -> + CTGroup | isNothing scope -> let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs in expireGroupChatItems user vr globalTTL createdAtCutoff chatId - _ -> throwChatError $ CECommandError "not supported" + _ -> throwCmdError "not supported" SetChatTTL chatName newTTL -> withUser' $ \user@User {userId} -> do chatRef <- getChatRef user chatName processChatCommand $ APISetChatTTL userId chatRef newTTL GetChatTTL chatName -> withUser' $ \user -> do - ChatRef cType chatId <- getChatRef user chatName + -- TODO [knocking] support scope in CLI apis + ChatRef cType chatId _ <- getChatRef user chatName ttl <- case cType of CTDirect -> withFastStore' (`getDirectChatTTL` chatId) CTGroup -> withFastStore' (`getGroupChatTTL` chatId) - _ -> throwChatError $ CECommandError "not supported" + _ -> throwCmdError "not supported" pure $ CRChatItemTTL user ttl APISetChatItemTTL userId newTTL -> withUserId userId $ \user -> checkStoreNotChanged $ @@ -1459,7 +1482,7 @@ processChatCommand' vr = \case ReconnectServer userId srv -> withUserId userId $ \user -> do lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv) ok_ - APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of + APISetChatSettings (ChatRef cType chatId scope) chatSettings -> withUser $ \user -> case cType of CTDirect -> do ct <- withFastStore $ \db -> do ct <- getContact db vr user chatId @@ -1468,7 +1491,7 @@ processChatCommand' vr = \case forM_ (contactConnId ct) $ \connId -> withAgent $ \a -> toggleConnectionNtfs a connId (chatHasNtfs chatSettings) ok user - CTGroup -> do + CTGroup | isNothing scope -> do ms <- withFastStore $ \db -> do Group _ ms <- getGroup db vr user chatId liftIO $ updateGroupSettings db user chatId chatSettings @@ -1522,7 +1545,7 @@ processChatCommand' vr = \case case memberConnId m of Just connId -> do connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId) - pure $ CRGroupMemberSwitchStarted user g m connectionStats + pure $ CEvtGroupMemberSwitchStarted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APIAbortSwitchContact contactId -> withUser $ \user -> do ct <- withFastStore $ \db -> getContact db vr user contactId @@ -1536,7 +1559,7 @@ processChatCommand' vr = \case case memberConnId m of Just connId -> do connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId - pure $ CRGroupMemberSwitchAborted user g m connectionStats + pure $ CEvtGroupMemberSwitchAborted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do ct <- withFastStore $ \db -> getContact db vr user contactId @@ -1551,8 +1574,9 @@ processChatCommand' vr = \case case memberConnId m of Just connId -> do cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force - createInternalChatItem user (CDGroupSnd g) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m) Nothing - pure $ CRGroupMemberRatchetSyncStarted user g m cStats + (g', m', scopeInfo) <- mkGroupChatScope g m + createInternalChatItem user (CDGroupSnd g' scopeInfo) (CISndConnEvent . SCERatchetSync rss . Just $ groupMemberRef m') Nothing + pure $ CRGroupMemberRatchetSyncStarted user g' m' cStats _ -> throwChatError CEGroupMemberNotActive APIGetContactCode contactId -> withUser $ \user -> do ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId @@ -1643,7 +1667,8 @@ processChatCommand' vr = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -1671,14 +1696,30 @@ processChatCommand' vr = \case case (pccConnStatus, connLinkInv) of (ConnNew, Just (CCLink cReqInv _)) -> do newUser <- privateGetUser newUserId - conn' <- recreateConn user conn newUser + conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) pure $ CRConnectionUserChanged user conn conn' newUser _ -> throwChatError CEConnectionUserChangeProhibited where + canKeepLink :: ConnReqInvitation -> User -> CM Bool + canKeepLink (CRInvitationUri crData _) newUser = do + let ConnReqUriData {crSmpQueues = q :| _} = crData + SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q + newUserServers <- + map protoServer' . L.filter (\ServerCfg {enabled} -> enabled) + <$> getKnownAgentServers SPSMP newUser + pure $ smpServer `elem` newUserServers + updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do + withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) + withFastStore' $ \db -> do + conn' <- updatePCCUser db userId conn newUserId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + pure conn' recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv - (agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -1708,7 +1749,7 @@ processChatCommand' vr = \case -- retrying join after error pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId joinPreparedConn (aConnId conn) pcc dm - Just ent -> throwChatError $ CECommandError $ "connection exists: " <> show (connEntityInfo ent) + Just ent -> throwCmdError $ "connection exists: " <> show (connEntityInfo ent) where joinNewConn chatV dm = do connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' @@ -1731,19 +1772,19 @@ processChatCommand' vr = \case Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId - when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") + when (isJust activeConn) $ throwCmdError "contact already has connection" ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do cReq <- getShortLinkConnReq user sLnk pure $ CCLink cReq $ Just sLnk - Nothing -> throwChatError (CECommandError "no address in contact profile") + Nothing -> throwCmdError "no address in contact profile" connectContactViaAddress user incognito ct ccLink ConnectSimplex incognito -> withUser $ \user -> do plan <- contactRequestPlan user adminContactReq `catchChatError` const (pure $ CPContactAddress CAPOk) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan - DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm - ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect + DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm + ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing APIListContacts userId -> withUserId userId $ \user -> CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> @@ -1751,16 +1792,17 @@ processChatCommand' vr = \case APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode let userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode pure $ CRUserContactLinkCreated user ccLink' CreateMyAddress short -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId short APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do - conns <- withFastStore $ \db -> getUserAddressConnections db vr user + conn <- withFastStore $ \db -> getUserAddressConnection db vr user withChatLock "deleteMyAddress" $ do - deleteAgentConnectionsAsync $ map aConnId conns + deleteAgentConnectionAsync $ aConnId conn withFastStore' (`deleteUserAddress` user) let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -1774,6 +1816,17 @@ processChatCommand' vr = \case CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> processChatCommand $ APIShowMyAddress userId + APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> do + (ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_}, conn) <- + withFastStore $ \db -> (,) <$> getUserAddress db user <*> getUserAddressConnection db vr user + when (isJust sLnk_) $ throwCmdError "address already has short link" + sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact "" Nothing) + case entityId conn of + Just uclId -> do + withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk + let ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk)} + pure $ CRUserContactLink user ucl' + Nothing -> throwChatError $ CEException "no user contact link id" APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -1786,7 +1839,7 @@ processChatCommand' vr = \case processChatCommand $ APISetProfileAddress userId onOff APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} -> - when (businessAddress && acceptIncognito) $ throwChatError $ CECommandError "requests to business address cannot be accepted incognito" + when (businessAddress && acceptIncognito) $ throwCmdError "requests to business address cannot be accepted incognito" contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) pure $ CRUserContactLinkUpdated user contactLink AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> @@ -1801,21 +1854,21 @@ processChatCommand' vr = \case contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId) (forwardedItemId :| []) Nothing + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId) (forwardedItemId :| []) Nothing + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId) (forwardedItemId :| []) Nothing - SendMessage (ChatName cType name) msg -> withUser $ \user -> do + processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing + SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg - case cType of - CTDirect -> + case sendName of + SNDirect name -> withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let sendRef = SRDirect ctId @@ -1830,18 +1883,18 @@ processChatCommand' vr = \case throwChatError $ CEContactNotFound name (Just suspectedMember) _ -> throwChatError $ CEContactNotFound name Nothing - CTGroup -> do - (gId, mentions) <- withFastStore $ \db -> do + SNGroup name scope_ -> do + (gId, cScope_, mentions) <- withFastStore $ \db -> do gId <- getGroupIdByName db user name - (gId,) <$> liftIO (getMessageMentions db user gId msg) - let sendRef = SRGroup gId Nothing + cScope_ <- + forM scope_ $ \(GSNMemberSupport mName_) -> + GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_ + (gId,cScope_,) <$> liftIO (getMessageMentions db user gId msg) + let sendRef = SRGroup gId cScope_ processChatCommand $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] - CTLocal - | name == "" -> do - folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand $ APICreateChatItems folderId [composedMessage Nothing mc] - | otherwise -> throwChatError $ CECommandError "not supported" - _ -> throwChatError $ CECommandError "not supported" + SNLocal -> do + folderId <- withFastStore (`getUserNoteFolderId` user) + processChatCommand $ APICreateChatItems folderId [composedMessage Nothing mc] SendMemberContactMessage gName mName msg -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName m <- withFastStore $ \db -> getGroupMember db vr user gId mId @@ -1849,7 +1902,7 @@ processChatCommand' vr = \case case memberContactId m of Nothing -> do g <- withFastStore $ \db -> getGroupInfo db vr user gId - unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwCmdError "direct messages not allowed" toView $ CEvtNoMemberContactCreating user g m processChatCommand (APICreateMemberContact gId mId) >>= \case CRNewMemberContact _ ct@Contact {contactId} _ _ -> do @@ -1902,7 +1955,7 @@ processChatCommand' vr = \case combineResults _ _ (Left e) = Left e createCI :: DB.Connection -> User -> UTCTime -> (Contact, SndMessage) -> IO () createCI db user createdAt (ct, sndMsg) = - void $ createNewSndChatItem db user (CDDirectSnd ct) Nothing sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt + void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg @@ -1935,7 +1988,7 @@ processChatCommand' vr = \case -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile - let cd = CDGroupSnd gInfo + let cd = CDGroupSnd gInfo Nothing createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo pure $ CRGroupCreated user gInfo @@ -1957,7 +2010,8 @@ processChatCommand' vr = \case Nothing -> do gVar <- asks random subMode <- chatReadVar subscriptionMode - (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + -- TODO [certs rcv] + (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2003,27 +2057,79 @@ processChatCommand' vr = \case updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` eToView pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct - APIAcceptMember groupId gmId role -> withUser $ \user -> do + APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId - assertUserGroupRole gInfo GRAdmin - when (memberStatus m /= GSMemPendingApproval) $ throwChatError $ CECommandError "member is not pending approval" - case memberConn m of - Just mConn -> do - let msg = XGrpLinkAcpt role - void $ sendDirectMemberMessage mConn msg groupId - m' <- withFastStore' $ \db -> updateGroupMemberAccepted db user m role - introduceToGroup vr user gInfo m' - pure $ CRJoinedGroupMember user gInfo m' - _ -> throwChatError CEGroupMemberNotActive + assertUserGroupRole gInfo GRModerator + case memberStatus m of + GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve + let GroupInfo {groupProfile = GroupProfile {memberAdmission}} = gInfo + case memberConn m of + Just mConn -> + case memberAdmission >>= review of + Just MCAll -> do + introduceToModerators vr user gInfo m + withFastStore' $ \db -> updateGroupMemberStatus db userId m GSMemPendingReview + let m' = m {memberStatus = GSMemPendingReview} + pure $ CRMemberAccepted user gInfo m' + Nothing -> do + let msg = XGrpLinkAcpt GAAccepted role (memberId' m) + void $ sendDirectMemberMessage mConn msg groupId + introduceToRemaining vr user gInfo m {memberRole = role} + when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m + (m', gInfo') <- withFastStore' $ \db -> do + m' <- updateGroupMemberAccepted db user m GSMemConnected role + gInfo' <- updateGroupMembersRequireAttention db user gInfo m m' + pure (m', gInfo') + -- create item in both scopes + createInternalChatItem user (CDGroupRcv gInfo' Nothing m') (CIRcvGroupEvent RGEMemberConnected) Nothing + let scopeInfo = Just GCSIMemberSupport {groupMember_ = Just m'} + gEvent = SGEMemberAccepted gmId (fromLocalProfile $ memberProfile m') + createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndGroupEvent gEvent) Nothing + pure $ CRMemberAccepted user gInfo' m' + Nothing -> throwChatError CEGroupMemberNotActive + GSMemPendingReview -> do + let scope = Just $ GCSMemberSupport $ Just (groupMemberId' m) + modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + let rcpModMs' = filter memberCurrent modMs + msg = XGrpLinkAcpt GAAccepted role (memberId' m) + void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg + when (maxVersion (memberChatVRange m) < groupKnockingVersion) $ + forM_ (memberConn m) $ \mConn -> do + let msg2 = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing + void $ sendDirectMemberMessage mConn msg2 groupId + when (memberCategory m == GCInviteeMember) $ do + introduceToRemaining vr user gInfo m {memberRole = role} + when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m + (m', gInfo') <- withFastStore' $ \db -> do + m' <- updateGroupMemberAccepted db user m newMemberStatus role + gInfo' <- updateGroupMembersRequireAttention db user gInfo m m' + pure (m', gInfo') + -- create item in both scopes + createInternalChatItem user (CDGroupRcv gInfo' Nothing m') (CIRcvGroupEvent RGEMemberConnected) Nothing + let scopeInfo = Just GCSIMemberSupport {groupMember_ = Just m'} + gEvent = SGEMemberAccepted gmId (fromLocalProfile $ memberProfile m') + createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndGroupEvent gEvent) Nothing + pure $ CRMemberAccepted user gInfo' m' + where + newMemberStatus = case memberConn m of + Just c | connReady c -> GSMemConnected + _ -> GSMemAnnounced + _ -> throwCmdError "member should be pending approval and invitee, or pending review and not invitee" + APIDeleteMemberSupportChat groupId gmId -> withUser $ \user -> do + (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId + when (isNothing $ supportChat m) $ throwCmdError "member has no support chat" + when (memberPending m) $ throwCmdError "member is pending" + (gInfo', m') <- withFastStore' $ \db -> deleteGroupMemberSupportChat db user gInfo m + pure $ CRMemberSupportChatDeleted user gInfo' m' APIMembersRole groupId memberIds newRole -> withUser $ \user -> withGroupLock "memberRole" groupId . procCmd $ do g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId - when (selfSelected gInfo) $ throwChatError $ CECommandError "can't change role for self" + when (selfSelected gInfo) $ throwCmdError "can't change role for self" let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ - throwChatError $ CECommandError "can't change role of multiple members when admins selected, or new role is admin" - when anyPending $ throwChatError $ CECommandError "can't change role of members pending approval" + throwCmdError "can't change role of multiple members when admins selected, or new role is admin" + when anyPending $ throwCmdError "can't change role of members pending approval" assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems @@ -2040,7 +2146,7 @@ processChatCommand' vr = \case | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin - anyPending' = anyPending || memberStatus == GSMemPendingApproval + anyPending' = anyPending || memberPending m in if | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending') @@ -2066,12 +2172,13 @@ processChatCommand' vr = \case Nothing -> pure ([], [], []) Just memsToChange' -> do let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' - (msgs_, _gsr) <- sendGroupMessages user gInfo members events + recipients = filter memberCurrent members + (msgs_, _gsr) <- sendGroupMessages user gInfo Nothing recipients events let itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) itemsData Nothing False when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) - let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_ pure (errs, changed, acis) where sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c @@ -2085,11 +2192,11 @@ processChatCommand' vr = \case APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user -> withGroupLock "blockForAll" groupId . procCmd $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId - when (selfSelected gInfo) $ throwChatError $ CECommandError "can't block/unblock self" + when (selfSelected gInfo) $ throwCmdError "can't block/unblock self" let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length blockMems /= length memberIds) $ throwChatError CEGroupMemberNotFound - when (length memberIds > 1 && anyAdmin) $ throwChatError $ CECommandError "can't block/unblock multiple members when admins selected" - when anyPending $ throwChatError $ CECommandError "can't block/unblock members pending approval" + when (length memberIds > 1 && anyAdmin) $ throwCmdError "can't block/unblock multiple members when admins selected" + when anyPending $ throwCmdError "can't block/unblock members pending approval" assertUserGroupRole gInfo $ max GRModerator maxRole blockMembers user gInfo blockMems remainingMems where @@ -2097,24 +2204,25 @@ processChatCommand' vr = \case selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) selectMembers = foldr' addMember ([], [], GRObserver, False, False) where - addMember m@GroupMember {groupMemberId, memberRole, memberStatus} (block, remaining, maxRole, anyAdmin, anyPending) + addMember m@GroupMember {groupMemberId, memberRole} (block, remaining, maxRole, anyAdmin, anyPending) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin - anyPending' = anyPending || memberStatus == GSMemPendingApproval + anyPending' = anyPending || memberPending m in (m : block, remaining, maxRole', anyAdmin', anyPending') | otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending) blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of - Nothing -> throwChatError $ CECommandError "no members to block/unblock" + Nothing -> throwCmdError "no members to block/unblock" Just blockMems' -> do let mrs = if blockFlag then MRSBlocked else MRSUnrestricted events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems' - (msgs_, _gsr) <- sendGroupMessages user gInfo remainingMems events + recipients = filter memberCurrent remainingMems + (msgs_, _gsr) <- sendGroupMessages_ user gInfo recipients events let itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) itemsData Nothing False when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" - let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_ unless (null acis) $ toView $ CEvtNewChatItems user acis (errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems) unless (null errs) $ toView $ CEvtChatErrors errs @@ -2130,34 +2238,43 @@ processChatCommand' vr = \case APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user -> withGroupLock "removeMembers" groupId . procCmd $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId - let (count, invitedMems, pendingMems, currentMems, maxRole, anyAdmin) = selectMembers members + let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers members memCount = S.size groupMemberIds when (count /= memCount) $ throwChatError CEGroupMemberNotFound - when (memCount > 1 && anyAdmin) $ throwChatError $ CECommandError "can't remove multiple members when admins selected" + when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected" assertUserGroupRole gInfo $ max GRAdmin maxRole (errs1, deleted1) <- deleteInvitedMems user invitedMems - (errs2, deleted2, acis2) <- deleteMemsSend user gInfo members currentMems - rs <- forM pendingMems $ \m -> deleteMemsSend user gInfo [m] [m] -- TODO [knocking] - let (errs3, deleted3, acis3) = concatTuples rs - acis = acis2 <> acis3 - errs = errs1 <> errs2 <> errs3 - unless (null acis) $ toView $ CEvtNewChatItems user acis + let recipients = filter memberCurrent members + (errs2, deleted2, acis2) <- deleteMemsSend user gInfo Nothing recipients currentMems + (errs3, deleted3, acis3) <- + foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], []) pendingApprvMems + let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members + (errs4, deleted4, acis4) <- + foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], []) pendingRvwMems + let acis = acis2 <> acis3 <> acis4 + errs = errs1 <> errs2 <> errs3 <> errs4 + deleted = deleted1 <> deleted2 <> deleted3 <> deleted4 + -- Read group info with updated membersRequireAttention + gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId + let acis' = map (updateACIGroupInfo gInfo') acis + unless (null acis') $ toView $ CEvtNewChatItems user acis' unless (null errs) $ toView $ CEvtChatErrors errs - when withMessages $ deleteMessages user gInfo $ currentMems <> pendingMems - pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) withMessages -- same order is not guaranteed + when withMessages $ deleteMessages user gInfo' deleted + pure $ CRUserDeletedMembers user gInfo' deleted withMessages -- same order is not guaranteed where - selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers = foldl' addMember (0, [], [], [], GRObserver, False) + selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) + selectMembers = foldl' addMember (0, [], [], [], [], GRObserver, False) where - addMember acc@(n, invited, pending, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} + addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} | groupMemberId `S.member` groupMemberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin n' = n + 1 in case memberStatus of - GSMemInvited -> (n', m : invited, pending, current, maxRole', anyAdmin') - GSMemPendingApproval -> (n', invited, m : pending, current, maxRole', anyAdmin') - _ -> (n', invited, pending, m : current, maxRole', anyAdmin') + GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin') + GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin') + GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin') + _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin') | otherwise = acc deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do @@ -2167,18 +2284,24 @@ processChatCommand' vr = \case delMember db m = do deleteGroupMember db user m pure m {memberStatus = GSMemRemoved} - deleteMemsSend :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) - deleteMemsSend user gInfo sendToMems memsToDelete = case L.nonEmpty memsToDelete of + deletePendingMember :: ([ChatError], [GroupMember], [AChatItem]) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem]) + deletePendingMember (accErrs, accDeleted, accACIs) user gInfo recipients m = do + (m', scopeInfo) <- mkMemberSupportChatInfo m + (errs, deleted, acis) <- deleteMemsSend user gInfo (Just scopeInfo) recipients [m'] + pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs) + deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem]) + deleteMemsSend user gInfo chatScopeInfo recipients memsToDelete = case L.nonEmpty memsToDelete of Nothing -> pure ([], [], []) Just memsToDelete' -> do - let events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' - (msgs_, _gsr) <- sendGroupMessages user gInfo sendToMems events + let chatScope = toChatScope <$> chatScopeInfo + events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' + (msgs_, _gsr) <- sendGroupMessages user gInfo chatScope recipients events let itemsData = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) Nothing itemsData Nothing False + cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) itemsData Nothing False when (length cis_ /= length memsToDelete) $ logError "deleteCurrentMems: memsToDelete and cis_ length mismatch" deleteMembersConnections' user memsToDelete True (errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) - let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ + let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo)) $ rights cis_ pure (errs, deleted, acis) where sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c @@ -2187,37 +2310,47 @@ processChatCommand' vr = \case ts = ciContentTexts content in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing delMember db m = do - deleteOrUpdateMemberRecordIO db user m + -- We're in a function used in batch member deletion, and since we're passing same gInfo for each member, + -- voided result (updated group info) may have incorrect state of membersRequireAttention. + -- To avoid complicating code by chaining group info updates, + -- instead we re-read it once after deleting all members before response. + void $ deleteOrUpdateMemberRecordIO db user gInfo m pure m {memberStatus = GSMemRemoved} deleteMessages user gInfo@GroupInfo {membership} ms | groupFeatureMemberAllowed SGFFullDelete membership gInfo = deleteGroupMembersCIs user gInfo ms membership | otherwise = markGroupMembersCIsDeleted user gInfo ms membership - concatTuples :: [([a], [b], [c])] -> ([a], [b], [c]) - concatTuples xs = (concat as, concat bs, concat cs) - where (as, bs, cs) = unzip3 xs APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo - -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) - msg <- sendGroupMessage' user gInfo members XGrpLeave - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) - toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] + let recipients = filter memberCurrentOrPending members + msg <- sendGroupMessage' user gInfo recipients XGrpLeave + (gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo + ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft) + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci] -- TODO delete direct connections that were unused - deleteGroupLinkIfExists user gInfo + deleteGroupLinkIfExists user gInfo' -- member records are not deleted to keep history deleteMembersConnections' user members True withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft - pure $ CRLeftMemberUser user gInfo {membership = membership {memberStatus = GSMemLeft}} + pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}} APIListMembers groupId -> withUser $ \user -> CRGroupMembers user <$> withFastStore (\db -> getGroup db vr user groupId) + -- -- validate: prohibit to delete/archive if member is pending (has to communicate approval or rejection) + -- APIDeleteGroupConversations groupId _gcId -> withUser $ \user -> do + -- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + -- ok_ -- CRGroupConversationsArchived + -- APIArchiveGroupConversations groupId _gcId -> withUser $ \user -> do + -- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + -- ok_ -- CRGroupConversationsDeleted AddMember gName cName memRole -> withUser $ \user -> do (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName processChatCommand $ APIAddMember groupId contactId memRole JoinGroup gName enableNtfs -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIJoinGroup groupId enableNtfs + AcceptMember gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIAcceptMember gId gMemberId memRole MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked RemoveMembers gName gMemberNames withMessages -> withUser $ \user -> do @@ -2231,13 +2364,18 @@ processChatCommand' vr = \case processChatCommand $ APILeaveGroup groupId DeleteGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) (CDMFull True) + processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) ClearGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIClearChat (ChatRef CTGroup groupId) + processChatCommand $ APIClearChat (ChatRef CTGroup groupId Nothing) ListMembers gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIListMembers groupId + ListMemberSupportChats gName -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + (Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + let memberSupportChats = filter (isJust . supportChat) members + pure $ CRMemberSupportChats user gInfo memberSupportChats APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> CRGroupsList user <$> withFastStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do @@ -2262,13 +2400,14 @@ processChatCommand' vr = \case subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId userData = shortLinkUserData short - (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode + -- TODO [certs rcv] + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo ccLink' mRole APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId - (groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo + (groupLinkId, groupLink, _, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo assertUserGroupRole gInfo GRAdmin when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' when (mRole' /= mRole) $ withFastStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' @@ -2279,19 +2418,32 @@ processChatCommand' vr = \case pure $ CRGroupLinkDeleted user gInfo APIGetGroupLink groupId -> withUser $ \user -> do gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId - (_, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo + (_, groupLink, _, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo pure $ CRGroupLink user gInfo groupLink mRole + APIAddGroupShortLink groupId -> withUser $ \user -> do + (gInfo, (uclId, _gLink@(CCLink connFullLink sLnk_), gLinkId, mRole), conn) <- withFastStore $ \db -> do + gInfo <- getGroupInfo db vr user groupId + gLink <- getGroupLink db user gInfo + conn <- getGroupLinkConnection db vr user gInfo + pure (gInfo, gLink, conn) + when (isJust sLnk_) $ throwCmdError "group link already has short link" + let crClientData = encodeJSON $ CRDataGroup gLinkId + sLnk <- shortenShortLink' =<< toShortGroupLink <$> withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact "" (Just crClientData)) + withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk + let groupLink' = CCLink connFullLink (Just sLnk) + pure $ CRGroupLink user gInfo groupLink' mRole APICreateMemberContact gId gMemberId -> withUser $ \user -> do (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId assertUserGroupRole g GRAuthor - unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwCmdError "direct messages not allowed" case memberConn m of Just mConn@Connection {peerChatVRange} -> do unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible - when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" + when (isJust $ memberContactId m) $ throwCmdError "member contact already exists" subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? - (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + -- TODO [certs rcv] + (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode -- TODO not sure it is correct to set connections status here? @@ -2300,10 +2452,11 @@ processChatCommand' vr = \case _ -> throwChatError CEGroupMemberNotActive APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do (g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db vr user contactId - when (contactGrpInvSent ct) $ throwChatError $ CECommandError "x.grp.direct.inv already sent" + when (contactGrpInvSent ct) $ throwCmdError "x.grp.direct.inv already sent" case memberConn m of Just mConn -> do - let msg = XGrpDirectInv cReq msgContent_ + -- TODO [knocking] send in correct scope - modiy API + let msg = XGrpDirectInv cReq msgContent_ Nothing (sndMsg, _, _) <- sendDirectMemberMessage mConn msg groupId withFastStore' $ \db -> setContactGrpInvSent db ct True let ct' = ct {contactGrpInvSent = True} @@ -2334,7 +2487,7 @@ processChatCommand' vr = \case processChatCommand $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand $ APIClearChat (ChatRef CTLocal folderId) + processChatCommand $ APIClearChat (ChatRef CTLocal folderId Nothing) LastChats count_ -> withUser' $ \user -> do let count = fromMaybe 5000 count_ (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) @@ -2371,7 +2524,7 @@ processChatCommand' vr = \case SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName case chatRef of - ChatRef CTLocal folderId -> processChatCommand $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] + ChatRef CTLocal folderId _ -> processChatCommand $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] _ -> withSendRef chatRef $ \sendRef -> processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName @@ -2408,18 +2561,21 @@ processChatCommand' vr = \case | otherwise -> do fileAgentConnIds <- cancelSndFile user ftm fts True deleteAgentConnectionsAsync fileAgentConnIds - withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case - Nothing -> pure () - Just (ChatRef CTDirect contactId) -> do + cref_ <- withFastStore' $ \db -> lookupChatRefByFileId db user fileId + aci_ <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId + case (cref_, aci_) of + (Nothing, _) -> + pure $ CRSndFileCancelled user Nothing ftm fts + (Just (ChatRef CTDirect contactId _), Just aci) -> do (contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId - Just (ChatRef CTGroup groupId) -> do - (Group gInfo ms, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getSharedMsgIdByFileId db userId fileId - -- TODO [knocking] send separately to pending approval member - void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId - Just _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" - ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId - pure $ CRSndFileCancelled user ci ftm fts + pure $ CRSndFileCancelled user (Just aci) ftm fts + (Just (ChatRef CTGroup groupId scope), Just aci) -> do + (gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getSharedMsgIdByFileId db userId fileId + (_chatScopeInfo, recipients) <- getGroupRecipients vr user gInfo scope groupKnockingVersion + void . sendGroupMessage user gInfo scope recipients $ XFileCancel sharedMsgId + pure $ CRSndFileCancelled user (Just aci) ftm fts + (Just _, _) -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" where fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} = s == FSCancelled || (s == FSComplete && isNothing xftpSndFile) @@ -2446,7 +2602,7 @@ processChatCommand' vr = \case pure $ CRFileTransferStatus user fileStatus Just ci@(AChatItem _ _ _ ChatItem {file}) -> case file of Just CIFile {fileProtocol = FPLocal} -> - throwChatError $ CECommandError "not supported for local files" + throwCmdError "not supported for local files" Just CIFile {fileProtocol = FPXFTP} -> pure $ CRFileTransferStatusXFTP user ci _ -> do @@ -2473,6 +2629,11 @@ processChatCommand' vr = \case SetGroupFeatureRole (AGFR f) gName enabled role -> updateGroupProfileByName gName $ \p -> p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} + SetGroupMemberAdmissionReview gName reviewAdmissionApplication -> + updateGroupProfileByName gName $ \p@GroupProfile {memberAdmission} -> + case memberAdmission of + Nothing -> p {memberAdmission = Just (emptyGroupMemberAdmission :: GroupMemberAdmission) {review = reviewAdmissionApplication}} + Just ma -> p {memberAdmission = Just (ma :: GroupMemberAdmission) {review = reviewAdmissionApplication}} SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do let allowed = if onOff then FAYes else FANo pref = TimedMessagesPreference allowed Nothing @@ -2534,12 +2695,12 @@ processChatCommand' vr = \case where getLocks ls = atomically $ M.mapKeys enityLockString . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls) enityLockString cle = case cle of - CLInvitation bs -> "Invitation " <> B.unpack bs - CLConnection connId -> "Connection " <> show connId - CLContact ctId -> "Contact " <> show ctId - CLGroup gId -> "Group " <> show gId - CLUserContact ucId -> "UserContact " <> show ucId - CLFile fId -> "File " <> show fId + CLInvitation bs -> "Invitation " <> safeDecodeUtf8 bs + CLConnection connId -> "Connection " <> tshow connId + CLContact ctId -> "Contact " <> tshow ctId + CLGroup gId -> "Group " <> tshow gId + CLUserContact ucId -> "UserContact " <> tshow ucId + CLFile fId -> "File " <> tshow fId DebugEvent event -> toView event >> ok_ GetAgentSubsTotal userId -> withUserId userId $ \user -> do users <- withStore' $ \db -> getUsers db @@ -2586,17 +2747,18 @@ processChatCommand' vr = \case ok_ = pure $ CRCmdOk Nothing ok = pure . CRCmdOk . Just getChatRef :: User -> ChatName -> CM ChatRef - getChatRef user (ChatName cType name) = - ChatRef cType <$> case cType of + getChatRef user (ChatName cType name) = do + chatId <- case cType of CTDirect -> withFastStore $ \db -> getContactIdByName db user name CTGroup -> withFastStore $ \db -> getGroupIdByName db user name CTLocal | name == "" -> withFastStore (`getUserNoteFolderId` user) - | otherwise -> throwChatError $ CECommandError "not supported" - _ -> throwChatError $ CECommandError "not supported" + | otherwise -> throwCmdError "not supported" + _ -> throwCmdError "not supported" + pure $ ChatRef cType chatId Nothing getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId) getChatRefAndMentions user cName msg = do - chatRef@(ChatRef cType chatId) <- getChatRef user cName + chatRef@(ChatRef cType chatId _) <- getChatRef user cName (chatRef,) <$> case cType of CTGroup -> withFastStore' $ \db -> getMessageMentions db user chatId msg _ -> pure [] @@ -2631,17 +2793,17 @@ processChatCommand' vr = \case withFastStore' $ \db -> setConnectionVerified db user connId Nothing pure $ CRConnectionVerified user False code' getSentChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 - getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of + getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId _scope) msg = case cType of CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText db user cId SMDSnd msg - _ -> throwChatError $ CECommandError "not supported" + _ -> throwCmdError "not supported" getChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 - getChatItemIdByText user (ChatRef cType cId) msg = case cType of + getChatItemIdByText user (ChatRef cType cId _scope) msg = case cType of CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText' db user cId msg CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg - _ -> throwChatError $ CECommandError "not supported" + _ -> throwCmdError "not supported" connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> CM ChatResponse connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli @@ -2800,15 +2962,18 @@ processChatCommand' vr = \case GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} <- withStore $ \db -> getGroupMemberByMemberId db vr user g businessId let p'' = p' {displayName, fullName, image} :: GroupProfile - -- TODO [knocking] send to pending approval members (move `memberCurrent` filter from sendGroupMessages_ to call sites) - void $ sendGroupMessage user g' oldMs (XGrpInfo p'') + recipients = filter memberCurrentOrPending oldMs + void $ sendGroupMessage user g' Nothing recipients (XGrpInfo p'') let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' - sendGroupMessage user g' newMs $ XGrpPrefs ps' - Nothing -> sendGroupMessage user g' ms (XGrpInfo p') - let cd = CDGroupSnd g' + recipients = filter memberCurrentOrPending newMs + sendGroupMessage user g' Nothing recipients $ XGrpPrefs ps' + Nothing -> do + let recipients = filter memberCurrentOrPending ms + sendGroupMessage user g' Nothing recipients (XGrpInfo p') + let cd = CDGroupSnd g' Nothing unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') - toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g') ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat g' Nothing) ci] createGroupFeatureChangedItems user cd CISndGroupFeature g g' pure $ CRGroupUpdated user g g' Nothing checkValidName :: GroupName -> CM () @@ -2823,16 +2988,14 @@ processChatCommand' vr = \case when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - delGroupChatItemsForMembers :: User -> GroupInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion] - delGroupChatItemsForMembers user gInfo ms items = do + delGroupChatItemsForMembers :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion] + delGroupChatItemsForMembers user gInfo chatScopeInfo ms items = do assertDeletable gInfo items assertUserGroupRole gInfo GRAdmin -- TODO GRModerator when most users migrate let msgMemIds = itemsMsgMemIds gInfo items events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId)) msgMemIds - -- TODO [knocking] validate: only current members or only single pending approval member, - -- TODO or prohibit pending approval members (only moderation and reports use this) - mapM_ (sendGroupMessages user gInfo ms) events - delGroupChatItems user gInfo items True + mapM_ (sendGroupMessages_ user gInfo ms) events + delGroupChatItems user gInfo chatScopeInfo items True where assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items' = @@ -2852,16 +3015,16 @@ processChatCommand' vr = \case CIGroupRcv GroupMember {memberId} -> (msgId, memberId) CIGroupSnd -> (msgId, membershipMemId) - delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> CM [ChatItemDeletion] - delGroupChatItems user gInfo@GroupInfo {membership} items moderation = do + delGroupChatItems :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Bool -> CM [ChatItemDeletion] + delGroupChatItems user gInfo@GroupInfo {membership} chatScopeInfo items moderation = do deletedTs <- liftIO getCurrentTime when moderation $ do ciIds <- concat <$> withStore' (\db -> forM items $ \(CChatItem _ ci) -> markMessageReportsDeleted db user gInfo ci membership deletedTs) unless (null ciIds) $ toView $ CEvtGroupChatItemsDeleted user gInfo ciIds True (Just membership) let m = if moderation then Just membership else Nothing if groupFeatureMemberAllowed SGFFullDelete membership gInfo - then deleteGroupCIs user gInfo items m deletedTs - else markGroupCIsDeleted user gInfo items m deletedTs + then deleteGroupCIs user gInfo chatScopeInfo items m deletedTs + else markGroupCIsDeleted user gInfo chatScopeInfo items m deletedTs updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> @@ -2940,7 +3103,7 @@ processChatCommand' vr = \case ci <- saveSndChatItem' user (CDDirectSnd ct) msg content Nothing Nothing Nothing timed_ False toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) + startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) drgRandomBytes :: Int -> CM ByteString drgRandomBytes n = asks random >>= atomically . C.randomBytes n privateGetUser :: UserId -> CM User @@ -3004,8 +3167,8 @@ processChatCommand' vr = \case gId <- getGroupIdByName db user name GroupInfo {chatSettings} <- getGroupInfo db vr user gId pure (gId, chatSettings) - _ -> throwChatError $ CECommandError "not supported" - processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings + _ -> throwCmdError "not supported" + processChatCommand $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) connectPlan user (ACL SCMInvitation cLink) = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing @@ -3032,7 +3195,7 @@ processChatCommand' vr = \case withFastStore' (\db -> getGroupInfoViaUserShortLink db vr user l') >>= \case Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g)) Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l') - CCTChannel -> throwChatError $ CECommandError "channel links are not supported in this version" + CCTChannel -> throwCmdError "channel links are not supported in this version" where contactReqAndPlan cReq sLnk_ = do plan <- contactRequestPlan user cReq `catchChatError` (pure . CPError) @@ -3071,7 +3234,7 @@ processChatCommand' vr = \case | otherwise -> case ct_ of Just ct -> pure $ CPInvitationLink (ILPKnown ct) Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" - _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection" contactRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan contactRequestPlan user (CRContactUri crData) = do let ConnReqUriData {crClientData} = crData @@ -3094,7 +3257,7 @@ processChatCommand' vr = \case | contactDeleted ct -> pure $ CPContactAddress CAPOk | otherwise -> pure $ CPContactAddress (CAPKnown ct) Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo - Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" + Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" -- group link Just _ -> withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case @@ -3108,7 +3271,7 @@ processChatCommand' vr = \case (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) | otherwise -> pure $ CPGroupLink GLPOk - (Nothing, Just _) -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" + (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" (Just gInfo, _) -> groupPlan gInfo where groupPlan gInfo@GroupInfo {membership} @@ -3140,13 +3303,14 @@ processChatCommand' vr = \case CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) shortLinkUserData short = if short then Just "" else Nothing + shortenShortLink' :: ConnShortLink m -> CM (ConnShortLink m) + shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config) shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) - shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM (\l -> (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config)) sLnk + shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM shortenShortLink' sLnk createdGroupLink :: CreatedLinkContact -> CreatedLinkContact - createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toGroupLink <$> shortLink) - where - toGroupLink :: ShortLinkContact -> ShortLinkContact - toGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k + createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toShortGroupLink <$> shortLink) + toShortGroupLink :: ShortLinkContact -> ShortLinkContact + toShortGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId @@ -3157,18 +3321,18 @@ processChatCommand' vr = \case timed_ <- contactCITimed ct updateDirectChatItemView user ct itemId aciContent False False timed_ Nothing forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, itemId) + startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, itemId) _ -> pure () -- prohibited assertAllowedContent :: MsgContent -> CM () assertAllowedContent = \case - MCReport {} -> throwChatError $ CECommandError "sending reports via this API is not supported" + MCReport {} -> throwCmdError "sending reports via this API is not supported" _ -> pure () assertAllowedContent' :: ComposedMessage -> CM () assertAllowedContent' ComposedMessage {msgContent} = assertAllowedContent msgContent assertNoMentions :: ComposedMessage -> CM () assertNoMentions ComposedMessage {mentions} | null mentions = pure () - | otherwise = throwChatError $ CECommandError "mentions are not supported in this chat" + | otherwise = throwCmdError "mentions are not supported in this chat" sendContactContentMessages :: User -> ContactId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse sendContactContentMessages user contactId live itemTTL cmrs = do assertMultiSendable live cmrs @@ -3180,7 +3344,7 @@ processChatCommand' vr = \case assertVoiceAllowed :: Contact -> CM () assertVoiceAllowed ct = when (not (featureAllowed SCFVoice forUser ct) && any (\(ComposedMessage {msgContent}, _, _, _) -> isVoice msgContent) cmrs) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) + throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFVoice) processComposedMessages :: Contact -> CM ChatResponse processComposedMessages ct = do (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers @@ -3189,11 +3353,11 @@ processChatCommand' vr = \case msgs_ <- sendDirectContactMessages user ct $ L.map XMsgNew msgContainers let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_ when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" - r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live + r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) itemsData timed_ live processSendErrs r forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ cis $ \ci -> - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt + startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId' ci) deleteAt pure $ CRNewChatItems user (map (AChatItem SCTDirect SMDSnd (DirectChat ct)) cis) where setupSndFileTransfers :: CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) @@ -3208,8 +3372,8 @@ processChatCommand' vr = \case prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, _), fInv_) -> do case (quotedItemId, itemForwarded) of - (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) - (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) + (Nothing, Nothing) -> pure (MCSimple (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Nothing) + (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Nothing) (Just qiId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- getDirectChatItem db user contactId qiId @@ -3217,7 +3381,7 @@ processChatCommand' vr = \case let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent mc origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just qiId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} - pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live)), Just quotedItem) + pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc M.empty fInv_ (ttl' <$> timed_) (justTrue live) Nothing), Just quotedItem) (Just _, Just _) -> throwError SEInvalidQuote where quoteData :: ChatItem c d -> ExceptT StoreError IO (MsgContent, CIQDirection 'CTDirect, Bool) @@ -3225,70 +3389,69 @@ processChatCommand' vr = \case quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) quoteData _ = throwError SEInvalidQuote - sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupMemberId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages user gInfo@GroupInfo {membership} directMemId_ live itemTTL cmrs = do + sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages user gInfo scope live itemTTL cmrs = do assertMultiSendable live cmrs - (ms, numFileInvs, notInHistory_) <- case directMemId_ of - Nothing -> do - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo - pure (ms, length $ filter memberCurrent ms, Nothing) - Just dmId -> do - when (dmId == groupMemberId' membership) $ throwChatError $ CECommandError "cannot send to self" - dm <- withFastStore $ \db -> getGroupMemberById db vr user dmId - unless (memberStatus dm == GSMemPendingApproval) $ throwChatError $ CECommandError "cannot send directly to member not pending approval" - pure ([dm], 1, Just NotInHistory) - sendGroupContentMessages_ user gInfo notInHistory_ ms numFileInvs live itemTTL cmrs - sendGroupContentMessages_ :: User -> GroupInfo -> Maybe NotInHistory -> [GroupMember] -> Int -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} notInHistory_ ms numFileInvs live itemTTL cmrs = do - -- TODO [knocking] pass GroupSndScope? - let allowedRole = case ms of - [m] | memberCategory m == GCHostMember && memberStatus membership == GSMemPendingApproval -> Nothing - _ -> Just GRAuthor + (chatScopeInfo, recipients) <- getGroupRecipients vr user gInfo scope modsCompatVersion + sendGroupContentMessages_ user gInfo scope chatScopeInfo recipients live itemTTL cmrs + where + hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs + modsCompatVersion = if hasReport then contentReportsVersion else groupKnockingVersion + sendGroupContentMessages_ :: User -> GroupInfo -> Maybe GroupChatScope -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} scope chatScopeInfo recipients live itemTTL cmrs = do forM_ allowedRole $ assertUserGroupRole gInfo assertGroupContentAllowed processComposedMessages where + allowedRole :: Maybe GroupMemberRole + allowedRole = case scope of + Nothing -> Just GRAuthor + Just (GCSMemberSupport Nothing) + | memberPending membership -> Nothing + | otherwise -> Just GRObserver + Just (GCSMemberSupport (Just _gmId)) -> Just GRModerator assertGroupContentAllowed :: CM () assertGroupContentAllowed = case findProhibited (L.toList cmrs) of - Just f -> throwChatError (CECommandError $ "feature not allowed " <> T.unpack (groupFeatureNameText f)) + Just f -> throwCmdError $ "feature not allowed " <> T.unpack (groupFeatureNameText f) Nothing -> pure () where findProhibited :: [ComposedMessageReq] -> Maybe GroupFeature findProhibited = foldr' - (\(ComposedMessage {fileSource, msgContent = mc}, _, (_, ft), _) acc -> prohibitedGroupContent gInfo membership mc ft fileSource True <|> acc) + (\(ComposedMessage {fileSource, msgContent = mc}, _, (_, ft), _) acc -> prohibitedGroupContent gInfo membership chatScopeInfo mc ft fileSource True <|> acc) Nothing - processComposedMessages :: CM ChatResponse + processComposedMessages :: CM ChatResponse processComposedMessages = do - (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers numFileInvs + (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length recipients) timed_ <- sndGroupCITimed live gInfo itemTTL (chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ - (msgs_, gsr) <- sendGroupMessages user gInfo ms chatMsgEvents + (msgs_, gsr) <- sendGroupMessages user gInfo Nothing recipients chatMsgEvents let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_) - cis_ <- saveSndChatItems user (CDGroupSnd gInfo) notInHistory_ itemsData timed_ live + cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) itemsData timed_ live when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" createMemberSndStatuses cis_ msgs_ gsr let r@(_, cis) = partitionEithers cis_ processSendErrs r forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ cis $ \ci -> - startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt - pure $ CRNewChatItems user (map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) cis) + startProximateTimedItemThread user (ChatRef CTGroup groupId scope, chatItemId' ci) deleteAt + pure $ CRNewChatItems user (map (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo)) cis) where setupSndFileTransfers :: Int -> CM (NonEmpty (Maybe FileInvitation, Maybe (CIFile 'MDSnd))) setupSndFileTransfers n = forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> case file_ of Just file -> do fileSize <- checkSndFile file - (fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo ms + (fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo recipients pure (Just fInv, Just ciFile) Nothing -> pure (Nothing, Nothing) prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))) prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> forM cmsFileInvs $ \((ComposedMessage {quotedItemId, msgContent = mc}, itemForwarded, _, ciMentions), fInv_) -> - let mentions = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions - in prepareGroupMsg db user gInfo mc mentions quotedItemId itemForwarded fInv_ timed_ live + let msgScope = toMsgScope gInfo <$> chatScopeInfo + mentions = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions + in prepareGroupMsg db user gInfo msgScope mc mentions quotedItemId itemForwarded fInv_ timed_ live createMemberSndStatuses :: [Either ChatError (ChatItem 'CTGroup 'MDSnd)] -> NonEmpty (Either ChatError SndMessage) -> @@ -3333,7 +3496,7 @@ processChatCommand' vr = \case -- UI doesn't allow composing with multiple quotes, so api prohibits it as well, and doesn't bother -- batching retrieval of quoted messages (prepareMsgs). when (live || length (L.filter (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) > 1) $ - throwChatError (CECommandError "invalid multi send: live and more than one quote not supported") + throwCmdError "invalid multi send: live and more than one quote not supported" xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> CM (FileInvitation, CIFile 'MDSnd) xftpSndFileTransfer user file fileSize n contactOrGroup = do (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup @@ -3416,7 +3579,7 @@ processChatCommand' vr = \case assertNoQuotes :: CM () assertNoQuotes = when (any (\(ComposedMessage {quotedItemId}, _, _, _) -> isJust quotedItemId) cmrs) $ - throwChatError (CECommandError "createNoteFolderContentItems: quotes not supported") + throwCmdError "createNoteFolderContentItems: quotes not supported" createLocalFiles :: NoteFolder -> UTCTime -> CM (NonEmpty (Maybe (CIFile 'MDSnd))) createLocalFiles nf createdAt = forM cmrs $ \(ComposedMessage {fileSource = file_}, _, _, _) -> @@ -3439,9 +3602,9 @@ processChatCommand' vr = \case CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) withSendRef :: ChatRef -> (SendRef -> CM ChatResponse) -> CM ChatResponse withSendRef chatRef a = case chatRef of - ChatRef CTDirect cId -> a $ SRDirect cId - ChatRef CTGroup gId -> a $ SRGroup gId Nothing - _ -> throwChatError $ CECommandError "not supported" + ChatRef CTDirect cId _ -> a $ SRDirect cId + ChatRef CTGroup gId scope -> a $ SRGroup gId scope + _ -> throwCmdError "not supported" protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) protocolServers p (operators, smpServers, xftpServers) = case p of @@ -3527,7 +3690,7 @@ startExpireCIThread user@User {userId} = do liftIO $ threadDelay' interval setChatItemsExpiration :: User -> Int64 -> Int -> CM' () -setChatItemsExpiration user newTTL ttlCount +setChatItemsExpiration user newTTL ttlCount | newTTL > 0 || ttlCount > 0 = do startExpireCIThread user whenM chatStarted $ setExpireCIFlag user True @@ -3562,8 +3725,9 @@ agentSubscriber = do where run action = action `catchChatError'` (eToView') -type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) +type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType (Maybe ClientServiceId))) +-- TODO [certs rcv] subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> User -> CM () subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do -- get user connections @@ -3657,7 +3821,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do pcs <- withStore_ getPendingContactConnections let connIds = map aConnId' pcs pure (connIds, M.fromList $ zip connIds pcs) - contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM () + contactSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId Contact -> Bool -> CM () contactSubsToView rs cts ce = do chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses) ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI @@ -3683,9 +3847,9 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" e -> show e -- TODO possibly below could be replaced with less noisy events for API - contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () + contactLinkSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId UserContact -> CM () contactLinkSubsToView rs = toView . CEvtUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs - groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () + groupSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> [ShortGroup] -> Map ConnId ShortGroupMember -> Bool -> CM () groupSubsToView rs gs ms ce = do mapM_ groupSub $ sortOn (\(ShortGroup ShortGroupInfo {groupName = g} _) -> g) gs @@ -3707,7 +3871,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do | membershipStatus == GSMemInvited = TEGroupInvitation user g | null members = TEGroupEmpty user g | otherwise = TEGroupSubscribed user g - sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM () + sndFileSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId SndFileTransfer -> CM () sndFileSubsToView rs sfts = do let sftRs = resultsFor rs sfts forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do @@ -3716,20 +3880,20 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do threadDelay 1000000 when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ sendFileChunk user ft - rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () + rcvFileSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId RcvFileTransfer -> CM () rcvFileSubsToView rs = mapM_ (toViewTE . uncurry (TERcvFileSubError user)) . filterErrors . resultsFor rs - pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () + pendingConnSubsToView :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId PendingContactConnection -> CM () pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> [] filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) - resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] + resultsFor :: Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Map ConnId a -> [(a, Maybe ChatError)] resultsFor rs = M.foldrWithKey' addResult [] where addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)] addResult connId = (:) . (,resultErr connId rs) - resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError + resultErr :: ConnId -> Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -> Maybe ChatError resultErr connId rs = case M.lookup connId rs of Just (Left e) -> Just $ ChatErrorAgent e Nothing Just _ -> Nothing @@ -3976,11 +4140,14 @@ chatCommandP = "/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), "/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI "/_accept member #" *> (APIAcceptMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole), + "/_delete member chat #" *> (APIDeleteMemberSupportChat <$> A.decimal <* A.space <*> A.decimal), "/_member role #" *> (APIMembersRole <$> A.decimal <*> _strP <*> memberRole), "/_block #" *> (APIBlockMembersForAll <$> A.decimal <*> _strP <* " blocked=" <*> onOffP), "/_remove #" *> (APIRemoveMembers <$> A.decimal <*> _strP <*> (" messages=" *> onOffP <|> pure False)), "/_leave #" *> (APILeaveGroup <$> A.decimal), "/_members #" *> (APIListMembers <$> A.decimal), + -- "/_archive conversations #" *> (APIArchiveGroupConversations <$> A.decimal <*> _strP), + -- "/_delete conversations #" *> (APIDeleteGroupConversations <$> A.decimal <*> _strP), "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), @@ -4061,6 +4228,7 @@ chatCommandP = "/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP), ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayNameP <*> (" mute" $> MFNone <|> pure MFAll)), + "/accept member " *> char_ '#' *> (AcceptMember <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> (memberRole <|> pure GRMember)), ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <*> memberRole), "/block for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure True), "/unblock for all #" *> (BlockForAll <$> displayNameP <* A.space <*> (char_ '@' *> displayNameP) <*> pure False), @@ -4072,6 +4240,7 @@ chatCommandP = "/clear #" *> (ClearGroup <$> displayNameP), "/clear " *> char_ '@' *> (ClearContact <$> displayNameP), ("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayNameP), + "/member support chats #" *> (ListMemberSupportChats <$> displayNameP), "/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> stringP)), ("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayNameP) <*> optional (A.space *> stringP)), "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), @@ -4085,6 +4254,7 @@ chatCommandP = "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal), + "/_short link #" *> (APIAddGroupShortLink <$> A.decimal), "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember) <*> shortP), "/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole), "/delete link #" *> (DeleteGroupLink <$> displayNameP), @@ -4106,8 +4276,7 @@ chatCommandP = ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, - SendMessage <$> chatNameP <* A.space <*> msgTextP, - "/* " *> (SendMessage (ChatName CTLocal "") <$> msgTextP), + SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), @@ -4141,6 +4310,7 @@ chatCommandP = ("/delete_address" <|> "/da") $> DeleteMyAddress, "/_show_address " *> (APIShowMyAddress <$> A.decimal), ("/show_address" <|> "/sa") $> ShowMyAddress, + "/_short_link_address " *> (APIAddMyAddressShortLink <$> A.decimal), "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), @@ -4171,6 +4341,7 @@ chatCommandP = "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), "/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP), "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole), + "/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)), ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, "/set device name " *> (SetLocalDeviceName <$> textP), "/list remote hosts" $> ListRemoteHosts, @@ -4276,6 +4447,7 @@ chatCommandP = history = Just HistoryGroupPreference {enable = FEOn} } pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences, memberAdmission = Nothing} + memberCriteriaP = ("all" $> Just MCAll) <|> ("off" $> Nothing) fullNameP = A.space *> textP <|> pure "" textP = safeDecodeUtf8 <$> A.takeByteString pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) @@ -4306,10 +4478,29 @@ chatCommandP = CTLocal -> pure $ ChatName CTLocal "" ct -> ChatName ct <$> displayNameP chatNameP' = ChatName <$> (chatTypeP <|> pure CTDirect) <*> displayNameP - chatRefP = ChatRef <$> chatTypeP <*> A.decimal + chatRefP = do + chatTypeP >>= \case + CTGroup -> ChatRef CTGroup <$> A.decimal <*> optional gcScopeP + cType -> (\chatId -> ChatRef cType chatId Nothing) <$> A.decimal sendRefP = (A.char '@' $> SRDirect <*> A.decimal) - <|> (A.char '#' $> SRGroup <*> A.decimal <*> optional (" @" *> A.decimal)) + <|> (A.char '#' $> SRGroup <*> A.decimal <*> optional gcScopeP) + gcScopeP = "(_support" *> (GCSMemberSupport <$> optional (A.char ':' *> A.decimal)) <* A.char ')' + sendNameP = + (A.char '@' $> SNDirect <*> displayNameP) + <|> (A.char '#' $> SNGroup <*> displayNameP <*> gScopeNameP) + <|> ("/*" $> SNLocal) + gScopeNameP = + (supportPfx *> (Just . GSNMemberSupport <$> optional supportMember) <* A.char ')') + -- this branch fails on "(support" followed by incorrect syntax, + -- to avoid sending message to the whole group as `optional gScopeNameP` would do + <|> (optional supportPfx >>= mapM (\_ -> fail "bad chat scope")) + where + supportPfx = A.takeWhile isSpace *> "(support" + supportMember = safeDecodeUtf8 <$> (A.char ':' *> A.takeWhile isSpace *> (A.take . lengthTillLastParen =<< A.lookAhead displayNameP_)) + lengthTillLastParen s = case B.unsnoc s of + Just (_, ')') -> B.length s - 1 + _ -> B.length s msgCountP = A.space *> A.decimal <|> pure 10 ciTTLDecimal = ("default" $> Nothing) <|> (Just <$> A.decimal) ciTTL = @@ -4375,7 +4566,11 @@ chatCommandP = char_ = optional . A.char displayNameP :: Parser Text -displayNameP = safeDecodeUtf8 <$> (quoted '\'' <|> takeNameTill (\c -> isSpace c || c == ',')) +displayNameP = safeDecodeUtf8 <$> displayNameP_ +{-# INLINE displayNameP #-} + +displayNameP_ :: Parser ByteString +displayNameP_ = quoted '\'' <|> takeNameTill (\c -> isSpace c || c == ',') where takeNameTill p = A.peekChar' >>= \c -> diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index bbefbcfde0..0f33c18a35 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -37,7 +37,7 @@ import Data.Foldable (foldr') import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) -import Data.List (find, mapAccumL, partition) +import Data.List (find, foldl', mapAccumL, partition) import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -111,37 +111,37 @@ maxRcvMentions = 5 maxSndMentions :: Int maxSndMentions = 3 -withChatLock :: String -> CM a -> CM a +withChatLock :: Text -> CM a -> CM a withChatLock name action = asks chatLock >>= \l -> withLock l name action -withEntityLock :: String -> ChatLockEntity -> CM a -> CM a +withEntityLock :: Text -> ChatLockEntity -> CM a -> CM a withEntityLock name entity action = do chatLock <- asks chatLock ls <- asks entityLocks atomically $ unlessM (isEmptyTMVar chatLock) retry withLockMap ls entity name action -withInvitationLock :: String -> ByteString -> CM a -> CM a +withInvitationLock :: Text -> ByteString -> CM a -> CM a withInvitationLock name = withEntityLock name . CLInvitation {-# INLINE withInvitationLock #-} -withConnectionLock :: String -> Int64 -> CM a -> CM a +withConnectionLock :: Text -> Int64 -> CM a -> CM a withConnectionLock name = withEntityLock name . CLConnection {-# INLINE withConnectionLock #-} -withContactLock :: String -> ContactId -> CM a -> CM a +withContactLock :: Text -> ContactId -> CM a -> CM a withContactLock name = withEntityLock name . CLContact {-# INLINE withContactLock #-} -withGroupLock :: String -> GroupId -> CM a -> CM a +withGroupLock :: Text -> GroupId -> CM a -> CM a withGroupLock name = withEntityLock name . CLGroup {-# INLINE withGroupLock #-} -withUserContactLock :: String -> Int64 -> CM a -> CM a +withUserContactLock :: Text -> Int64 -> CM a -> CM a withUserContactLock name = withEntityLock name . CLUserContact {-# INLINE withUserContactLock #-} -withFileLock :: String -> Int64 -> CM a -> CM a +withFileLock :: Text -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} @@ -190,13 +190,13 @@ toggleNtf m ntfOn = forM_ (memberConnId m) $ \connId -> withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView -prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) -prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of +prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) +prepareGroupMsg db user g@GroupInfo {membership} msgScope mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of (Nothing, Nothing) -> - let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) + let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope in pure (XMsgNew mc', Nothing) (Nothing, Just _) -> - let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) + let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope in pure (XMsgNew mc', Nothing) (Just quotedItemId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <- @@ -206,7 +206,7 @@ prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemF qmc = quoteContent mc origQmc file (qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'} - mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live)) + mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope) pure (XMsgNew mc', Just quotedItem) (Just _, Just _) -> throwError SEInvalidQuote where @@ -323,12 +323,12 @@ quoteContent mc qmc ciFile_ qFileName = maybe qText (T.pack . getFileName) ciFile_ qTextOrFile = if T.null qText then qFileName else qText -prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature -prohibitedGroupContent gInfo@GroupInfo {membership = GroupMember {memberRole = userRole}} m mc ft file_ sent +prohibitedGroupContent :: GroupInfo -> GroupMember -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe MarkdownList -> Maybe f -> Bool -> Maybe GroupFeature +prohibitedGroupContent gInfo@GroupInfo {membership = GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice - | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles - | isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports - | prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks + | isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles + | isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports + | isNothing scopeInfo && prohibitedSimplexLinks gInfo m ft = Just GFSimplexLinks | otherwise = Nothing where -- admins cannot send reports, non-admins cannot receive reports @@ -453,20 +453,53 @@ deleteDirectCIs user ct items = do deleteDirectChatItem db user ct ci pure $ contactDeletion md ct ci Nothing -deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] -deleteGroupCIs user gInfo items byGroupMember_ deletedTs = do +deleteGroupCIs :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +deleteGroupCIs user gInfo chatScopeInfo items 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 $ CEvtChatErrors errs - pure deletions + vr <- chatVersionRange + deletions' <- case chatScopeInfo of + Nothing -> pure deletions + Just scopeInfo@GCSIMemberSupport {groupMember_} -> do + let decStats = countDeletedUnreadItems groupMember_ deletions + gInfo' <- withFastStore' $ \db -> updateGroupScopeUnreadStats db vr user gInfo scopeInfo decStats + pure $ map (updateDeletionGroupInfo gInfo') deletions + pure deletions' where 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' + pure $ groupDeletion md gInfo chatScopeInfo ci ci' + countDeletedUnreadItems :: Maybe GroupMember -> [ChatItemDeletion] -> (Int, Int, Int) + countDeletedUnreadItems scopeMember_ = foldl' countItem (0, 0, 0) + where + countItem :: (Int, Int, Int) -> ChatItemDeletion -> (Int, Int, Int) + countItem (!unread, !unanswered, !mentions) ChatItemDeletion {deletedChatItem} + | aChatItemIsRcvNew deletedChatItem = + let unread' = unread + 1 + unanswered' = case (scopeMember_, aChatItemRcvFromMember deletedChatItem) of + (Just scopeMember, Just rcvFromMember) + | groupMemberId' rcvFromMember == groupMemberId' scopeMember -> unanswered + 1 + _ -> unanswered + mentions' = if isACIUserMention deletedChatItem then mentions + 1 else mentions + in (unread', unanswered', mentions') + | otherwise = (unread, unanswered, mentions) + updateDeletionGroupInfo :: GroupInfo -> ChatItemDeletion -> ChatItemDeletion + updateDeletionGroupInfo gInfo' ChatItemDeletion {deletedChatItem, toChatItem} = + ChatItemDeletion + { deletedChatItem = updateACIGroupInfo gInfo' deletedChatItem, + toChatItem = updateACIGroupInfo gInfo' <$> toChatItem + } + +updateACIGroupInfo :: GroupInfo -> AChatItem -> AChatItem +updateACIGroupInfo gInfo' = \case + AChatItem SCTGroup dir (GroupChat _gInfo chatScopeInfo) ci -> + AChatItem SCTGroup dir (GroupChat gInfo' chatScopeInfo) ci + aci -> aci deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM () deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do @@ -517,8 +550,8 @@ markDirectCIsDeleted user ct items deletedTs = do ci' <- markDirectChatItemDeleted db user ct ci deletedTs pure $ contactDeletion md ct ci (Just ci') -markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] -markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do +markGroupCIsDeleted :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion] +markGroupCIsDeleted user gInfo chatScopeInfo items 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) @@ -528,7 +561,7 @@ markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do where markDeleted db (CChatItem md ci) = do ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs - pure $ groupDeletion md gInfo ci (Just ci') + pure $ groupDeletion md gInfo chatScopeInfo ci (Just ci') markGroupMemberCIsDeleted :: User -> GroupInfo -> GroupMember -> GroupMember -> CM () markGroupMemberCIsDeleted user gInfo member byGroupMember = do @@ -548,10 +581,10 @@ markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs = do markMemberCIsDeleted db user gInfo member byGroupMember deletedTs pure fs -groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion -groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') +groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> Maybe GroupChatScopeInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion +groupDeletion md g chatScopeInfo ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') where - gItem = AChatItem SCTGroup md (GroupChat g) + gItem = AChatItem SCTGroup md (GroupChat g chatScopeInfo) contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci') @@ -565,7 +598,7 @@ updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatu timed_ <- callTimed ct aciContent updateDirectChatItemView user ct chatItemId aciContent False False timed_ msgId_ forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId) + startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId) updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM () updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do @@ -668,10 +701,10 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI _ -> do chatRef <- withStore $ \db -> getChatRefByFileId db user fileId case (chatRef, grpMemberId) of - (ChatRef CTDirect contactId, Nothing) -> do + (ChatRef CTDirect contactId _, Nothing) -> do ct <- withStore $ \db -> getContact db vr user contactId acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage user ct msg - (ChatRef CTGroup groupId, Just memId) -> do + (ChatRef CTGroup groupId _, Just memId) -> do GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user groupId memId case activeConn of Just conn -> do @@ -856,7 +889,8 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = pure (ct, conn, ExistingIncognito <$> incognitoProfile) let profileToSend = profileToSendOnAccept user incognitoProfile False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - (ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) + -- TODO [certs rcv] + (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> PQSupport -> CM Contact acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile pqSup = do @@ -880,7 +914,7 @@ acceptGroupJoinRequestAsync gLinkMemRole incognitoProfile = do gVar <- asks random - let initialStatus = acceptanceToStatus gAccepted + let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted (groupMemberId, memberId) <- withStore $ \db -> do liftIO $ deleteContactRequestRec db user ucr createJoiningMember db gVar user gInfo ucr gLinkMemRole initialStatus @@ -965,7 +999,7 @@ acceptBusinessJoinRequestAsync let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode - let cd = CDGroupSnd gInfo + let cd = CDGroupSnd gInfo Nothing createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo pure gInfo @@ -981,13 +1015,41 @@ profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> NewIncognito p -> p ExistingIncognito lp -> fromLocalProfile lp -introduceToGroup :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () -introduceToGroup _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" -introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = do +introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do + forM_ (memberConn m) $ \mConn -> do + let msg = + if (maxVersion (memberChatVRange m) >= groupKnockingVersion) + then XGrpLinkAcpt GAPendingReview memberRole memberId + else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing + void $ sendDirectMemberMessage mConn msg groupId + modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + let rcpModMs = filter (\mem -> memberCurrent mem && maxVersion (memberChatVRange mem) >= groupKnockingVersion) modMs + introduceMember vr user gInfo m rcpModMs (Just $ MSMember $ memberId' m) + +introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceToAll vr user gInfo m = do members <- withStore' $ \db -> getGroupMembers db vr user gInfo - void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m - sendIntroductions members - when (groupFeatureAllowed SGFHistory gInfo) sendHistory + let recipients = filter memberCurrent members + introduceMember vr user gInfo m recipients Nothing + +introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceToRemaining vr user gInfo m = do + (members, introducedGMIds) <- + withStore' $ \db -> (,) <$> getGroupMembers db vr user gInfo <*> getIntroducedGroupMemberIds db m + let recipients = filter (introduceMemP introducedGMIds) members + introduceMember vr user gInfo m recipients Nothing + where + introduceMemP introducedGMIds mem = + memberCurrent mem + && groupMemberId' mem `notElem` introducedGMIds + && groupMemberId' mem /= groupMemberId' m + +introduceMember :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM () +introduceMember _ _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active" +introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do + void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo m) msgScope + sendIntroductions introduceToMembers where sendIntroductions members = do intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m @@ -1003,7 +1065,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac memberIntro reMember = let mInfo = memberInfo reMember mRestrictions = memberRestrictions reMember - in XGrpMemIntro mInfo mRestrictions + in XGrpMemIntro mInfo mRestrictions shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro] shuffleIntros intros = do let (admins, others) = partition isAdmin intros @@ -1016,15 +1078,19 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac processIntro intro@GroupMemberIntro {introId} = do void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId withStore' $ \db -> updateIntroStatus db introId GMIntroSent - sendHistory = - when (m `supportsVersion` batchSendVersion) $ do - (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) - (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items - let errors = map ChatErrorStore errs <> errs' - unless (null errors) $ toView $ CEvtChatErrors errors - let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ - forM_ (L.nonEmpty events') $ \events'' -> - sendGroupMemberMessages user conn events'' groupId + +sendHistory :: User -> GroupInfo -> GroupMember -> CM () +sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" +sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = + when (m `supportsVersion` batchSendVersion) $ do + (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) + (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items + let errors = map ChatErrorStore errs <> errs' + unless (null errors) $ toView $ CEvtChatErrors errors + let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ + forM_ (L.nonEmpty events') $ \events'' -> + sendGroupMemberMessages user conn events'' groupId + where descrEvent_ :: Maybe (ChatMsgEvent 'Json) descrEvent_ | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do @@ -1070,7 +1136,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac | fileDescrComplete = let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} fInv = xftpFileInvitation fileName fileSize fInvDescr - in Just (fInv, fileDescrText) + in Just (fInv, fileDescrText) | otherwise = Nothing processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json] processContentItem sender ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = @@ -1082,7 +1148,8 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac fInv_ = fst <$> fInvDescr_ (mc', _, mentions') = updatedMentionNames mc formattedText mentions mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions' - (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False + -- TODO [knocking] send history to other scopes too? + (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False let senderVRange = memberChatVRange' sender xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of @@ -1147,7 +1214,7 @@ startTimedItemThread user itemRef deleteAt = do atomically $ writeTVar threadTVar (Just tId) deleteTimedItem :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () -deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do +deleteTimedItem user (ChatRef cType chatId scope, itemId) deleteAt = do ts <- liftIO getCurrentTime liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts lift waitChatStartedAndActivated @@ -1160,7 +1227,8 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do CTGroup -> do (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId deletedTs <- liftIO getCurrentTime - deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs + chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + deletions <- deleteGroupCIs user gInfo chatScopeInfo [ci] Nothing deletedTs toView $ CEvtChatItemsDeleted user deletions True True _ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType" @@ -1276,6 +1344,103 @@ parseChatMessage conn s = do errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) {-# INLINE parseChatMessage #-} +getChatScopeInfo :: VersionRangeChat -> User -> GroupChatScope -> CM GroupChatScopeInfo +getChatScopeInfo vr user = \case + GCSMemberSupport Nothing -> pure $ GCSIMemberSupport Nothing + GCSMemberSupport (Just gmId) -> do + supportMem <- withFastStore $ \db -> getGroupMemberById db vr user gmId + pure $ GCSIMemberSupport (Just supportMem) + +-- TODO [knocking] refactor to GroupChatScope -> "a" function, "a" is some new type? Or possibly split to get scope/get recipients steps +getGroupRecipients :: VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScope -> VersionChat -> CM (Maybe GroupChatScopeInfo, [GroupMember]) +getGroupRecipients vr user gInfo@GroupInfo {membership} scope modsCompatVersion = case scope of + Nothing -> do + unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" + ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + let recipients = filter memberCurrent ms + pure (Nothing, recipients) + Just (GCSMemberSupport Nothing) -> do + modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs + when (null rcpModMs') $ throwChatError $ CECommandError "no admins support this message" + let scopeInfo = GCSIMemberSupport Nothing + pure (Just scopeInfo, rcpModMs') + Just (GCSMemberSupport (Just gmId)) -> do + unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" + supportMem <- withFastStore $ \db -> getGroupMemberById db vr user gmId + unless (memberCurrentOrPending supportMem) $ throwChatError $ CECommandError "support member not current or pending" + let scopeInfo = GCSIMemberSupport (Just supportMem) + if memberStatus supportMem == GSMemPendingApproval + then pure (Just scopeInfo, [supportMem]) + else do + modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs + pure (Just scopeInfo, [supportMem] <> rcpModMs') + where + compatible GroupMember {activeConn, memberChatVRange} = + maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= modsCompatVersion + +mkLocalGroupChatScope :: GroupInfo -> CM (GroupInfo, Maybe GroupChatScopeInfo) +mkLocalGroupChatScope gInfo@GroupInfo {membership} + | memberPending membership = do + (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo + pure (gInfo', Just scopeInfo) + | otherwise = + pure (gInfo, Nothing) + +mkGroupChatScope :: GroupInfo -> GroupMember -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) +mkGroupChatScope gInfo@GroupInfo {membership} m + | memberPending membership = do + (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo + pure (gInfo', m, Just scopeInfo) + | memberPending m = do + (m', scopeInfo) <- mkMemberSupportChatInfo m + pure (gInfo, m', Just scopeInfo) + | otherwise = + pure (gInfo, m, Nothing) + +mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) +mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m msgScope_ = + mkGroupChatScope gInfo m >>= \case + groupScope@(_gInfo', _m', Just _scopeInfo) -> pure groupScope + (_, _, Nothing) -> case msgScope_ of + Nothing -> pure (gInfo, m, Nothing) + Just (MSMember mId) + | sameMemberId mId membership -> do + (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo + pure (gInfo', m, Just scopeInfo) + | otherwise -> do + referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId + -- TODO [knocking] return patched _referredMember' too? + (_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember + pure (gInfo, m, Just scopeInfo) + +mkGroupSupportChatInfo :: GroupInfo -> CM (GroupInfo, GroupChatScopeInfo) +mkGroupSupportChatInfo gInfo@GroupInfo {membership} = + case supportChat membership of + Nothing -> do + chatTs <- liftIO getCurrentTime + withStore' $ \db -> setSupportChatTs db (groupMemberId' membership) chatTs + let gInfo' = gInfo {membership = membership {supportChat = Just $ GroupSupportChat chatTs 0 0 0 Nothing}} + scopeInfo = GCSIMemberSupport {groupMember_ = Nothing} + pure (gInfo', scopeInfo) + Just _supportChat -> + let scopeInfo = GCSIMemberSupport {groupMember_ = Nothing} + in pure (gInfo, scopeInfo) + +mkMemberSupportChatInfo :: GroupMember -> CM (GroupMember, GroupChatScopeInfo) +mkMemberSupportChatInfo m@GroupMember {groupMemberId, supportChat} = + case supportChat of + Nothing -> do + chatTs <- liftIO getCurrentTime + withStore' $ \db -> setSupportChatTs db groupMemberId chatTs + let m' = m {supportChat = Just $ GroupSupportChat chatTs 0 0 0 Nothing} + scopeInfo = GCSIMemberSupport {groupMember_ = Just m'} + pure (m', scopeInfo) + Just _supportChat -> + let scopeInfo = GCSIMemberSupport {groupMember_ = Just m} + in pure (m, scopeInfo) + sendFileChunk :: User -> SndFileTransfer -> CM () sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} = unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ do @@ -1433,15 +1598,20 @@ deleteMemberConnection' GroupMember {activeConn} waitDelivery = do deleteAgentConnectionAsync' (aConnId conn) waitDelivery withStore' $ \db -> updateConnectionStatus db conn ConnDeleted -deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () -deleteOrUpdateMemberRecord user member = - withStore' $ \db -> deleteOrUpdateMemberRecordIO db user member +deleteOrUpdateMemberRecord :: User -> GroupInfo -> GroupMember -> CM GroupInfo +deleteOrUpdateMemberRecord user gInfo member = + withStore' $ \db -> deleteOrUpdateMemberRecordIO db user gInfo member -deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupMember -> IO () -deleteOrUpdateMemberRecordIO db user@User {userId} member = +deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo +deleteOrUpdateMemberRecordIO db user@User {userId} gInfo member = do + gInfo' <- + if gmRequiresAttention member + then decreaseGroupMembersRequireAttention db user gInfo + else pure gInfo checkGroupMemberHasItems db user member >>= \case Just _ -> updateGroupMemberStatus db userId member GSMemRemoved Nothing -> deleteGroupMember db user member + pure gInfo' sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages user ct events = do @@ -1628,9 +1798,9 @@ deliverMessagesB msgReqs = do where updatePQ = updateConnPQSndEnabled db connId pqSndEnabled' -sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage -sendGroupMessage user gInfo members chatMsgEvent = do - sendGroupMessages user gInfo members (chatMsgEvent :| []) >>= \case +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage user gInfo gcScope members chatMsgEvent = do + sendGroupMessages user gInfo gcScope members (chatMsgEvent :| []) >>= \case ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message" @@ -1640,9 +1810,9 @@ sendGroupMessage' user gInfo members chatMsgEvent = ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" -sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) -sendGroupMessages user gInfo members events = do - -- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step? +sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages user gInfo scope members events = do + -- TODO [knocking] send current profile to pending member after approval? when shouldSendProfileUpdate $ sendProfileUpdate `catchChatError` eToView sendGroupMessages_ user gInfo members events @@ -1650,6 +1820,7 @@ sendGroupMessages user gInfo members events = do User {profile = p, userMemberProfileUpdatedAt} = user GroupInfo {userMemberProfileSentAt} = gInfo shouldSendProfileUpdate + | isJust scope = False -- why not sending profile updates to scopes? | incognitoMembership gInfo = False | otherwise = case (userMemberProfileSentAt, userMemberProfileUpdatedAt) of @@ -1670,16 +1841,13 @@ data GroupSndResult = GroupSndResult } sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) -sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do +sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do let idsEvts = L.map (GroupId groupId,) events sndMsgs_ <- lift $ createSndMessages idsEvts - -- TODO [knocking] Possibly we need to pass GroupSndScope through all functions to here to avoid ad-hoc filtering. - recipientMembers <- case members of - [m] | memberStatus m == GSMemPendingApproval -> pure [m] - _ -> liftIO $ shuffleMembers (filter memberCurrent members) + recipientMembers' <- liftIO $ shuffleMembers recipientMembers let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} (toSendSeparate, toSendBatched, toPending, forwarded, _, dups) = - foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers + foldr' (addMember recipientMembers') ([], [], [], [], S.empty, 0 :: Int) recipientMembers' when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members" -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here -- Deliver to toSend members @@ -1701,7 +1869,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do liftM2 (<>) (shuffle adminMs) (shuffle otherMs) where isAdmin GroupMember {memberRole} = memberRole >= GRAdmin - addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) = + addMember members 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) @@ -1878,7 +2046,7 @@ saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothi saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd) saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = do let itemTexts = ciContentTexts content - saveSndChatItems user cd Nothing [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case + saveSndChatItems user cd [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case [Right ci] -> pure ci _ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item" @@ -1897,43 +2065,43 @@ saveSndChatItems :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> - Maybe NotInHistory -> [Either ChatError (NewSndChatItemData c)] -> Maybe CITimed -> Bool -> CM [Either ChatError (ChatItem c 'MDSnd)] -saveSndChatItems user cd notInHistory_ itemsData itemTimed live = do +saveSndChatItems user cd itemsData itemTimed live = do createdAt <- liftIO getCurrentTime + vr <- chatVersionRange when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ - withStore' (\db -> updateChatTs db user cd createdAt) + void $ withStore' (\db -> updateChatTsStats db vr user cd createdAt Nothing) lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) where createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do - ciId <- createNewSndChatItem db user cd notInHistory_ msg content quotedItem itemForwarded itemTimed live createdAt + ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt let ci = mkChatItem_ cd ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt Right <$> case cd of - CDGroupSnd g | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions + CDGroupSnd g _scope | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions _ -> pure ci -saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv) +saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv, ChatInfo c) saveRcvChatItemNoParse user cd msg brokerTs = saveRcvChatItem user cd msg brokerTs . ciContentNoParse -saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv) +saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv, ChatInfo c) saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content = - saveRcvChatItem' user cd Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty ciContentNoParse :: CIContent 'MDRcv -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) ciContentNoParse content = (content, (ciContentToText content, Nothing)) -saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv) -saveRcvChatItem' user cd notInHistory_ msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do +saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv, ChatInfo c) +saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do createdAt <- liftIO getCurrentTime + vr <- chatVersionRange withStore' $ \db -> do - when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt (mentions' :: Map MemberName CIMention, userMention) <- case cd of - CDGroupRcv g@GroupInfo {membership} _ -> do + CDGroupRcv g@GroupInfo {membership} _scope _m -> do mentions' <- getRcvCIMentions db user g ft_ mentions let userReply = case cmToQuotedMsg chatMsgEvent of Just QuotedMsg {msgRef = MsgRef {memberId = Just mId}} -> sameMemberId mId membership @@ -1941,12 +2109,23 @@ saveRcvChatItem' user cd notInHistory_ msg@RcvMessage {chatMsgEvent, forwardedBy userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' in pure (mentions', userMention') CDDirectRcv _ -> pure (M.empty, False) - (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd notInHistory_ msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt + cInfo' <- if (ciRequiresAttention content || contactChatDeleted cd) + then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) + else pure $ toChatInfo cd + (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt let ci = mkChatItem_ cd ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt - case cd of - CDGroupRcv g _ | not (null mentions') -> createGroupCIMentions db g ci mentions' + ci' <- case cd of + CDGroupRcv g _scope _m | not (null mentions') -> createGroupCIMentions db g ci mentions' _ -> pure ci + pure (ci', cInfo') + where + memberChatStats :: Bool -> Maybe (Int, MemberAttention, Int) + memberChatStats userMention = case cd of + CDGroupRcv _g (Just scope) m -> do + let unread = fromEnum $ ciCreateStatus content == CISRcvNew + in Just (unread, memberAttentionChange unread (Just brokerTs) m scope, fromEnum userMention) + _ -> Nothing -- TODO [mentions] optimize by avoiding unnecessary parsing mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d @@ -2162,19 +2341,39 @@ createInternalItemsForChats :: createInternalItemsForChats user itemTs_ dirsCIContents = do createdAt <- liftIO getCurrentTime let itemTs = fromMaybe createdAt itemTs_ - void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents + vr <- chatVersionRange' + void . withStoreBatch' $ \db -> map (uncurry $ updateChat db vr createdAt) dirsCIContents withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents where - updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO () - updateChat db createdAt cd contents - | any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt + updateChat :: DB.Connection -> VersionRangeChat -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO () + updateChat db vr createdAt cd contents + | any ciRequiresAttention contents || contactChatDeleted cd = void $ updateChatTsStats db vr user cd createdAt memberChatStats | otherwise = pure () + where + memberChatStats :: Maybe (Int, MemberAttention, Int) + memberChatStats = case cd of + CDGroupRcv _g (Just scope) m -> do + let unread = length $ filter ciRequiresAttention contents + in Just (unread, memberAttentionChange unread itemTs_ m scope, 0) + _ -> Nothing createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] createACIs db itemTs createdAt cd = map $ \content -> do ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci +memberAttentionChange :: Int -> (Maybe UTCTime) -> GroupMember -> GroupChatScopeInfo -> MemberAttention +memberAttentionChange unread brokerTs_ rcvMem = \case + GCSIMemberSupport (Just suppMem) + | groupMemberId' suppMem == groupMemberId' rcvMem -> MAInc unread brokerTs_ + | msgIsNewerThanLastUnanswered -> MAReset + | otherwise -> MAInc 0 Nothing + where + msgIsNewerThanLastUnanswered = case (supportChat suppMem >>= lastMsgFromMemberTs, brokerTs_) of + (Just lastMsgTs, Just brokerTs) -> lastMsgTs < brokerTs + _ -> False + GCSIMemberSupport Nothing -> MAInc 0 Nothing + createLocalChatItems :: User -> ChatDirection 'CTLocal 'MDSnd -> @@ -2182,14 +2381,15 @@ createLocalChatItems :: UTCTime -> CM [ChatItem 'CTLocal 'MDSnd] createLocalChatItems user cd itemsData createdAt = do - withStore' $ \db -> updateChatTs db user cd createdAt + vr <- chatVersionRange + void $ withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData) unless (null errs) $ toView $ CEvtChatErrors errs pure items where createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) createItem db (content, ciFile, itemForwarded, ts) = do - ciId <- createNewChatItem_ db user cd Nothing Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt + ciId <- createNewChatItem_ db user cd Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure $ mkChatItem_ cd ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index d60faef639..42c4cbc4f8 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -162,9 +162,9 @@ processAgentMsgSndFile _corrId aFileId msg = do throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId where withEntityLock_ :: Maybe ChatRef -> CM a -> CM a - withEntityLock_ cRef_ = case cRef_ of - Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgSndFile" contactId - Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgSndFile" groupId + withEntityLock_ = \case + Just (ChatRef CTDirect contactId _) -> withContactLock "processAgentMsgSndFile" contactId + Just (ChatRef CTGroup groupId _scope) -> withGroupLock "processAgentMsgSndFile" groupId _ -> id process :: User -> FileTransferId -> CM () process user fileId = do @@ -212,7 +212,7 @@ processAgentMsgSndFile _corrId aFileId msg = do Left e -> eToView e Nothing -> eToView $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) - (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do + (_, _, SMDSnd, GroupChat g@GroupInfo {groupId} _scope) -> do ms <- withStore' $ \db -> getGroupMembers db vr user g let rfdsMemberFTs = zipWith (\rfd (conn, sft) -> (conn, sft, fileDescrText rfd)) rfds (memberFTs ms) extraRFDs = drop (length rfdsMemberFTs) rfds @@ -304,9 +304,9 @@ processAgentMsgRcvFile _corrId aFileId msg = do throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId where withEntityLock_ :: Maybe ChatRef -> CM a -> CM a - withEntityLock_ cRef_ = case cRef_ of - Just (ChatRef CTDirect contactId) -> withContactLock "processAgentMsgRcvFile" contactId - Just (ChatRef CTGroup groupId) -> withGroupLock "processAgentMsgRcvFile" groupId + withEntityLock_ = \case + Just (ChatRef CTDirect contactId _) -> withContactLock "processAgentMsgRcvFile" contactId + Just (ChatRef CTGroup groupId _scope) -> withGroupLock "processAgentMsgRcvFile" groupId _ -> id process :: User -> FileTransferId -> CM () process user fileId = do @@ -385,7 +385,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = agentMsgConnStatus :: AEvent e -> Maybe ConnStatus agentMsgConnStatus = \case - JOINED True -> Just ConnSndReady + JOINED True _ -> Just ConnSndReady CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady CON _ -> Just ConnReady @@ -430,7 +430,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> @@ -449,7 +450,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () Just ct@Contact {contactId} -> case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> -- [async agent commands] XGrpMemIntro continuation on receiving INV withCompletedCommand conn agentMsg $ \_ -> case cReq of @@ -486,7 +488,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = 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 + XMsgUpdate sharedMsgId mContent _ ttl live _msgScope -> 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 @@ -638,7 +640,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED sqSecured -> + -- TODO [certs rcv] + JOINED sqSecured _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do @@ -676,7 +679,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of @@ -716,7 +720,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = } (_msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv -- we could link chat item with sent group invitation message (_msg) - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + createInternalChatItem user (CDGroupRcv gInfo Nothing m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing -- TODO REMOVE LEGACY ^^^ _ -> throwChatError $ CECommandError "unexpected cmdFunction" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" @@ -762,54 +766,75 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XOk -> pure () _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" pure () - CON _pqEnc -> unless (memberStatus m == GSMemRejected) $ do - status' <- case memberStatus m of - GSMemPendingApproval -> pure GSMemPendingApproval - _ -> do - withStore' $ \db -> do - updateGroupMemberStatus db userId m GSMemConnected - unless (memberActive membership) $ - updateGroupMemberStatus db userId membership GSMemConnected - -- possible improvement: check for each pending message, requires keeping track of connection state - unless (connDisabled conn) $ sendPendingGroupMessages user m conn - pure GSMemConnected + CON _pqEnc -> unless (memberStatus m == GSMemRejected || memberStatus membership == GSMemRejected) $ do + -- TODO [knocking] send pending messages after accepting? + -- possible improvement: check for each pending message, requires keeping track of connection state + unless (connDisabled conn) $ sendPendingGroupMessages user m conn withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ chatHasNtfs chatSettings case memberCategory m of GCHostMember -> do - toView $ CEvtUserJoinedGroup user gInfo {membership = membership {memberStatus = status'}} m {memberStatus = status'} - let cd = CDGroupRcv gInfo m + (m', gInfo') <- withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemConnected + gInfo' <- + if not (memberPending membership) + then do + updateGroupMemberStatus db userId membership GSMemConnected + pure gInfo {membership = membership {memberStatus = GSMemConnected}} + else pure gInfo + pure (m {memberStatus = GSMemConnected}, gInfo') + toView $ CEvtUserJoinedGroup user gInfo' m' + (gInfo'', m'', scopeInfo) <- mkGroupChatScope gInfo' m' + let cd = CDGroupRcv gInfo'' scopeInfo m'' createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing - createGroupFeatureItems user cd CIRcvGroupFeature gInfo - let GroupInfo {groupProfile = GroupProfile {description}} = gInfo - memberConnectedChatItem gInfo m - unless expectHistory $ forM_ description $ groupDescriptionChatItem gInfo m - where - expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion + createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' + memberConnectedChatItem gInfo'' scopeInfo m'' + unless (memberPending membership) $ maybeCreateGroupDescrLocal gInfo'' m'' GCInviteeMember -> do - memberConnectedChatItem gInfo m - toView $ CEvtJoinedGroupMember user gInfo m {memberStatus = status'} + (gInfo', mStatus) <- + if not (memberPending m) + then do + mStatus <- withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected $> GSMemConnected + pure (gInfo, mStatus) + else do + gInfo' <- withStore' $ \db -> increaseGroupMembersRequireAttention db user gInfo + pure (gInfo', memberStatus m) + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + memberConnectedChatItem gInfo'' scopeInfo m' + case scopeInfo of + Just (GCSIMemberSupport _) -> do + createInternalChatItem user (CDGroupRcv gInfo'' scopeInfo m') (CIRcvGroupEvent RGENewMemberPendingReview) Nothing + _ -> pure () + toView $ CEvtJoinedGroupMember user gInfo'' m' {memberStatus = mStatus} let Connection {viaUserContactLink} = conn - when (isJust viaUserContactLink && isNothing (memberContactId m)) sendXGrpLinkMem + when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' when (connChatVersion < batchSend2Version) sendGroupAutoReply - unless (status' == GSMemPendingApproval) $ introduceToGroup vr user gInfo m + case mStatus of + GSMemPendingApproval -> pure () + GSMemPendingReview -> introduceToModerators vr user gInfo'' m' + _ -> do + introduceToAll vr user gInfo'' m' + when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m' where - sendXGrpLinkMem = do - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + sendXGrpLinkMem gInfo'' = do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo'' profileToSend = profileToSendOnAccept user profileMode True void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId _ -> do + unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected let memCategory = memberCategory m withStore' (\db -> getViaGroupContact db vr user m) >>= \case Nothing -> do notifyMemberConnected gInfo m Nothing let connectedIncognito = memberIncognito membership - when (memCategory == GCPreMember) $ probeMatchingMemberContact m connectedIncognito + when (memCategory == GCPreMember) $ + probeMatchingMemberContact m connectedIncognito Just ct@Contact {activeConn} -> forM_ activeConn $ \Connection {connStatus} -> when (connStatus == ConnReady) $ do notifyMemberConnected gInfo m $ Just ct let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo - when (memCategory == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True + when (memCategory == GCPreMember && not (memberPending membership)) $ + probeMatchingContactsAndMembers ct connectedIncognito True sendXGrpMemCon memCategory where GroupMember {memberId} = m @@ -828,10 +853,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = MSG msgMeta _msgFlags msgBody -> do tags <- newTVarIO [] withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do - checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () + -- possible improvement is to choose scope based on event (some events specify scope) + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure () forM_ aChatMsgs $ \case Right (ACMsg _ chatMsg) -> - processEvent tags eInfo chatMsg `catchChatError` \e -> eToView e + processEvent gInfo' m' tags eInfo chatMsg `catchChatError` \e -> eToView e Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e @@ -841,43 +868,46 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgs = parseChatMessages msgBody brokerTs = metaBrokerTs msgMeta - processEvent :: TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () - processEvent tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + processEvent :: GroupInfo -> GroupMember -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent gInfo' m' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do let tag = toCMEventTag chatMsgEvent atomically $ modifyTVar' tags (tshow tag :) logInfo $ "group msg=" <> tshow tag <> " " <> eInfo - (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg + (m'', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta msgBody chatMsg case event of - XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False - XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live -> memberCanSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent mentions msg brokerTs ttl live - XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg brokerTs - XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg brokerTs + XMsgNew mc -> memberCanSend m'' scope $ newGroupContentMessage gInfo' m'' mc msg brokerTs False + where ExtMsgContent {scope} = mcExtMsgContent mc + -- file description is always allowed, to allow sending files to support scope + XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live + XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo' m'' sharedMsgId memberId msg brokerTs + XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo' m'' sharedMsgId memberId reaction add msg brokerTs -- TODO discontinue XFile - XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg brokerTs - XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName - XInfo p -> xInfoMember gInfo m' p brokerTs - XGrpLinkMem p -> xGrpLinkMem gInfo m' conn' p - XGrpLinkAcpt role -> xGrpLinkAcpt gInfo m' role - XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg brokerTs - XGrpMemIntro memInfo memRestrictions_ -> xGrpMemIntro gInfo m' memInfo memRestrictions_ - XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv - XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv - XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg brokerTs - XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo m' memId memRestrictions msg brokerTs - XGrpMemCon memId -> xGrpMemCon gInfo m' memId - XGrpMemDel memId withMessages -> xGrpMemDel gInfo m' memId withMessages msg brokerTs - XGrpLeave -> xGrpLeave gInfo m' msg brokerTs - XGrpDel -> xGrpDel gInfo m' msg brokerTs - XGrpInfo p' -> xGrpInfo gInfo m' p' msg brokerTs - XGrpPrefs ps' -> xGrpPrefs gInfo m' ps' - XGrpDirectInv connReq mContent_ -> memberCanSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg brokerTs - XGrpMsgForward memberId msg' msgTs -> xGrpMsgForward gInfo m' memberId msg' msgTs - XInfoProbe probe -> xInfoProbe (COMGroupMember m') probe - XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash - XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe - BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta + XFile fInv -> processGroupFileInvitation' gInfo' m'' fInv msg brokerTs + XFileCancel sharedMsgId -> xFileCancelGroup gInfo' m'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName + XInfo p -> xInfoMember gInfo' m'' p brokerTs + XGrpLinkMem p -> xGrpLinkMem gInfo' m'' conn' p + XGrpLinkAcpt acceptance role memberId -> xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs + XGrpMemNew memInfo msgScope -> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs + XGrpMemIntro memInfo memRestrictions_ -> xGrpMemIntro gInfo' m'' memInfo memRestrictions_ + XGrpMemInv memId introInv -> xGrpMemInv gInfo' m'' memId introInv + XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo' m'' memInfo introInv + XGrpMemRole memId memRole -> xGrpMemRole gInfo' m'' memId memRole msg brokerTs + XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs + XGrpMemCon memId -> xGrpMemCon gInfo' m'' memId + XGrpMemDel memId withMessages -> xGrpMemDel gInfo' m'' memId withMessages msg brokerTs + XGrpLeave -> xGrpLeave gInfo' m'' msg brokerTs + XGrpDel -> xGrpDel gInfo' m'' msg brokerTs + XGrpInfo p' -> xGrpInfo gInfo' m'' p' msg brokerTs + XGrpPrefs ps' -> xGrpPrefs gInfo' m'' ps' + -- TODO [knocking] why don't we forward these messages? + XGrpDirectInv connReq mContent_ msgScope -> memberCanSend m'' msgScope $ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs + XGrpMsgForward memberId msg' msgTs -> xGrpMsgForward gInfo' m'' memberId msg' msgTs + XInfoProbe probe -> xInfoProbe (COMGroupMember m'') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m'') probeHash + XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m'') probe + BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo' sharedMsgId chunk msgMeta _ -> messageError $ "unsupported message: " <> tshow event checkSendRcpt :: [AChatMessage] -> CM Bool checkSendRcpt aMsgs = do @@ -892,6 +922,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = hasDeliveryReceipt (toCMEventTag chatMsgEvent) forwardMsgs :: [AChatMessage] -> CM () forwardMsgs aMsgs = do + -- TODO [knocking] forward to/from GSMemPendingReview members let GroupMember {memberRole = membershipMemRole} = membership when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ do let forwardedMsgs = mapMaybe (\(ACMsg _ chatMsg) -> forwardedGroupMsg chatMsg) aMsgs @@ -907,7 +938,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupMember {memberId} = m ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) forwardedMsgs' events = L.map (\cm -> XGrpMsgForward memberId cm brokerTs) forwardedMsgs' - unless (null ms) $ void $ sendGroupMessages user gInfo ms events + unless (null ms) $ void $ sendGroupMessages_ user gInfo ms events RCVD msgMeta msgRcpt -> withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt @@ -919,40 +950,43 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m when (phase == SPStarted || phase == SPCompleted) $ case qd of - QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing - QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing - RSYNC rss cryptoErr_ cStats -> + QDRcv -> createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m') Nothing + QDSnd -> createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvConnEvent $ RCESwitchQueue phase) Nothing + RSYNC rss cryptoErr_ cStats -> do + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m case (rss, connectionCode, cryptoErr_) of - (RSRequired, _, Just cryptoErr) -> processErr cryptoErr - (RSAllowed, _, Just cryptoErr) -> processErr cryptoErr + (RSRequired, _, Just cryptoErr) -> processErr gInfo' scopeInfo m' cryptoErr + (RSAllowed, _, Just cryptoErr) -> processErr gInfo' scopeInfo m' cryptoErr (RSAgreed, Just _, _) -> do withStore' $ \db -> setConnectionVerified db user connId Nothing - let m' = m {activeConn = Just (conn {connectionCode = Nothing} :: Connection)} :: GroupMember - ratchetSyncEventItem m' - toViewTE $ TEGroupMemberVerificationReset user gInfo m' - createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent RCEVerificationCodeReset) Nothing - _ -> ratchetSyncEventItem m + let m'' = m' {activeConn = Just (conn {connectionCode = Nothing} :: Connection)} :: GroupMember + ratchetSyncEventItem gInfo' scopeInfo m'' + toViewTE $ TEGroupMemberVerificationReset user gInfo' m'' + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m'') (CIRcvConnEvent RCEVerificationCodeReset) Nothing + _ -> ratchetSyncEventItem gInfo' scopeInfo m' where - processErr cryptoErr = do + processErr gInfo' scopeInfo m' cryptoErr = do let e@(mde, n) = agentMsgDecryptError cryptoErr ci_ <- withStore $ \db -> - getGroupMemberChatItemLast db user groupId (groupMemberId' m) + getGroupMemberChatItemLast db user groupId (groupMemberId' m') >>= liftIO . mapM (\(ci, content') -> updateGroupChatItem db user groupId ci content' False False Nothing) . mdeUpdatedCI e case ci_ of - Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + Just ci -> toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo' scopeInfo) ci) _ -> do - toView $ CEvtGroupMemberRatchetSync user gInfo m (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvDecryptionError mde n) Nothing - ratchetSyncEventItem m' = do - toView $ CEvtGroupMemberRatchetSync user gInfo m' (RatchetSyncProgress rss cStats) - createInternalChatItem user (CDGroupRcv gInfo m') (CIRcvConnEvent $ RCERatchetSync rss) Nothing + toView $ CEvtGroupMemberRatchetSync user gInfo' m' (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvDecryptionError mde n) Nothing + ratchetSyncEventItem gInfo' scopeInfo m' = do + toView $ CEvtGroupMemberRatchetSync user gInfo' m' (RatchetSyncProgress rss cStats) + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvConnEvent $ RCERatchetSync rss) Nothing OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED sqSecured -> + -- TODO [certs rcv] + JOINED sqSecured _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (sqSecured && connChatVersion >= batchSend2Version) sendGroupAutoReply @@ -998,9 +1032,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure Nothing send mc = do msg <- sendGroupMessage' user gInfo [m] (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndMsgContent mc) + ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc) withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew - toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci] + toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci] agentMsgDecryptError :: AgentCryptoError -> (MsgDecryptError, Word32) agentMsgDecryptError = \case @@ -1056,7 +1090,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do ci <- withStore $ \db -> do liftIO (lookupChatRefByFileId db user fileId) >>= \case - Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled + Just (ChatRef CTDirect _ _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled _ -> pure () lookupChatItemByFileId db vr user fileId toView $ CEvtSndFileRcvCancelled user ci ft @@ -1066,7 +1100,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId-> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do @@ -1078,7 +1113,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = case agentMsg of - INV (ACR _ cReq) -> + -- TODO [certs rcv] + INV (ACR _ cReq) _serviceId -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of fileInvConnReq@(CRInvitationUri _ _) -> case cmdFunction of @@ -1115,7 +1151,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - JOINED _ -> + -- TODO [certs rcv] + JOINED _ _serviceId -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do @@ -1215,8 +1252,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode - createInternalChatItem user (CDGroupRcv gInfo mem) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CEvtAcceptingGroupJoinRequestMember user gInfo mem + (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' Left rjctReason | v < groupJoinRejectVersion -> messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" @@ -1225,11 +1263,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason _ -> toView $ CEvtReceivedContactRequest user cReq - -- TODO [knocking] review - memberCanSend :: GroupMember -> CM () -> CM () - memberCanSend GroupMember {memberRole, memberStatus} a - | memberRole > GRObserver || memberStatus == GSMemPendingApproval = a - | otherwise = messageError "member is not allowed to send messages" + memberCanSend :: GroupMember -> Maybe MsgScope -> CM () -> CM () + memberCanSend m@GroupMember {memberRole} msgScope a = case msgScope of + Just MSMember {} -> a + Nothing + | memberRole > GRObserver || memberPending m -> a + | otherwise -> messageError "member is not allowed to send messages" processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () processConnMERR connEntity conn err = do @@ -1352,20 +1391,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync throwChatError $ CEFileRcvChunk err - memberConnectedChatItem :: GroupInfo -> GroupMember -> CM () - memberConnectedChatItem gInfo m = + memberConnectedChatItem :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> CM () + memberConnectedChatItem gInfo scopeInfo m = -- ts should be broker ts but we don't have it for CON - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing - - groupDescriptionChatItem :: GroupInfo -> GroupMember -> Text -> CM () - groupDescriptionChatItem gInfo m descr = - createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvMsgContent $ MCText descr) Nothing + createInternalChatItem user (CDGroupRcv gInfo scopeInfo m) (CIRcvGroupEvent RGEMemberConnected) Nothing notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> CM () notifyMemberConnected gInfo m ct_ = do - memberConnectedChatItem gInfo m + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + memberConnectedChatItem gInfo' scopeInfo m' lift $ mapM_ (`setContactNetworkStatus` NSConnected) ct_ - toView $ CEvtConnectedToGroupMember user gInfo m ct_ + toView $ CEvtConnectedToGroupMember user gInfo' m' ct_ probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> CM () probeMatchingContactsAndMembers ct connectedIncognito doProbeContacts = do @@ -1429,7 +1465,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newContentMessage :: Contact -> MsgContainer -> RcvMessage -> MsgMeta -> CM () newContentMessage ct mc msg@RcvMessage {sharedMsgId_} msgMeta = do - let ExtMsgContent content _ fInv_ _ _ = mcExtMsgContent mc + let ExtMsgContent content _ fInv_ _ _ _ = mcExtMsgContent mc -- Uncomment to test stuck delivery on errors - see test testDirectMessageDelete -- case content of -- MCText "hello 111" -> @@ -1440,7 +1476,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do void $ newChatItem (ciContentNoParse $ CIRcvChatFeatureRejected CFVoice) Nothing Nothing False else do - let ExtMsgContent _ _ _ itemTTL live_ = mcExtMsgContent mc + let ExtMsgContent _ _ _ itemTTL live_ _ = mcExtMsgContent mc timed_ = rcvContactCITimed ct itemTTL live = fromMaybe False live_ file_ <- processFileInvitation fInv_ content $ \db -> createRcvFileTransfer db userId ct @@ -1449,9 +1485,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where brokerTs = metaBrokerTs msgMeta newChatItem content ciFile_ timed_ live = do - ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile_ timed_ live M.empty reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getDirectCIReactions db ct sharedMsgId) sharedMsgId_ - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci {reactions}] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci {reactions}] autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do @@ -1467,7 +1503,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> CM () groupMessageFileDescription g@GroupInfo {groupId} m sharedMsgId fileDescr = do fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - processFDMessage (CDGroupRcv g m) sharedMsgId fileId fileDescr + -- here scope we pass only affects how chat item is searched in getAChatItemBySharedMsgId, and it ignores scope + processFDMessage (CDGroupRcv g Nothing m) sharedMsgId fileId fileDescr processFDMessage :: ChatTypeQuotable c => ChatDirection c 'MDRcv -> SharedMsgId -> FileTransferId -> FileDescr -> CM () processFDMessage cd sharedMsgId fileId fileDescr = do @@ -1516,11 +1553,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- Chat item and update message which created it will have different sharedMsgId in this case... let timed_ = rcvContactCITimed ct ttl ts = ciContentTexts content - ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live M.empty ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc updateDirectChatItem' db user contactId ci content True live Nothing Nothing - toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv cInfo ci') where brokerTs = metaBrokerTs msgMeta content = CIRcvMsgContent mc @@ -1540,7 +1577,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let edited = itemLive /= Just True updateDirectChatItem' db user contactId ci {reactions} content edited live Nothing $ Just msgId toView $ CEvtChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + startUpdatedTimedItemThread user (ChatRef CTDirect contactId Nothing) ci ci' else toView $ CEvtChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) _ -> messageError "x.msg.update: contact attempted invalid message update" @@ -1598,12 +1635,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateChatItemReaction = do cEvt_ <- withStore $ \db -> do CChatItem md ci <- getGroupMemberCIBySharedMsgId db user g itemMemberId sharedMsgId + scopeInfo <- getGroupChatScopeInfoForItem db vr user g (chatItemId' ci) if ciReactionAllowed ci then liftIO $ do setGroupReaction db g m itemMemberId sharedMsgId False reaction add msgId brokerTs reactions <- getGroupCIReactions db g itemMemberId sharedMsgId let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDRcv (GroupChat g) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction + r = ACIReaction SCTGroup SMDRcv (GroupChat g scopeInfo) $ CIReaction (CIGroupRcv m) ci' brokerTs reaction pure $ Just $ CEvtChatItemReaction user add r else pure Nothing mapM_ toView cEvt_ @@ -1618,63 +1656,66 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = e -> throwError e newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM () - newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded - | blockedByAdmin m = createBlockedByAdmin - | otherwise = case prohibitedGroupContent gInfo m content ft_ fInv_ False of - Just f -> rejected f - Nothing -> - withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration ciModeration - withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ - Nothing -> createContentItem + newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded = do + (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m msgScope_ + if blockedByAdmin m' + then createBlockedByAdmin gInfo' m' scopeInfo + else + case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of + Just f -> rejected gInfo' m' scopeInfo f + Nothing -> + withStore' (\db -> getCIModeration db vr user gInfo' memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration gInfo' m' scopeInfo ciModeration + withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ + Nothing -> createContentItem gInfo' m' scopeInfo where - rejected f = newChatItem (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False - timed' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo itemTTL + rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False + timed' gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL live' = fromMaybe False live_ - ExtMsgContent content mentions fInv_ itemTTL live_ = mcExtMsgContent mc + ExtMsgContent content mentions fInv_ itemTTL live_ msgScope_ = mcExtMsgContent mc ts@(_, ft_) = msgContentTexts content - saveRcvCI = saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg sharedMsgId_ brokerTs - createBlockedByAdmin - | groupFeatureAllowed SGFFullDelete gInfo = do + saveRcvCI gInfo' m' scopeInfo = saveRcvChatItem' user (CDGroupRcv gInfo' scopeInfo m') msg sharedMsgId_ brokerTs + createBlockedByAdmin gInfo' m' scopeInfo + | groupFeatureAllowed SGFFullDelete gInfo' = do -- ignores member role when blocked by admin - ci <- saveRcvCI (ciContentNoParse CIRcvBlocked) Nothing timed' False M.empty - ci' <- withStore' $ \db -> updateGroupCIBlockedByAdmin db user gInfo ci brokerTs - groupMsgToView gInfo ci' + (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo (ciContentNoParse CIRcvBlocked) Nothing (timed' gInfo') False M.empty + ci' <- withStore' $ \db -> updateGroupCIBlockedByAdmin db user gInfo' ci brokerTs + groupMsgToView cInfo ci' | otherwise = do - file_ <- processFileInv - ci <- createNonLive file_ - ci' <- withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo ci - groupMsgToView gInfo ci' - applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, moderatedAt} + file_ <- processFileInv m' + (ci, cInfo) <- createNonLive gInfo' m' scopeInfo file_ + ci' <- withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo' ci + groupMsgToView cInfo ci' + applyModeration gInfo' m' scopeInfo CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, moderatedAt} | moderatorRole < GRModerator || moderatorRole < memberRole = - createContentItem - | groupFeatureMemberAllowed SGFFullDelete moderator gInfo = do - ci <- saveRcvCI (ciContentNoParse CIRcvModerated) Nothing timed' False M.empty - ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo ci moderator moderatedAt - groupMsgToView gInfo ci' + createContentItem gInfo' m' scopeInfo + | groupFeatureMemberAllowed SGFFullDelete moderator gInfo' = do + (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo (ciContentNoParse CIRcvModerated) Nothing (timed' gInfo') False M.empty + ci' <- withStore' $ \db -> updateGroupChatItemModerated db user gInfo' ci moderator moderatedAt + groupMsgToView cInfo ci' | otherwise = do - file_ <- processFileInv - ci <- createNonLive file_ - deletions <- markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] (Just moderator) moderatedAt + file_ <- processFileInv m' + (ci, _cInfo) <- createNonLive gInfo' m' scopeInfo file_ + deletions <- markGroupCIsDeleted user gInfo' scopeInfo [CChatItem SMDRcv ci] (Just moderator) moderatedAt toView $ CEvtChatItemsDeleted user deletions False False - createNonLive file_ = - saveRcvCI (CIRcvMsgContent content, ts) (snd <$> file_) timed' False mentions - createContentItem = do - file_ <- processFileInv - newChatItem (CIRcvMsgContent content, ts) (snd <$> file_) timed' live' - when (showMessages $ memberSettings m) $ autoAcceptFile file_ - processFileInv = - processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m - newChatItem ciContent ciFile_ timed_ live = do - let mentions' = if showMessages (memberSettings m) then mentions else [] - ci <- saveRcvCI ciContent ciFile_ timed_ live mentions' - ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci - reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo memberId sharedMsgId) sharedMsgId_ - groupMsgToView gInfo ci' {reactions} + createNonLive gInfo' m' scopeInfo file_ = do + saveRcvCI gInfo' m' scopeInfo (CIRcvMsgContent content, ts) (snd <$> file_) (timed' gInfo') False mentions + createContentItem gInfo' m' scopeInfo = do + file_ <- processFileInv m' + newChatItem gInfo' m' scopeInfo (CIRcvMsgContent content, ts) (snd <$> file_) (timed' gInfo') live' + when (showMessages $ memberSettings m') $ autoAcceptFile file_ + processFileInv m' = + processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId m' + newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed_ live = do + let mentions' = if showMessages (memberSettings m') then mentions else [] + (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed_ live mentions' + ci' <- blockedMember m' ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo' ci + reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo' memberId sharedMsgId) sharedMsgId_ + groupMsgToView cInfo ci' {reactions} - groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM () - groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc mentions msg@RcvMessage {msgId} brokerTs ttl_ live_ + groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM () + groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ | prohibitedSimplexLinks gInfo m ft_ = messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks | otherwise = do @@ -1684,18 +1725,20 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- Chat item and update message which created it will have different sharedMsgId in this case... let timed_ = rcvGroupCITimed gInfo ttl_ mentions' = if showMessages (memberSettings m) then mentions else [] - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) (memberNotInHistory m) msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' + (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m msgScope_ + (ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo' scopeInfo m') msg (Just sharedMsgId) brokerTs (content, ts) Nothing timed_ live mentions' ci' <- withStore' $ \db -> do createChatItemVersion db (chatItemId' ci) brokerTs mc ci' <- updateGroupChatItem db user groupId ci content True live Nothing - blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' - toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + blockedMember m' ci' $ markGroupChatItemBlocked db user gInfo' ci' + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci') where content = CIRcvMsgContent mc ts@(_, ft_) = msgContentTexts mc live = fromMaybe False live_ updateRcvChatItem = do cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user gInfo groupMemberId sharedMsgId + scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) case cci of CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} -> if sameMemberId memberId m' @@ -1711,17 +1754,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ciMentions <- getRcvCIMentions db user gInfo ft_ mentions ci' <- updateGroupChatItem db user groupId ci {reactions} content edited live $ Just msgId updateGroupCIMentions db gInfo ci' ciMentions - toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - else toView $ CEvtChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo scopeInfo) ci') + startUpdatedTimedItemThread user (ChatRef CTGroup groupId $ toChatScope <$> scopeInfo) ci ci' + else toView $ CEvtChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo scopeInfo) ci) else messageError "x.msg.update: group member attempted to update a message of another member" _ -> messageError "x.msg.update: group member attempted invalid message update" - memberNotInHistory :: GroupMember -> Maybe NotInHistory - memberNotInHistory = \case - GroupMember {memberStatus = GSMemPendingApproval} -> Just NotInHistory - _ -> Nothing - groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> UTCTime -> CM () groupMessageDelete gInfo@GroupInfo {membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do let msgMemberId = fromMaybe memberId sndMemberId_ @@ -1760,9 +1798,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = a delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM () delete cci byGroupMember = do + scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) deletions <- if groupFeatureMemberAllowed SGFFullDelete m gInfo - then deleteGroupCIs user gInfo [cci] byGroupMember brokerTs - else markGroupCIsDeleted user gInfo [cci] byGroupMember brokerTs + then deleteGroupCIs user gInfo scopeInfo [cci] byGroupMember brokerTs + else markGroupCIsDeleted user gInfo scopeInfo [cci] byGroupMember brokerTs toView $ CEvtChatItemsDeleted user deletions False False archiveMessageReports :: CChatItem 'CTGroup -> GroupMember -> CM () archiveMessageReports (CChatItem _ ci) byMember = do @@ -1779,8 +1818,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" - ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] where brokerTs = metaBrokerTs msgMeta @@ -1793,9 +1832,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) Nothing msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty + (ci, cInfo) <- saveRcvChatItem' user (CDGroupRcv gInfo Nothing m) msg sharedMsgId_ brokerTs content ciFile Nothing False M.empty ci' <- blockedMember m ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci - groupMsgToView gInfo ci' + groupMsgToView cInfo ci' blockedMember :: Monad m' => GroupMember -> ChatItem c d -> m' (ChatItem c d) -> m' (ChatItem c d) blockedMember m ci blockedCI @@ -1946,9 +1985,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 :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () - groupMsgToView gInfo ci = - toView $ CEvtNewChatItems user [AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci] + groupMsgToView :: forall d. MsgDirectionI d => ChatInfo 'CTGroup -> ChatItem 'CTGroup d -> CM () + groupMsgToView cInfo ci = do + toView $ CEvtNewChatItems user [AChatItem SCTGroup (msgDirection @d) cInfo ci] processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta = do @@ -1973,9 +2012,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) else do let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole - ci <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content + (ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] toView $ CEvtReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} where brokerTs = metaBrokerTs msgMeta @@ -2001,8 +2040,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} let ct'' = ct' {activeConn = activeConn'} :: Contact - ci <- saveRcvChatItemNoParse user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct'') ci] + (ci, cInfo) <- saveRcvChatItemNoParse user (CDDirectRcv ct'') msg brokerTs (CIRcvDirectEvent RDEContactDeleted) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] toView $ CEvtContactDeletedByContact user ct'' else do contactConns <- withStore' $ \db -> getContactConnections db vr userId c @@ -2056,26 +2095,71 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xInfoMember gInfo m p' brokerTs = void $ processMemberProfileUpdate gInfo m p' True (Just brokerTs) xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM () - xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory, memberStatus} Connection {viaGroupLink} p' = do + xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived then do m' <- processMemberProfileUpdate gInfo m p' False Nothing withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True - unless (memberStatus == GSMemPendingApproval) $ do - let connectedIncognito = memberIncognito membership - probeMatchingMemberContact m' connectedIncognito + let connectedIncognito = memberIncognito membership + probeMatchingMemberContact m' connectedIncognito else messageError "x.grp.link.mem error: invalid group link host profile update" - xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupMemberRole -> CM () - xGrpLinkAcpt gInfo@GroupInfo {membership} m role = do - membership' <- withStore' $ \db -> do - updateGroupMemberStatus db userId m GSMemConnected - updateGroupMemberAccepted db user membership role - let m' = m {memberStatus = GSMemConnected} - toView $ CEvtUserJoinedGroup user gInfo {membership = membership'} m' - let connectedIncognito = memberIncognito membership - probeMatchingMemberContact m' connectedIncognito + xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupAcceptance -> GroupMemberRole -> MemberId -> RcvMessage -> UTCTime -> CM () + xGrpLinkAcpt gInfo@GroupInfo {membership} m acceptance role memberId msg brokerTs + | sameMemberId memberId membership = processUserAccepted + | otherwise = + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case + Left _ -> messageError "x.grp.link.acpt error: referenced member does not exist" + Right referencedMember -> do + (referencedMember', gInfo') <- withFastStore' $ \db -> do + referencedMember' <- updateGroupMemberAccepted db user referencedMember (newMemberStatus referencedMember) role + gInfo' <- updateGroupMembersRequireAttention db user gInfo referencedMember referencedMember' + pure (referencedMember', gInfo') + when (memberCategory referencedMember == GCInviteeMember) $ introduceToRemainingMembers referencedMember' + -- create item in both scopes + memberConnectedChatItem gInfo' Nothing referencedMember' + let scopeInfo = Just $ GCSIMemberSupport {groupMember_ = Just referencedMember'} + gEvent = RGEMemberAccepted (groupMemberId' referencedMember') (fromLocalProfile $ memberProfile referencedMember') + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m) msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView cInfo ci + toView $ CEvtMemberAcceptedByOther user gInfo' m referencedMember' + where + newMemberStatus refMem = case memberConn refMem of + Just c | connReady c -> GSMemConnected + _ -> GSMemAnnounced + where + processUserAccepted = case acceptance of + GAAccepted -> do + membership' <- withStore' $ \db -> updateGroupMemberAccepted db user membership GSMemConnected role + -- create item in both scopes + let gInfo' = gInfo {membership = membership'} + cd = CDGroupRcv gInfo' Nothing m + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createGroupFeatureItems user cd CIRcvGroupFeature gInfo' + maybeCreateGroupDescrLocal gInfo' m + createInternalChatItem user cd (CIRcvGroupEvent RGEUserAccepted) Nothing + let scopeInfo = Just $ GCSIMemberSupport {groupMember_ = Nothing} + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m) (CIRcvGroupEvent RGEUserAccepted) Nothing + toView $ CEvtUserJoinedGroup user gInfo' m + GAPendingReview -> do + membership' <- withStore' $ \db -> updateGroupMemberAccepted db user membership GSMemPendingReview role + let gInfo' = gInfo {membership = membership'} + scopeInfo = Just $ GCSIMemberSupport {groupMember_ = Nothing} + createInternalChatItem user (CDGroupSnd gInfo' scopeInfo) (CISndGroupEvent SGEUserPendingReview) Nothing + toView $ CEvtMemberAcceptedByOther user gInfo' m membership' + GAPendingApproval -> + messageWarning "x.grp.link.acpt: unexpected group acceptance - pending approval" + introduceToRemainingMembers acceptedMember = do + introduceToRemaining vr user gInfo acceptedMember + when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo acceptedMember + + maybeCreateGroupDescrLocal :: GroupInfo -> GroupMember -> CM () + maybeCreateGroupDescrLocal gInfo@GroupInfo {groupProfile = GroupProfile {description}} m = + unless expectHistory $ forM_ description $ \descr -> + createInternalChatItem user (CDGroupRcv gInfo Nothing m) (CIRcvMsgContent $ MCText descr) Nothing + where + expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Bool -> Maybe UTCTime -> CM GroupMember processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' createItems itemTs_ @@ -2116,8 +2200,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = BCCustomer -> customerId == memberId createProfileUpdatedItem m' = when createItems $ do + (gInfo', m'', scopeInfo) <- mkGroupChatScope gInfo m' let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' - createInternalChatItem user (CDGroupRcv gInfo m') ciContent itemTs_ + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m'') ciContent itemTs_ createFeatureEnabledItems :: Contact -> CM () createFeatureEnabledItems ct@Contact {mergedPreferences} = @@ -2204,7 +2289,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do g <- asks random dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing - ci <- saveCallItem CISCallPending + (ci, cInfo) <- saveCallItem CISCallPending callUUID <- UUID.toText <$> liftIO V4.nextRandom let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} @@ -2217,15 +2302,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing toView $ CEvtCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] else featureRejected CFCalls where brokerTs = metaBrokerTs msgMeta saveCallItem status = saveRcvChatItemNoParse user (CDDirectRcv ct) msg brokerTs (CIRcvCall status 0) featureRejected f = do let content = ciContentNoParse $ CIRcvChatFeatureRejected f - ci <- saveRcvChatItem' user (CDDirectRcv ct) Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] + (ci, cInfo) <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] -- to party initiating call xCallOffer :: Contact -> CallId -> CallOffer -> RcvMessage -> CM () @@ -2301,7 +2386,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = timed_ <- callTimed ct aciContent updateDirectChatItemView user ct chatItemId aciContent False False timed_ $ Just msgId forM_ (timed_ >>= timedDeleteAt') $ - startProximateTimedItemThread user (ChatRef CTDirect ctId', chatItemId) + startProximateTimedItemThread user (ChatRef CTDirect ctId' Nothing, chatItemId) msgCallStateError :: Text -> Call -> CM () msgCallStateError eventName Call {callState} = @@ -2390,25 +2475,48 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO show/log error, other events in SMP confirmation _ -> pure (conn', False) - xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> UTCTime -> CM () - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msg brokerTs = do + xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM () + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _) msgScope_ msg brokerTs = do checkHostRole m memRole unless (sameMemberId memId $ membership gInfo) $ withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do - updatedMember <- withStore $ \db -> updateUnknownMemberAnnounced db vr user m unknownMember memInfo - toView $ CEvtUnknownMemberAnnounced user gInfo m unknownMember updatedMember - memberAnnouncedToView updatedMember + (updatedMember, gInfo') <- withStore $ \db -> do + updatedMember <- updateUnknownMemberAnnounced db vr user m unknownMember memInfo initialStatus + gInfo' <- if memberPending updatedMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (updatedMember, gInfo') + toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember + memberAnnouncedToView updatedMember gInfo' Right _ -> messageError "x.grp.mem.new error: member already exists" Left _ -> do - newMember <- withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced - memberAnnouncedToView newMember + (newMember, gInfo') <- withStore $ \db -> do + newMember <- createNewGroupMember db user gInfo m memInfo GCPostMember initialStatus + gInfo' <- if memberPending newMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (newMember, gInfo') + memberAnnouncedToView newMember gInfo' where - memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} = do + initialStatus = case msgScope_ of + Just (MSMember _) -> GSMemPendingReview + _ -> GSMemAnnounced + memberAnnouncedToView announcedMember@GroupMember {groupMemberId, memberProfile} gInfo' = do + (announcedMember', scopeInfo) <- getMemNewChatScope announcedMember let event = RGEMemberAdded groupMemberId (fromLocalProfile memberProfile) - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent event) - groupMsgToView gInfo ci - toView $ CEvtJoinedGroupMemberConnecting user gInfo m announcedMember + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m) msg brokerTs (CIRcvGroupEvent event) + groupMsgToView cInfo ci + case scopeInfo of + Just (GCSIMemberSupport _) -> do + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m) (CIRcvGroupEvent RGENewMemberPendingReview) (Just brokerTs) + _ -> pure () + toView $ CEvtJoinedGroupMemberConnecting user gInfo' m announcedMember' + getMemNewChatScope announcedMember = case msgScope_ of + Nothing -> pure (announcedMember, Nothing) + Just (MSMember _) -> do + (announcedMember', scopeInfo) <- mkMemberSupportChatInfo announcedMember + pure (announcedMember', Just scopeInfo) xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MemberRestrictions -> CM () xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _) memRestrictions = do @@ -2464,7 +2572,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- For now, this branch compensates for the lack of delayed message delivery. Left _ -> withStore $ \db -> createNewGroupMember db user gInfo m memInfo GCPostMember GSMemAnnounced Right m' -> pure m' - withStore' $ \db -> saveMemberInvitation db toMember introInv + -- TODO [knocking] separate pending statuses from GroupMemberStatus? + -- TODO add GSMemIntroInvitedPending, GSMemConnectedPending, etc.? + -- TODO keep as is? (GSMemIntroInvited has no purpose) + let newMemberStatus = if memberPending toMember then memberStatus toMember else GSMemIntroInvited + withStore' $ \db -> saveMemberInvitation db toMember introInv newMemberStatus subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito let membershipProfile = redactedMemberProfile $ fromLocalProfile $ memberProfile membership @@ -2492,9 +2604,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | senderRole < GRAdmin || senderRole < fromRole = messageError "x.grp.mem.role with insufficient member permissions" | otherwise = do withStore' $ \db -> updateGroupMemberRole db user member memRole - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) - groupMsgToView gInfo ci - toView CEvtMemberRole {user, groupInfo = gInfo', byMember = m, member = member {memberRole = memRole}, fromRole, toRole = memRole} + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView cInfo ci + toView CEvtMemberRole {user, groupInfo = gInfo'', byMember = m', member = member {memberRole = memRole}, fromRole, toRole = memRole} checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = @@ -2520,9 +2633,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = bm' <- setMemberBlocked bm toggleNtf bm' (not blocked) let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent - groupMsgToView gInfo ci - toView CEvtMemberBlockedForAll {user, groupInfo = gInfo, byMember = m, member = bm, blocked} + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs ciContent + groupMsgToView cInfo ci + toView CEvtMemberBlockedForAll {user, groupInfo = gInfo', byMember = m', member = bm, blocked} Left (SEGroupMemberNotFoundByMemberId _) -> do bm <- createUnknownMember gInfo memId bm' <- setMemberBlocked bm @@ -2594,18 +2708,19 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave deleteMemberConnection member -- undeleted "member connected" chat item will prevent deletion of member record - deleteOrUpdateMemberRecord user member + gInfo' <- deleteOrUpdateMemberRecord user gInfo member when withMessages $ deleteMessages member SMDRcv deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CEvtDeletedMember user gInfo m member {memberStatus = GSMemRemoved} withMessages + toView $ CEvtDeletedMember user gInfo' m member {memberStatus = GSMemRemoved} withMessages where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = messageError "x.grp.mem.del with insufficient member permissions" | otherwise = a deleteMemberItem gEvent = do - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent gEvent) - groupMsgToView gInfo ci + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView cInfo ci deleteMessages :: MsgDirectionI d => GroupMember -> SMsgDirection d -> CM () deleteMessages delMem msgDir | groupFeatureMemberAllowed SGFFullDelete m gInfo = deleteGroupMemberCIs user gInfo delMem m msgDir @@ -2615,10 +2730,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpLeave gInfo m msg brokerTs = do deleteMemberConnection m -- member record is not deleted to allow creation of "member left" chat item - withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft) - groupMsgToView gInfo ci - toView $ CEvtLeftMember user gInfo m {memberStatus = GSMemLeft} + gInfo' <- withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemLeft + if gmRequiresAttention m + then decreaseGroupMembersRequireAttention db user gInfo + else pure gInfo + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft) + groupMsgToView cInfo ci + toView $ CEvtLeftMember user gInfo'' m' {memberStatus = GSMemLeft} xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do @@ -2629,9 +2749,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure members -- member records are not deleted to keep history deleteMembersConnections user ms - ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) - groupMsgToView gInfo ci - toView $ CEvtGroupDeleted user gInfo {membership = membership {memberStatus = GSMemGroupDeleted}} m + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) + groupMsgToView cInfo ci + toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM () xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs @@ -2639,12 +2760,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = case businessChat of Nothing -> unless (p == p') $ do g' <- withStore $ \db -> updateGroupProfile db user g p' - toView $ CEvtGroupUpdated user g g' (Just m) - let cd = CDGroupRcv g' m + (g'', m', scopeInfo) <- mkGroupChatScope g' m + toView $ CEvtGroupUpdated user g g'' (Just m') + let cd = CDGroupRcv g'' scopeInfo m' unless (sameGroupProfileInfo p p') $ do - ci <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') - groupMsgToView g' ci - createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + (ci, cInfo) <- saveRcvChatItemNoParse user cd msg brokerTs (CIRcvGroupEvent $ RGEGroupUpdated p') + groupMsgToView cInfo ci + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' Just _ -> updateGroupPrefs_ g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> CM () @@ -2657,8 +2779,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (groupPreferences p == Just ps') $ do g' <- withStore' $ \db -> updateGroupPreferences db user g ps' toView $ CEvtGroupUpdated user g g' (Just m) - let cd = CDGroupRcv g' m - createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + (g'', m', scopeInfo) <- mkGroupChatScope g' m + let cd = CDGroupRcv g'' scopeInfo m' + createGroupFeatureChangedItems user cd CIRcvGroupFeature g g'' xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM () xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do @@ -2698,11 +2821,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = dm <- encodeConnInfo $ XInfo p joinAgentConnectionAsync user True connReq dm subMode createItems mCt' m' = do - createInternalChatItem user (CDGroupRcv g m') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing - toView $ CEvtNewMemberContactReceivedInv user mCt' g m' + (g', m'', scopeInfo) <- mkGroupChatScope g m' + createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing + toView $ CEvtNewMemberContactReceivedInv user mCt' g' m'' forM_ mContent_ $ \mc -> do - ci <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc, msgContentTexts mc) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat mCt') ci] + (ci, cInfo) <- saveRcvChatItem user (CDDirectRcv mCt') msg brokerTs (CIRcvMsgContent mc, msgContentTexts mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] securityCodeChanged :: Contact -> CM () securityCodeChanged ct = do @@ -2726,14 +2850,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let body = LB.toStrict $ J.encode msg rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg case event of - XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True - XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live -> memberCanSend author $ groupMessageUpdate gInfo author sharedMsgId mContent mentions rcvMsg msgTs ttl live + XMsgNew mc -> memberCanSend author scope $ newGroupContentMessage gInfo author mc rcvMsg msgTs True + where ExtMsgContent {scope} = mcExtMsgContent mc + -- file description is always allowed, to allow sending files to support scope + XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo author sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend author msgScope $ groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live XMsgDel sharedMsgId memId -> groupMessageDelete gInfo author sharedMsgId memId rcvMsg msgTs XMsgReact sharedMsgId (Just memId) reaction add -> groupMsgReaction gInfo author sharedMsgId memId reaction add rcvMsg msgTs XFileCancel sharedMsgId -> xFileCancelGroup gInfo author sharedMsgId XInfo p -> xInfoMember gInfo author p msgTs - XGrpMemNew memInfo -> xGrpMemNew gInfo author memInfo rcvMsg msgTs + XGrpMemNew memInfo msgScope -> xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs XGrpMemRole memId memRole -> xGrpMemRole gInfo author memId memRole rcvMsg msgTs XGrpMemDel memId withMessages -> xGrpMemDel gInfo author memId withMessages rcvMsg msgTs XGrpLeave -> xGrpLeave gInfo author rcvMsg msgTs @@ -2756,10 +2882,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> CM () groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do - checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure () forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus - updateGroupItemsStatus gInfo m conn agentMsgId (GSSRcvd msgRcptStatus) Nothing + updateGroupItemsStatus gInfo' m' conn agentMsgId (GSSRcvd msgRcptStatus) Nothing -- Searches chat items for many agent message IDs and updates their status updateDirectItemsStatusMsgs :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM () @@ -2804,10 +2931,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateGroupItemsStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ = do items <- withStore' (\db -> getGroupChatItemsByAgentMsgId db user groupId connId msgId) cis <- catMaybes <$> withStore (\db -> mapM (updateItem db) items) - let acis = map gItem cis + -- SENT and RCVD events are received for messages that may be batched in single scope, + -- so we can look up scope of first item + scopeInfo <- case cis of + (ci : _) -> withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci) + _ -> pure Nothing + let acis = map (gItem scopeInfo) cis unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis where - gItem = AChatItem SCTGroup SMDSnd (GroupChat gInfo) + gItem scopeInfo ci = AChatItem SCTGroup SMDSnd (GroupChat gInfo scopeInfo) ci updateItem :: DB.Connection -> CChatItem 'CTGroup -> ExceptT StoreError IO (Maybe (ChatItem 'CTGroup 'MDSnd)) updateItem db = \case (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure Nothing diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 79d416dee5..dfd37527d1 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -61,9 +61,38 @@ import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection deriving (Eq, Show, Ord) +data GroupChatScope + = GCSMemberSupport {groupMemberId_ :: Maybe GroupMemberId} -- Nothing means own conversation with support + deriving (Eq, Show, Ord) + +data GroupChatScopeTag + = GCSTMemberSupport_ + deriving (Eq, Show) + +instance FromField GroupChatScopeTag where fromField = fromTextField_ textDecode + +instance ToField GroupChatScopeTag where toField = toField . textEncode + +instance TextEncoding GroupChatScopeTag where + textDecode = \case + "member_support" -> Just GCSTMemberSupport_ + _ -> Nothing + textEncode = \case + GCSTMemberSupport_ -> "member_support" + data ChatName = ChatName {chatType :: ChatType, chatName :: Text} deriving (Show) +data SendName + = SNDirect ContactName + | SNGroup GroupName (Maybe GroupScopeName) + | SNLocal + deriving (Show) + +data GroupScopeName + = GSNMemberSupport (Maybe ContactName) + deriving (Show) + chatTypeStr :: ChatType -> Text chatTypeStr = \case CTDirect -> "@" @@ -75,44 +104,52 @@ chatTypeStr = \case chatNameStr :: ChatName -> String chatNameStr (ChatName cType name) = T.unpack $ chatTypeStr cType <> if T.any isSpace name then "'" <> name <> "'" else name -data ChatRef = ChatRef ChatType Int64 +data ChatRef = ChatRef ChatType Int64 (Maybe GroupChatScope) deriving (Eq, Show, Ord) data ChatInfo (c :: ChatType) where DirectChat :: Contact -> ChatInfo 'CTDirect - GroupChat :: GroupInfo -> ChatInfo 'CTGroup + GroupChat :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatInfo 'CTGroup LocalChat :: NoteFolder -> ChatInfo 'CTLocal ContactRequest :: UserContactRequest -> ChatInfo 'CTContactRequest ContactConnection :: PendingContactConnection -> ChatInfo 'CTContactConnection deriving instance Show (ChatInfo c) -chatInfoChatTs :: ChatInfo c -> Maybe UTCTime -chatInfoChatTs = \case - DirectChat Contact {chatTs} -> chatTs - GroupChat GroupInfo {chatTs} -> chatTs - _ -> Nothing +data GroupChatScopeInfo + = GCSIMemberSupport {groupMember_ :: Maybe GroupMember} + deriving (Show) + +toChatScope :: GroupChatScopeInfo -> GroupChatScope +toChatScope = \case + GCSIMemberSupport {groupMember_} -> GCSMemberSupport $ groupMemberId' <$> groupMember_ + +toMsgScope :: GroupInfo -> GroupChatScopeInfo -> MsgScope +toMsgScope GroupInfo {membership} = \case + GCSIMemberSupport {groupMember_} -> MSMember $ memberId' $ fromMaybe membership groupMember_ chatInfoToRef :: ChatInfo c -> ChatRef chatInfoToRef = \case - DirectChat Contact {contactId} -> ChatRef CTDirect contactId - GroupChat GroupInfo {groupId} -> ChatRef CTGroup groupId - LocalChat NoteFolder {noteFolderId} -> ChatRef CTLocal noteFolderId - ContactRequest UserContactRequest {contactRequestId} -> ChatRef CTContactRequest contactRequestId - ContactConnection PendingContactConnection {pccConnId} -> ChatRef CTContactConnection pccConnId + DirectChat Contact {contactId} -> ChatRef CTDirect contactId Nothing + GroupChat GroupInfo {groupId} scopeInfo -> ChatRef CTGroup groupId (toChatScope <$> scopeInfo) + LocalChat NoteFolder {noteFolderId} -> ChatRef CTLocal noteFolderId Nothing + ContactRequest UserContactRequest {contactRequestId} -> ChatRef CTContactRequest contactRequestId Nothing + ContactConnection PendingContactConnection {pccConnId} -> ChatRef CTContactConnection pccConnId Nothing chatInfoMembership :: ChatInfo c -> Maybe GroupMember chatInfoMembership = \case - GroupChat GroupInfo {membership} -> Just membership + GroupChat GroupInfo {membership} _scopeInfo -> Just membership _ -> Nothing data JSONChatInfo = JCInfoDirect {contact :: Contact} - | JCInfoGroup {groupInfo :: GroupInfo} + | JCInfoGroup {groupInfo :: GroupInfo, groupChatScope :: Maybe GroupChatScopeInfo} | JCInfoLocal {noteFolder :: NoteFolder} | JCInfoContactRequest {contactRequest :: UserContactRequest} | JCInfoContactConnection {contactConnection :: PendingContactConnection} +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCSI") ''GroupChatScopeInfo) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCInfo") ''JSONChatInfo) instance ChatTypeI c => FromJSON (ChatInfo c) where @@ -125,7 +162,7 @@ instance ToJSON (ChatInfo c) where jsonChatInfo :: ChatInfo c -> JSONChatInfo jsonChatInfo = \case DirectChat c -> JCInfoDirect c - GroupChat g -> JCInfoGroup g + GroupChat g s -> JCInfoGroup g s LocalChat l -> JCInfoLocal l ContactRequest g -> JCInfoContactRequest g ContactConnection c -> JCInfoContactConnection c @@ -137,7 +174,7 @@ deriving instance Show AChatInfo jsonAChatInfo :: JSONChatInfo -> AChatInfo jsonAChatInfo = \case JCInfoDirect c -> AChatInfo SCTDirect $ DirectChat c - JCInfoGroup g -> AChatInfo SCTGroup $ GroupChat g + JCInfoGroup g s -> AChatInfo SCTGroup $ GroupChat g s JCInfoLocal l -> AChatInfo SCTLocal $ LocalChat l JCInfoContactRequest g -> AChatInfo SCTContactRequest $ ContactRequest g JCInfoContactConnection c -> AChatInfo SCTContactConnection $ ContactConnection c @@ -163,8 +200,6 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem } deriving (Show) -data NotInHistory = NotInHistory - data CIMention = CIMention { memberId :: MemberId, -- member record can be created later than the mention is received @@ -180,6 +215,9 @@ data CIMentionMember = CIMentionMember } deriving (Eq, Show) +isACIUserMention :: AChatItem -> Bool +isACIUserMention (AChatItem _ _ _ ci) = isUserMention ci + isUserMention :: ChatItem c d -> Bool isUserMention ChatItem {meta = CIMeta {userMention}} = userMention @@ -260,6 +298,16 @@ chatItemMember GroupInfo {membership} ChatItem {chatDir} = case chatDir of CIGroupSnd -> membership CIGroupRcv m -> m +chatItemRcvFromMember :: ChatItem c d -> Maybe GroupMember +chatItemRcvFromMember ChatItem {chatDir} = case chatDir of + CIGroupRcv m -> Just m + _ -> Nothing + +chatItemIsRcvNew :: ChatItem c d -> Bool +chatItemIsRcvNew ChatItem {meta = CIMeta {itemStatus}} = case itemStatus of + CISRcvNew -> True + _ -> False + ciReactionAllowed :: ChatItem c d -> Bool ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content @@ -267,8 +315,8 @@ ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content data ChatDirection (c :: ChatType) (d :: MsgDirection) where CDDirectSnd :: Contact -> ChatDirection 'CTDirect 'MDSnd CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv - CDGroupSnd :: GroupInfo -> ChatDirection 'CTGroup 'MDSnd - CDGroupRcv :: GroupInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv + CDGroupSnd :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatDirection 'CTGroup 'MDSnd + CDGroupRcv :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv CDLocalSnd :: NoteFolder -> ChatDirection 'CTLocal 'MDSnd CDLocalRcv :: NoteFolder -> ChatDirection 'CTLocal 'MDRcv @@ -276,8 +324,8 @@ toCIDirection :: ChatDirection c d -> CIDirection c d toCIDirection = \case CDDirectSnd _ -> CIDirectSnd CDDirectRcv _ -> CIDirectRcv - CDGroupSnd _ -> CIGroupSnd - CDGroupRcv _ m -> CIGroupRcv m + CDGroupSnd _ _ -> CIGroupSnd + CDGroupRcv _ _ m -> CIGroupRcv m CDLocalSnd _ -> CILocalSnd CDLocalRcv _ -> CILocalRcv @@ -285,8 +333,8 @@ toChatInfo :: ChatDirection c d -> ChatInfo c toChatInfo = \case CDDirectSnd c -> DirectChat c CDDirectRcv c -> DirectChat c - CDGroupSnd g -> GroupChat g - CDGroupRcv g _ -> GroupChat g + CDGroupSnd g s -> GroupChat g s + CDGroupRcv g s _ -> GroupChat g s CDLocalSnd l -> LocalChat l CDLocalRcv l -> LocalChat l @@ -360,6 +408,12 @@ aChatItemTs (AChatItem _ _ _ ci) = chatItemTs' ci aChatItemDir :: AChatItem -> MsgDirection aChatItemDir (AChatItem _ sMsgDir _ _) = toMsgDirection sMsgDir +aChatItemRcvFromMember :: AChatItem -> Maybe GroupMember +aChatItemRcvFromMember (AChatItem _ _ _ ci) = chatItemRcvFromMember ci + +aChatItemIsRcvNew :: AChatItem -> Bool +aChatItemIsRcvNew (AChatItem _ _ _ ci) = chatItemIsRcvNew ci + updateFileStatus :: forall c d. ChatItem c d -> CIFileStatus d -> ChatItem c d updateFileStatus ci@ChatItem {file} status = case file of Just f -> ci {file = Just (f :: CIFile d) {fileStatus = status}} @@ -931,7 +985,10 @@ ciStatusNew = case msgDirection @d of ciCreateStatus :: forall d. MsgDirectionI d => CIContent d -> CIStatus d ciCreateStatus content = case msgDirection @d of SMDSnd -> ciStatusNew - SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead + SMDRcv + | isCIReport content -> CISRcvRead + | ciRequiresAttention content -> ciStatusNew + | otherwise -> CISRcvRead membersGroupItemStatus :: [(GroupSndStatus, Int)] -> CIStatus 'MDSnd membersGroupItemStatus memStatusCounts @@ -1374,6 +1431,8 @@ instance MsgDirectionI d => ToJSON (CIFile d) where toJSON = $(JQ.mkToJSON defaultJSON ''CIFile) toEncoding = $(JQ.mkToEncoding defaultJSON ''CIFile) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCS") ''GroupChatScope) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCI") ''JSONCIDirection) instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIDirection c d) where diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 60d5464b79..cc6529831c 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -182,6 +182,9 @@ ciMsgContent = \case CIRcvMsgContent mc -> Just mc _ -> Nothing +isCIReport :: CIContent d -> Bool +isCIReport = maybe False isReport . ciMsgContent + data MsgDecryptError = MDERatchetHeader | MDETooManySkipped @@ -206,6 +209,8 @@ ciRequiresAttention content = case msgDirection @d of CIRcvGroupEvent rge -> case rge of RGEMemberAdded {} -> False RGEMemberConnected -> False + RGEMemberAccepted {} -> False + RGEUserAccepted -> False RGEMemberLeft -> False RGEMemberRole {} -> False RGEMemberBlocked {} -> False @@ -217,6 +222,7 @@ ciRequiresAttention content = case msgDirection @d of RGEInvitedViaGroupLink -> False RGEMemberCreatedContact -> False RGEMemberProfileUpdated {} -> False + RGENewMemberPendingReview -> True CIRcvConnEvent _ -> True CIRcvChatFeature {} -> False CIRcvChatPreference {} -> False @@ -317,6 +323,8 @@ rcvGroupEventToText :: RcvGroupEvent -> Text rcvGroupEventToText = \case RGEMemberAdded _ p -> "added " <> profileToText p RGEMemberConnected -> "connected" + RGEMemberAccepted _ p -> "accepted " <> profileToText p + RGEUserAccepted -> "accepted you" RGEMemberLeft -> "left" RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p @@ -328,6 +336,7 @@ rcvGroupEventToText = \case RGEInvitedViaGroupLink -> "invited via your group link" RGEMemberCreatedContact -> "started direct connection with you" RGEMemberProfileUpdated {} -> "updated profile" + RGENewMemberPendingReview -> "new member wants to join the group" sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case @@ -337,6 +346,18 @@ sndGroupEventToText = \case SGEMemberDeleted _ p -> "removed " <> profileToText p SGEUserLeft -> "left" SGEGroupUpdated _ -> "group profile updated" + SGEMemberAccepted _ _p -> "you accepted this member" + SGEUserPendingReview -> "please wait for group moderators to review your request to join the group" + +-- used to send to members with old version +pendingReviewMessage :: Text +pendingReviewMessage = + "Please wait for group moderators to review your request to join the group." + +-- used to send to members with old version +acceptedToGroupMessage :: Text +acceptedToGroupMessage = + "You are accepted to the group." rcvConnEventToText :: RcvConnEvent -> Text rcvConnEventToText = \case diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 054530e06f..539c1f524c 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -14,7 +14,9 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) data RcvGroupEvent = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting - | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember + | RGEMemberConnected -- CEvtUserJoinedGroup, CRJoinedGroupMember, CEvtConnectedToGroupMember + | RGEMemberAccepted {groupMemberId :: GroupMemberId, profile :: Profile} + | RGEUserAccepted | RGEMemberLeft -- CRLeftMember | RGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} | RGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMemberBlockedForAll @@ -29,6 +31,7 @@ data RcvGroupEvent | RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink | RGEMemberCreatedContact -- CRNewMemberContactReceivedInv | RGEMemberProfileUpdated {fromProfile :: Profile, toProfile :: Profile} -- CRGroupMemberUpdated + | RGENewMemberPendingReview deriving (Show) data SndGroupEvent @@ -38,6 +41,8 @@ data SndGroupEvent | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMembers | SGEUserLeft -- CRLeftMemberUser | SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated + | SGEMemberAccepted {groupMemberId :: GroupMemberId, profile :: Profile} + | SGEUserPendingReview deriving (Show) data RcvConnEvent diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 8c4490a2c4..c9b5e020d9 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -37,7 +37,6 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isNothing, mapMaybe) -import Data.Scientific (floatingOrInteger) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) @@ -46,11 +45,11 @@ import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions -import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types (User) import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) -import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_) +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) @@ -69,32 +68,6 @@ usageConditionsText = in [|stripFrontMatter $(lift (safeDecodeUtf8 s))|] ) -data DBStored = DBStored | DBNew - -data SDBStored (s :: DBStored) where - SDBStored :: SDBStored 'DBStored - SDBNew :: SDBStored 'DBNew - -deriving instance Show (SDBStored s) - -class DBStoredI s where sdbStored :: SDBStored s - -instance DBStoredI 'DBStored where sdbStored = SDBStored - -instance DBStoredI 'DBNew where sdbStored = SDBNew - -data DBEntityId' (s :: DBStored) where - DBEntityId :: Int64 -> DBEntityId' 'DBStored - DBNewEntity :: DBEntityId' 'DBNew - -deriving instance Show (DBEntityId' s) - -deriving instance Eq (DBEntityId' s) - -type DBEntityId = DBEntityId' 'DBStored - -type DBNewEntity = DBEntityId' 'DBNew - data OperatorTag = OTSimplex | OTFlux deriving (Eq, Ord, Show) @@ -118,19 +91,6 @@ instance TextEncoding OperatorTag where OTSimplex -> "simplex" OTFlux -> "flux" --- this and other types only define instances of serialization for known DB IDs only, --- entities without IDs cannot be serialized to JSON -instance FromField DBEntityId -#if defined(dbPostgres) - where - fromField f dat = DBEntityId <$> fromField f dat -#else - where - fromField f = DBEntityId <$> fromField f -#endif - -instance ToField DBEntityId where toField (DBEntityId i) = toField i - data UsageConditions = UsageConditions { conditionsId :: Int64, conditionsCommit :: Text, @@ -486,25 +446,6 @@ validateUserServers curr others = currUserErrs <> concatMap otherUserErrs others userServers :: (UserServersClass u, UserProtocol p) => SProtocolType p -> [u] -> [AUserServer p] userServers p = map aUserServer' . concatMap (servers' p) -instance ToJSON (DBEntityId' s) where - toEncoding = \case - DBEntityId i -> toEncoding i - DBNewEntity -> JE.null_ - toJSON = \case - DBEntityId i -> toJSON i - DBNewEntity -> J.Null - -instance DBStoredI s => FromJSON (DBEntityId' s) where - parseJSON v = case (v, sdbStored @s) of - (J.Null, SDBNew) -> pure DBNewEntity - (J.Number n, SDBStored) -> case floatingOrInteger n of - Left (_ :: Double) -> fail "bad DBEntityId" - Right i -> pure $ DBEntityId (fromInteger i) - _ -> fail "bad DBEntityId" - omittedField = case sdbStored @s of - SDBStored -> Nothing - SDBNew -> Just DBNewEntity - $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index 06c2e19fab..18a0b7ebed 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -9,6 +9,7 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Simplex.Chat.Operators import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Protocol (ProtocolType (..), SMPServer) operatorSimpleXChat :: NewServerOperator diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 2281c1aefa..151b1b0d27 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -77,12 +77,13 @@ import Simplex.Messaging.Version hiding (version) -- 11 - fix profile update in business chats (2024-12-05) -- 12 - support sending and receiving content reports (2025-01-03) -- 14 - support sending and receiving group join rejection (2025-02-24) +-- 15 - support specifying message scopes for group messages (2025-03-12) -- 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 14 +currentChatVersion = VersionChat 15 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -137,6 +138,10 @@ contentReportsVersion = VersionChat 12 groupJoinRejectVersion :: VersionChat groupJoinRejectVersion = VersionChat 14 +-- support group knocking (MsgScope) +groupKnockingVersion :: VersionChat +groupKnockingVersion = VersionChat 15 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -243,6 +248,12 @@ instance ToJSON SharedMsgId where toJSON = strToJSON toEncoding = strToJEncoding +data MsgScope + = MSMember {memberId :: MemberId} -- Admins can use any member id; members can use only their own id + deriving (Eq, Show) + +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MS") ''MsgScope) + $(JQ.deriveJSON defaultJSON ''AppMessageJson) data MsgRef = MsgRef @@ -319,7 +330,7 @@ data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMess data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json - XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, mentions :: Map MemberName MsgMention, ttl :: Maybe Int, live :: Maybe Bool} -> ChatMsgEvent 'Json + XMsgUpdate :: {msgId :: SharedMsgId, content :: MsgContent, mentions :: Map MemberName MsgMention, ttl :: Maybe Int, live :: Maybe Bool, scope :: Maybe MsgScope} -> ChatMsgEvent 'Json XMsgDel :: SharedMsgId -> Maybe MemberId -> ChatMsgEvent 'Json XMsgDeleted :: ChatMsgEvent 'Json XMsgReact :: {msgId :: SharedMsgId, memberId :: Maybe MemberId, reaction :: MsgReaction, add :: Bool} -> ChatMsgEvent 'Json @@ -335,8 +346,8 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json XGrpLinkMem :: Profile -> ChatMsgEvent 'Json - XGrpLinkAcpt :: GroupMemberRole -> ChatMsgEvent 'Json - XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json + XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json + XGrpMemNew :: MemberInfo -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMemIntro :: MemberInfo -> Maybe MemberRestrictions -> ChatMsgEvent 'Json XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemFwd :: MemberInfo -> IntroInvitation -> ChatMsgEvent 'Json @@ -350,7 +361,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json - XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> ChatMsgEvent 'Json + XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json XGrpMsgForward :: MemberId -> ChatMessage 'Json -> UTCTime -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json @@ -383,7 +394,7 @@ isForwardedGroupMsg ev = case ev of XMsgReact {} -> True XFileCancel _ -> True XInfo _ -> True - XGrpMemNew _ -> True + XGrpMemNew {} -> True XGrpMemRole {} -> True XGrpMemRestrict {} -> True XGrpMemDel {} -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) @@ -640,7 +651,8 @@ data ExtMsgContent = ExtMsgContent mentions :: Map MemberName MsgMention, file :: Maybe FileInvitation, ttl :: Maybe Int, - live :: Maybe Bool + live :: Maybe Bool, + scope :: Maybe MsgScope } deriving (Eq, Show) @@ -720,10 +732,11 @@ parseMsgContainer v = ttl <- v .:? "ttl" live <- v .:? "live" mentions <- fromMaybe M.empty <$> (v .:? "mentions") - pure ExtMsgContent {content, mentions, file, ttl, live} + scope <- v .:? "scope" + pure ExtMsgContent {content, mentions, file, ttl, live, scope} extMsgContent :: MsgContent -> Maybe FileInvitation -> ExtMsgContent -extMsgContent mc file = ExtMsgContent mc M.empty file Nothing Nothing +extMsgContent mc file = ExtMsgContent mc M.empty file Nothing Nothing Nothing justTrue :: Bool -> Maybe Bool justTrue True = Just True @@ -772,8 +785,8 @@ msgContainerJSON = \case MCSimple mc -> o $ msgContent mc where o = JM.fromList - msgContent ExtMsgContent {content, mentions, file, ttl, live} = - ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) ["content" .= content] + msgContent ExtMsgContent {content, mentions, file, ttl, live, scope} = + ("file" .=? file) $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) $ ("scope" .=? scope) ["content" .= content] nonEmptyMap :: Map k v -> Maybe (Map k v) nonEmptyMap m = if M.null m then Nothing else Just m @@ -983,8 +996,8 @@ toCMEventTag msg = case msg of XGrpLinkInv _ -> XGrpLinkInv_ XGrpLinkReject _ -> XGrpLinkReject_ XGrpLinkMem _ -> XGrpLinkMem_ - XGrpLinkAcpt _ -> XGrpLinkAcpt_ - XGrpMemNew _ -> XGrpMemNew_ + XGrpLinkAcpt {} -> XGrpLinkAcpt_ + XGrpMemNew {} -> XGrpMemNew_ XGrpMemIntro _ _ -> XGrpMemIntro_ XGrpMemInv _ _ -> XGrpMemInv_ XGrpMemFwd _ _ -> XGrpMemFwd_ @@ -998,7 +1011,7 @@ toCMEventTag msg = case msg of XGrpDel -> XGrpDel_ XGrpInfo _ -> XGrpInfo_ XGrpPrefs _ -> XGrpPrefs_ - XGrpDirectInv _ _ -> XGrpDirectInv_ + XGrpDirectInv {} -> XGrpDirectInv_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ @@ -1070,7 +1083,14 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do msg = \case XMsgNew_ -> XMsgNew <$> JT.parseEither parseMsgContainer params XMsgFileDescr_ -> XMsgFileDescr <$> p "msgId" <*> p "fileDescr" - XMsgUpdate_ -> XMsgUpdate <$> p "msgId" <*> p "content" <*> (fromMaybe M.empty <$> opt "mentions") <*> opt "ttl" <*> opt "live" + XMsgUpdate_ -> do + msgId' <- p "msgId" + content <- p "content" + mentions <- fromMaybe M.empty <$> opt "mentions" + ttl <- opt "ttl" + live <- opt "live" + scope <- opt "scope" + pure XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope} XMsgDel_ -> XMsgDel <$> p "msgId" <*> opt "memberId" XMsgDeleted_ -> pure XMsgDeleted XMsgReact_ -> XMsgReact <$> p "msgId" <*> opt "memberId" <*> p "reaction" <*> p "add" @@ -1086,8 +1106,8 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation" XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection" XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" - XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "role" - XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" + XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId" + XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo" <*> opt "scope" XGrpMemIntro_ -> XGrpMemIntro <$> p "memberInfo" <*> opt "memberRestrictions" XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" XGrpMemFwd_ -> XGrpMemFwd <$> p "memberInfo" <*> p "memberIntro" @@ -1101,7 +1121,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences" - XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" + XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope" XGrpMsgForward_ -> XGrpMsgForward <$> p "memberId" <*> p "msg" <*> p "msgTs" XInfoProbe_ -> XInfoProbe <$> p "probe" XInfoProbeCheck_ -> XInfoProbeCheck <$> p "probeHash" @@ -1134,7 +1154,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ params = \case XMsgNew container -> msgContainerJSON container XMsgFileDescr msgId' fileDescr -> o ["msgId" .= msgId', "fileDescr" .= fileDescr] - XMsgUpdate msgId' content mentions ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content] + XMsgUpdate {msgId = msgId', content, mentions, ttl, live, scope} -> o $ ("ttl" .=? ttl) $ ("live" .=? live) $ ("scope" .=? scope) $ ("mentions" .=? nonEmptyMap mentions) ["msgId" .= msgId', "content" .= content] XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId'] XMsgDeleted -> JM.empty XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add] @@ -1150,8 +1170,8 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv] XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct] XGrpLinkMem profile -> o ["profile" .= profile] - XGrpLinkAcpt role -> o ["role" .= role] - XGrpMemNew memInfo -> o ["memberInfo" .= memInfo] + XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId] + XGrpMemNew memInfo scope -> o $ ("scope" .=? scope) ["memberInfo" .= memInfo] XGrpMemIntro memInfo memRestrictions -> o $ ("memberRestrictions" .=? memRestrictions) ["memberInfo" .= memInfo] XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] XGrpMemFwd memInfo memIntro -> o ["memberInfo" .= memInfo, "memberIntro" .= memIntro] @@ -1165,7 +1185,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] XGrpPrefs p -> o ["groupPreferences" .= p] - XGrpDirectInv connReq content -> o $ ("content" .=? content) ["connReq" .= connReq] + XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq] XGrpMsgForward memberId msg msgTs -> o ["memberId" .= memberId, "msg" .= msg, "msgTs" .= msgTs] XInfoProbe probe -> o ["probe" .= probe] XInfoProbeCheck probeHash -> o ["probeHash" .= probeHash] diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index d41957bbac..f0a91e6d45 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -58,7 +58,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String (StrEncoding (..)) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (TLS, closeConnection, tlsUniq) +import Simplex.Messaging.Transport (TLS, TransportPeer (..), closeConnection, tlsUniq) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2ClientError, closeHTTP2Client) import Simplex.Messaging.Transport.HTTP2.Server (HTTP2Request (..)) import Simplex.Messaging.Util @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 3, 4, 1] +minRemoteCtrlVersion = AppVersion [6, 4, 0, 3] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 3, 4, 1] +minRemoteHostVersion = AppVersion [6, 4, 0, 3] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version @@ -184,7 +184,7 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do action `catchChatError` \err -> do logError $ "startRemoteHost.waitForHostSession crashed: " <> tshow err readTVarIO rhKeyVar >>= cancelRemoteHostSession (Just (sessSeq, RHSRCrashed err)) - waitForHostSession :: Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> CM () + waitForHostSession :: Maybe RemoteHostInfo -> RHKey -> SessionSeq -> Maybe RCCtrlAddress -> TVar RHKey -> RCStepTMVar (ByteString, TLS 'TServer, RCStepTMVar (RCHostSession, RCHostHello, RCHostPairing)) -> CM () waitForHostSession remoteHost_ rhKey sseq rcAddr_ rhKeyVar vars = do (sessId, tls, vars') <- timeoutThrow (ChatErrorRemoteHost rhKey RHETimeout) 60000000 $ takeRCStep vars let sessionCode = verificationCode sessId @@ -474,7 +474,7 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) where validateRemoteCtrl RCInvitation {idkey} RemoteCtrl {ctrlPairing = RCCtrlPairing {idPubKey}} = unless (idkey == idPubKey) $ throwError $ ChatErrorRemoteCtrl $ RCEProtocolError $ PRERemoteControl RCEIdentity - waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM () + waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS 'TClient, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM () waitForCtrlSession rc_ ctrlName rcsClient vars = do (uniq, tls, rcsWaitConfirmation) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars let sessionCode = verificationCode uniq diff --git a/src/Simplex/Chat/Remote/RevHTTP.hs b/src/Simplex/Chat/Remote/RevHTTP.hs index 4df5bcac2a..20484c0b10 100644 --- a/src/Simplex/Chat/Remote/RevHTTP.hs +++ b/src/Simplex/Chat/Remote/RevHTTP.hs @@ -8,18 +8,18 @@ module Simplex.Chat.Remote.RevHTTP where -import Simplex.Messaging.Transport (TLS) +import Simplex.Messaging.Transport (TLS, TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2 (defaultHTTP2BufferSize, getHTTP2Body) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client, HTTP2ClientError (..), attachHTTP2Client, bodyHeadSize, connTimeout, defaultHTTP2ClientConfig) import Simplex.Messaging.Transport.HTTP2.Server (HTTP2Request (..), runHTTP2ServerWith) import Simplex.RemoteControl.Discovery -attachRevHTTP2Client :: IO () -> TLS -> IO (Either HTTP2ClientError HTTP2Client) +attachRevHTTP2Client :: IO () -> TLS 'TServer -> IO (Either HTTP2ClientError HTTP2Client) attachRevHTTP2Client disconnected = attachHTTP2Client config ANY_ADDR_V4 "0" disconnected defaultHTTP2BufferSize where config = defaultHTTP2ClientConfig {bodyHeadSize = doNotPrefetchHead, connTimeout = maxBound} -attachHTTP2Server :: TLS -> (HTTP2Request -> IO ()) -> IO () +attachHTTP2Server :: TLS 'TClient -> (HTTP2Request -> IO ()) -> IO () attachHTTP2Server tls processRequest = runHTTP2ServerWith defaultHTTP2BufferSize ($ tls) $ \sessionId sessionALPN r sendResponse -> do reqBody <- getHTTP2Body r doNotPrefetchHead diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index a5396e7945..defbe7e72c 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -25,7 +25,7 @@ import Simplex.Chat.Types (verificationCode) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) -import Simplex.Messaging.Transport (TLS (..), TSbChainKeys (..)) +import Simplex.Messaging.Transport (TLS (..), TSbChainKeys (..), TransportPeer (..)) import Simplex.Messaging.Transport.HTTP2.Client (HTTP2Client) import qualified Simplex.Messaging.TMap as TM import Simplex.RemoteControl.Client @@ -102,11 +102,11 @@ data RHPendingSession = RHPendingSession data RemoteHostSession = RHSessionStarting | RHSessionConnecting {invitation :: Text, rhPendingSession :: RHPendingSession} - | RHSessionPendingConfirmation {sessionCode :: Text, tls :: TLS, rhPendingSession :: RHPendingSession} - | RHSessionConfirmed {tls :: TLS, rhPendingSession :: RHPendingSession} + | RHSessionPendingConfirmation {sessionCode :: Text, tls :: TLS 'TServer, rhPendingSession :: RHPendingSession} + | RHSessionConfirmed {tls :: TLS 'TServer, rhPendingSession :: RHPendingSession} | RHSessionConnected { rchClient :: RCHostClient, - tls :: TLS, + tls :: TLS 'TServer, rhClient :: RemoteHostClient, pollAction :: Async (), storePath :: FilePath @@ -128,7 +128,7 @@ rhsSessionState = \case RHSessionConfirmed {tls} -> RHSConfirmed {sessionCode = tlsSessionCode tls} RHSessionConnected {tls} -> RHSConnected {sessionCode = tlsSessionCode tls} -tlsSessionCode :: TLS -> Text +tlsSessionCode :: TLS p -> Text tlsSessionCode = verificationCode . tlsUniq data RemoteProtocolError diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 5c177969b9..b69f6f646c 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -137,17 +137,20 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - m.created_at, m.updated_at + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 9318f62f76..4de832a8b1 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -66,6 +66,7 @@ module Simplex.Chat.Store.Direct updateContactAccepted, getUserByContactRequestId, getPendingContactConnections, + updatePCCUser, getContactConnections, getConnectionById, getConnectionsContacts, @@ -439,6 +440,19 @@ updatePCCIncognito db User {userId} conn customUserProfileId = do (customUserProfileId, updatedAt, userId, pccConnId conn) pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} +updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> IO PendingContactConnection +updatePCCUser db userId conn newUserId = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (newUserId, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, updatedAt} + deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () deletePCCIncognitoProfile db User {userId} profileId = DB.execute diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 95e169e400..81dbb4ca54 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -371,9 +371,9 @@ getXFTPRcvFileDBIds db aRcvFileId = toFileRef :: (FileTransferId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (Maybe ChatRef, FileTransferId) toFileRef = \case - (fileId, Just contactId, Nothing, Nothing) -> Right (Just $ ChatRef CTDirect contactId, fileId) - (fileId, Nothing, Just groupId, Nothing) -> Right (Just $ ChatRef CTGroup groupId, fileId) - (fileId, Nothing, Nothing, Just folderId) -> Right (Just $ ChatRef CTLocal folderId, fileId) + (fileId, Just contactId, Nothing, Nothing) -> Right (Just $ ChatRef CTDirect contactId Nothing, fileId) + (fileId, Nothing, Just groupId, Nothing) -> Right (Just $ ChatRef CTGroup groupId Nothing, fileId) + (fileId, Nothing, Nothing, Just folderId) -> Right (Just $ ChatRef CTLocal folderId Nothing, fileId) (fileId, _, _, _) -> Right (Nothing, fileId) updateFileCancelled :: MsgDirectionI d => DB.Connection -> User -> Int64 -> CIFileStatus d -> IO () @@ -444,8 +444,8 @@ getChatRefByFileId db user fileId = liftIO (lookupChatRefByFileId db user fileId lookupChatRefByFileId :: DB.Connection -> User -> Int64 -> IO (Maybe ChatRef) lookupChatRefByFileId db User {userId} fileId = getChatRef <&> \case - [(Just contactId, Nothing)] -> Just $ ChatRef CTDirect contactId - [(Nothing, Just groupId)] -> Just $ ChatRef CTGroup groupId + [(Just contactId, Nothing)] -> Just $ ChatRef CTDirect contactId Nothing + [(Nothing, Just groupId)] -> Just $ ChatRef CTGroup groupId Nothing _ -> Nothing where getChatRef = diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index fc23c9ef44..f3661699cb 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -80,6 +80,10 @@ module Simplex.Chat.Store.Groups updateGroupMemberStatus, updateGroupMemberStatusById, updateGroupMemberAccepted, + deleteGroupMemberSupportChat, + updateGroupMembersRequireAttention, + decreaseGroupMembersRequireAttention, + increaseGroupMembersRequireAttention, createNewGroupMember, checkGroupMemberHasItems, deleteGroupMember, @@ -89,6 +93,7 @@ module Simplex.Chat.Store.Groups updateIntroStatus, saveIntroInvitation, getIntroduction, + getIntroducedGroupMemberIds, getForwardIntroducedMembers, getForwardInvitedMembers, createIntroReMember, @@ -176,11 +181,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) +type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime) toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) = - Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt)) +toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs)) = + Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs)) toMaybeGroupMember _ _ = Nothing createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () @@ -251,12 +256,12 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do (userId, groupId) DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId) -getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, CreatedLinkContact, GroupMemberRole) +getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, CreatedLinkContact, GroupLinkId, GroupMemberRole) getGroupLink db User {userId} gInfo@GroupInfo {groupId} = ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $ - DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) + DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) where - groupLink (linkId, cReq, shortLink, mRole_) = (linkId, CCLink cReq shortLink, fromMaybe GRMember mRole_) + groupLink (linkId, cReq, shortLink, gLinkId, mRole_) = (linkId, CCLink cReq shortLink, gLinkId, fromMaybe GRMember mRole_) getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId) getGroupLinkId db User {userId} GroupInfo {groupId} = @@ -278,17 +283,20 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -359,7 +367,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, - customData = Nothing + customData = Nothing, + membersRequireAttention = 0 } -- | creates a new group record for the group the current user was invited to, or returns an existing one @@ -372,9 +381,9 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db vr user gId hostId <- getHostMemberId_ db user gId let GroupMember {groupMemberId, memberId, memberRole} = membership - MemberIdRole {memberId = memberId', memberRole = memberRole'} = invitedMember - liftIO . when (memberId /= memberId' || memberRole /= memberRole') $ - DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (memberId', memberRole', groupMemberId) + MemberIdRole {memberId = invMemberId, memberRole = memberRole'} = invitedMember + liftIO . when (memberId /= invMemberId || memberRole /= memberRole') $ + DB.execute db "UPDATE group_members SET member_id = ?, member_role = ? WHERE group_member_id = ?" (invMemberId, memberRole', groupMemberId) gInfo' <- if p' == groupProfile then pure gInfo @@ -429,7 +438,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, - customData = Nothing + customData = Nothing, + membersRequireAttention = 0 }, groupMemberId ) @@ -476,7 +486,8 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe activeConn = Nothing, memberChatVRange, createdAt, - updatedAt = createdAt + updatedAt = createdAt, + supportChat = Nothing } where memberChatVRange@(VersionRange minV maxV) = vr @@ -524,7 +535,7 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName - initialStatus = maybe GSMemAccepted acceptanceToStatus accepted + initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus createGroupRejectedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) @@ -543,13 +554,13 @@ createGroupViaLink' invitedMember groupProfile business - memStatus = do + membershipStatus = do currentTs <- liftIO getCurrentTime groupId <- insertGroup_ currentTs hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember memStatus IBUnknown customUserProfileId currentTs vr + void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId currentTs vr liftIO $ setViaGroupLinkHash db groupId connId (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where @@ -584,7 +595,7 @@ createGroupViaLink' user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, memberId, memberRole, GCHostMember, memStatus, fromInvitedBy userContactId IBUnknown) + ( (groupId, memberId, memberRole, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) ) insertedRowId db @@ -764,10 +775,12 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - mu.created_at, mu.updated_at + mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -832,6 +845,7 @@ groupMemberQuery = m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -1001,7 +1015,8 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, activeConn = Nothing, memberChatVRange = peerChatVRange, createdAt, - updatedAt = createdAt + updatedAt = createdAt, + supportChat = Nothing } where insertMember_ = @@ -1204,8 +1219,8 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do |] (memStatus, currentTs, userId, groupMemberId) -updateGroupMemberAccepted :: DB.Connection -> User -> GroupMember -> GroupMemberRole -> IO GroupMember -updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} role = do +updateGroupMemberAccepted :: DB.Connection -> User -> GroupMember -> GroupMemberStatus -> GroupMemberRole -> IO GroupMember +updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} status role = do currentTs <- getCurrentTime DB.execute db @@ -1214,8 +1229,73 @@ updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} role = SET member_status = ?, member_role = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ? |] - (GSMemConnected, role, currentTs, userId, groupMemberId) - pure m {memberStatus = GSMemConnected, memberRole = role, updatedAt = currentTs} + (status, role, currentTs, userId, groupMemberId) + pure m {memberStatus = status, memberRole = role, updatedAt = currentTs} + +deleteGroupMemberSupportChat :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO (GroupInfo, GroupMember) +deleteGroupMemberSupportChat db user g m@GroupMember {groupMemberId} = do + let requiredAttention = gmRequiresAttention m + currentTs <- getCurrentTime + DB.execute + db + [sql| + DELETE FROM chat_items + WHERE group_scope_group_member_id = ? + |] + (Only groupMemberId) + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_ts = NULL, + support_chat_items_unread = 0, + support_chat_items_member_attention = 0, + support_chat_items_mentions = 0, + support_chat_last_msg_from_member_ts = NULL, + updated_at = ? + WHERE group_member_id = ? + |] + (currentTs, groupMemberId) + let m' = m {supportChat = Nothing, updatedAt = currentTs} + g' <- if requiredAttention + then decreaseGroupMembersRequireAttention db user g + else pure g + pure (g', m') + +updateGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> IO GroupInfo +updateGroupMembersRequireAttention db user g member member' + | nowRequires && not didRequire = + increaseGroupMembersRequireAttention db user g + | not nowRequires && didRequire = + decreaseGroupMembersRequireAttention db user g + | otherwise = pure g + where + didRequire = gmRequiresAttention member + nowRequires = gmRequiresAttention member' + +decreaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO GroupInfo +decreaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do + DB.execute + db + [sql| + UPDATE groups + SET members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) + pure g {membersRequireAttention = membersRequireAttention - 1} + +increaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO GroupInfo +increaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do + DB.execute + db + [sql| + UPDATE groups + SET members_require_attention = members_require_attention + 1 + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) + pure g {membersRequireAttention = membersRequireAttention + 1} -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember @@ -1299,7 +1379,8 @@ createNewMember_ activeConn, memberChatVRange, createdAt, - updatedAt = createdAt + updatedAt = createdAt, + supportChat = Nothing } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -1397,8 +1478,8 @@ saveIntroInvitation db reMember toMember introInv@IntroInvitation {groupConnReq} (GMIntroInvReceived, groupConnReq, directConnReq introInv, currentTs, introId intro) pure intro {introInvitation = Just introInv, introStatus = GMIntroInvReceived} -saveMemberInvitation :: DB.Connection -> GroupMember -> IntroInvitation -> IO () -saveMemberInvitation db GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} = do +saveMemberInvitation :: DB.Connection -> GroupMember -> IntroInvitation -> GroupMemberStatus -> IO () +saveMemberInvitation db GroupMember {groupMemberId} IntroInvitation {groupConnReq, directConnReq} newMemberStatus = do currentTs <- getCurrentTime DB.execute db @@ -1410,7 +1491,7 @@ saveMemberInvitation db GroupMember {groupMemberId} IntroInvitation {groupConnRe updated_at = ? WHERE group_member_id = ? |] - (GSMemIntroInvited, groupConnReq, directConnReq, currentTs, groupMemberId) + (newMemberStatus, groupConnReq, directConnReq, currentTs, groupMemberId) getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> ExceptT StoreError IO GroupMemberIntro getIntroduction db reMember toMember = ExceptT $ do @@ -1430,6 +1511,14 @@ getIntroduction db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound +getIntroducedGroupMemberIds :: DB.Connection -> GroupMember -> IO [GroupMemberId] +getIntroducedGroupMemberIds db invitee = + map fromOnly <$> + DB.query + db + "SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ?" + (Only $ groupMemberId' invitee) + getForwardIntroducedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember] getForwardIntroducedMembers db vr user invitee highlyAvailable = do memberIds <- map fromOnly <$> query @@ -1545,17 +1634,20 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -2424,8 +2516,8 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g where VersionRange minV maxV = vr -updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember -updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do +updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember +updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} status = do _ <- updateMemberProfile db user unknownMember profile currentTs <- liftIO getCurrentTime liftIO $ @@ -2442,7 +2534,7 @@ updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMemb updated_at = ? WHERE user_id = ? AND group_member_id = ? |] - ( (memberRole, GCPostMember, GSMemAnnounced, groupMemberId' invitingMember) + ( (memberRole, GCPostMember, status, groupMemberId' invitingMember) :. (minV, maxV, currentTs, userId, groupMemberId) ) getGroupMemberById db vr user groupMemberId diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 752a4a2c6d..1c40586bc1 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} @@ -10,6 +11,7 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-} @@ -33,7 +35,9 @@ module Simplex.Chat.Store.Messages getPendingGroupMessages, deletePendingGroupMessage, deleteOldMessages, - updateChatTs, + MemberAttention (..), + updateChatTsStats, + setSupportChatTs, createNewSndChatItem, createNewRcvChatItem, createNewChatItemNoMsg, @@ -41,6 +45,7 @@ module Simplex.Chat.Store.Messages getChatPreviews, getDirectChat, getGroupChat, + getGroupChatScopeInfoForItem, getLocalChat, getDirectChatItemLast, getAllChatItems, @@ -75,6 +80,7 @@ module Simplex.Chat.Store.Messages updateGroupChatItemsRead, getGroupUnreadTimedItems, updateGroupChatItemsReadList, + updateGroupScopeUnreadStats, setGroupChatItemsDeleteAt, updateLocalChatItemsRead, getChatRefViaItemId, @@ -140,7 +146,7 @@ import Data.Bifunctor (first) import Data.ByteString.Char8 (ByteString) import Data.Either (fromRight, rights) import Data.Int (Int64) -import Data.List (sortBy) +import Data.List (foldl', sortBy) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -361,28 +367,136 @@ deleteOldMessages db createdAtCutoff = do type NewQuoteRow = (Maybe SharedMsgId, Maybe UTCTime, Maybe MsgContent, Maybe Bool, Maybe MemberId) -updateChatTs :: DB.Connection -> User -> ChatDirection c d -> UTCTime -> IO () -updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirection of - DirectChat Contact {contactId} -> +-- For support chats with members we track unanswered count - number of messages from the member +-- that weren't followed up by a message from any of moderators. +data MemberAttention + -- Message was received from member, increase unanswered counter and set support_chat_last_msg_from_member_ts. + -- `MAInc 0 Nothing` is used in two cases: + -- - when message from moderator is older than the last message from member (support_chat_last_msg_from_member_ts); + -- - for user's chat with moderators, where unanswered count is not tracked. + = MAInc Int (Maybe UTCTime) + -- Message was received from moderator, reset unanswered counter. + | MAReset + deriving (Show) + +updateChatTsStats :: DB.Connection -> VersionRangeChat -> User -> ChatDirection c d -> UTCTime -> Maybe (Int, MemberAttention, Int) -> IO (ChatInfo c) +updateChatTsStats db vr user@User {userId} chatDirection chatTs chatStats_ = case toChatInfo chatDirection of + DirectChat ct@Contact {contactId} -> do DB.execute db "UPDATE contacts SET chat_ts = ?, chat_deleted = 0 WHERE user_id = ? AND contact_id = ?" (chatTs, userId, contactId) - GroupChat GroupInfo {groupId} -> + pure $ DirectChat ct {chatTs = Just chatTs} + GroupChat g@GroupInfo {groupId} Nothing -> do DB.execute db "UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?" (chatTs, userId, groupId) - LocalChat NoteFolder {noteFolderId} -> + pure $ GroupChat g {chatTs = Just chatTs} Nothing + GroupChat g@GroupInfo {groupId, membership, membersRequireAttention} (Just GCSIMemberSupport {groupMember_}) -> + case groupMember_ of + Nothing -> do + membership' <- updateGMStats membership + DB.execute + db + "UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?" + (chatTs, userId, groupId) + pure $ GroupChat g {membership = membership', chatTs = Just chatTs} (Just $ GCSIMemberSupport Nothing) + Just member -> do + member' <- updateGMStats member + let didRequire = gmRequiresAttention member + nowRequires = gmRequiresAttention member' + if + | nowRequires && not didRequire -> do + DB.execute + db + [sql| + UPDATE groups + SET chat_ts = ?, + members_require_attention = members_require_attention + 1 + WHERE user_id = ? AND group_id = ? + |] + (chatTs, userId, groupId) + pure $ GroupChat g {membersRequireAttention = membersRequireAttention + 1, chatTs = Just chatTs} (Just $ GCSIMemberSupport (Just member')) + | not nowRequires && didRequire -> do + DB.execute + db + [sql| + UPDATE groups + SET chat_ts = ?, + members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + |] + (chatTs, userId, groupId) + pure $ GroupChat g {membersRequireAttention = membersRequireAttention - 1, chatTs = Just chatTs} (Just $ GCSIMemberSupport (Just member')) + | otherwise -> do + DB.execute + db + "UPDATE groups SET chat_ts = ? WHERE user_id = ? AND group_id = ?" + (chatTs, userId, groupId) + pure $ GroupChat g {chatTs = Just chatTs} (Just $ GCSIMemberSupport (Just member')) + where + updateGMStats m@GroupMember {groupMemberId} = do + case chatStats_ of + Nothing -> + DB.execute + db + "UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ?" + (chatTs, groupMemberId) + Just (unread, MAInc unanswered Nothing, mentions) -> + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = support_chat_items_member_attention + ?, + support_chat_items_mentions = support_chat_items_mentions + ? + WHERE group_member_id = ? + |] + (chatTs, unread, unanswered, mentions, groupMemberId) + Just (unread, MAInc unanswered (Just lastMsgFromMemberTs), mentions) -> + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = support_chat_items_member_attention + ?, + support_chat_items_mentions = support_chat_items_mentions + ?, + support_chat_last_msg_from_member_ts = ? + WHERE group_member_id = ? + |] + (chatTs, unread, unanswered, mentions, lastMsgFromMemberTs, groupMemberId) + Just (unread, MAReset, mentions) -> + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = 0, + support_chat_items_mentions = support_chat_items_mentions + ? + WHERE group_member_id = ? + |] + (chatTs, unread, mentions, groupMemberId) + m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + pure $ either (const m) id m_ -- Left shouldn't happen, but types require it + LocalChat nf@NoteFolder {noteFolderId} -> do DB.execute db "UPDATE note_folders SET chat_ts = ? WHERE user_id = ? AND note_folder_id = ?" (chatTs, userId, noteFolderId) - _ -> pure () + pure $ LocalChat nf {chatTs = chatTs} + cInfo -> pure cInfo -createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> Maybe NotInHistory -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId -createNewSndChatItem db user chatDirection notInHistory_ SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = - createNewChatItem_ db user chatDirection notInHistory_ createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt +setSupportChatTs :: DB.Connection -> GroupMemberId -> UTCTime -> IO () +setSupportChatTs db groupMemberId chatTs = + DB.execute db "UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ?" (chatTs, groupMemberId) + +createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> UTCTime -> IO ChatItemId +createNewSndChatItem db user chatDirection SndMessage {msgId, sharedMsgId} ciContent quotedItem itemForwarded timed live createdAt = + createNewChatItem_ db user chatDirection createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False createdAt Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -396,9 +510,9 @@ createNewSndChatItem db user chatDirection notInHistory_ SndMessage {msgId, shar CIQGroupRcv (Just GroupMember {memberId}) -> (Just False, Just memberId) CIQGroupRcv Nothing -> (Just False, Nothing) -createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) -createNewRcvChatItem db user chatDirection notInHistory_ RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do - ciId <- createNewChatItem_ db user chatDirection notInHistory_ (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt +createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) +createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, forwardedByMember} sharedMsgId_ ciContent timed live userMention itemTs createdAt = do + ciId <- createNewChatItem_ db user chatDirection (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where @@ -410,24 +524,24 @@ createNewRcvChatItem db user chatDirection notInHistory_ RcvMessage {msgId, chat Just QuotedMsg {msgRef = MsgRef {msgId = sharedMsgId, sentAt, sent, memberId}, content} -> uncurry (sharedMsgId,Just sentAt,Just content,,) $ case chatDirection of CDDirectRcv _ -> (Just $ not sent, Nothing) - CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ -> + CDGroupRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ _ -> (Just $ Just userMemberId == memberId, memberId) createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> CIContent d -> UTCTime -> UTCTime -> IO ChatItemId createNewChatItemNoMsg db user chatDirection ciContent itemTs = - createNewChatItem_ db user chatDirection Nothing Nothing Nothing ciContent quoteRow Nothing Nothing False False itemTs Nothing + createNewChatItem_ db user chatDirection Nothing Nothing ciContent quoteRow Nothing Nothing False False itemTs Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe NotInHistory -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection notInHistory_ msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention itemTs forwardedByMember createdAt = do DB.execute db [sql| INSERT INTO chat_items ( -- user and IDs - user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, + user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, timed_ttl, timed_delete_at, @@ -435,9 +549,9 @@ createNewChatItem_ db User {userId} chatDirection notInHistory_ msgId_ sharedMsg quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, msgId_) :. idsRow :. itemRow :. quoteRow' :. forwardedFromRow) + ((userId, msgId_) :. idsRow :. groupScopeRow :. itemRow :. quoteRow' :. forwardedFromRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId @@ -445,18 +559,27 @@ createNewChatItem_ db User {userId} chatDirection notInHistory_ msgId_ sharedMsg itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt) :. (Maybe Int, Maybe UTCTime) itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, msgContentTag <$> ciMsgContent ciContent, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> (justTrue live), BI userMention) :. ciTimedRow timed quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e) - idsRow :: (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) + idsRow :: (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId, Maybe NoteFolderId) idsRow = case chatDirection of CDDirectRcv Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) CDDirectSnd Contact {contactId} -> (Just contactId, Nothing, Nothing, Nothing) - CDGroupRcv GroupInfo {groupId} GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId, Nothing) - CDGroupSnd GroupInfo {groupId} -> (Nothing, Just groupId, Nothing, Nothing) + CDGroupRcv GroupInfo {groupId} _ GroupMember {groupMemberId} -> (Nothing, Just groupId, Just groupMemberId, Nothing) + CDGroupSnd GroupInfo {groupId} _ -> (Nothing, Just groupId, Nothing, Nothing) CDLocalRcv NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) CDLocalSnd NoteFolder {noteFolderId} -> (Nothing, Nothing, Nothing, Just noteFolderId) + groupScope :: Maybe (Maybe GroupChatScopeInfo) + groupScope = case chatDirection of + CDGroupRcv _ scope _ -> Just scope + CDGroupSnd _ scope -> Just scope + _ -> Nothing + groupScopeRow :: (Maybe GroupChatScopeTag, Maybe GroupMemberId) + groupScopeRow = case groupScope of + Just (Just GCSIMemberSupport {groupMember_}) -> (Just GCSTMemberSupport_, groupMemberId' <$> groupMember_) + _ -> (Nothing, Nothing) includeInHistory :: Bool - includeInHistory = - let (_, groupId_, _, _) = idsRow - in isJust groupId_ && isNothing notInHistory_ && isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_) + includeInHistory = case groupScope of + Just Nothing -> isJust (ciMsgContent ciContent) && ((msgContentTag <$> ciMsgContent ciContent) /= Just MCReport_) + _ -> False forwardedFromRow :: (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) forwardedFromRow = case itemForwarded of Nothing -> @@ -479,7 +602,7 @@ getChatItemQuote_ :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirectio getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRef = MsgRef {msgId, sentAt, sent, memberId}, content} = case chatDirection of CDDirectRcv Contact {contactId} -> getDirectChatItemQuote_ contactId (not sent) - CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} sender@GroupMember {groupMemberId = senderGMId, memberId = senderMemberId} -> + CDGroupRcv GroupInfo {groupId, membership = GroupMember {memberId = userMemberId}} _s sender@GroupMember {groupMemberId = senderGMId, memberId = senderMemberId} -> case memberId of Just mId | mId == userMemberId -> (`ciQuote` CIQGroupSnd) <$> getUserGroupChatItemId_ groupId @@ -524,7 +647,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - m.created_at, m.updated_at + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -578,15 +702,17 @@ data ChatPreviewData (c :: ChatType) where data AChatPreviewData = forall c. ChatTypeI c => ACPD (SChatType c) (ChatPreviewData c) -type ChatStatsRow = (Int, Int, ChatItemId, BoolInt) +type ChatStatsRow = (Int, ChatItemId, BoolInt) toChatStats :: ChatStatsRow -> ChatStats -toChatStats (unreadCount, reportsCount, minUnreadItemId, BI unreadChat) = ChatStats {unreadCount, unreadMentions = 0, reportsCount, minUnreadItemId, unreadChat} +toChatStats (unreadCount, minUnreadItemId, BI unreadChat) = + ChatStats {unreadCount, unreadMentions = 0, reportsCount = 0, minUnreadItemId, unreadChat} type GroupStatsRow = (Int, Int, Int, ChatItemId, BoolInt) toGroupStats :: GroupStatsRow -> ChatStats -toGroupStats (unreadCount, unreadMentions, reportsCount, minUnreadItemId, BI unreadChat) = ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat} +toGroupStats (unreadCount, unreadMentions, reportsCount, minUnreadItemId, BI unreadChat) = + ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat} findDirectChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findDirectChatPreviews_ db User {userId} pagination clq = @@ -608,7 +734,6 @@ findDirectChatPreviews_ db User {userId} pagination clq = LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -701,7 +826,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -714,7 +839,7 @@ findGroupChatPreviews_ db User {userId} pagination clq = LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -790,7 +915,7 @@ getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do lastItem <- case lastItemId_ of Just lastItemId -> (: []) <$> getGroupCIWithReactions db user groupInfo lastItemId Nothing -> pure [] - pure $ AChat SCTGroup (Chat (GroupChat groupInfo) lastItem stats) + pure $ AChat SCTGroup (Chat (GroupChat groupInfo Nothing) lastItem stats) findLocalChatPreviews_ :: DB.Connection -> User -> PaginationByTime -> ChatListQuery -> IO [AChatPreviewData] findLocalChatPreviews_ db User {userId} pagination clq = @@ -812,7 +937,6 @@ findLocalChatPreviews_ db User {userId} pagination clq = LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -1225,32 +1349,101 @@ getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do :. (userId, contactId, ciCreatedAt afterCI, cChatItemId afterCI) ) -getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChat db vr user groupId contentFilter pagination search_ = do +getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe GroupChatScope -> Maybe MsgContentTag -> ChatPagination -> Maybe String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChat db vr user groupId scope_ contentFilter pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db vr user groupId + scopeInfo <- mapM (getCreateGroupChatScopeInfo db vr user g) scope_ case pagination of - CPLast count -> liftIO $ (,Nothing) <$> getGroupChatLast_ db user g contentFilter count search emptyChatStats - CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g contentFilter afterId count search - CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g contentFilter beforeId count search - CPAround aroundId count -> getGroupChatAround_ db user g contentFilter aroundId count search + CPLast count -> (,Nothing) <$> getGroupChatLast_ db user g scopeInfo contentFilter count search emptyChatStats + CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g scopeInfo contentFilter afterId count search + CPBefore beforeId count -> (,Nothing) <$> getGroupChatBefore_ db user g scopeInfo contentFilter beforeId count search + CPAround aroundId count -> getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search CPInitial count -> do unless (null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" - getGroupChatInitial_ db user g contentFilter count + getGroupChatInitial_ db user g scopeInfo contentFilter count -getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Int -> String -> ChatStats -> IO (Chat 'CTGroup) -getGroupChatLast_ db user g contentFilter count search stats = do - ciIds <- getGroupChatItemIDs db user g contentFilter GRLast count search - ts <- getCurrentTime - cis <- mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) (reverse cis) stats +getCreateGroupChatScopeInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo +getCreateGroupChatScopeInfo db vr user GroupInfo {membership} = \case + GCSMemberSupport Nothing -> do + when (isNothing $ supportChat membership) $ do + ts <- liftIO getCurrentTime + liftIO $ setSupportChatTs db (groupMemberId' membership) ts + pure $ GCSIMemberSupport {groupMember_ = Nothing} + GCSMemberSupport (Just gmId) -> do + m <- getGroupMemberById db vr user gmId + when (isNothing $ supportChat m) $ do + ts <- liftIO getCurrentTime + liftIO $ setSupportChatTs db gmId ts + pure GCSIMemberSupport {groupMember_ = Just m} + +getGroupChatScopeInfoForItem :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO (Maybe GroupChatScopeInfo) +getGroupChatScopeInfoForItem db vr user g itemId = + getGroupChatScopeForItem_ db itemId >>= mapM (getGroupChatScopeInfo db vr user g) + +getGroupChatScopeInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo +getGroupChatScopeInfo db vr user GroupInfo {membership} = \case + GCSMemberSupport Nothing -> case supportChat membership of + Nothing -> throwError $ SEInternalError "no moderators support chat" + Just _supportChat -> pure $ GCSIMemberSupport {groupMember_ = Nothing} + GCSMemberSupport (Just gmId) -> do + m <- getGroupMemberById db vr user gmId + case supportChat m of + Nothing -> throwError $ SEInternalError "no support chat" + Just _supportChat -> pure GCSIMemberSupport {groupMember_ = Just m} + +getGroupChatScopeForItem_ :: DB.Connection -> ChatItemId -> ExceptT StoreError IO (Maybe GroupChatScope) +getGroupChatScopeForItem_ db itemId = + ExceptT . firstRow toScope (SEChatItemNotFound itemId) $ + DB.query + db + [sql| + SELECT group_scope_tag, group_scope_group_member_id + FROM chat_items + WHERE chat_item_id = ? + |] + (Only itemId) + where + toScope (scopeTag, scopeMemberId) = + case (scopeTag, scopeMemberId) of + (Just GCSTMemberSupport_, Just gmId) -> Just $ GCSMemberSupport gmId + (Just GCSTMemberSupport_, Nothing) -> Just $ GCSMemberSupport Nothing + (Nothing, Nothing) -> Nothing + (Nothing, Just _) -> Nothing -- shouldn't happen + +getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatLast_ db user g scopeInfo_ contentFilter count search stats = do + ciIds <- getGroupChatItemIDs db user g scopeInfo_ contentFilter GRLast count search + ts <- liftIO getCurrentTime + cis <- mapM (liftIO . safeGetGroupItem db user g ts) ciIds + pure $ Chat (GroupChat g scopeInfo_) (reverse cis) stats data GroupItemIDsRange = GRLast | GRAfter UTCTime ChatItemId | GRBefore UTCTime ChatItemId -getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> String -> IO [ChatItemId] -getGroupChatItemIDs db User {userId} GroupInfo {groupId} contentFilter range count search = case contentFilter of - Just mcTag -> idsQuery (baseCond <> " AND msg_content_tag = ? ") (userId, groupId, mcTag) - Nothing -> idsQuery baseCond (userId, groupId) +getGroupChatItemIDs :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> GroupItemIDsRange -> Int -> String -> ExceptT StoreError IO [ChatItemId] +getGroupChatItemIDs db User {userId} GroupInfo {groupId} scopeInfo_ contentFilter range count search = case (scopeInfo_, contentFilter) of + (Nothing, Nothing) -> + liftIO $ + idsQuery + (baseCond <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ") + (userId, groupId) + (Nothing, Just mcTag) -> + liftIO $ + idsQuery + (baseCond <> " AND msg_content_tag = ? ") + (userId, groupId, mcTag) + (Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) -> + liftIO $ + idsQuery + (baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? ") + (userId, groupId, GCSTMemberSupport_, groupMemberId' m) + (Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) -> + liftIO $ + idsQuery + (baseCond <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL ") + (userId, groupId, GCSTMemberSupport_) + (Just _scope, Just _mcTag) -> + throwError $ SEInternalError "group scope and content filter are not supported together" where baseQuery = " SELECT chat_item_id FROM chat_items WHERE " baseCond = " user_id = ? AND group_id = ? " @@ -1322,81 +1515,82 @@ getGroupMemberChatItemLast db user@User {userId} groupId groupMemberId = do (userId, groupId, groupMemberId) getGroupChatItem db user groupId chatItemId -getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatAfter_ db user g@GroupInfo {groupId} contentFilter afterId count search = do +getGroupChatAfter_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatAfter_ db user g@GroupInfo {groupId} scopeInfo contentFilter afterId count search = do afterCI <- getGroupChatItem db user groupId afterId let range = GRAfter (chatItemTs afterCI) (cChatItemId afterCI) - ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search + ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) cis emptyChatStats + pure $ Chat (GroupChat g scopeInfo) cis emptyChatStats -getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) -getGroupChatBefore_ db user g@GroupInfo {groupId} contentFilter beforeId count search = do +getGroupChatBefore_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup) +getGroupChatBefore_ db user g@GroupInfo {groupId} scopeInfo contentFilter beforeId count search = do beforeCI <- getGroupChatItem db user groupId beforeId let range = GRBefore (chatItemTs beforeCI) (cChatItemId beforeCI) - ciIds <- liftIO $ getGroupChatItemIDs db user g contentFilter range count search + ciIds <- getGroupChatItemIDs db user g scopeInfo contentFilter range count search ts <- liftIO getCurrentTime cis <- liftIO $ mapM (safeGetGroupItem db user g ts) ciIds - pure $ Chat (GroupChat g) (reverse cis) emptyChatStats + pure $ Chat (GroupChat g scopeInfo) (reverse cis) emptyChatStats -getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatAround_ db user g contentFilter aroundId count search = do - stats <- liftIO $ getGroupStats_ db user g - getGroupChatAround' db user g contentFilter aroundId count search stats +getGroupChatAround_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround_ db user g scopeInfo contentFilter aroundId count search = do + stats <- getGroupStats_ db user g scopeInfo + getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats -getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatAround' db user g contentFilter aroundId count search stats = do +getGroupChatAround' :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ChatItemId -> Int -> String -> ChatStats -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatAround' db user g scopeInfo contentFilter aroundId count search stats = do aroundCI <- getGroupCIWithReactions db user g aroundId let beforeRange = GRBefore (chatItemTs aroundCI) (cChatItemId aroundCI) afterRange = GRAfter (chatItemTs aroundCI) (cChatItemId aroundCI) - beforeIds <- liftIO $ getGroupChatItemIDs db user g contentFilter beforeRange count search - afterIds <- liftIO $ getGroupChatItemIDs db user g contentFilter afterRange count search + beforeIds <- getGroupChatItemIDs db user g scopeInfo contentFilter beforeRange count search + afterIds <- getGroupChatItemIDs db user g scopeInfo contentFilter afterRange count search ts <- liftIO getCurrentTime beforeCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) beforeIds afterCIs <- liftIO $ mapM (safeGetGroupItem db user g ts) afterIds let cis = reverse beforeCIs <> [aroundCI] <> afterCIs navInfo <- liftIO $ getNavInfo cis - pure (Chat (GroupChat g) cis stats, Just navInfo) + pure (Chat (GroupChat g scopeInfo) cis stats, Just navInfo) where getNavInfo cis_ = case cis_ of [] -> pure $ NavigationInfo 0 0 cis -> getGroupNavInfo_ db user g (last cis) -getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChatInitial_ db user g contentFilter count = do - liftIO (getGroupMinUnreadId_ db user g contentFilter) >>= \case +getGroupChatInitial_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Int -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChatInitial_ db user g scopeInfo_ contentFilter count = do + getGroupMinUnreadId_ db user g scopeInfo_ contentFilter >>= \case Just minUnreadItemId -> do - stats <- liftIO $ getStats minUnreadItemId =<< getGroupUnreadCount_ db user g Nothing - getGroupChatAround' db user g contentFilter minUnreadItemId count "" stats - Nothing -> liftIO $ do - stats <- getStats 0 (0, 0) - (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g contentFilter count "" stats + unreadCounts <- getGroupUnreadCount_ db user g scopeInfo_ Nothing + stats <- liftIO $ getStats minUnreadItemId unreadCounts + getGroupChatAround' db user g scopeInfo_ contentFilter minUnreadItemId count "" stats + Nothing -> do + stats <- liftIO $ getStats 0 (0, 0) + (,Just $ NavigationInfo 0 0) <$> getGroupChatLast_ db user g scopeInfo_ contentFilter count "" stats where getStats minUnreadItemId (unreadCount, unreadMentions) = do reportsCount <- getGroupReportsCount_ db user g False pure ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat = False} -getGroupStats_ :: DB.Connection -> User -> GroupInfo -> IO ChatStats -getGroupStats_ db user g = do - minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g Nothing - (unreadCount, unreadMentions) <- getGroupUnreadCount_ db user g Nothing - reportsCount <- getGroupReportsCount_ db user g False +getGroupStats_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> ExceptT StoreError IO ChatStats +getGroupStats_ db user g scopeInfo_ = do + minUnreadItemId <- fromMaybe 0 <$> getGroupMinUnreadId_ db user g scopeInfo_ Nothing + (unreadCount, unreadMentions) <- getGroupUnreadCount_ db user g scopeInfo_ Nothing + reportsCount <- liftIO $ getGroupReportsCount_ db user g False pure ChatStats {unreadCount, unreadMentions, reportsCount, minUnreadItemId, unreadChat = False} -getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> IO (Maybe ChatItemId) -getGroupMinUnreadId_ db user g contentFilter = +getGroupMinUnreadId_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ExceptT StoreError IO (Maybe ChatItemId) +getGroupMinUnreadId_ db user g scopeInfo_ contentFilter = fmap join . maybeFirstRow fromOnly $ - queryUnreadGroupItems db user g contentFilter baseQuery orderLimit + queryUnreadGroupItems db user g scopeInfo_ contentFilter baseQuery orderLimit where baseQuery = "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? " orderLimit = " ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1" -getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> IO (Int, Int) -getGroupUnreadCount_ db user g contentFilter = - head <$> queryUnreadGroupItems db user g contentFilter baseQuery "" +getGroupUnreadCount_ :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> ExceptT StoreError IO (Int, Int) +getGroupUnreadCount_ db user g scopeInfo_ contentFilter = + head <$> queryUnreadGroupItems db user g scopeInfo_ contentFilter baseQuery "" where - baseQuery = "SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? " + baseQuery = "SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL " getGroupReportsCount_ :: DB.Connection -> User -> GroupInfo -> Bool -> IO Int getGroupReportsCount_ db User {userId} GroupInfo {groupId} archived = @@ -1406,19 +1600,35 @@ getGroupReportsCount_ db User {userId} GroupInfo {groupId} archived = "SELECT COUNT(1) FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? AND item_deleted = ? AND item_sent = 0" (userId, groupId, MCReport_, BI archived) -queryUnreadGroupItems :: FromRow r => DB.Connection -> User -> GroupInfo -> Maybe MsgContentTag -> Query -> Query -> IO [r] -queryUnreadGroupItems db User {userId} GroupInfo {groupId} contentFilter baseQuery orderLimit = - case contentFilter of - Just mcTag -> - DB.query - db - (baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit) - (userId, groupId, mcTag, CISRcvNew) - Nothing -> - DB.query - db - (baseQuery <> " AND item_status = ? " <> orderLimit) - (userId, groupId, CISRcvNew) +queryUnreadGroupItems :: FromRow r => DB.Connection -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe MsgContentTag -> Query -> Query -> ExceptT StoreError IO [r] +queryUnreadGroupItems db User {userId} GroupInfo {groupId} scopeInfo_ contentFilter baseQuery orderLimit = + case (scopeInfo_, contentFilter) of + (Nothing, Nothing) -> + liftIO $ + DB.query + db + (baseQuery <> " AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? " <> orderLimit) + (userId, groupId, CISRcvNew) + (Nothing, Just mcTag) -> + liftIO $ + DB.query + db + (baseQuery <> " AND msg_content_tag = ? AND item_status = ? " <> orderLimit) + (userId, groupId, mcTag, CISRcvNew) + (Just GCSIMemberSupport {groupMember_ = Just m}, Nothing) -> + liftIO $ + DB.query + db + (baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id = ? AND item_status = ? " <> orderLimit) + (userId, groupId, GCSTMemberSupport_, groupMemberId' m, CISRcvNew) + (Just GCSIMemberSupport {groupMember_ = Nothing}, Nothing) -> + liftIO $ + DB.query + db + (baseQuery <> " AND group_scope_tag = ? AND group_scope_group_member_id IS NULL AND item_status = ? " <> orderLimit) + (userId, groupId, GCSTMemberSupport_, CISRcvNew) + (Just _scope, Just _mcTag) -> + throwError $ SEInternalError "group scope and content filter are not supported together" getGroupNavInfo_ :: DB.Connection -> User -> GroupInfo -> CChatItem 'CTGroup -> IO NavigationInfo getGroupNavInfo_ db User {userId} GroupInfo {groupId} afterCI = do @@ -1691,12 +1901,22 @@ getLocalNavInfo_ db User {userId} NoteFolder {noteFolderId} afterCI = do :. (userId, noteFolderId, ciCreatedAt afterCI, cChatItemId afterCI) ) -toChatItemRef :: (ChatItemId, Maybe Int64, Maybe Int64, Maybe Int64) -> Either StoreError (ChatRef, ChatItemId) +toChatItemRef :: + (ChatItemId, Maybe ContactId, Maybe GroupId, Maybe GroupChatScopeTag, Maybe GroupMemberId, Maybe NoteFolderId) -> + Either StoreError (ChatRef, ChatItemId) toChatItemRef = \case - (itemId, Just contactId, Nothing, Nothing) -> Right (ChatRef CTDirect contactId, itemId) - (itemId, Nothing, Just groupId, Nothing) -> Right (ChatRef CTGroup groupId, itemId) - (itemId, Nothing, Nothing, Just folderId) -> Right (ChatRef CTLocal folderId, itemId) - (itemId, _, _, _) -> Left $ SEBadChatItem itemId Nothing + (itemId, Just contactId, Nothing, Nothing, Nothing, Nothing) -> + Right (ChatRef CTDirect contactId Nothing, itemId) + (itemId, Nothing, Just groupId, Nothing, Nothing, Nothing) -> + Right (ChatRef CTGroup groupId Nothing, itemId) + (itemId, Nothing, Just groupId, Just GCSTMemberSupport_, Nothing, Nothing) -> + Right (ChatRef CTGroup groupId (Just (GCSMemberSupport Nothing)), itemId) + (itemId, Nothing, Just groupId, Just GCSTMemberSupport_, Just scopeGMId, Nothing) -> + Right (ChatRef CTGroup groupId (Just (GCSMemberSupport $ Just scopeGMId)), itemId) + (itemId, Nothing, Nothing, Nothing, Nothing, Just folderId) -> + Right (ChatRef CTLocal folderId Nothing, itemId) + (itemId, _, _, _, _, _) -> + Left $ SEBadChatItem itemId Nothing updateDirectChatItemsRead :: DB.Connection -> User -> ContactId -> IO () updateDirectChatItemsRead db User {userId} contactId = do @@ -1763,8 +1983,8 @@ setDirectChatItemsDeleteAt db User {userId} contactId itemIds currentTs = forM i (deleteAt, userId, contactId, chatItemId) pure (chatItemId, deleteAt) -updateGroupChatItemsRead :: DB.Connection -> User -> GroupId -> IO () -updateGroupChatItemsRead db User {userId} groupId = do +updateGroupChatItemsRead :: DB.Connection -> User -> GroupInfo -> Maybe GroupChatScope -> IO () +updateGroupChatItemsRead db User {userId} GroupInfo {groupId, membership} scope = do currentTs <- getCurrentTime DB.execute db @@ -1773,6 +1993,20 @@ updateGroupChatItemsRead db User {userId} groupId = do WHERE user_id = ? AND group_id = ? AND item_status = ? |] (CISRcvRead, currentTs, userId, groupId, CISRcvNew) + case scope of + Nothing -> pure () + Just GCSMemberSupport {groupMemberId_} -> do + let gmId = fromMaybe (groupMemberId' membership) groupMemberId_ + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_items_unread = 0, + support_chat_items_member_attention = 0, + support_chat_items_mentions = 0 + WHERE group_member_id = ? + |] + (Only gmId) getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> IO [(ChatItemId, Int)] getGroupUnreadTimedItems db User {userId} groupId = @@ -1785,33 +2019,83 @@ getGroupUnreadTimedItems db User {userId} groupId = |] (userId, groupId, CISRcvNew) -updateGroupChatItemsReadList :: DB.Connection -> User -> GroupId -> NonEmpty ChatItemId -> IO [(ChatItemId, Int)] -updateGroupChatItemsReadList db User {userId} groupId itemIds = do - currentTs <- getCurrentTime - catMaybes . L.toList <$> mapM (getUpdateGroupItem currentTs) itemIds +updateGroupChatItemsReadList :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> NonEmpty ChatItemId -> ExceptT StoreError IO ([(ChatItemId, Int)], GroupInfo) +updateGroupChatItemsReadList db vr user@User {userId} g@GroupInfo {groupId} scopeInfo_ itemIds = do + currentTs <- liftIO getCurrentTime + -- Possible improvement is to differentiate retrieval queries for each scope, + -- but we rely on UI to not pass item IDs from incorrect scope. + readItemsData <- liftIO $ catMaybes . L.toList <$> mapM (getUpdateGroupItem currentTs) itemIds + g' <- case scopeInfo_ of + Nothing -> pure g + Just scopeInfo@GCSIMemberSupport {groupMember_} -> do + let decStats = countReadItems groupMember_ readItemsData + liftIO $ updateGroupScopeUnreadStats db vr user g scopeInfo decStats + pure (timedItems readItemsData, g') where - getUpdateGroupItem currentTs itemId = do - ttl_ <- maybeFirstRow fromOnly getUnreadTimedItem - setItemRead - pure $ (itemId,) <$> ttl_ + getUpdateGroupItem :: UTCTime -> ChatItemId -> IO (Maybe (ChatItemId, Maybe Int, Maybe UTCTime, Maybe GroupMemberId, Maybe BoolInt)) + getUpdateGroupItem currentTs itemId = + maybeFirstRow id $ + DB.query + db + [sql| + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? + RETURNING chat_item_id, timed_ttl, timed_delete_at, group_member_id, user_mention + |] + (CISRcvRead, currentTs, userId, groupId, CISRcvNew, itemId) + countReadItems :: Maybe GroupMember -> [(ChatItemId, Maybe Int, Maybe UTCTime, Maybe GroupMemberId, Maybe BoolInt)] -> (Int, Int, Int) + countReadItems scopeMember_ readItemsData = + let unread = length readItemsData + (unanswered, mentions) = foldl' countItem (0, 0) readItemsData + in (unread, unanswered, mentions) where - getUnreadTimedItem = - DB.query - db - [sql| - SELECT timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - |] - (userId, groupId, CISRcvNew, itemId) - setItemRead = - DB.execute - db - [sql| - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? - |] - (CISRcvRead, currentTs, userId, groupId, CISRcvNew, itemId) + countItem :: (Int, Int) -> (ChatItemId, Maybe Int, Maybe UTCTime, Maybe GroupMemberId, Maybe BoolInt) -> (Int, Int) + countItem (!unanswered, !mentions) (_, _, _, itemGMId_, userMention_) = + let unanswered' = case (scopeMember_, itemGMId_) of + (Just scopeMember, Just itemGMId) | itemGMId == groupMemberId' scopeMember -> unanswered + 1 + _ -> unanswered + mentions' = case userMention_ of + Just (BI True) -> mentions + 1 + _ -> mentions + in (unanswered', mentions') + timedItems :: [(ChatItemId, Maybe Int, Maybe UTCTime, Maybe GroupMemberId, Maybe BoolInt)] -> [(ChatItemId, Int)] + timedItems = foldl' addTimedItem [] + where + addTimedItem acc (itemId, Just ttl, Nothing, _, _) = (itemId, ttl) : acc + addTimedItem acc _ = acc + +updateGroupScopeUnreadStats :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScopeInfo -> (Int, Int, Int) -> IO GroupInfo +updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unread, unanswered, mentions) = + case scopeInfo of + GCSIMemberSupport {groupMember_} -> case groupMember_ of + Nothing -> do + membership' <- updateGMStats membership + pure g {membership = membership'} + Just member -> do + member' <- updateGMStats member + let didRequire = gmRequiresAttention member + nowRequires = gmRequiresAttention member' + if (not nowRequires && didRequire) + then decreaseGroupMembersRequireAttention db user g + else pure g + where + updateGMStats m@GroupMember {groupMemberId} = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET support_chat_items_unread = support_chat_items_unread - ?, + support_chat_items_member_attention = support_chat_items_member_attention - ?, + support_chat_items_mentions = support_chat_items_mentions - ?, + updated_at = ? + WHERE group_member_id = ? + |] + (unread, unanswered, mentions, currentTs, groupMemberId) + m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + pure $ either (const m) id m_ -- Left shouldn't happen, but types require it + +deriving instance Show BoolInt setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] setGroupChatItemsDeleteAt db User {userId} groupId itemIds currentTs = forM itemIds $ \(chatItemId, ttl) -> do @@ -1915,48 +2199,69 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction direction _ _ = Nothing -- this function can be changed so it never fails, not only avoid failure on invalid json -toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive, BI userMention) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do - chatItem $ fromRight invalid $ dbParseACIContent itemContentText - where - member_ = toMaybeGroupMember userContactId memberRow_ - quotedMember_ = toMaybeGroupMember userContactId quotedMemberRow_ - deletedByGroupMember_ = toMaybeGroupMember userContactId deletedByGroupMemberRow_ - invalid = ACIContent msgDir $ CIInvalidJSON itemContentText - chatItem itemContent = case (itemContent, itemStatus, member_, fileStatus_) of - (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Just (AFS SMDSnd fileStatus)) -> - Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent (maybeCIFile fileStatus) - (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Nothing) -> - Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent Nothing - (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Just (AFS SMDRcv fileStatus)) -> - Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent (maybeCIFile fileStatus) - (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Nothing) -> - Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent Nothing - _ -> badItem - maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) - maybeCIFile fileStatus = - case (fileId_, fileName_, fileSize_, fileProtocol_) of - (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> - let cfArgs = CFArgs <$> fileKey <*> fileNonce - fileSource = (`CryptoFile` cfArgs) <$> filePath - in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} - _ -> Nothing - cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup - cItem d chatDir ciStatus content file = - CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, mentions = M.empty, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file} - badItem = Left $ SEBadChatItem itemId (Just itemTs) - ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTGroup d - ciMeta content status = - let itemDeleted' = case itemDeleted of - DBCINotDeleted -> Nothing - DBCIBlocked -> Just (CIBlocked deletedTs) - DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) - _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) - itemEdited' = maybe False unBI itemEdited - itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs forwardedByMember createdAt updatedAt - ciTimed :: Maybe CITimed - ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} +toGroupChatItem :: + UTCTime -> + Int64 -> + ChatItemRow + :. Only (Maybe GroupMemberId) + :. MaybeGroupMemberRow + :. GroupQuoteRow + :. MaybeGroupMemberRow -> + Either StoreError (CChatItem 'CTGroup) +toGroupChatItem + currentTs + userContactId + ( ( (itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) + :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) + :. forwardedFromRow + :. (timedTTL, timedDeleteAt, itemLive, BI userMention) + :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_) + ) + :. Only forwardedByMember + :. memberRow_ + :. (quoteRow :. quotedMemberRow_) + :. deletedByGroupMemberRow_ + ) = do + chatItem $ fromRight invalid $ dbParseACIContent itemContentText + where + member_ = toMaybeGroupMember userContactId memberRow_ + quotedMember_ = toMaybeGroupMember userContactId quotedMemberRow_ + deletedByGroupMember_ = toMaybeGroupMember userContactId deletedByGroupMemberRow_ + invalid = ACIContent msgDir $ CIInvalidJSON itemContentText + chatItem itemContent = case (itemContent, itemStatus, member_, fileStatus_) of + (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Just (AFS SMDSnd fileStatus)) -> + Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent (maybeCIFile fileStatus) + (ACIContent SMDSnd ciContent, ACIStatus SMDSnd ciStatus, _, Nothing) -> + Right $ cItem SMDSnd CIGroupSnd ciStatus ciContent Nothing + (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Just (AFS SMDRcv fileStatus)) -> + Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent (maybeCIFile fileStatus) + (ACIContent SMDRcv ciContent, ACIStatus SMDRcv ciStatus, Just member, Nothing) -> + Right $ cItem SMDRcv (CIGroupRcv member) ciStatus ciContent Nothing + _ -> badItem + maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) + maybeCIFile fileStatus = + case (fileId_, fileName_, fileSize_, fileProtocol_) of + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} + _ -> Nothing + cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup + cItem d chatDir ciStatus content file = + CChatItem d ChatItem {chatDir, meta = ciMeta content ciStatus, content, mentions = M.empty, formattedText = parseMaybeMarkdownList itemText, quotedItem = toGroupQuote quoteRow quotedMember_, reactions = [], file} + badItem = Left $ SEBadChatItem itemId (Just itemTs) + ciMeta :: CIContent d -> CIStatus d -> CIMeta 'CTGroup d + ciMeta content status = + let itemDeleted' = case itemDeleted of + DBCINotDeleted -> Nothing + DBCIBlocked -> Just (CIBlocked deletedTs) + DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) + _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) + itemEdited' = maybe False unBI itemEdited + itemForwarded = toCIForwardedFrom forwardedFromRow + in mkCIMeta itemId content itemText status (unBI <$> sentViaProxy) sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed (unBI <$> itemLive) userMention currentTs itemTs forwardedByMember createdAt updatedAt + ciTimed :: Maybe CITimed + ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe String -> ExceptT StoreError IO [AChatItem] getAllChatItems db vr user@User {userId} pagination search_ = do @@ -1982,7 +2287,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC @@ -1993,7 +2298,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do DB.query db [sql| - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) @@ -2006,7 +2311,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) @@ -2018,7 +2323,7 @@ getAllChatItems db vr user@User {userId} pagination search_ = do DB.query db [sql| - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE chat_item_id = ? |] @@ -2590,6 +2895,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember @@ -2597,13 +2903,17 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, - dbm.created_at, dbm.updated_at + dbm.created_at, dbm.updated_at, + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + LEFT JOIN group_members gsm ON gsm.group_member_id = i.group_scope_group_member_id + LEFT JOIN contact_profiles gsp ON gsp.contact_profile_id = COALESCE(gsm.member_profile_id, gsm.contact_profile_id) LEFT JOIN group_members m ON m.group_member_id = i.group_member_id LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id @@ -2765,7 +3075,7 @@ getChatItemByFileId db vr user@User {userId} fileId = do DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id FROM chat_items i JOIN files f ON f.chat_item_id = i.chat_item_id WHERE f.user_id = ? AND f.file_id = ? @@ -2787,7 +3097,7 @@ getChatItemByGroupId db vr user@User {userId} groupId = do DB.query db [sql| - SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id FROM chat_items i JOIN groups g ON g.chat_item_id = i.chat_item_id WHERE g.user_id = ? AND g.group_id = ? @@ -2802,24 +3112,26 @@ getChatRefViaItemId db User {userId} itemId = do DB.query db "SELECT contact_id, group_id FROM chat_items WHERE user_id = ? AND chat_item_id = ?" (userId, itemId) where toChatRef = \case - (Just contactId, Nothing) -> Right $ ChatRef CTDirect contactId - (Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId + (Just contactId, Nothing) -> Right $ ChatRef CTDirect contactId Nothing + -- Only used in CLI and unused APIs + (Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId Nothing (_, _) -> Left $ SEBadChatItem itemId Nothing getAChatItem :: DB.Connection -> VersionRangeChat -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem -getAChatItem db vr user chatRef itemId = do - aci <- case chatRef of - ChatRef CTDirect contactId -> do - ct <- getContact db vr user contactId - (CChatItem msgDir ci) <- getDirectChatItem db user contactId itemId +getAChatItem db vr user (ChatRef cType chatId scope) itemId = do + aci <- case cType of + CTDirect -> do + ct <- getContact db vr user chatId + (CChatItem msgDir ci) <- getDirectChatItem db user chatId itemId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci - ChatRef CTGroup groupId -> do - gInfo <- getGroupInfo db vr user groupId - (CChatItem msgDir ci) <- getGroupChatItem db user groupId itemId - pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) ci - ChatRef CTLocal folderId -> do - nf <- getNoteFolder db user folderId - CChatItem msgDir ci <- getLocalChatItem db user folderId itemId + CTGroup -> do + gInfo <- getGroupInfo db vr user chatId + (CChatItem msgDir ci) <- getGroupChatItem db user chatId itemId + scopeInfo <- mapM (getGroupChatScopeInfo db vr user gInfo) scope + pure $ AChatItem SCTGroup msgDir (GroupChat gInfo scopeInfo) ci + CTLocal -> do + nf <- getNoteFolder db user chatId + CChatItem msgDir ci <- getLocalChatItem db user chatId itemId pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci _ -> throwError $ SEChatItemNotFound itemId liftIO $ getACIReactions db aci @@ -2829,9 +3141,9 @@ getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of CDDirectRcv ct@Contact {contactId} -> do (CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci - CDGroupRcv g GroupMember {groupMemberId} -> do + CDGroupRcv g scopeInfo GroupMember {groupMemberId} -> do (CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user g groupMemberId sharedMsgId - pure $ AChatItem SCTGroup msgDir (GroupChat g) ci + pure $ AChatItem SCTGroup msgDir (GroupChat g scopeInfo) ci getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion] getChatItemVersions db itemId = do @@ -2912,7 +3224,7 @@ getACIReactions db aci@(AChatItem _ md chat ci@ChatItem {meta = CIMeta {itemShar DirectChat ct -> do reactions <- getDirectCIReactions db ct itemSharedMId pure $ AChatItem SCTDirect md chat ci {reactions} - GroupChat g -> do + GroupChat g _s -> do let GroupMember {memberId} = chatItemMember g ci reactions <- getGroupCIReactions db g memberId itemSharedMId pure $ AChatItem SCTGroup md chat ci {reactions} @@ -3024,16 +3336,23 @@ getTimedItems db User {userId} startTimedThreadCutoff = <$> DB.query db [sql| - SELECT chat_item_id, contact_id, group_id, timed_delete_at + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, timed_delete_at FROM chat_items WHERE user_id = ? AND timed_delete_at IS NOT NULL AND timed_delete_at <= ? |] (userId, startTimedThreadCutoff) where - toCIRefDeleteAt :: (ChatItemId, Maybe ContactId, Maybe GroupId, UTCTime) -> Maybe ((ChatRef, ChatItemId), UTCTime) + toCIRefDeleteAt :: (ChatItemId, Maybe ContactId, Maybe GroupId, Maybe GroupChatScopeTag, Maybe GroupMemberId, UTCTime) -> Maybe ((ChatRef, ChatItemId), UTCTime) toCIRefDeleteAt = \case - (itemId, Just contactId, Nothing, deleteAt) -> Just ((ChatRef CTDirect contactId, itemId), deleteAt) - (itemId, Nothing, Just groupId, deleteAt) -> Just ((ChatRef CTGroup groupId, itemId), deleteAt) + (itemId, Just contactId, Nothing, Nothing, Nothing, deleteAt) -> + Just ((ChatRef CTDirect contactId Nothing, itemId), deleteAt) + (itemId, Nothing, Just groupId, scopeTag_, scopeGMId_, deleteAt) -> + let scope = case (scopeTag_, scopeGMId_) of + (Nothing, Nothing) -> Nothing + (Just GCSTMemberSupport_, Just groupMemberId) -> Just $ GCSMemberSupport (Just groupMemberId) + (Just GCSTMemberSupport_, Nothing) -> Just $ GCSMemberSupport Nothing + (Nothing, Just _) -> Nothing -- should not happen + in Just ((ChatRef CTGroup groupId scope, itemId), deleteAt) _ -> Nothing getChatItemTTL :: DB.Connection -> User -> IO Int64 @@ -3205,7 +3524,6 @@ getGroupSndStatusCounts db itemId = |] (Only itemId) --- TODO [knocking] filter out messages sent to member only getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] getGroupHistoryItems db user@User {userId} g@GroupInfo {groupId} m count = do ciIds <- getLastItemIds_ diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index c392c17db1..2e9ff571e9 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -7,13 +7,15 @@ import Data.Text (Text) import Simplex.Chat.Store.Postgres.Migrations.M20241220_initial import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission +import Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] schemaMigrations = [ ("20241220_initial", m20241220_initial, Nothing), ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), - ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), + ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250513_group_scope.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250513_group_scope.hs new file mode 100644 index 0000000000..cca2cff969 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250513_group_scope.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250513_group_scope :: Text +m20250513_group_scope = + T.pack + [r| +ALTER TABLE group_members ADD COLUMN support_chat_ts TIMESTAMPTZ; +ALTER TABLE group_members ADD COLUMN support_chat_items_unread BIGINT NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_items_member_attention BIGINT NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_items_mentions BIGINT NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_last_msg_from_member_ts TIMESTAMPTZ; + +ALTER TABLE groups ADD COLUMN members_require_attention BIGINT NOT NULL DEFAULT 0; + +ALTER TABLE chat_items ADD COLUMN group_scope_tag TEXT; +ALTER TABLE chat_items ADD COLUMN group_scope_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE CASCADE; + +CREATE INDEX idx_chat_items_group_scope_group_member_id ON chat_items(group_scope_group_member_id); + +CREATE INDEX idx_chat_items_group_scope_item_ts ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_ts +); + +CREATE INDEX idx_chat_items_group_scope_item_status ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_status, + item_ts +); +|] + +down_m20250513_group_scope :: Text +down_m20250513_group_scope = + T.pack + [r| +DROP INDEX idx_chat_items_group_scope_item_status; + +DROP INDEX idx_chat_items_group_scope_item_ts; + +DROP INDEX idx_chat_items_group_scope_group_member_id; + +ALTER TABLE chat_items DROP COLUMN group_scope_tag; +ALTER TABLE chat_items DROP COLUMN group_scope_group_member_id; + +ALTER TABLE groups DROP COLUMN members_require_attention; + +ALTER TABLE group_members DROP COLUMN support_chat_ts; +ALTER TABLE group_members DROP COLUMN support_chat_items_unread; +ALTER TABLE group_members DROP COLUMN support_chat_items_member_attention; +ALTER TABLE group_members DROP COLUMN support_chat_items_mentions; +ALTER TABLE group_members DROP COLUMN support_chat_last_msg_from_member_ts; +|] diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index a7dc154d9d..38b2615686 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -43,7 +43,7 @@ module Simplex.Chat.Store.Profiles setUserProfileContactLink, getUserContactProfiles, createUserContactLink, - getUserAddressConnections, + getUserAddressConnection, getUserContactLinks, deleteUserAddress, getUserAddress, @@ -51,6 +51,7 @@ module Simplex.Chat.Store.Profiles getGroupLinkInfo, getUserContactLinkByConnReq, getUserContactLinkViaShortLink, + setUserContactLinkShortLink, getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, @@ -110,6 +111,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode) +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) #if defined(dbPostgres) @@ -363,26 +365,21 @@ createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMo userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff -getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] -getUserAddressConnections db vr User {userId} = do - cs <- liftIO getUserAddressConnections_ - if null cs then throwError SEUserContactLinkNotFound else pure cs - where - getUserAddressConnections_ :: IO [Connection] - getUserAddressConnections_ = - map (toConnection vr) - <$> DB.query - db - [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM connections c - JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id - WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - |] - (userId, userId) +getUserAddressConnection :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO Connection +getUserAddressConnection db vr User {userId} = do + ExceptT . firstRow (toConnection vr) SEUserContactLinkNotFound $ + DB.query + db + [sql| + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + |] + (userId, userId) getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)] getUserContactLinks db vr User {userId} = @@ -504,14 +501,14 @@ toGroupLinkInfo (groupId_, mRole_) = getGroupLinkInfo :: DB.Connection -> UserId -> GroupId -> IO (Maybe GroupLinkInfo) getGroupLinkInfo db userId groupId = - fmap join $ maybeFirstRow toGroupLinkInfo $ + fmap join $ maybeFirstRow toGroupLinkInfo $ DB.query db [sql| SELECT group_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? - |] + |] (userId, groupId) getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) @@ -531,6 +528,17 @@ userContactLinkQuery = FROM user_contact_links |] +setUserContactLinkShortLink :: DB.Connection -> Int64 -> ShortLinkContact -> IO () +setUserContactLinkShortLink db userContactLinkId shortLink = + DB.execute + db + [sql| + UPDATE user_contact_links + SET short_link_contact = ? + WHERE user_contact_link_id = ? + |] + (shortLink, userContactLinkId) + getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do ctId_ <- diff --git a/src/Simplex/Chat/Store/Remote.hs b/src/Simplex/Chat/Store/Remote.hs index 4921369b10..cc626ddcf0 100644 --- a/src/Simplex/Chat/Store/Remote.hs +++ b/src/Simplex/Chat/Store/Remote.hs @@ -10,6 +10,7 @@ import Control.Monad.Except import Data.Int (Int64) import Data.Text (Text) import Data.Text.Encoding (decodeASCII, encodeUtf8) +import qualified Data.X509 as X import Data.Word (Word16) import Simplex.Chat.Remote.Types import Simplex.Chat.Store.Shared @@ -66,7 +67,7 @@ remoteHostQuery = FROM remote_hosts |] -toRemoteHost :: (Int64, Text, FilePath, C.APrivateSignKey, C.SignedObject C.Certificate, C.PrivateKeyEd25519, C.KeyHash, C.PublicKeyX25519, Maybe Text, Maybe Text, Maybe Word16) -> RemoteHost +toRemoteHost :: (Int64, Text, FilePath, C.APrivateSignKey, C.SignedObject X.Certificate, C.PrivateKeyEd25519, C.KeyHash, C.PublicKeyX25519, Maybe Text, Maybe Text, Maybe Word16) -> RemoteHost toRemoteHost (remoteHostId, hostDeviceName, storePath, caKey, C.SignedObject caCert, idPrivKey, hostFingerprint, hostDhPubKey, ifaceName_, ifaceAddr_, bindPort_) = RemoteHost {remoteHostId, hostDeviceName, storePath, hostPairing, bindAddress_, bindPort_} where @@ -133,7 +134,7 @@ toRemoteCtrl :: ( RemoteCtrlId, Text, C.APrivateSignKey, - C.SignedObject C.Certificate, + C.SignedObject X.Certificate, C.KeyHash, C.PublicKeyEd25519, C.PrivateKeyX25519, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 183d699f01..871cb62220 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -130,6 +130,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission +import Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -259,7 +260,8 @@ schemaMigrations = ("20250129_delete_unused_contacts", m20250129_delete_unused_contacts, Just down_m20250129_delete_unused_contacts), ("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes), ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), - ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission) + ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), + ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250513_group_scope.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250513_group_scope.hs new file mode 100644 index 0000000000..34ec6ee382 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250513_group_scope.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250513_group_scope :: Query +m20250513_group_scope = + [sql| +ALTER TABLE group_members ADD COLUMN support_chat_ts TEXT; +ALTER TABLE group_members ADD COLUMN support_chat_items_unread INTEGER NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_items_member_attention INTEGER NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_items_mentions INTEGER NOT NULL DEFAULT 0; +ALTER TABLE group_members ADD COLUMN support_chat_last_msg_from_member_ts TEXT; + +ALTER TABLE groups ADD COLUMN members_require_attention INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE chat_items ADD COLUMN group_scope_tag TEXT; +ALTER TABLE chat_items ADD COLUMN group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE; + +CREATE INDEX idx_chat_items_group_scope_group_member_id ON chat_items(group_scope_group_member_id); + +CREATE INDEX idx_chat_items_group_scope_item_ts ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_ts +); + +CREATE INDEX idx_chat_items_group_scope_item_status ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_status, + item_ts +); +|] + +down_m20250513_group_scope :: Query +down_m20250513_group_scope = + [sql| +DROP INDEX idx_chat_items_group_scope_item_status; + +DROP INDEX idx_chat_items_group_scope_item_ts; + +DROP INDEX idx_chat_items_group_scope_group_member_id; + +ALTER TABLE chat_items DROP COLUMN group_scope_tag; +ALTER TABLE chat_items DROP COLUMN group_scope_group_member_id; + +ALTER TABLE groups DROP COLUMN members_require_attention; + +ALTER TABLE group_members DROP COLUMN support_chat_ts; +ALTER TABLE group_members DROP COLUMN support_chat_items_unread; +ALTER TABLE group_members DROP COLUMN support_chat_items_member_attention; +ALTER TABLE group_members DROP COLUMN support_chat_items_mentions; +ALTER TABLE group_members DROP COLUMN support_chat_last_msg_from_member_ts; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index a85ba4a4cb..13215dcb75 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -1071,6 +1071,10 @@ Query: UPDATE connections SET smp_agent_version = ? WHERE conn_id = ? Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: UPDATE connections SET user_id = ? WHERE conn_id = ? and user_id = ? +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + Query: UPDATE messages SET msg_body = x'' WHERE conn_id = ? AND internal_id = ? Plan: SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index e9ade30f93..de30073f9b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1,3 +1,21 @@ +Query: + UPDATE groups + SET chat_ts = ?, + members_require_attention = members_require_attention + 1 + WHERE user_id = ? AND group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE groups + SET chat_ts = ?, + members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, @@ -36,17 +54,20 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- from GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - m.created_at, m.updated_at + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -79,6 +100,40 @@ Query: Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = 0, + support_chat_items_mentions = support_chat_items_mentions + ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = support_chat_items_member_attention + ?, + support_chat_items_mentions = support_chat_items_mentions + ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET support_chat_ts = ?, + support_chat_items_unread = support_chat_items_unread + ?, + support_chat_items_member_attention = support_chat_items_member_attention + ?, + support_chat_items_mentions = support_chat_items_mentions + ?, + support_chat_last_msg_from_member_ts = ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: INSERT INTO contact_requests (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, @@ -209,21 +264,6 @@ Query: Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) -Query: - SELECT timed_ttl - FROM chat_items - WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL - -Plan: -SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) - -Query: - UPDATE chat_items SET item_status = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? - -Plan: -SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE contact_profiles SET display_name = ?, @@ -462,20 +502,7 @@ Plan: SEARCH messages USING INDEX idx_messages_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) Query: - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM connections c - JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id - WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - -Plan: -SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) - -Query: - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts < ? OR (item_ts = ? AND chat_item_id < ?)) @@ -483,18 +510,18 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -574,7 +601,8 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, - m.created_at, m.updated_at + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -590,7 +618,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=? AND shared_msg_id=?) LEFT-JOIN Query: - SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id FROM chat_items i JOIN files f ON f.chat_item_id = i.chat_item_id WHERE f.user_id = ? AND f.file_id = ? @@ -625,6 +653,14 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) +Query: + UPDATE chat_items SET item_status = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? + RETURNING chat_item_id, timed_ttl, timed_delete_at, group_member_id, user_mention + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + Query: DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ? AND reaction_sent = ? AND reaction = ? @@ -724,6 +760,7 @@ Query: m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember @@ -731,13 +768,17 @@ Query: rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, rp.display_name, rp.full_name, rp.image, rp.contact_link, rp.local_alias, rp.preferences, rm.created_at, rm.updated_at, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, dbp.display_name, dbp.full_name, dbp.image, dbp.contact_link, dbp.local_alias, dbp.preferences, - dbm.created_at, dbm.updated_at + dbm.created_at, dbm.updated_at, + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id + LEFT JOIN group_members gsm ON gsm.group_member_id = i.group_scope_group_member_id + LEFT JOIN contact_profiles gsp ON gsp.contact_profile_id = COALESCE(gsm.member_profile_id, gsm.contact_profile_id) LEFT JOIN group_members m ON m.group_member_id = i.group_member_id LEFT JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN chat_items ri ON ri.shared_msg_id = i.quoted_shared_msg_id AND ri.group_id = i.group_id @@ -750,6 +791,7 @@ Query: Plan: SEARCH i USING INTEGER PRIMARY KEY (rowid=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN +SEARCH gsm USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH p USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH ri USING INDEX idx_chat_items_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) LEFT-JOIN @@ -806,17 +848,20 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, -- via GroupMember m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -851,10 +896,12 @@ Query: SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - mu.created_at, mu.updated_at + mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu USING (group_id) @@ -977,7 +1024,7 @@ Plan: SEARCH chat_items USING INDEX idx_chat_items_group_id (group_id=?) Query: - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE chat_item_id = ? @@ -985,7 +1032,7 @@ Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT chat_item_id, contact_id, group_id, note_folder_id + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, note_folder_id FROM chat_items WHERE user_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' AND (item_ts > ? OR (item_ts = ? AND chat_item_id > ?)) @@ -993,7 +1040,7 @@ Query: LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=?) USE TEMP B-TREE FOR ORDER BY Query: @@ -1142,7 +1189,7 @@ SEARCH i USING INDEX idx_chat_items_group_id (group_id=?) SEARCH m USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT i.chat_item_id, i.contact_id, i.group_id, i.note_folder_id + SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id FROM chat_items i JOIN groups g ON g.chat_item_id = i.chat_item_id WHERE g.user_id = ? AND g.group_id = ? @@ -1233,6 +1280,27 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET support_chat_items_unread = 0, + support_chat_items_member_attention = 0, + support_chat_items_mentions = 0 + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE group_members + SET support_chat_items_unread = support_chat_items_unread - ?, + support_chat_items_member_attention = support_chat_items_member_attention - ?, + support_chat_items_mentions = support_chat_items_mentions - ?, + updated_at = ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_profiles SET display_name = ?, full_name = ?, description = ?, image = ?, preferences = ?, member_admission = ?, updated_at = ? @@ -1537,7 +1605,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1578,7 +1645,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1613,7 +1679,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1647,7 +1712,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1681,7 +1745,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1715,7 +1778,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1749,7 +1811,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1783,7 +1844,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1817,7 +1877,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1848,7 +1907,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1879,7 +1937,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), ct.unread_chat FROM contacts ct @@ -1905,7 +1962,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -1918,7 +1975,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -1940,7 +1997,7 @@ Query: ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) @@ -1948,7 +2005,7 @@ SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -1957,7 +2014,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -1970,7 +2027,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -1987,14 +2044,14 @@ Query: ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2003,7 +2060,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2016,7 +2073,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2032,14 +2089,14 @@ Query: AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts ? ORDER BY g.chat_ts ASC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2093,7 +2150,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2106,7 +2163,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2122,14 +2179,14 @@ Query: ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2138,7 +2195,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2151,7 +2208,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2167,14 +2224,14 @@ Query: AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts ? ORDER BY g.chat_ts ASC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2228,7 +2285,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2241,7 +2298,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2257,14 +2314,14 @@ Query: ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2273,7 +2330,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2286,7 +2343,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2299,14 +2356,14 @@ Query: WHERE g.user_id = ? AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts ? ORDER BY g.chat_ts ASC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=? AND chat_ts>?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2357,7 +2414,7 @@ Query: ( SELECT chat_item_id FROM chat_items ci - WHERE ci.user_id = ? AND ci.group_id = g.group_id + WHERE ci.user_id = ? AND ci.group_id = g.group_id AND ci.group_scope_tag IS NULL ORDER BY ci.item_ts DESC LIMIT 1 ) AS chat_item_id, @@ -2370,7 +2427,7 @@ Query: LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, SUM(user_mention) as UnreadMentions, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE user_id = ? AND group_id IS NOT NULL AND item_status = ? + WHERE user_id = ? AND group_id IS NOT NULL AND group_scope_tag IS NULL AND item_status = ? GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN ( @@ -2383,14 +2440,14 @@ Query: WHERE g.user_id = ? ORDER BY g.chat_ts DESC LIMIT ? Plan: MATERIALIZE ChatStats -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) +SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id>?) MATERIALIZE ReportCount SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_deleted (user_id=? AND group_id>?) SEARCH g USING INDEX idx_groups_chat_ts (user_id=?) SEARCH ChatStats USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN SEARCH ReportCount USING AUTOMATIC COVERING INDEX (group_id=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 -SEARCH ci USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH ci USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) Query: SELECT @@ -2404,7 +2461,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2440,7 +2496,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2475,7 +2530,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2510,7 +2564,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2545,7 +2598,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2580,7 +2632,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2615,7 +2666,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2650,7 +2700,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2682,7 +2731,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2714,7 +2762,6 @@ Query: LIMIT 1 ) AS chat_item_id, COALESCE(ChatStats.UnreadCount, 0), - 0, COALESCE(ChatStats.MinUnread, 0), nf.unread_chat FROM note_folders nf @@ -2763,6 +2810,19 @@ Plan: SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) +Query: + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM connections c + JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id + WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL + +Plan: +SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) +SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) + Query: SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, @@ -2926,7 +2986,7 @@ Plan: SEARCH chat_items USING INDEX chat_items_note_folder_id (note_folder_id=?) Query: - SELECT chat_item_id, contact_id, group_id, timed_delete_at + SELECT chat_item_id, contact_id, group_id, group_scope_tag, group_scope_group_member_id, timed_delete_at FROM chat_items WHERE user_id = ? AND timed_delete_at IS NOT NULL AND timed_delete_at <= ? @@ -3114,6 +3174,14 @@ Query: Plan: SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?) +Query: + SELECT group_scope_tag, group_scope_group_member_id + FROM chat_items + WHERE chat_item_id = ? + +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT group_snd_item_status FROM group_snd_item_statuses @@ -3407,6 +3475,21 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + DELETE FROM chat_items + WHERE group_scope_group_member_id = ? + +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + Query: DELETE FROM chat_items WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? @@ -3924,7 +4007,7 @@ Plan: Query: INSERT INTO chat_items ( -- user and IDs - user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, + user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, timed_ttl, timed_delete_at, @@ -3932,7 +4015,7 @@ Query: quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4215,6 +4298,14 @@ Query: Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE connections + SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + WHERE user_id = ? AND connection_id = ? + +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contact_profiles SET contact_link = ?, updated_at = ? @@ -4322,6 +4413,19 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET support_chat_ts = NULL, + support_chat_items_unread = 0, + support_chat_items_member_attention = 0, + support_chat_items_mentions = 0, + support_chat_last_msg_from_member_ts = NULL, + updated_at = ? + WHERE group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_profiles SET preferences = ?, updated_at = ? @@ -4352,6 +4456,22 @@ Query: Plan: SEARCH group_snd_item_statuses USING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) +Query: + UPDATE groups + SET members_require_attention = members_require_attention + 1 + WHERE user_id = ? AND group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE groups + SET members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?) @@ -4451,12 +4571,14 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - mu.created_at, mu.updated_at + mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -4473,12 +4595,14 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - mu.created_at, mu.updated_at + mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id @@ -4495,6 +4619,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4527,6 +4652,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4551,6 +4677,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4575,6 +4702,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4599,6 +4727,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4623,6 +4752,7 @@ Query: m.group_member_id, m.group_id, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, @@ -4669,7 +4799,7 @@ Query: JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? Plan: -SEARCH i USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH i USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) Query: @@ -4931,36 +5061,44 @@ Query: Plan: SCAN usage_conditions -Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts < ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id < ? )) ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts ? ) OR ( user_id = ? AND group_id = ? AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE (( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts > ? ) OR ( user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_ts = ? AND chat_item_id > ? )) ORDER BY item_ts ASC, chat_item_id ASC LIMIT ? Plan: MULTI-INDEX OR INDEX 1 -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts>?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts>?) INDEX 2 -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=? AND item_ts=? AND rowid>?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_ts=? AND rowid>?) USE TEMP B-TREE FOR ORDER BY -Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag = ? AND group_scope_group_member_id IS NULL ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND LOWER(item_text) LIKE '%' || LOWER(?) || '%' ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) + +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? +Plan: +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=?) Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND msg_content_tag = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_msg_content_tag_item_ts (user_id=? AND group_id=? AND msg_content_tag=?) -Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? ORDER BY item_ts DESC, chat_item_id DESC LIMIT ? -Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_item_ts (user_id=? AND group_id=?) - Query: CREATE TABLE temp_conn_ids (conn_id BLOB) Error: SQLite3 returned ErrorError while attempting to perform prepare "explain query plan CREATE TABLE temp_conn_ids (conn_id BLOB)": table temp_conn_ids already exists @@ -5144,6 +5282,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) @@ -5167,6 +5306,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) @@ -5301,7 +5441,7 @@ SEARCH protocol_servers USING COVERING INDEX idx_smp_servers_user_id (user_id=?) SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) SEARCH commands USING COVERING INDEX idx_commands_user_id (user_id=?) SEARCH calls USING COVERING INDEX idx_calls_user_id (user_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_ts (user_id=?) SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_2 (user_id=?) SEARCH user_contact_links USING COVERING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) SEARCH connections USING COVERING INDEX idx_connections_group_member (user_id=?) @@ -5458,9 +5598,9 @@ Query: SELECT COUNT(1) FROM groups WHERE user_id = ? AND chat_item_ttl > 0 Plan: SEARCH groups USING INDEX idx_groups_chat_ts (user_id=?) -Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? AND item_status = ? +Query: SELECT COUNT(1), COALESCE(SUM(user_mention), 0) FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_item_status (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?) Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ? Plan: @@ -5486,9 +5626,9 @@ Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND contact_id = ? Plan: SEARCH chat_items USING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) -Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND item_status = ? ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1 +Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? ORDER BY item_ts ASC, chat_item_id ASC LIMIT 1 Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_groups (user_id=? AND group_id=? AND item_status=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_item_status (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?) Query: SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? LIMIT 1 Plan: @@ -5642,6 +5782,10 @@ Query: SELECT quota_err_counter FROM connections WHERE user_id = ? AND connectio Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ? +Plan: +SEARCH group_member_intros USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) + Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -5650,7 +5794,7 @@ Query: SELECT user_contact_link_id FROM contact_requests WHERE contact_request_i Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) -Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 +Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 Plan: SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?) @@ -5842,11 +5986,7 @@ Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_memb Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1 -Plan: -SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) - -Query: UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2 +Query: UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 6fbed97d27..91670ffc7b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -134,7 +134,8 @@ CREATE TABLE groups( business_xcontact_id BLOB NULL, customer_member_id BLOB NULL, chat_item_ttl INTEGER, - local_alias TEXT DEFAULT '', -- received + local_alias TEXT DEFAULT '', + members_require_attention INTEGER NOT NULL DEFAULT 0, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -167,6 +168,11 @@ CREATE TABLE group_members( peer_chat_min_version INTEGER NOT NULL DEFAULT 1, peer_chat_max_version INTEGER NOT NULL DEFAULT 1, member_restriction TEXT, + support_chat_ts TEXT, + support_chat_items_unread INTEGER NOT NULL DEFAULT 0, + support_chat_items_member_attention INTEGER NOT NULL DEFAULT 0, + support_chat_items_mentions INTEGER NOT NULL DEFAULT 0, + support_chat_last_msg_from_member_ts TEXT, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -411,7 +417,9 @@ CREATE TABLE chat_items( via_proxy INTEGER, msg_content_tag TEXT, include_in_history INTEGER NOT NULL DEFAULT 0, - user_mention INTEGER NOT NULL DEFAULT 0 + user_mention INTEGER NOT NULL DEFAULT 0, + group_scope_tag TEXT, + group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE ); CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( @@ -1022,3 +1030,21 @@ CREATE INDEX idx_chat_items_group_id_shared_msg_id ON chat_items( group_id, shared_msg_id ); +CREATE INDEX idx_chat_items_group_scope_group_member_id ON chat_items( + group_scope_group_member_id +); +CREATE INDEX idx_chat_items_group_scope_item_ts ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_ts +); +CREATE INDEX idx_chat_items_group_scope_item_status ON chat_items( + user_id, + group_id, + group_scope_tag, + group_scope_group_member_id, + item_status, + item_ts +); diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index b32fd07bb5..8c9c50e9de 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -579,27 +579,37 @@ safeDeleteLDN db User {userId} localDisplayName = do type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow -type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) +type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow - in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData} + in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember -toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt)) = +toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) = let memberProfile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + supportChat = case supportChatTs_ of + Just chatTs -> + Just GroupSupportChat { + chatTs, + unread = supportChatUnread, + memberAttention = supportChatMemberAttention, + mentions = supportChatMentions, + lastMsgFromMemberTs = supportChatLastMsgFromMemberTs + } + _ -> Nothing in GroupMember {..} toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo @@ -613,12 +623,14 @@ groupInfoQuery = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.business_chat, g.business_member_id, g.customer_member_id, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences, - mu.created_at, mu.updated_at + mu.created_at, mu.updated_at, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id JOIN group_members mu ON mu.group_id = g.group_id diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 79fc08397c..1ec09d43d6 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -183,7 +183,7 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case whenCurrUser cc u $ setActiveChat t cInfo case (cInfo, chatDir) of (DirectChat ct, _) -> sendNtf (viewContactName ct <> "> ", text) - (GroupChat g, CIGroupRcv m) -> sendNtf (fromGroup_ g m, text) + (GroupChat g scopeInfo, CIGroupRcv m) -> sendNtf (fromGroup_ g scopeInfo m, text) _ -> pure () where text = msgText mc formattedText @@ -207,6 +207,8 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case sendNtf ("#" <> viewGroupName g, "you are connected to group") CEvtJoinedGroupMember u g m -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") + CEvtJoinedGroupMemberConnecting u g _ m | memberStatus m == GSMemPendingReview -> + when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is pending review") CEvtConnectedToGroupMember u g m _ -> when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected") CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} -> @@ -233,7 +235,7 @@ chatActiveTo (ChatName cType name) = case cType of chatInfoActiveTo :: ChatInfo c -> String chatInfoActiveTo = \case DirectChat c -> contactActiveTo c - GroupChat g -> groupActiveTo g + GroupChat g _scopeInfo -> groupActiveTo g _ -> "" contactActiveTo :: Contact -> String diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 0063f711c7..567baf0cdf 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -421,7 +421,8 @@ data GroupInfo = GroupInfo chatTags :: [ChatTagId], chatItemTTL :: Maybe Int64, uiThemes :: Maybe UIThemeEntityOverrides, - customData :: Maybe CustomData + customData :: Maybe CustomData, + membersRequireAttention :: Int } deriving (Eq, Show) @@ -841,10 +842,27 @@ data GroupMember = GroupMember -- but it's correctly set on read (see toGroupInfo) memberChatVRange :: VersionRangeChat, createdAt :: UTCTime, - updatedAt :: UTCTime + updatedAt :: UTCTime, + supportChat :: Maybe GroupSupportChat } deriving (Eq, Show) +data GroupSupportChat = GroupSupportChat + { chatTs :: UTCTime, + unread :: Int64, + memberAttention :: Int64, + mentions :: Int64, + lastMsgFromMemberTs :: Maybe UTCTime + } + deriving (Eq, Show) + +gmRequiresAttention :: GroupMember -> Bool +gmRequiresAttention m@GroupMember {supportChat} = + memberPending m || maybe False supportChatAttention supportChat + where + supportChatAttention GroupSupportChat {memberAttention, mentions} = + memberAttention > 0 || mentions > 0 + data GroupMemberRef = GroupMemberRef {groupMemberId :: Int64, profile :: Profile} deriving (Eq, Show) @@ -872,6 +890,9 @@ supportsVersion m v = maxVersion (memberChatVRange' m) >= v groupMemberId' :: GroupMember -> GroupMemberId groupMemberId' GroupMember {groupMemberId} = groupMemberId +memberId' :: GroupMember -> MemberId +memberId' GroupMember {memberId} = memberId + memberIncognito :: GroupMember -> IncognitoEnabled memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId @@ -1015,6 +1036,7 @@ data GroupMemberStatus | GSMemUnknown -- unknown member, whose message was forwarded by an admin (likely member wasn't introduced due to not being a current member, but message was included in history) | GSMemInvited -- member is sent to or received invitation to join the group | GSMemPendingApproval -- member is connected to host but pending host approval before connecting to other members ("knocking") + | GSMemPendingReview -- member is introduced to admins but pending admin review before connecting to other members ("knocking") | GSMemIntroduced -- user received x.grp.mem.intro for this member (only with GCPreMember) | GSMemIntroInvited -- member is sent to or received from intro invitation | GSMemAccepted -- member accepted invitation (only User and Invitee) @@ -1035,10 +1057,12 @@ instance ToJSON GroupMemberStatus where toJSON = J.String . textEncode toEncoding = JE.text . textEncode -acceptanceToStatus :: GroupAcceptance -> GroupMemberStatus -acceptanceToStatus = \case - GAAccepted -> GSMemAccepted - GAPending -> GSMemPendingApproval +acceptanceToStatus :: Maybe GroupMemberAdmission -> GroupAcceptance -> GroupMemberStatus +acceptanceToStatus memberAdmission groupAcceptance + | groupAcceptance == GAPendingApproval = GSMemPendingApproval + | groupAcceptance == GAPendingReview = GSMemPendingReview + | (memberAdmission >>= review) == Just MCAll = GSMemPendingReview + | otherwise = GSMemAccepted memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of @@ -1049,6 +1073,7 @@ memberActive m = case memberStatus m of GSMemUnknown -> False GSMemInvited -> False GSMemPendingApproval -> True + GSMemPendingReview -> True GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -1060,6 +1085,15 @@ memberActive m = case memberStatus m of memberCurrent :: GroupMember -> Bool memberCurrent = memberCurrent' . memberStatus +memberPending :: GroupMember -> Bool +memberPending m = case memberStatus m of + GSMemPendingApproval -> True + GSMemPendingReview -> True + _ -> False + +memberCurrentOrPending :: GroupMember -> Bool +memberCurrentOrPending m = memberCurrent m || memberPending m + -- update getGroupSummary if this is changed memberCurrent' :: GroupMemberStatus -> Bool memberCurrent' = \case @@ -1070,6 +1104,7 @@ memberCurrent' = \case GSMemUnknown -> False GSMemInvited -> False GSMemPendingApproval -> False + GSMemPendingReview -> False GSMemIntroduced -> True GSMemIntroInvited -> True GSMemAccepted -> True @@ -1087,6 +1122,7 @@ memberRemoved m = case memberStatus m of GSMemUnknown -> False GSMemInvited -> False GSMemPendingApproval -> False + GSMemPendingReview -> False GSMemIntroduced -> False GSMemIntroInvited -> False GSMemAccepted -> False @@ -1104,6 +1140,7 @@ instance TextEncoding GroupMemberStatus where "unknown" -> Just GSMemUnknown "invited" -> Just GSMemInvited "pending_approval" -> Just GSMemPendingApproval + "pending_review" -> Just GSMemPendingReview "introduced" -> Just GSMemIntroduced "intro-inv" -> Just GSMemIntroInvited "accepted" -> Just GSMemAccepted @@ -1120,6 +1157,7 @@ instance TextEncoding GroupMemberStatus where GSMemUnknown -> "unknown" GSMemInvited -> "invited" GSMemPendingApproval -> "pending_approval" + GSMemPendingReview -> "pending_review" GSMemIntroduced -> "introduced" GSMemIntroInvited -> "intro-inv" GSMemAccepted -> "accepted" @@ -1856,6 +1894,8 @@ $(JQ.deriveJSON defaultJSON ''Connection) $(JQ.deriveJSON defaultJSON ''PendingContactConnection) +$(JQ.deriveJSON defaultJSON ''GroupSupportChat) + $(JQ.deriveJSON defaultJSON ''GroupMember) $(JQ.deriveJSON (enumJSON $ dropPrefix "MF") ''MsgFilter) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index 4c6adae4e9..60ebe9d033 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -49,20 +49,17 @@ instance ToJSON GroupMemberRole where toJSON = strToJSON toEncoding = strToJEncoding -data GroupAcceptance = GAAccepted | GAPending deriving (Eq, Show) - --- TODO [knocking] encoding doesn't match field type -instance FromField GroupAcceptance where fromField = blobFieldDecoder strDecode - -instance ToField GroupAcceptance where toField = toField . strEncode +data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show) instance StrEncoding GroupAcceptance where strEncode = \case GAAccepted -> "accepted" - GAPending -> "pending" + GAPendingApproval -> "pending" + GAPendingReview -> "pending_review" strDecode = \case "accepted" -> Right GAAccepted - "pending" -> Right GAPending + "pending" -> Right GAPendingApproval + "pending_review" -> Right GAPendingReview r -> Left $ "bad GroupAcceptance " <> B.unpack r strP = strDecode <$?> A.takeByteString diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4ba5acbb43..54e888ea86 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -60,6 +60,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) @@ -134,9 +135,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte "server queue info: " <> viewJSON qInfo ] CRContactSwitchStarted {} -> ["switch started"] - CRGroupMemberSwitchStarted {} -> ["switch started"] + CEvtGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] - CRGroupMemberSwitchAborted {} -> ["switch aborted"] + CEvtGroupMemberSwitchAborted {} -> ["switch aborted"] CRContactRatchetSyncStarted {} -> ["connection synchronization started"] CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"] CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] @@ -174,6 +175,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRGroupMembers u g -> ttyUser u $ viewGroupMembers g + CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms + -- CRGroupConversationsArchived u _g _conversations -> ttyUser u [] + -- CRGroupConversationsDeleted u _g _conversations -> ttyUser u [] CRGroupsList u gs -> ttyUser u $ viewGroupsList gs CRSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus @@ -189,6 +193,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView + CRItemsReadForChat u _chatId -> ttyUser u ["items read for chat"] CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c @@ -217,6 +222,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else [] CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m + CRMemberAccepted u g m -> ttyUser u $ viewMemberAccepted g m + CRMemberSupportChatDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " support chat deleted"] CRMembersRoleUser u g members r' -> ttyUser u $ viewMemberRoleUserChanged g members r' CRMembersBlockedForAllUser u g members blocked -> ttyUser u $ viewMembersBlockedForAllUser g members blocked CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m @@ -311,7 +318,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte where toChatView :: AChat -> (Text, Text, Maybe ConnStatus) toChatView (AChat _ (Chat (DirectChat Contact {localDisplayName, activeConn}) items _)) = ("@" <> localDisplayName, toCIPreview items Nothing, connStatus <$> activeConn) - toChatView (AChat _ (Chat (GroupChat GroupInfo {membership, localDisplayName}) items _)) = ("#" <> localDisplayName, toCIPreview items (Just membership), Nothing) + toChatView (AChat _ (Chat (GroupChat GroupInfo {membership, localDisplayName} _scopeInfo) items _)) = ("#" <> localDisplayName, toCIPreview items (Just membership), Nothing) toChatView (AChat _ (Chat (LocalChat _) items _)) = ("*", toCIPreview items Nothing, Nothing) toChatView (AChat _ (Chat (ContactRequest UserContactRequest {localDisplayName}) items _)) = ("<@" <> localDisplayName, toCIPreview items Nothing, Nothing) toChatView (AChat _ (Chat (ContactConnection PendingContactConnection {pccConnId, pccConnStatus}) items _)) = (":" <> T.pack (show pccConnId), toCIPreview items Nothing, Just pccConnStatus) @@ -457,8 +464,9 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m CEvtHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] CEvtHostDisconnected p h -> [plain $ "disconnected from " <> viewHostEvent p h] - CEvtJoinedGroupMemberConnecting u g host m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] - CEvtConnectedToGroupMember u g m _ -> ttyUser u [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] + CEvtJoinedGroupMemberConnecting u g host m -> ttyUser u $ viewJoinedGroupMemberConnecting g host m + CEvtConnectedToGroupMember u g m _ -> ttyUser u $ viewConnectedToGroupMember g m + CEvtMemberAcceptedByOther u g acceptingMember m -> ttyUser u $ viewMemberAcceptedByOther g acceptingMember m CEvtMemberRole u g by m r r' -> ttyUser u $ viewMemberRoleChanged g by m r r' CEvtMemberBlockedForAll u g by m blocked -> ttyUser u $ viewMemberBlockedForAll g by m blocked CEvtDeletedMemberUser u g by wm -> ttyUser u $ [ttyGroup' g <> ": " <> ttyMember by <> " removed you from the group" <> withMessages wm] <> groupPreserved g @@ -525,6 +533,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] TERcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e -> ttyUser u ["received file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] + CEvtCustomChatEvent u r -> ttyUser' u $ map plain $ T.lines r where ttyUser :: User -> [StyledString] -> [StyledString] ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss @@ -549,7 +558,7 @@ userNtf User {showNtfs, activeUser} = showNtfs || activeUser chatDirNtf :: User -> ChatInfo c -> CIDirection c d -> Bool -> Bool chatDirNtf user cInfo chatDir mention = case (cInfo, chatDir) of (DirectChat ct, CIDirectRcv) -> contactNtf user ct mention - (GroupChat g, CIGroupRcv m) -> groupNtf user g mention && not (blockedByAdmin m) && showMessages (memberSettings m) + (GroupChat g _scopeInfo, CIGroupRcv m) -> groupNtf user g mention && not (blockedByAdmin m) && showMessages (memberSettings m) _ -> True contactNtf :: User -> Contact -> Bool -> Bool @@ -619,7 +628,7 @@ viewChats ts tz = concatMap chatPreview . reverse where chatName = case chat of DirectChat ct -> [" " <> ttyToContact' ct] - GroupChat g -> [" " <> ttyToGroup g] + GroupChat g scopeInfo -> [" " <> ttyToGroup g scopeInfo] _ -> [] viewChatItems :: @@ -664,22 +673,22 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa (maybe [] forwardedFrom itemForwarded) (directQuote chatDir) quotedItem - GroupChat g -> case chatDir of + GroupChat g scopeInfo -> case chatDir of CIGroupSnd -> case content of CISndMsgContent mc -> hideLive meta $ withSndFile to $ sndMsg to context mc CISndGroupInvitation {} -> showSndItemProhibited to _ -> showSndItem to where - to = ttyToGroup g + to = ttyToGroup g scopeInfo CIGroupRcv m -> case content of CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta CIRcvGroupInvitation {} -> showRcvItemProhibited from - CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False - CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g m) context meta [plainContent content] False + CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m) context meta [plainContent content] False + CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m) context meta [plainContent content] False _ -> showRcvItem from where - from = ttyFromGroupAttention g m userMention + from = ttyFromGroupAttention g scopeInfo m userMention where context = maybe @@ -757,8 +766,8 @@ viewChatItemInfo (AChatItem _ msgDir _ ChatItem {meta = CIMeta {itemTs, itemTime fwdDir_ = case (fwdMsgDir, fwdChatInfo) of (SMDSnd, DirectChat ct) -> Just $ "you @" <> viewContactName ct (SMDRcv, DirectChat ct) -> Just $ "@" <> viewContactName ct - (SMDSnd, GroupChat gInfo) -> Just $ "you #" <> viewGroupName gInfo - (SMDRcv, GroupChat gInfo) -> Just $ "#" <> viewGroupName gInfo + (SMDSnd, GroupChat gInfo _scopeInfo) -> Just $ "you #" <> viewGroupName gInfo + (SMDRcv, GroupChat gInfo _scopeInfo) -> Just $ "#" <> viewGroupName gInfo _ -> Nothing fwdItemId = "chat item id: " <> (T.pack . show $ aChatItemId fwdACI) _ -> [] @@ -809,19 +818,19 @@ viewItemUpdate chat ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, itemEd (maybe [] forwardedFrom itemForwarded) (directQuote chatDir) quotedItem - GroupChat g -> case chatDir of + GroupChat g scopeInfo -> case chatDir of CIGroupRcv m -> case content of CIRcvMsgContent mc | itemLive == Just True && not liveItems -> [] | otherwise -> viewReceivedUpdatedMessage from context mc ts tz meta _ -> [] where - from = if itemEdited then ttyFromGroupEdited g m else ttyFromGroup g m + from = if itemEdited then ttyFromGroupEdited g scopeInfo m else ttyFromGroup g scopeInfo m CIGroupSnd -> case content of CISndMsgContent mc -> hideLive meta $ viewSentMessage to context mc ts tz meta _ -> [] where - to = if itemEdited then ttyToGroupEdited g else ttyToGroup g + to = if itemEdited then ttyToGroupEdited g scopeInfo else ttyToGroup g scopeInfo where context = maybe @@ -864,10 +873,10 @@ viewItemDelete chat ci@ChatItem {chatDir, meta, content = deletedContent} toItem DirectChat c -> case (chatDir, deletedContent) of (CIDirectRcv, CIRcvMsgContent mc) -> viewReceivedMessage (ttyFromContactDeleted c deletedText_) [] mc ts tz meta _ -> prohibited - GroupChat g -> case ciMsgContent deletedContent of + GroupChat g scopeInfo -> case ciMsgContent deletedContent of Just mc -> let m = chatItemMember g ci - in viewReceivedMessage (ttyFromGroupDeleted g m deletedText_) [] mc ts tz meta + in viewReceivedMessage (ttyFromGroupDeleted g scopeInfo m deletedText_) [] mc ts tz meta _ -> prohibited _ -> prohibited where @@ -886,11 +895,11 @@ viewItemReaction showReactions chat CIReaction {chatDir, chatItem = CChatItem md where from = ttyFromContact c reactionMsg mc = quoteText mc $ if toMsgDirection md == MDSnd then ">>" else ">" - (GroupChat g, CIGroupRcv m) -> case ciMsgContent content of + (GroupChat g scopeInfo, CIGroupRcv m) -> case ciMsgContent content of Just mc -> view from $ reactionMsg mc _ -> [] where - from = ttyFromGroup g m + from = ttyFromGroup g scopeInfo m reactionMsg mc = quoteText mc . ttyQuotedMember . Just $ sentByMember' g itemDir (LocalChat _, CILocalRcv) -> case ciMsgContent content of Just mc -> view from $ reactionMsg mc @@ -997,7 +1006,7 @@ viewContactNotFound cName suspectedMember = viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] - GroupChat gi -> [ttyGroup' gi <> ": all messages are removed locally ONLY"] + GroupChat gi _scopeInfo -> [ttyGroup' gi <> ": all messages are removed locally ONLY"] LocalChat _ -> ["notes: all messages are removed"] ContactRequest _ -> [] ContactConnection _ -> [] @@ -1158,6 +1167,7 @@ viewUserJoinedGroup g@GroupInfo {membership} = where pendingApproval_ = case memberStatus membership of GSMemPendingApproval -> ", pending approval" + GSMemPendingReview -> ", connecting to group moderators for admission to group" _ -> "" viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString] @@ -1166,7 +1176,37 @@ viewJoinedGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, member [ (ttyGroup' g <> ": " <> ttyMember m <> " connected and pending approval, ") <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " ") <> " to accept member") ] - _ -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] + GSMemPendingReview -> [ttyGroup' g <> ": " <> ttyMember m <> " connected and pending review"] + _ -> [ttyGroup' g <> ": " <> ttyMember m <> " joined the group"] + +viewMemberAccepted :: GroupInfo -> GroupMember -> [StyledString] +viewMemberAccepted g m@GroupMember {memberStatus} = case memberStatus of + GSMemPendingReview -> [ttyGroup' g <> ": " <> ttyMember m <> " accepted and pending review (will introduce moderators)"] + _ -> [ttyGroup' g <> ": " <> ttyMember m <> " accepted"] + +viewMemberAcceptedByOther :: GroupInfo -> GroupMember -> GroupMember -> [StyledString] +viewMemberAcceptedByOther g acceptingMember m@GroupMember {memberCategory, memberStatus} = case memberCategory of + GCUserMember -> case memberStatus of + GSMemPendingReview -> [ttyGroup' g <> ": " <> ttyMember acceptingMember <> " accepted you to the group, pending review"] + _ -> [ttyGroup' g <> ": " <> ttyMember acceptingMember <> " accepted you to the group [warning - unexpected]"] + GCInviteeMember -> [ttyGroup' g <> ": " <> ttyMember acceptingMember <> " accepted " <> ttyMember m <> " to the group (will introduce remaining members)"] + _ -> [ttyGroup' g <> ": " <> ttyMember acceptingMember <> " accepted " <> ttyMember m <> " to the group"] + +viewJoinedGroupMemberConnecting :: GroupInfo -> GroupMember -> GroupMember -> [StyledString] +viewJoinedGroupMemberConnecting g@GroupInfo {groupId} host m@GroupMember {groupMemberId, memberStatus} = case memberStatus of + GSMemPendingReview -> + [ (ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting and pending review...), ") + <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " ") <> " to accept member") + ] + _ -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] + +viewConnectedToGroupMember :: GroupInfo -> GroupMember -> [StyledString] +viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, memberStatus} = case memberStatus of + GSMemPendingReview -> + [ (ttyGroup' g <> ": " <> connectedMember m <> " is connected and pending review, ") + <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " ") <> " to accept member") + ] + _ -> [ttyGroup' g <> ": " <> connectedMember m <> " is connected"] viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] viewReceivedGroupInvitation g c role = @@ -1238,6 +1278,18 @@ viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filt | not (showMessages $ memberSettings m) = ["blocked"] | otherwise = [] +viewMemberSupportChats :: GroupInfo -> [GroupMember] -> [StyledString] +viewMemberSupportChats GroupInfo {membership} ms = support <> map groupMember ms + where + support = case supportChat membership of + Just sc -> ["support: " <> chatStats sc] + Nothing -> [] + groupMember m@GroupMember {supportChat} = case supportChat of + Just sc -> memIncognito m <> ttyFullMember m <> (" (id " <> sShow (groupMemberId' m) <> "): ") <> chatStats sc + Nothing -> "" + chatStats GroupSupportChat {unread, memberAttention, mentions} = + "unread: " <> sShow unread <> ", require attention: " <> sShow memberAttention <> ", mentions: " <> sShow mentions + viewContactConnected :: Contact -> Maybe Profile -> Bool -> [StyledString] viewContactConnected ct userIncognitoProfile testView = case userIncognitoProfile of @@ -1747,9 +1799,10 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] -viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = - case connLinkInv' of - Just ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink' +viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connLinkInv} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = + case (connLinkInv, connLinkInv') of + (Just ccLink, Just ccLink') + | ccLink /= ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink' _ -> [userChangedStr] where userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' @@ -1795,7 +1848,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} = \case GLPConnectingProhibit (Just g) -> [grpOrBiz g <> " link: connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] GLPKnown g -> [ grpOrBiz g <> " link: known " <> grpOrBiz g <> " " <> ttyGroup' g, - "use " <> ttyToGroup g <> highlight' "" <> " to send messages" + "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" ] where grpLink = ("group link: " <>) @@ -1925,7 +1978,7 @@ uploadingFile :: StyledString -> AChatItem -> [StyledString] uploadingFile status = \case AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectSnd} -> [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyContact c] - AChatItem _ _ (GroupChat g) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd} -> + AChatItem _ _ (GroupChat g _scopeInfo) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIGroupSnd} -> [status <> " uploading " <> fileTransferStr fileId fileName <> " for " <> ttyGroup' g] _ -> [status <> " uploading file"] @@ -2471,26 +2524,26 @@ ttyFullGroup :: GroupInfo -> StyledString ttyFullGroup GroupInfo {localDisplayName = g, groupProfile = GroupProfile {fullName}} = ttyGroup g <> optFullName g fullName -ttyFromGroup :: GroupInfo -> GroupMember -> StyledString -ttyFromGroup g m = ttyFromGroupAttention g m False +ttyFromGroup :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> StyledString +ttyFromGroup g scopeInfo m = ttyFromGroupAttention g scopeInfo m False -ttyFromGroupAttention :: GroupInfo -> GroupMember -> Bool -> StyledString -ttyFromGroupAttention g m attention = membershipIncognito g <> ttyFrom (fromGroupAttention_ g m attention) +ttyFromGroupAttention :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> Bool -> StyledString +ttyFromGroupAttention g scopeInfo m attention = membershipIncognito g <> ttyFrom (fromGroupAttention_ g scopeInfo m attention) -ttyFromGroupEdited :: GroupInfo -> GroupMember -> StyledString -ttyFromGroupEdited g m = membershipIncognito g <> ttyFrom (fromGroup_ g m <> "[edited] ") +ttyFromGroupEdited :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> StyledString +ttyFromGroupEdited g scopeInfo m = membershipIncognito g <> ttyFrom (fromGroup_ g scopeInfo m <> "[edited] ") -ttyFromGroupDeleted :: GroupInfo -> GroupMember -> Maybe Text -> StyledString -ttyFromGroupDeleted g m deletedText_ = - membershipIncognito g <> ttyFrom (fromGroup_ g m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) +ttyFromGroupDeleted :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> Maybe Text -> StyledString +ttyFromGroupDeleted g scopeInfo m deletedText_ = + membershipIncognito g <> ttyFrom (fromGroup_ g scopeInfo m <> maybe "" (\t -> "[" <> t <> "] ") deletedText_) -fromGroup_ :: GroupInfo -> GroupMember -> Text -fromGroup_ g m = fromGroupAttention_ g m False +fromGroup_ :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> Text +fromGroup_ g scopeInfo m = fromGroupAttention_ g scopeInfo m False -fromGroupAttention_ :: GroupInfo -> GroupMember -> Bool -> Text -fromGroupAttention_ g m attention = +fromGroupAttention_ :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> Bool -> Text +fromGroupAttention_ g scopeInfo m attention = let attn = if attention then "!" else "" - in "#" <> viewGroupName g <> " " <> viewMemberName m <> attn <> "> " + in "#" <> viewGroupName g <> " " <> groupScopeInfoStr scopeInfo <> viewMemberName m <> attn <> "> " ttyFrom :: Text -> StyledString ttyFrom = styled $ colored Yellow @@ -2498,11 +2551,18 @@ ttyFrom = styled $ colored Yellow ttyTo :: Text -> StyledString ttyTo = styled $ colored Cyan -ttyToGroup :: GroupInfo -> StyledString -ttyToGroup g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " ") +ttyToGroup :: GroupInfo -> Maybe GroupChatScopeInfo -> StyledString +ttyToGroup g scopeInfo = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " " <> groupScopeInfoStr scopeInfo) -ttyToGroupEdited :: GroupInfo -> StyledString -ttyToGroupEdited g = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> " [edited] ") +ttyToGroupEdited :: GroupInfo -> Maybe GroupChatScopeInfo -> StyledString +ttyToGroupEdited g scopeInfo = membershipIncognito g <> ttyTo ("#" <> viewGroupName g <> groupScopeInfoStr scopeInfo <> " [edited] ") + +groupScopeInfoStr :: Maybe GroupChatScopeInfo -> Text +groupScopeInfoStr = \case + Nothing -> "" + Just (GCSIMemberSupport {groupMember_}) -> case groupMember_ of + Nothing -> "(support) " + Just m -> "(support: " <> viewMemberName m <> ") " ttyFilePath :: FilePath -> StyledString ttyFilePath = plain diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index f8a5aa8b80..c8b3d33916 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -1071,10 +1071,11 @@ testCapthaScreening ps = bob <## "Use /filter 1 [name] [captcha] to enable and /filter 1 off to disable filter." -- connect with captcha screen _ <- join cath groupLink - cath #> "#privacy 123" -- sending incorrect captcha - cath <# "#privacy SimpleX-Directory!> > cath 123" + cath ##> "/_send #1(_support) text 123" -- sending incorrect captcha + cath <# "#privacy (support) 123" + cath <# "#privacy (support) SimpleX-Directory!> > cath 123" cath <## " Incorrect text, please try again." - captcha <- dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + captcha <- dropStrPrefix "#privacy (support) SimpleX-Directory> " . dropTime <$> getTermLine cath sendCaptcha cath captcha cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://" cath <## "#privacy: member bob (Bob) is connected" @@ -1118,13 +1119,14 @@ testCapthaScreening ps = cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group, pending approval" - cath <# "#privacy SimpleX-Directory> Captcha is generated by SimpleX Directory service." + cath <# "#privacy (support) SimpleX-Directory> Captcha is generated by SimpleX Directory service." cath <## "" cath <## "Send captcha text to join the group privacy." - dropStrPrefix "#privacy SimpleX-Directory> " . dropTime <$> getTermLine cath + dropStrPrefix "#privacy (support) SimpleX-Directory> " . dropTime <$> getTermLine cath sendCaptcha cath captcha = do - cath #> ("#privacy " <> captcha) - cath <# ("#privacy SimpleX-Directory!> > cath " <> captcha) + cath ##> ("/_send #1(_support) text " <> captcha) + cath <# ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) SimpleX-Directory!> > cath " <> captcha) cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index e3bab5a0ec..9330e14aef 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -17,7 +17,7 @@ import Control.Concurrent (forkIOWithUnmask, killThread, threadDelay) import Control.Concurrent.Async import Control.Concurrent.STM import Control.Exception (bracket, bracket_) -import Control.Logger.Simple +import Control.Logger.Simple (LogLevel (..)) import Control.Monad import Control.Monad.Except import Control.Monad.Reader @@ -41,7 +41,7 @@ import Simplex.Chat.Types import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) -import Simplex.FileTransfer.Transport (supportedFileServerVRange) +import Simplex.FileTransfer.Transport (alpnSupportedXFTPhandshakes, supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) @@ -55,10 +55,10 @@ import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) -import Simplex.Messaging.Server.Env.STM (AServerStoreCfg (..), ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) -import Simplex.Messaging.Server.MsgStore.Types (SQSType (..), SMSType (..)) +import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Server (ServerCredentials (..), defaultTransportServerConfig) +import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive) @@ -463,19 +463,35 @@ testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_ test_ _ = error "expected 3 chat clients" testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () -testChat4 = testChatCfg4 testCfg +testChat4 = testChatCfgOpts4 testCfg testOpts testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () -testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_ +testChatCfg4 cfg = testChatCfgOpts4 cfg testOpts + +testChatOpts4 :: HasCallStack => ChatOpts -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () +testChatOpts4 = testChatCfgOpts4 testCfg + +testChatCfgOpts4 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () +testChatCfgOpts4 cfg opts p1 p2 p3 p4 test = testChatN cfg opts [p1, p2, p3, p4] test_ where test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 test_ _ = error "expected 4 chat clients" +testChat5 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () +testChat5 = testChatCfg5 testCfg + +testChatCfg5 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> TestParams -> IO () +testChatCfg5 cfg p1 p2 p3 p4 p5 test = testChatN cfg testOpts [p1, p2, p3, p4, p5] test_ + where + test_ :: HasCallStack => [TestCC] -> IO () + test_ [tc1, tc2, tc3, tc4, tc5] = test tc1 tc2 tc3 tc4 tc5 + test_ _ = error "expected 5 chat clients" + concurrentlyN_ :: [IO a] -> IO () concurrentlyN_ = mapConcurrently_ id -smpServerCfg :: ServerConfig +smpServerCfg :: ServerConfig STMMsgStore smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], @@ -485,7 +501,7 @@ smpServerCfg = maxJournalStateLines = 4, queueIdBytes = 24, msgIdBytes = 6, - serverStoreCfg = ASSCfg SQSMemory SMSMemory $ SSCMemory Nothing, + serverStoreCfg = SSCMemory Nothing, storeNtfsFile = Nothing, allowNewQueues = True, -- server password is disabled as otherwise v1 tests fail @@ -513,23 +529,23 @@ smpServerCfg = pendingENDInterval = 500000, ntfDeliveryInterval = 200000, smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, + transportConfig = mkTransportServerConfig True (Just alpnSupportedSMPHandshakes) True, smpHandshakeTimeout = 1000000, controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig, allowSMPProxy = True, serverClientConcurrency = 16, information = Nothing, - startOptions = StartOptions {maintenance = False, logLevel = LogError, compactLog = False, skipWarnings = False, confirmMigrations = MCYesUp} + startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp} } -persistentServerStoreCfg :: FilePath -> AServerStoreCfg -persistentServerStoreCfg tmp = ASSCfg SQSMemory SMSMemory $ SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} +persistentServerStoreCfg :: FilePath -> ServerStoreCfg STMMsgStore +persistentServerStoreCfg tmp = SSCMemory $ Just StorePaths {storeLogFile = tmp <> "/smp-server-store.log", storeMsgsFile = Just $ tmp <> "/smp-server-messages.log"} withSmpServer :: IO () -> IO () withSmpServer = withSmpServer' smpServerCfg -withSmpServer' :: ServerConfig -> IO a -> IO a +withSmpServer' :: ServerConfig STMMsgStore -> IO a -> IO a withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) xftpTestPort :: ServiceName @@ -566,7 +582,7 @@ xftpServerConfig = serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, controlPort = Nothing, - transportConfig = defaultTransportServerConfig, + transportConfig = mkTransportServerConfig True (Just alpnSupportedXFTPhandshakes) False, responseDelay = 0 } @@ -578,7 +594,7 @@ withXFTPServer' cfg = serverBracket ( \started -> do createDirectoryIfMissing False xftpServerFiles - runXFTPServerBlocking started cfg Nothing + runXFTPServerBlocking started cfg ) serverBracket :: (TMVar Bool -> IO ()) -> IO a -> IO a diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 817e16113f..21ee1e9218 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -372,7 +372,7 @@ testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do bob ##> "/last_item_id" i :: ChatItemId <- read <$> getTermLine bob let itemIds = intercalate "," $ map show [i - 3 .. i] - bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + bob #$> ("/_read chat items @2 " <> itemIds, id, "items read for chat") testChatPaginationInitial :: HasCallStack => TestParams -> IO () testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do @@ -391,7 +391,7 @@ testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice -- Read next 2 items let itemIds = intercalate "," $ map itemId [1 .. 2] - bob #$> ("/_read chat items @2 " <> itemIds, id, "ok") + bob #$> ("/_read chat items @2 " <> itemIds, id, "items read for chat") bob #$> ("/_get chat @2 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) -- Read all items diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 431f19c77f..6b834acf01 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -103,11 +103,12 @@ chatGroupTests = do it "group link member role" testGroupLinkMemberRole it "host profile received" testGroupLinkHostProfileReceived it "existing contact merged" testGroupLinkExistingContactMerged - describe "group links - join rejection" $ do - it "reject member joining via group link - blocked name" testGLinkRejectBlockedName - describe "group links - manual acceptance" $ do - it "manually accept member joining via group link" testGLinkManualAcceptMember - it "delete pending member" testGLinkDeletePendingMember + describe "group links - member screening" $ do + it "reject member - blocked name" testGLinkRejectBlockedName + it "accept member - only host approval" testGLinkApproveMember + it "accept member - only moderators review" testGLinkReviewMember + it "accept member - host approval, then moderators review" testGLinkApproveThenReviewMember + it "delete pending approval member" testGLinkDeletePendingApprovalMember describe "group link connection plan" $ do it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn @@ -194,8 +195,12 @@ chatGroupTests = do it "should send updated mentions in history" testGroupHistoryWithMentions describe "uniqueMsgMentions" testUniqueMsgMentions describe "updatedMentionNames" testUpdatedMentionNames - describe "group direct messages" $ do - it "should send group direct messages" testGroupDirectMessages + describe "group scoped messages" $ do + it "should send scoped messages to support (single moderator)" testScopedSupportSingleModerator + it "should send scoped messages to support (many moderators)" testScopedSupportManyModerators + it "should send messages to admins and members" testSupportCLISendCommand + it "should correctly maintain unread stats for support chats on reading chat items" testScopedSupportUnreadStatsOnRead + it "should correctly maintain unread stats for support chats on deleting chat items" testScopedSupportUnreadStatsOnDelete testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = @@ -314,7 +319,7 @@ testGroupShared alice bob cath checkMessages = do (bob <# "#team alice> hello") (cath "#team hello" - cath <## "you are no longer a member of the group" + cath <## "bad chat command: not current member" -- delete contact alice ##> "/d bob" alice <## "bob: contact is deleted" @@ -371,7 +376,7 @@ testMarkReadGroup = testChat2 aliceProfile bobProfile $ \alice bob -> do bob ##> "/last_item_id" i :: ChatItemId <- read <$> getTermLine bob let itemIds = intercalate "," $ map show [i - 3 .. i] - bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + bob #$> ("/_read chat items #1 " <> itemIds, id, "items read for chat") testChatPaginationInitial :: HasCallStack => TestParams -> IO () testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do @@ -392,7 +397,7 @@ testChatPaginationInitial = testChatOpts2 opts aliceProfile bobProfile $ \alice -- Read next 2 items let itemIds = intercalate "," $ map groupItemId [1 .. 2] - bob #$> ("/_read chat items #1 " <> itemIds, id, "ok") + bob #$> ("/_read chat items #1 " <> itemIds, id, "items read for chat") bob #$> ("/_get chat #1 initial=2", chat, [(0, "1"), (0, "2"), (0, "3"), (0, "4"), (0, "5")]) -- Read all items @@ -574,7 +579,7 @@ testGroup2 = ] dan <##> alice -- show last messages - alice ##> "/t #club 18" + alice ##> "/t #club 19" alice -- these strings are expected in any order because of sorting by time and rounding of time for sent <##? ( map (ConsoleString . ("#club " <> )) groupFeatureStrs @@ -651,7 +656,7 @@ testGroup2 = (dan "#club how is it going?" - dan <## "you are no longer a member of the group" + dan <## "bad chat command: not current member" dan ##> "/d #club" dan <## "#club: you deleted the group" dan <##> alice @@ -673,7 +678,7 @@ testGroup2 = (alice <# "#club cath> hey") (bob "#club how is it going?" - bob <## "you are no longer a member of the group" + bob <## "bad chat command: not current member" bob ##> "/d #club" bob <## "#club: you deleted the group" bob <##> alice @@ -698,7 +703,7 @@ testGroupDelete = bob ##> "/d #team" bob <## "#team: you deleted the group" cath ##> "#team hi" - cath <## "you are no longer a member of the group" + cath <## "bad chat command: not current member" cath ##> "/d #team" cath <## "#team: you deleted the group" alice <##> bob @@ -1611,7 +1616,7 @@ testGroupModerate = testGroupModerateOwn :: HasCallStack => TestParams -> IO () testGroupModerateOwn = - withTestOutput $ testChat2 aliceProfile bobProfile $ + testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob -- disableFullDeletion2 "team" alice bob @@ -2960,14 +2965,18 @@ testGLinkRejectBlockedName = DB.query_ db "SELECT count(1) FROM group_members" :: IO [[Int]] memCount `shouldBe` [[1]] + -- rejected member can't send messages to group + bob ##> "#team hello" + bob <## "bad chat command: not current member" + bob ##> ("/c " <> gLink) bob <## "group link: known group #team" bob <## "use #team to send messages" where cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Left GRRBlockedName)}} -testGLinkManualAcceptMember :: HasCallStack => TestParams -> IO () -testGLinkManualAcceptMember = +testGLinkApproveMember :: HasCallStack => TestParams -> IO () +testGLinkApproveMember = testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup2 "team" alice bob @@ -2984,26 +2993,30 @@ testGLinkManualAcceptMember = cath <## "#team: you joined the group, pending approval" ] - -- pending approval member doesn't see messages sent in group + -- pending member doesn't see messages sent in group alice #> "#team hi group" bob <# "#team alice> hi group" bob #> "#team hey" alice <# "#team bob> hey" - -- pending approval member and host can send messages to each other - alice ##> "/_send #1 @3 text send me proofs" - alice <# "#team send me proofs" - cath <# "#team alice> send me proofs" + -- pending member can't send messages to group + cath ##> "#team hello" + cath <## "bad chat command: not current member" - cath ##> "/_send #1 @1 text proofs" - cath <# "#team proofs" - alice <# "#team cath> proofs" + -- pending member and host can send messages to each other + alice ##> "/_send #1(_support:3) text send me proofs" + alice <# "#team (support: cath) send me proofs" + cath <# "#team (support) alice> send me proofs" + + cath ##> "/_send #1(_support) text proofs" + cath <# "#team (support) proofs" + alice <# "#team (support: cath) cath> proofs" -- accept member - alice ##> "/_accept member #1 3 member" + alice ##> "/accept member #team cath" concurrentlyN_ - [ alice <## "#team: cath joined the group", + [ alice <## "#team: cath accepted", cath <### [ "#team: you joined the group", WithTime "#team alice> hi group [>>]", @@ -3024,10 +3037,293 @@ testGLinkManualAcceptMember = cath #> "#team hi group" [alice, bob] *<# "#team cath> hi group" where - cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}} -testGLinkDeletePendingMember :: HasCallStack => TestParams -> IO () -testGLinkDeletePendingMember = +testGLinkReviewMember :: HasCallStack => TestParams -> IO () +testGLinkReviewMember = + testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $ + \alice bob cath dan eve -> do + createGroup4 "team" alice (bob, GRMember) (cath, GRModerator) (dan, GRModerator) + + alice ##> "/set admission review #team all" + alice <## "changed member admission rules" + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "changed member admission rules", + do + cath <## "alice updated group #team:" + cath <## "changed member admission rules", + do + dan <## "alice updated group #team:" + dan <## "changed member admission rules" + ] + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + eve ##> ("/c " <> gLink) + eve <## "connection request sent!" + alice <## "eve (Eve): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: eve connected and pending review", + eve + <### [ "#team: alice accepted you to the group, pending review", + "#team: joining the group...", + "#team: you joined the group, connecting to group moderators for admission to group", + "#team: member cath (Catherine) is connected", + "#team: member dan (Daniel) is connected" + ], + do + cath <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + cath <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member", + do + dan <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + dan <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member" + ] + + -- pending member doesn't see messages sent in group + alice #> "#team 1" + [bob, cath, dan] *<# "#team alice> 1" + + bob #> "#team 2" + [alice, cath, dan] *<# "#team bob> 2" + + cath #> "#team 3" + [alice, bob, dan] *<# "#team cath> 3" + + dan #> "#team 4" + [alice, bob, cath] *<# "#team dan> 4" + + (eve "#team hello" + eve <## "bad chat command: not current member" + + -- pending member and moderators can send messages to each other + alice ##> "/_send #1(_support:5) text 5" + alice <# "#team (support: eve) 5" + [cath, dan] *<# "#team (support: eve) alice> 5" + eve <# "#team (support) alice> 5" + + cath ##> "/_send #1(_support:5) text 6" + cath <# "#team (support: eve) 6" + [alice, dan] *<# "#team (support: eve) cath> 6" + eve <# "#team (support) cath> 6" + + dan ##> "/_send #1(_support:5) text 7" + dan <# "#team (support: eve) 7" + [alice, cath] *<# "#team (support: eve) dan> 7" + eve <# "#team (support) dan> 7" + + eve ##> "/_send #1(_support) text 8" + eve <# "#team (support) 8" + [alice, cath, dan] *<# "#team (support: eve) eve> 8" + + (bob "/_delete member chat #1 5" + alice <## "bad chat command: member is pending" + + -- accept member + dan ##> "/_accept member #1 5 member" + concurrentlyN_ + [ dan <## "#team: eve accepted", + alice <## "#team: dan accepted eve to the group (will introduce remaining members)", + cath <## "#team: dan accepted eve to the group", + eve + <### [ "#team: you joined the group", + WithTime "#team alice> 1 [>>]", + WithTime "#team bob> 2 [>>]", + WithTime "#team cath> 3 [>>]", + WithTime "#team dan> 4 [>>]", + "#team: member bob (Bob) is connected" + ], + do + bob <## "#team: alice added eve (Eve) to the group (connecting...)" + bob <## "#team: new member eve is connected" + ] + + alice #> "#team 9" + [bob, cath, dan, eve] *<# "#team alice> 9" + + bob #> "#team 10" + [alice, cath, dan, eve] *<# "#team bob> 10" + + cath #> "#team 11" + [alice, bob, dan, eve] *<# "#team cath> 11" + + dan #> "#team 12" + [alice, bob, cath, eve] *<# "#team dan> 12" + + eve #> "#team 13" + [alice, bob, cath, dan] *<# "#team eve> 13" + +testGLinkApproveThenReviewMember :: HasCallStack => TestParams -> IO () +testGLinkApproveThenReviewMember = + testChatCfg5 cfg aliceProfile bobProfile cathProfile danProfile eveProfile $ + \alice bob cath dan eve -> do + createGroup4 "team" alice (bob, GRMember) (cath, GRModerator) (dan, GRModerator) + + alice ##> "/set admission review #team all" + alice <## "changed member admission rules" + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "changed member admission rules", + do + cath <## "alice updated group #team:" + cath <## "changed member admission rules", + do + dan <## "alice updated group #team:" + dan <## "changed member admission rules" + ] + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + eve ##> ("/c " <> gLink) + eve <## "connection request sent!" + alice <## "eve (Eve): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: eve connected and pending approval, use /_accept member #1 5 to accept member", + do + eve <## "#team: joining the group..." + eve <## "#team: you joined the group, pending approval" + ] + + -- pending member doesn't see messages sent in group + alice #> "#team 1" + [bob, cath, dan] *<# "#team alice> 1" + + bob #> "#team 2" + [alice, cath, dan] *<# "#team bob> 2" + + cath #> "#team 3" + [alice, bob, dan] *<# "#team cath> 3" + + dan #> "#team 4" + [alice, bob, cath] *<# "#team dan> 4" + + (eve "#team hello" + eve <## "bad chat command: not current member" + + -- pending member and host can send messages to each other + alice ##> "/_send #1(_support:5) text 5" + alice <# "#team (support: eve) 5" + eve <# "#team (support) alice> 5" + + eve ##> "/_send #1(_support) text 6" + eve <# "#team (support) 6" + alice <# "#team (support: eve) eve> 6" + + (bob "/_accept member #1 5 member" + concurrentlyN_ + [ alice <## "#team: eve accepted and pending review (will introduce moderators)", + eve + <### [ "#team: alice accepted you to the group, pending review", + "#team: member cath (Catherine) is connected", + "#team: member dan (Daniel) is connected" + ], + do + cath <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + cath <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member", + do + dan <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + dan <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member" + ] + + -- pending member doesn't see messages sent in group + alice #> "#team 7" + [bob, cath, dan] *<# "#team alice> 7" + + bob #> "#team 8" + [alice, cath, dan] *<# "#team bob> 8" + + cath #> "#team 9" + [alice, bob, dan] *<# "#team cath> 9" + + dan #> "#team 10" + [alice, bob, cath] *<# "#team dan> 10" + + (eve "#team hello" + eve <## "bad chat command: not current member" + + -- pending member and moderators can send messages to each other + alice ##> "/_send #1(_support:5) text 11" + alice <# "#team (support: eve) 11" + [cath, dan] *<# "#team (support: eve) alice> 11" + eve <# "#team (support) alice> 11" + + cath ##> "/_send #1(_support:5) text 12" + cath <# "#team (support: eve) 12" + [alice, dan] *<# "#team (support: eve) cath> 12" + eve <# "#team (support) cath> 12" + + dan ##> "/_send #1(_support:5) text 13" + dan <# "#team (support: eve) 13" + [alice, cath] *<# "#team (support: eve) dan> 13" + eve <# "#team (support) dan> 13" + + eve ##> "/_send #1(_support) text 14" + eve <# "#team (support) 14" + [alice, cath, dan] *<# "#team (support: eve) eve> 14" + + (bob "/_accept member #1 5 member" + concurrentlyN_ + [ dan <## "#team: eve accepted", + alice <## "#team: dan accepted eve to the group (will introduce remaining members)", + cath <## "#team: dan accepted eve to the group", + eve + <### [ "#team: you joined the group", + WithTime "#team alice> 1 [>>]", + WithTime "#team bob> 2 [>>]", + WithTime "#team cath> 3 [>>]", + WithTime "#team dan> 4 [>>]", + WithTime "#team alice> 7 [>>]", + WithTime "#team bob> 8 [>>]", + WithTime "#team cath> 9 [>>]", + WithTime "#team dan> 10 [>>]", + "#team: member bob (Bob) is connected" + ], + do + bob <## "#team: alice added eve (Eve) to the group (connecting...)" + bob <## "#team: new member eve is connected" + ] + + alice #> "#team 15" + [bob, cath, dan, eve] *<# "#team alice> 15" + + bob #> "#team 16" + [alice, cath, dan, eve] *<# "#team bob> 16" + + cath #> "#team 17" + [alice, bob, dan, eve] *<# "#team cath> 17" + + dan #> "#team 18" + [alice, bob, cath, eve] *<# "#team dan> 18" + + eve #> "#team 19" + [alice, bob, cath, dan] *<# "#team eve> 19" + where + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}} + +testGLinkDeletePendingApprovalMember :: HasCallStack => TestParams -> IO () +testGLinkDeletePendingApprovalMember = testChatCfg3 cfg aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup2 "team" alice bob @@ -3049,7 +3345,7 @@ testGLinkDeletePendingMember = cath <## "#team: alice removed you from the group" cath <## "use /d #team to delete the group" where - cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPending, GRObserver))}} + cfg = testCfg {chatHooks = defaultChatHooks {acceptMember = Just (\_ _ _ -> pure $ Right (GAPendingApproval, GRObserver))}} testPlanGroupLinkKnown :: HasCallStack => TestParams -> IO () testPlanGroupLinkKnown = @@ -4174,8 +4470,8 @@ testRecreateMemberContactManyGroups = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob - createGroup2' "team" alice bob False - createGroup2' "club" alice bob False + createGroup2' "team" alice (bob, GRAdmin) False + createGroup2' "club" alice (bob, GRAdmin) False -- alice can message bob via team and via club alice ##> "@#team bob 1" @@ -4294,14 +4590,14 @@ testGroupMsgForwardReport = cath <## "#team: alice changed your role from admin to member" ] cath ##> "/report #team content hi there" - cath <# "#team > bob hi there" + cath <# "#team (support) > bob hi there" cath <## " report content" concurrentlyN_ [ do - alice <# "#team cath> > bob hi there" + alice <# "#team (support: cath) cath> > bob hi there" alice <## " report content", do - bob <# "#team cath!> > bob hi there [>>]" + bob <# "#team (support: cath) cath!> > bob hi there [>>]" bob <## " report content [>>]" ] @@ -4313,11 +4609,11 @@ testGroupMsgForwardReport = ] cath ##> "/report #team content hi there" - cath <# "#team > bob hi there" + cath <# "#team (support) > bob hi there" cath <## " report content" concurrentlyN_ [ do - alice <# "#team cath> > bob hi there" + alice <# "#team (support: cath) cath> > bob hi there" alice <## " report content", (bob do createGroup2 "team" alice bob - createGroup2' "club" alice bob False + createGroup2' "club" alice (bob, GRAdmin) False alice ##> "/d bob" alice <## "bob: contact is deleted" @@ -6326,14 +6622,14 @@ testGroupMemberReports = dan <# "#jokes cath> inappropriate joke" ] dan ##> "/report #jokes content inappropriate joke" - dan <# "#jokes > cath inappropriate joke" + dan <# "#jokes (support) > cath inappropriate joke" dan <## " report content" concurrentlyN_ [ do - alice <# "#jokes dan> > cath inappropriate joke" + alice <# "#jokes (support: dan) dan> > cath inappropriate joke" alice <## " report content", do - bob <# "#jokes dan> > cath inappropriate joke" + bob <# "#jokes (support: dan) dan> > cath inappropriate joke" bob <## " report content", (cath ok joke" ] dan ##> "/report #jokes content ok joke" - dan <# "#jokes > cath ok joke" + dan <# "#jokes (support) > cath ok joke" dan <## " report content" dan ##> "/report #jokes spam ok joke" - dan <# "#jokes > cath ok joke" + dan <# "#jokes (support) > cath ok joke" dan <## " report spam" concurrentlyN_ [ do - alice <# "#jokes dan> > cath ok joke" + alice <# "#jokes (support: dan) dan> > cath ok joke" alice <## " report content" - alice <# "#jokes dan> > cath ok joke" + alice <# "#jokes (support: dan) dan> > cath ok joke" alice <## " report spam", do - bob <# "#jokes dan> > cath ok joke" + bob <# "#jokes (support: dan) dan> > cath ok joke" bob <## " report content" - bob <# "#jokes dan> > cath ok joke" + bob <# "#jokes (support: dan) dan> > cath ok joke" bob <## " report spam", (cath ok joke 2" ] dan ##> "/report #jokes content ok joke 2" - dan <# "#jokes > cath ok joke 2" + dan <# "#jokes (support) > cath ok joke 2" dan <## " report content" concurrentlyN_ [ do - alice <# "#jokes dan> > cath ok joke 2" + alice <# "#jokes (support: dan) dan> > cath ok joke 2" alice <## " report content", do - bob <# "#jokes dan> > cath ok joke 2" + bob <# "#jokes (support: dan) dan> > cath ok joke 2" bob <## " report content", (cath getTermLine alice alice ##> ("/_delete reports #1 " <> show i <> " broadcast") alice <## "message marked deleted by you" - bob <# "#jokes dan> [marked deleted by alice] report content" + bob <# "#jokes (support: dan) dan> [marked deleted by alice] report content" alice #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by you]")]) bob #$> ("/_get chat #1 content=report count=100", chat, [(0, "report content [marked deleted by alice]")]) dan #$> ("/_get chat #1 content=report count=100", chat, [(1, "report content")]) @@ -6605,18 +6901,18 @@ testUpdatedMentionNames = do test (mm [("alice", Just "alice"), ("cath", Just "alice")]) "hello @alice @cath" `shouldBe` "hello @alice @alice_1" where - test mentions t = - let (mc', _, _) = updatedMentionNames (MCText t) (parseMaybeMarkdownList t) mentions + test mentionsMap t = + let (mc', _, _) = updatedMentionNames (MCText t) (parseMaybeMarkdownList t) mentionsMap in msgContentText mc' mm = M.fromList . map (second mentionedMember) mentionedMember name_ = CIMention {memberId = MemberId "abcd", memberRef = ciMentionMember <$> name_} where ciMentionMember name = CIMentionMember {groupMemberId = 1, displayName = name, localAlias = Nothing, memberRole = GRMember} -testGroupDirectMessages :: HasCallStack => TestParams -> IO () -testGroupDirectMessages = +testScopedSupportSingleModerator :: HasCallStack => TestParams -> IO () +testScopedSupportSingleModerator = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - createGroup3 "team" alice bob cath + createGroup3' "team" alice (bob, GRMember) (cath, GRMember) alice #> "#team 1" [bob, cath] *<# "#team alice> 1" @@ -6624,25 +6920,266 @@ testGroupDirectMessages = bob #> "#team 2" [alice, cath] *<# "#team bob> 2" - void $ withCCTransaction alice $ \db -> - DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 2" + alice ##> "/_send #1(_support:2) text 3" + alice <# "#team (support: bob) 3" + bob <# "#team (support) alice> 3" - alice ##> "/_send #1 @2 text 3" - alice <# "#team 3" - bob <# "#team alice> 3" + bob ##> "/_send #1(_support) text 4" + bob <# "#team (support) 4" + alice <# "#team (support: bob) bob> 4" - void $ withCCTransaction bob $ \db -> - DB.execute_ db "UPDATE group_members SET member_status='pending_approval' WHERE group_member_id = 1" + cath ##> "/_send #1(_support:3) text 5" + cath <## "#team: you have insufficient permissions for this action, the required role is moderator" - bob ##> "/_send #1 @1 text 4" - bob <# "#team 4" - alice <# "#team bob> 4" + alice ##> "/_delete member chat #1 2" + alice <## "#team: bob support chat deleted" - -- GSMemPendingApproval members don't receive messages sent to group. - -- Though in test we got here synthetically, in reality this status - -- means they are not yet part of group (not memberCurrent). - alice #> "#team 5" - cath <# "#team alice> 5" +testScopedSupportManyModerators :: HasCallStack => TestParams -> IO () +testScopedSupportManyModerators = + testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + createGroup4 "team" alice (bob, GRMember) (cath, GRMember) (dan, GRModerator) - bob #> "#team 6" - cath <# "#team bob> 6" + threadDelay 1000000 + + alice #> "#team 1" + [bob, cath, dan] *<# "#team alice> 1" + + threadDelay 1000000 + + bob #> "#team 2" + [alice, cath, dan] *<# "#team bob> 2" + + threadDelay 1000000 + + alice ##> "/_send #1(_support:2) text 3" + alice <# "#team (support: bob) 3" + bob <# "#team (support) alice> 3" + dan <# "#team (support: bob) alice> 3" + + threadDelay 1000000 + + bob ##> "/_send #1(_support) text 4" + bob <# "#team (support) 4" + [alice, dan] *<# "#team (support: bob) bob> 4" + + threadDelay 1000000 + + dan ##> "/_send #1(_support:3) text 5" + dan <# "#team (support: bob) 5" + alice <# "#team (support: bob) dan> 5" + bob <# "#team (support) dan> 5" + + alice @@@ [("#team","2"), ("@dan","sent invitation to join group team as moderator"), ("@cath","sent invitation to join group team as member"), ("@bob","sent invitation to join group team as member")] + bob @@@ [("#team","2"), ("@alice","received invitation to join group team as member")] + dan @@@ [("#team","2"), ("@alice","received invitation to join group team as moderator")] + cath @@@ [("#team","2"), ("@alice","received invitation to join group team as member")] + + alice #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (1, "1"), (0, "2")]) + alice #$> ("/_get chat #1(_support:2) count=100", chat, [(1, "3"), (0, "4"), (0, "5")]) + bob #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (0, "1"), (1, "2")]) + bob #$> ("/_get chat #1(_support) count=100", chat, [(0, "3"), (1, "4"), (0, "5")]) + dan #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (0, "1"), (0, "2")]) + dan #$> ("/_get chat #1(_support:3) count=100", chat, [(0, "3"), (0, "4"), (1, "5")]) + cath #$> ("/_get chat #1 count=3", chat, [(0, "connected"), (0, "1"), (0, "2")]) + cath #$> ("/_get chat #1(_support:3) count=100", chat, []) + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 0, require attention: 0, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 0, require attention: 0, mentions: 0" + cath ##> "/member support chats #team" + cath <## "bob (Bob) (id 3): unread: 0, require attention: 0, mentions: 0" + +testSupportCLISendCommand :: HasCallStack => TestParams -> IO () +testSupportCLISendCommand = + testChat2 aliceProfile bobProfile $ \alice bob -> do + createGroup2' "team" alice (bob, GRObserver) True + + alice #> "#team 1" + bob <# "#team alice> 1" + + bob ##> "#team 2" + bob <## "#team: you don't have permission to send messages" + (alice "#team (support: bob) 3" + bob <# "#team (support) alice> 3" + + bob #> "#team (support) 4" + alice <# "#team (support: bob) bob> 4" + + bob ##> "#team (support 4" + bob <## "bad chat command: Failed reading: empty" + +testScopedSupportUnreadStatsOnRead :: HasCallStack => TestParams -> IO () +testScopedSupportUnreadStatsOnRead = + testChatOpts4 opts aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + createGroup4 "team" alice (bob, GRMember) (cath, GRMember) (dan, GRModerator) + + alice #> "#team 1" + [bob, cath, dan] *<# "#team alice> 1" + + bob #> "#team 2" + [alice, cath, dan] *<# "#team bob> 2" + + alice ##> "/_send #1(_support:2) text 3" + alice <# "#team (support: bob) 3" + bob <# "#team (support) alice> 3" + dan <# "#team (support: bob) alice> 3" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 1, require attention: 0, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 1, require attention: 0, mentions: 0" + + bob ##> "/_send #1(_support) text 4" + bob <# "#team (support) 4" + [alice, dan] *<# "#team (support: bob) bob> 4" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 1, require attention: 1, mentions: 0" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 1, require attention: 0, mentions: 0" + + threadDelay 1000000 + + dan ##> "/_send #1(_support:3) text 5" + dan <# "#team (support: bob) 5" + alice <# "#team (support: bob) dan> 5" + bob <# "#team (support) dan> 5" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 2, require attention: 0, mentions: 0" + -- In test "answering" doesn't reset unanswered, but in UI items would be marked read on opening chat + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 2, require attention: 0, mentions: 0" + + threadDelay 1000000 + + dan ##> "/_send #1(_support:3) json [{\"msgContent\": {\"type\": \"text\", \"text\": \"@alice 6\"}, \"mentions\": {\"alice\": 1}}]" + dan <# "#team (support: bob) @alice 6" + alice <# "#team (support: bob) dan!> @alice 6" + bob <# "#team (support) dan> @alice 6" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 3, require attention: 0, mentions: 1" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 2, require attention: 1, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 3, require attention: 0, mentions: 0" + + aliceMentionedByDanItemId <- lastItemId alice + + threadDelay 1000000 + + bob ##> "/_send #1(_support) json [{\"msgContent\": {\"type\": \"text\", \"text\": \"@alice 7\"}, \"mentions\": {\"alice\": 1}}]" + bob <# "#team (support) @alice 7" + alice <# "#team (support: bob) bob!> @alice 7" + dan <# "#team (support: bob) bob> @alice 7" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 4, require attention: 1, mentions: 2" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 3, require attention: 2, mentions: 0" + bob ##> "/member support chats #team" + bob <## "support: unread: 3, require attention: 0, mentions: 0" + + aliceMentionedByBobItemId <- lastItemId alice + + bob ##> "/_send #1(_support) json [{\"msgContent\": {\"type\": \"text\", \"text\": \"@dan 8\"}, \"mentions\": {\"dan\": 4}}]" + bob <# "#team (support) @dan 8" + alice <# "#team (support: bob) bob> @dan 8" + dan <# "#team (support: bob) bob!> @dan 8" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 5, require attention: 2, mentions: 2" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 4, require attention: 3, mentions: 1" + bob ##> "/member support chats #team" + bob <## "support: unread: 3, require attention: 0, mentions: 0" + + alice #$> ("/_read chat items #1(_support:2) " <> aliceMentionedByDanItemId, id, "items read for chat") + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 4, require attention: 2, mentions: 1" + + alice #$> ("/_read chat items #1(_support:2) " <> aliceMentionedByBobItemId, id, "items read for chat") + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 3, require attention: 1, mentions: 0" + + threadDelay 1000000 + + dan ##> "/_send #1(_support:3) json [{\"msgContent\": {\"type\": \"text\", \"text\": \"@bob 9\"}, \"mentions\": {\"bob\": 3}}]" + dan <# "#team (support: bob) @bob 9" + alice <# "#team (support: bob) dan> @bob 9" + bob <# "#team (support) dan!> @bob 9" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 4, require attention: 0, mentions: 0" + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 4, require attention: 3, mentions: 1" + bob ##> "/member support chats #team" + bob <## "support: unread: 4, require attention: 0, mentions: 1" + + alice #$> ("/_read chat #1(_support:2)", id, "ok") + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + + dan #$> ("/_read chat #1(_support:3)", id, "ok") + + dan ##> "/member support chats #team" + dan <## "bob (Bob) (id 3): unread: 0, require attention: 0, mentions: 0" + + bob #$> ("/_read chat #1(_support)", id, "ok") + + bob ##> "/member support chats #team" + bob <## "support: unread: 0, require attention: 0, mentions: 0" + + cath ##> "/member support chats #team" + cath TestParams -> IO () +testScopedSupportUnreadStatsOnDelete = + testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + + alice ##> "/set delete #team on" + alice <## "updated group preferences:" + alice <## "Full deletion: on" + bob <## "alice updated group #team:" + bob <## "updated group preferences:" + bob <## "Full deletion: on" + + bob #> "#team (support) 1" + alice <# "#team (support: bob) bob> 1" + + msgIdBob <- lastItemId bob + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 1, require attention: 1, mentions: 0" + + bob #$> ("/_delete item #1(_support) " <> msgIdBob <> " broadcast", id, "message deleted") + alice <# "#team (support: bob) bob> [deleted] 1" + + alice ##> "/member support chats #team" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + where + opts = + testOpts + { markRead = False + } diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index adff745200..fe933d5b98 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -746,7 +746,7 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice (biz <# "#bob bob_1> hey there") testBusinessUpdateProfiles :: HasCallStack => TestParams -> IO () -testBusinessUpdateProfiles = withTestOutput $ testChat4 businessProfile aliceProfile bobProfile cathProfile $ +testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile cathProfile $ \biz alice bob cath -> do biz ##> "/ad" cLink <- getContactLink biz True @@ -1630,7 +1630,7 @@ testJoinGroupIncognito = (cath "#secret_club hello" - cath <## "you are no longer a member of the group" + cath <## "bad chat command: not current member" testCantInviteContactIncognito :: HasCallStack => TestParams -> IO () testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ @@ -1827,7 +1827,7 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite alice ##> "/connect" - _ <- getInvitation alice + inv <- getInvitation alice -- Create new user and go back to original user alice ##> "/create user alisa" showActiveUser alice "alisa" @@ -1837,18 +1837,12 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa, new link:" - alice <## "" - _ <- getTermLine alice - alice <## "" + alice <## "connection 1 changed from user alice to user alisa" alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to other user alice ##> "/_set conn user :1 3" - alice <## "connection 1 changed from user alisa to user alisa2, new link:" - alice <## "" - inv <- getTermLine alice - alice <## "" + alice <## "connection 1 changed from user alisa to user alisa2" alice ##> "/user alisa2" showActiveUser alice "alisa2" -- Connect @@ -1857,14 +1851,13 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alisa2: contact is connected") - alice <##> bob testChangePCCUserFromIncognito :: HasCallStack => TestParams -> IO () testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite and set as incognito alice ##> "/connect" - _ <- getInvitation alice + inv <- getInvitation alice alice ##> "/_set incognito :1 on" alice <## "connection 1 changed to incognito" -- Create new user and go back to original user @@ -1874,19 +1867,13 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa, new link:" - alice <## "" - _ <- getTermLine alice - alice <## "" + alice <## "connection 1 changed from user alice to user alisa" alice `hasContactProfiles` ["alice"] alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to initial user alice ##> "/_set conn user :1 1" - alice <## "connection 1 changed from user alisa to user alice, new link:" - alice <## "" - inv <- getTermLine alice - alice <## "" + alice <## "connection 1 changed from user alisa to user alice" alice ##> "/user alice" showActiveUser alice "alice (Alice)" -- Connect @@ -1895,14 +1882,13 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") - alice <##> bob testChangePCCUserAndThenIncognito :: HasCallStack => TestParams -> IO () testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- Create a new invite and set as incognito alice ##> "/connect" - _ <- getInvitation alice + inv <- getInvitation alice -- Create new user and go back to original user alice ##> "/create user alisa" showActiveUser alice "alisa" @@ -1910,10 +1896,7 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ showActiveUser alice "alice (Alice)" -- Change connection to newly created user alice ##> "/_set conn user :1 2" - alice <## "connection 1 changed from user alice to user alisa, new link:" - alice <## "" - inv <- getTermLine alice - alice <## "" + alice <## "connection 1 changed from user alice to user alisa" alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection to incognito and make sure it's attached to the newly created user profile @@ -1928,10 +1911,6 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito) alice <## ("use /i bob to print out this incognito profile again") ] - alice ?#> "@bob hi" - bob <# (alisaIncognito <> "> hi") - bob #> ("@" <> alisaIncognito <> " hey") - alice ?<# "bob> hey" testChangePCCUserDiffSrv :: HasCallStack => TestParams -> IO () testChangePCCUserDiffSrv ps = do @@ -1972,7 +1951,6 @@ testChangePCCUserDiffSrv ps = do concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alisa: contact is connected") - alice <##> bob where serverCfg' = smpServerCfg diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 3ae3f61ca7..38c8c308c8 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -71,6 +71,9 @@ cathProfile = Profile {displayName = "cath", fullName = "Catherine", image = Not danProfile :: Profile danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} +eveProfile :: Profile +eveProfile = Profile {displayName = "eve", fullName = "Eve", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} + businessProfile :: Profile businessProfile = Profile {displayName = "biz", fullName = "Biz Inc", image = Nothing, contactLink = Nothing, preferences = defaultPrefs} @@ -654,16 +657,16 @@ showName (TestCC ChatController {currentUser} _ _ _ _ _) = do pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName createGroup2 :: HasCallStack => String -> TestCC -> TestCC -> IO () -createGroup2 gName cc1 cc2 = createGroup2' gName cc1 cc2 True +createGroup2 gName cc1 cc2 = createGroup2' gName cc1 (cc2, GRAdmin) True -createGroup2' :: HasCallStack => String -> TestCC -> TestCC -> Bool -> IO () -createGroup2' gName cc1 cc2 doConnectUsers = do +createGroup2' :: HasCallStack => String -> TestCC -> (TestCC, GroupMemberRole) -> Bool -> IO () +createGroup2' gName cc1 (cc2, role2) doConnectUsers = do when doConnectUsers $ connectUsers cc1 cc2 name2 <- userName cc2 cc1 ##> ("/g " <> gName) cc1 <## ("group #" <> gName <> " is created") cc1 <## ("to add members use /a " <> gName <> " or /create link #" <> gName) - addMember gName cc1 cc2 GRAdmin + addMember gName cc1 cc2 role2 cc2 ##> ("/j " <> gName) concurrently_ (cc1 <## ("#" <> gName <> ": " <> name2 <> " joined the group")) @@ -681,13 +684,17 @@ disableFullDeletion2 gName cc1 cc2 = do createGroup3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO () createGroup3 gName cc1 cc2 cc3 = do - createGroup2 gName cc1 cc2 + createGroup3' gName cc1 (cc2, GRAdmin) (cc3, GRAdmin) + +createGroup3' :: HasCallStack => String -> TestCC -> (TestCC, GroupMemberRole) -> (TestCC, GroupMemberRole) -> IO () +createGroup3' gName cc1 (cc2, role2) (cc3, role3) = do + createGroup2' gName cc1 (cc2, role2) True connectUsers cc1 cc3 name1 <- userName cc1 name3 <- userName cc3 sName2 <- showName cc2 sName3 <- showName cc3 - addMember gName cc1 cc3 GRAdmin + addMember gName cc1 cc3 role3 cc3 ##> ("/j " <> gName) concurrentlyN_ [ cc1 <## ("#" <> gName <> ": " <> name3 <> " joined the group"), @@ -699,6 +706,31 @@ createGroup3 gName cc1 cc2 cc3 = do cc2 <## ("#" <> gName <> ": new member " <> name3 <> " is connected") ] +createGroup4 :: HasCallStack => String -> TestCC -> (TestCC, GroupMemberRole) -> (TestCC, GroupMemberRole) -> (TestCC, GroupMemberRole) -> IO () +createGroup4 gName cc1 (cc2, role2) (cc3, role3) (cc4, role4) = do + createGroup3' gName cc1 (cc2, role2) (cc3, role3) + connectUsers cc1 cc4 + name1 <- userName cc1 + name4 <- userName cc4 + sName2 <- showName cc2 + sName3 <- showName cc3 + sName4 <- showName cc4 + addMember gName cc1 cc4 role4 + cc4 ##> ("/j " <> gName) + concurrentlyN_ + [ cc1 <## "#team: dan joined the group", + do + cc4 <## ("#" <> gName <> ": you joined the group") + cc4 <## ("#" <> gName <> ": member " <> sName2 <> " is connected") + cc4 <## ("#" <> gName <> ": member " <> sName3 <> " is connected"), + do + cc2 <## ("#" <> gName <> ": " <> name1 <> " added " <> sName4 <> " to the group (connecting...)") + cc2 <## ("#" <> gName <> ": new member " <> name4 <> " is connected"), + do + cc3 <## ("#" <> gName <> ": " <> name1 <> " added " <> sName4 <> " to the group (connecting...)") + cc3 <## ("#" <> gName <> ": new member " <> name4 <> " is connected") + ] + disableFullDeletion3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO () disableFullDeletion3 gName cc1 cc2 cc3 = do disableFullDeletion2 gName cc1 cc2 @@ -710,7 +742,7 @@ disableFullDeletion3 gName cc1 cc2 cc3 = do create2Groups3 :: HasCallStack => String -> String -> TestCC -> TestCC -> TestCC -> IO () create2Groups3 gName1 gName2 cc1 cc2 cc3 = do createGroup3 gName1 cc1 cc2 cc3 - createGroup2' gName2 cc1 cc2 False + createGroup2' gName2 cc1 (cc2, GRAdmin) False name1 <- userName cc1 name3 <- userName cc3 addMember gName2 cc1 cc3 GRAdmin diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index dbfde6a03d..656f0ae0e2 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -23,6 +23,7 @@ import Simplex.Chat.Operators.Presets import Simplex.Chat.Types import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Protocol import Test.Hspec diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 1d37a52459..61e75d116b 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -116,10 +116,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)) it "x.msg.new simple text - timed message TTL" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing)) + #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing Nothing)) it "x.msg.new simple text - live message" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" - #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True))) + #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True) Nothing)) it "x.msg.new simple link" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" #==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing)) @@ -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-14\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-15\",\"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\"}}}}" @@ -146,22 +146,22 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing (Just 3600) Nothing))) + (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing (Just 3600) Nothing Nothing))) it "x.msg.new quote - live message" $ "{\"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\"}},\"live\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") - (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing Nothing (Just True)))) + (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") [] Nothing Nothing (Just True) Nothing))) it "x.msg.new forward" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing)) it "x.msg.new forward - timed message TTL" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing)) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing (Just 3600) Nothing Nothing)) it "x.msg.new forward - live message" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" - ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True))) + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") [] Nothing Nothing (Just True) Nothing)) it "x.msg.new simple text with file" $ "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) @@ -193,7 +193,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) it "x.msg.update" $ "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing + #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing Nothing it "x.msg.del" $ "{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing @@ -247,15 +247,15 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XGrpAcpt (MemberId "\1\2\3\4") it "x.grp.mem.new" $ "{\"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} + #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing 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-14\",\"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} + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-15\",\"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} Nothing 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-14\",\"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-15\",\"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\"}}}}}}" @@ -270,7 +270,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-4%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-4%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-4%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-14\",\"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-4%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-15\",\"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\"}}}}}" @@ -292,10 +292,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ==# XGrpDel it "x.grp.direct.inv" $ "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - #==# XGrpDirectInv testConnReq (Just $ MCText "hello") + #==# XGrpDirectInv testConnReq (Just $ MCText "hello") Nothing it "x.grp.direct.inv without content" $ "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" - #==# XGrpDirectInv testConnReq Nothing + #==# XGrpDirectInv testConnReq Nothing Nothing -- it "x.grp.msg.forward" -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" -- #==# XGrpMsgForward diff --git a/website/langs/cs.json b/website/langs/cs.json index 47ab2c99d0..146626551f 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -2,7 +2,7 @@ "simplex-private-card-10-point-2": "Umožňuje doručovat zprávy bez identifikátoru uživatelských profilů, což poskytuje lepší soukromí metadat než alternativy.", "simplex-unique-4-overlay-1-title": "Plně decentralizované — uživatelé vlastní síť SimpleX", "hero-overlay-card-1-p-6": "Přečtěte si více v SimpleX whitepaper.", - "hero-overlay-card-1-p-2": "Pro doručování zpráv používá SimpleX namísto ID uživatelů používaných všemi ostatními platformami dočasné anonymní párové identifikátory front zpráv, oddělené pro každé z vašich připojení — neexistují žádné dlouhodobé identifikátory.", + "hero-overlay-card-1-p-2": "K doručování zpráv používá SimpleX namísto ID uživatelů používaných všemi ostatními sítěmi, dočasné anonymní párové identifikátory front zpráv, oddělené pro každé z vašich připojení — neexistují žádné dlouhodobé identifikátory.", "hero-overlay-card-1-p-3": "Definujete, které servery se mají používat k přijímání zpráv, vašich kontaktů — servery, které používáte k odesílání zpráv. Každá konverzace bude pravděpodobně používat dva různé servery.", "hero-overlay-card-2-p-3": "I v těch nejsoukromějších aplikacích, které používají služby Tor v3, pokud mluvíte se dvěma různými kontakty prostřednictvím stejného profilu, může být prokázáno, že jsou spojeni se stejnou osobou.", "simplex-network-overlay-card-1-p-1": "P2P protokoly a aplikace pro zasílání zpráv mají různé problémy, které je činí méně spolehlivými než SimpleX, složitějšími na analýzu a zranitelnými vůči několika typům útoků.", @@ -19,7 +19,7 @@ "simplex-explained-tab-2-text": "2. Jak to funguje", "simplex-explained-tab-3-text": "3. Co vidí servery", "simplex-explained-tab-1-p-2": "Jak může pracovat s jednosměrnými frontami a bez identifikátorů profilu uživatele?", - "simplex-explained-tab-2-p-2": "Servery předávají zprávy pouze jedním směrem, aniž by měly úplný obraz o konverzaci nebo spojení uživatele.", + "simplex-explained-tab-2-p-2": "Servery předávají zprávy jednosměrně, aniž by měly úplný obraz o konverzacích nebo spojeních uživatele.", "simplex-explained-tab-3-p-1": "Servery mají samostatné anonymní přihlašovací údaje pro každou frontu a nevědí, ke kterým uživatelům patří.", "chat-bot-example": "Příklad chat bota", "smp-protocol": "SMP protokol", @@ -57,7 +57,7 @@ "simplex-private-card-4-point-1": "Pro ochranu vaší IP adresy můžete přistupovat k serverům přes Tor nebo jinou transportní překryvnou síť.", "simplex-private-6-title": "Out-of-band
výměna klíčů", "simplex-private-card-5-point-2": "Díky tomu vypadají zprávy různých velikostí pro servery a síťové pozorovatele stejně.", - "simplex-private-card-6-point-1": "Mnoho komunikačních platforem je zranitelných vůči útokům MITM ze strany serverů nebo poskytovatelů sítí.", + "simplex-private-card-6-point-1": "Mnoho komunikačních sítí je zranitelných vůči útokům MITM ze strany serverů nebo poskytovatelů sítí.", "simplex-private-card-7-point-1": "Aby byla zaručena integrita, jsou zprávy postupně číslovány a obsahují kontrolní součet předchozí zprávy.", "simplex-private-card-7-point-2": "Pokud je jakákoli zpráva přidána, odstraněna nebo změněna, příjemce bude upozorněn.", "simplex-private-card-9-point-1": "Každá fronta zpráv předává zprávy v jednom směru s různými adresami pro odesílání a přijímání.", @@ -86,27 +86,27 @@ "hero-overlay-card-2-p-4": "SimpleX chrání před těmito útoky tím, že nemá žádné ID uživatele ve svém designu. A pokud používáte režim Inkognito, budete mít pro každý kontakt jiné zobrazované jméno, čímž se vyhnete sdílení dat mezi nimi.", "simplex-network-overlay-card-1-li-2": "SimpleX, na rozdíl od většiny P2P sítí, nemá žádné globální uživatelské identifikátory jakéhokoli druhu, dokonce ani dočasné, a používá pouze dočasné párové identifikátory, které poskytují lepší anonymitu a ochranu metadat.", "simplex-network-overlay-card-1-li-3": "P2P neřeší problém MITM útoku a většina existujících implementací nepoužívá out-of-band zprávy pro počáteční výměnu klíčů. SimpleX používá out-of-band zprávy nebo v některých případech již existující zabezpečená a důvěryhodná připojení pro počáteční výměnu klíčů.", - "simplex-network-overlay-card-1-li-4": "Implementace P2P mohou být některými poskytovateli internetu blokovány (například BitTorrent). SimpleX je transportně nezávislý - může pracovat přes standardní webové protokoly, např. přes WebSockets.", + "simplex-network-overlay-card-1-li-4": "Implementace P2P mohou být některými poskytovateli internetu blokovány (například BitTorrent). SimpleX je transportně nezávislý — může pracovat přes standardní webové protokoly, např. WebSockety.", "privacy-matters-overlay-card-1-p-1": "Mnoho velkých společností používá informace o tom, s kým jste ve spojení, k odhadu vašich příjmů, prodeji produktů, které ve skutečnosti nepotřebujete, a ke stanovení cen.", "privacy-matters-overlay-card-1-p-3": "Některé finanční a pojišťovací společnosti používají sociální grafy k určení úrokových sazeb a pojistného. Často přiměje lidi s nižšími příjmy platit více —, je to známé jako „prémie za chudobu“.", "privacy-matters-overlay-card-2-p-1": "Není to tak dávno, co jsme pozorovali, jak velké volby zmanipulovala renomovaná poradenská společnost, která používala naše sociální grafy ke zkreslení našeho pohledu na skutečný svět a manipulovala s našimi hlasy.", - "privacy-matters-overlay-card-2-p-3": "SimpleX je první platforma, která nemá žádné uživatelské identifikátory záměrně, a tímto způsobem chrání váš graf připojení lépe než jakákoli známá alternativa.", + "privacy-matters-overlay-card-2-p-3": "SimpleX je první síť, která nemá žádné uživatelské identifikátory záměrně, a tímto způsobem chrání váš graf připojení lépe než jakákoli známá alternativa.", "privacy-matters-overlay-card-3-p-1": "Každý by se měl starat o soukromí a bezpečnost své komunikace — neškodné rozhovory vás mohou vystavit nebezpečí, i když nemáte co skrývat.", "privacy-matters-overlay-card-3-p-3": "Obyčejní lidé jsou zatčeni za to, co sdílejí online, dokonce i prostřednictvím svých „anonymních“ účtů, i v demokratických zemích.", "privacy-matters-overlay-card-3-p-4": "Nestačí používat end-to-end šifrovaný messenger, všichni bychom měli používat messengery, které chrání soukromí našich osobních sítí — s kým jsme spojeni.", - "simplex-unique-overlay-card-1-p-3": "Tento design chrání soukromí toho, s kým komunikujete, a skrývá ho před servery platformy SimpleX a před jakýmikoli pozorovateli. Chcete-li skrýt svou IP adresu před servery, můžete se připojit k serverům SimpleX přes Tor.", - "simplex-unique-overlay-card-2-p-1": "Protože na platformě SimpleX nemáte žádný identifikátor, nikdo vás nemůže kontaktovat pokud nesdílíte jednorázovou nebo dočasnou uživatelskou adresu, jako QR kód nebo odkaz.", + "simplex-unique-overlay-card-1-p-3": "Tento design chrání soukromí těch, s kým komunikujete, skrývá ho před servery sítě SimpleX a před jakýmikoli pozorovateli. Pro skrytí své IP adresy před servery, můžete se připojit k serverům SimpleX přes Tor.", + "simplex-unique-overlay-card-2-p-1": "Protože na síti SimpleX nemáte žádný identifikátor, nikdo vás nemůže kontaktovat pokud nenasdílíte jednorázovou nebo dočasnou uživatelskou adresu, jako QR kód nebo odkaz.", "simplex-unique-overlay-card-2-p-2": "Dokonce i s volitelnou uživatelskou adresou, kterou lze použít k zasílání spamových požadavků na kontakt, ji však můžete změnit nebo úplně odstranit, aniž byste ztratili jakékoli spojení.", "simplex-unique-overlay-card-3-p-2": "End-to-end šifrované zprávy jsou dočasně uchovávány na přenosových serverech SimpleX, dokud nejsou přijaty, poté jsou trvale odstraněny.", "simplex-unique-overlay-card-3-p-3": "Na rozdíl od serverů federovaných sítí (e-mail, XMPP nebo Matrix) servery SimpleX neukládají uživatelské účty, pouze předávají zprávy, čímž chrání soukromí obou stran.", - "simplex-unique-overlay-card-4-p-1": "SimpleX můžete použít se svými vlastními servery a přesto komunikovat s lidmi, kteří používají námi poskytované předkonfigurované servery.", - "simplex-unique-overlay-card-4-p-3": "Pokud uvažujete o vývoji pro platformu SimpleX, například chat bota pro uživatele aplikace SimpleX nebo o integraci knihovny SimpleX Chat do vašich mobilních aplikací, kontaktujte pro jakoukoli radu a podporu.", - "simplex-unique-card-1-p-1": "SimpleX chrání soukromí vašeho profilu, kontaktů a metadat a skrývá je před servery platformy SimpleX a jakýmikoli pozorovateli.", - "simplex-unique-card-1-p-2": "Na rozdíl od jakékoli jiné existující platformy pro zasílání zpráv nemá SimpleX žádné identifikátory přiřazené uživatelům — ani náhodná čísla.", + "simplex-unique-overlay-card-4-p-1": "Můžete použít SimpleX se svými vlastními servery a přesto komunikovat s lidmi, kteří používají přednastavené servery v aplikacích.", + "simplex-unique-overlay-card-4-p-3": "Pokud uvažujete o vývoji pro SimpleX síť, například chat bot pro uživatele aplikace SimpleX nebo integraci knihovny SimpleX chat do Vasí mobilní aplikace, prosím buďte ve spojení pro jakoukoli radu a podporu.", + "simplex-unique-card-1-p-1": "SimpleX chrání soukromí vašeho profilu, kontaktů a metadat a skrývá je před servery SimpleX sítě a jakýmikoli pozorovateli.", + "simplex-unique-card-1-p-2": "Na rozdíl od jakékoli jiné existující síti pro zasílání zpráv nemá SimpleX žádné identifikátory přiřazené uživatelům — ani náhodná čísla.", "simplex-unique-card-3-p-1": "SimpleX Chat ukládá všechna uživatelská data pouze na klientských zařízeních pomocí přenosného šifrovaného databázového formátu, který lze exportovat a přenést na jakékoli podporované zařízení.", "simplex-unique-card-3-p-2": "End-to-end šifrované zprávy jsou dočasně uchovávány na přenosových serverech SimpleX, dokud nejsou přijaty, poté jsou trvale odstraněny.", "join": "Připojit", - "simplex-unique-card-4-p-1": "Síť SimpleX je plně decentralizovaná a nezávislá na jakékoli kryptoměně nebo jakékoli jiné platformě kromě internetu.", + "simplex-unique-card-4-p-1": "Síť SimpleX je plně decentralizovaná a nezávislá na jakékoli kryptoměně nebo jakékoli jiné síti než internetu.", "simplex-unique-card-4-p-2": "SimpleX můžete použít s vašimi vlastními servery nebo s námi poskytovanými servery — a přesto se připojit k libovolnému uživateli.", "we-invite-you-to-join-the-conversation": "Zveme vás k účasti na konverzaci", "join-the-REDDIT-community": "Připojte se ke komunitě REDDIT", @@ -134,7 +134,7 @@ "use-this-command": "Použijte tento příkaz:", "see-simplex-chat": "Viz SimpleX Chat", "github-repository": "GitHub repozitář", - "the-instructions--source-code": "návod, jak jej stáhnout nebo zkompilovat ze zdrojového kódu.", + "the-instructions--source-code": "Pro návod, jak stáhnout nebo zkompilovat ze zdrojového kódu.", "simplex-chat-for-the-terminal": "SimpleX Chat pro terminál", "copy-the-command-below-text": "Zkopírujte níže uvedený příkaz a použijte jej v chatu:", "privacy-matters-section-header": "Proč na soukromí záleží", @@ -171,23 +171,23 @@ "comparison-section-list-point-3": "Veřejný klíč nebo jiné globálně jedinečné ID", "comparison-section-list-point-4": "Pokud jsou servery operátora kompromitovány. Ověřte bezpečnostní kód v Signálu a nějaké jiné aplikaci ke zmírnění", "comparison-section-list-point-5": "Nechrání soukromí metadat uživatelů", - "comparison-section-list-point-6": "Zatímco P2P jsou distribuovány, nejsou federované – fungují jako jediná síť", + "comparison-section-list-point-6": "Zatímco P2P jsou distribuovány, nejsou federované — fungují jako jediná síť", "comparison-section-list-point-7": "P2P sítě mají buď centrální autoritu, nebo může být ohrožena celá síť", "see-here": "viz zde", "simplex-network-overlay-card-1-li-5": "Všechny známé P2P sítě mohou být zranitelné vůči Sybil útoku, protože každý uzel je zjistitelný a síť funguje jako celek. Známá opatření ke zmírnění tohoto problému vyžadují buď centralizovanou součást, nebo drahé prokázání práce. Síť SimpleX nemá možnost zjistitelnosti serveru, je fragmentovaná a funguje jako několik izolovaných podsítí, což znemožňuje útoky v celé síti.", "simplex-network-overlay-card-1-li-6": "Sítě P2P mohou být zranitelné vůči útoku DRDoS, kdy klienti mohou znovu vysílat a zesílit provoz, což vede k odmítnutí služby v celé síti. SimpleX klienti pouze přenášejí provoz ze známého spojení a nemohou být zneužiti útočníkem k zesílení provozu v celé síti.", "privacy-matters-overlay-card-1-p-2": "Internetoví prodejci vědí, že lidé s nižšími příjmy častěji provádějí urgentní nákupy, takže mohou účtovat vyšší ceny nebo odebírat slevy.", - "privacy-matters-overlay-card-1-p-4": "Platforma SimpleX chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery poskytované SimpleX Chat, neznáme počet uživatelů ani jejich připojení.", - "privacy-matters-overlay-card-2-p-2": "Chcete-li být objektivní a činit nezávislá rozhodnutí, musíte mít svůj informační prostor pod kontrolou. Je to možné pouze v případě, že používáte soukromou komunikační platformu, která nemá přístup k vašemu sociálnímu grafu.", + "privacy-matters-overlay-card-1-p-4": "SimpleX síť chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery přednastavené v SimpleX Chat apce, operátoři serverů neznají počet uživatelů ani jejich připojení.", + "privacy-matters-overlay-card-2-p-2": "Chcete-li být objektivní a činit nezávislá rozhodnutí, musíte mít svůj informační prostor pod kontrolou. Je to možné pouze v případě, že používáte soukromou komunikační síť, která nemá přístup k vašemu sociálnímu grafu.", "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá párové anonymní adresy jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery.", "privacy-matters-overlay-card-3-p-2": "Jedním z nejvíce šokujících příběhů je zkušenost Mohamedoua Oulda Salahiho popsaná v jeho pamětech a zobrazená v Mauritánském filmu. Byl umístěn do tábora na Guantánamu bez soudu a byl tam 15 let mučen po telefonátu svému příbuznému v Afghánistánu pro podezření z účasti na útocích z 11. září, i když předchozích 10 let žil v Německu.", - "simplex-unique-overlay-card-1-p-1": "Na rozdíl od jiných platforem pro zasílání zpráv nemá SimpleX žádné identifikátory přiřazené uživatelům. Nespoléhá se na telefonní čísla, adresy založené na doméně (jako je e-mail nebo XMPP), uživatelská jména, veřejné klíče nebo dokonce náhodná čísla k identifikaci svých uživatelů —. Nevíme', kolik lidí používá naše servery SimpleX.", + "simplex-unique-overlay-card-1-p-1": "Na rozdíl od jiných sítí pro zasílání zpráv nemá SimpleX žádné identifikátory přiřazené uživatelům. Nespoléhá na telefonní čísla, adresy založené na doméně (jako e-mail nebo XMPP), uživatelská jména, veřejné klíče nebo dokonce náhodná čísla k identifikaci svých uživatelů — Operátoři SimpleX serverů neví, kolik lidí používá jejich servery.", "invitation-hero-header": "Byl vám zaslán odkaz pro připojení na SimpleX Chat", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat ukládá všechna uživatelská data pouze na klientských zařízeních pomocí přenosného šifrovaného databázového formátu, který lze exportovat a přenést na jakékoli podporované zařízení.", "contact-hero-p-1": "Veřejné klíče a adresa fronty zpráv v tomto odkazu NEJSOU při zobrazení této stránky odesílány přes síť — jsou obsaženy ve fragmentu kontrolního součtu adresy URL odkazu.", "simplex-unique-overlay-card-3-p-4": "Mezi odeslaným a přijatým provozem serveru nejsou žádné společné identifikátory ani šifrovaný text — pokud to někdo pozoruje, nemůže snadno určit, kdo s kým komunikuje, i když je TLS kompromitován.", - "simplex-unique-card-2-p-1": "Protože na platformě SimpleX nemáte žádný identifikátor nebo pevnou adresu, nikdo vás nemůže kontaktovat pokud nesdílíte jednorázovou nebo dočasnou uživatelskou adresu, jako QR kód nebo odkaz.", - "simplex-unique-overlay-card-4-p-2": "Platforma SimpleX používá otevřený protokol a poskytuje SDK k vytváření chat botů, což umožňuje implementaci služeb, se kterými mohou uživatelé komunikovat prostřednictvím chatovacích aplikací SimpleX —, Nemůžeme se dočkat, až uvídíme, jaké služby SimpleX dokážete vytvořit.", + "simplex-unique-card-2-p-1": "Protože na SimpleX síti nemáte žádný identifikátor nebo pevnou adresu, nikdo vás nemůže kontaktovat pokud nenasdílíte jednorázovou nebo dočasnou uživatelskou adresu, jako QR kód nebo odkaz.", + "simplex-unique-overlay-card-4-p-2": "SimpleX síť používá otevřený protokol a poskytuje SDK k vytváření chat botů, což umožní implementaci služeb, díky nimž boudou moci uživatelé komunikovat prostřednictvím aplikací SimpleX Chat — Opravdu se těšíme, jaké služby SimpleX vytvoříte.", "simplex-network": "SimpleX síť", "simplex-explained-tab-2-p-1": "Pro každé připojení používáte dvě samostatné fronty zasílání zpráv k odesílání a přijímání zpráv prostřednictvím různých serverů.", "simplex-explained-tab-1-p-1": "Můžete vytvářet kontakty a skupiny a vést obousměrné konverzace, stejně jako v jakémkoli jiném messengeru.", @@ -219,7 +219,7 @@ "guide-dropdown-8": "Nastavení aplikace", "guide-dropdown-9": "Vytvoření spojení", "guide": "Průvodce", - "docs-dropdown-1": "SimpleX platforma", + "docs-dropdown-1": "SimpleX síť", "docs-dropdown-2": "Přístup k Android souborům", "docs-dropdown-3": "Přístup k chat databázi", "docs-dropdown-4": "Hostovat SMP server", diff --git a/website/langs/hu.json b/website/langs/hu.json index 9dfd3d9f65..91dbe94cb1 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -6,7 +6,7 @@ "features": "Funkciók", "why-simplex": "Miért válassza a SimpleXet", "simplex-privacy": "A SimpleX adatvédelme", - "simplex-network": "A SimpleX-hálózat", + "simplex-network": "A SimpleX hálózat", "simplex-explained": "A SimpleX bemutatása", "simplex-explained-tab-1-text": "1. Felhasználói élmény", "simplex-explained-tab-2-text": "2. Hogyan működik", @@ -16,7 +16,7 @@ "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési sorbaállítást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyetlen irányba továbbítják az üzeneteket, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenetsorbaállításhoz külön névtelen hitelesítő-adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", - "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", + "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor hálózat használatával férnek hozzá a kiszolgálókhoz, így megakadályozva az IP-cím szerinti korrelációt.", "smp-protocol": "SMP-protokoll", "chat-protocol": "Csevegési protokoll", "donate": "Adományozás", @@ -29,25 +29,25 @@ "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
A SimpleX azonban nem, még véletlenszerű számokkal sem.
Ez radikálisan javítja az adatvédelmet.", "hero-overlay-1-textlink": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", - "hero-overlay-3-textlink": "Biztonsági felmérések", + "hero-overlay-3-textlink": "Biztonsági auditok", "hero-2-header": "Privát kapcsolat létrehozása", - "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat a partneréhez egy egyszer használható QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívási hivatkozás megosztásával is kapcsolódhat.", + "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat a partneréhez egy egyszer használható QR-kód segítségével, személyesen vagy videóhíváson keresztül. Ugyanakkor egy meghívási hivatkozás megosztásával is kapcsolódhat.", "hero-overlay-1-title": "Hogyan működik a SimpleX?", "hero-overlay-2-title": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", "hero-overlay-3-title": "Biztonsági felmérések", - "feature-1-title": "E2E-titkosított üzenetek markdown formázással és szerkesztéssel", - "feature-2-title": "E2E-titkosított
képek, videók és fájlok", - "feature-3-title": "E2E-titkosított decentralizált csoportok — csak a felhasználók tudják, hogy ezek léteznek", - "feature-4-title": "E2E-titkosított hangüzenetek", + "feature-1-title": "Végpontok között titkosított üzenetek markdown formázással és szerkesztéssel", + "feature-2-title": "Végpontok között titkosított
képek, videók és fájlok", + "feature-3-title": "Végpontok között titkosított, decentralizált csoportok — csak a felhasználók tudják, hogy ezek léteznek", + "feature-4-title": "Végpontok között titkosított hangüzenetek", "feature-5-title": "Eltűnő üzenetek", - "feature-6-title": "E2E-titkosított
hang- és videohívások", + "feature-6-title": "Végpontok között titkosított
hang- és videóhívások", "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil átköltöztetése egy másik eszközre", "feature-8-title": "Az inkognitómód —
egyedülálló a SimpleX Chatben", "simplex-network-overlay-1-title": "Összehasonlítás más P2P-üzenetküldő protokollokkal", "simplex-private-1-title": "2 rétegű végpontok közötti titkosítás", "simplex-private-2-title": "További rétege a
kiszolgáló-titkosítás", - "simplex-private-4-title": "Nem kötelező
hozzáférés Tor-on keresztül", - "simplex-private-5-title": "Több rétegű
tartalom kitöltés", + "simplex-private-4-title": "Hozzáférés a Tor hálózaton keresztül
(nem kötelező)", + "simplex-private-5-title": "Több rétegű
tartalomkitöltés", "simplex-private-6-title": "Sávon kívüli
kulcscsere", "simplex-private-7-title": "Üzenetintegritás
hitelesítés", "simplex-private-8-title": "Üzenetek keverése
a korreláció csökkentése érdekében", @@ -59,14 +59,14 @@ "simplex-private-card-3-point-1": "A kliens és a kiszolgálók közötti kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használja.", "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", "simplex-private-card-3-point-3": "Az újrakapcsolódás le van tiltva a munkamenet elleni támadások megelőzése érdekében.", - "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor-on vagy más átvitel-átfedő-hálózaton keresztül is elérheti.", + "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor hálózaton vagy más átvitelátfedő hálózaton keresztül is elérheti.", "simplex-private-card-6-point-1": "Számos kommunikációs hálózat sebezhető a kiszolgálók vagy a hálózat-szolgáltatók MITM-támadásaival szemben.", "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hasítóértékét.", "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", "simplex-private-card-8-point-1": "A SimpleX-kiszolgálók alacsony késleltetésű keverési csomópontokként működnek — a bejövő és kimenő üzenetek sorrendje eltérő.", "simplex-private-card-9-point-1": "Minden üzenetsorbaállítás egy irányba továbbítja az üzeneteket, a különböző küldési és vételi címekkel.", - "simplex-private-card-9-point-2": "A hagyományos üzenetküldőkhöz képest csökkenti a támadási vektorokat és a rendelkezésre álló metaadatokat.", + "simplex-private-card-9-point-2": "Kevesebb támadási felülettel rendelkezik, mint a hagyományos üzenetváltó alkalmazások, és kevesebb metaadatot tesz elérhetővé.", "simplex-private-card-10-point-1": "A SimpleX ideiglenes, névtelen, páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolathoz vagy csoporttaghoz.", "simplex-private-card-10-point-2": "Lehetővé teszi az üzenetek felhasználói profilazonosítók nélküli kézbesítését, ami az alternatíváknál jobb metaadat-védelmet biztosít.", "privacy-matters-1-overlay-1-title": "Az adatvédelemmel pénzt spórol meg", @@ -83,8 +83,8 @@ "simplex-unique-2-overlay-1-title": "A legjobb védelem a kéretlen tartalmak és a visszaélések ellen", "simplex-unique-3-title": "Ön kezeli az adatait", "simplex-unique-3-overlay-1-title": "Az adatok biztonsága és kezelése az Ön kezében van", - "simplex-unique-4-title": "Öné a SimpleX-hálózat", - "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX-hálózat a felhasználóké", + "simplex-unique-4-title": "Öné a SimpleX hálózat", + "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX hálózat a felhasználóké", "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: ha a SimpleXnek nincsenek felhasználói azonosítói, honnan tudja, hogy hová kell eljuttatni az üzeneteket?", "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi hálózat által használt felhasználói azonosítók helyett a SimpleX az üzenetek sorbaállításához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", @@ -95,16 +95,16 @@ "hero-overlay-card-2-p-3": "Még a Tor v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitómódot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések teljes bizonyítása.", "hero-overlay-card-3-p-1": "Trail of Bits egy vezető biztonsági és technológiai tanácsadó cég, amelynek az ügyfelei közé tartoznak nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", - "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében áttekintette a SimpleX-platform kriptográfiai és hálózati komponenseit. További információk.", - "simplex-network-overlay-card-1-li-1": "A P2P-hálózatok az üzenetek továbbítására a DHT valamelyik változatát használják. A DHT kialakításakor egyensúlyt kell teremteni a kézbesítési garancia és a késleltetés között. A SimpleX jobb kézbesítési garanciával és alacsonyabb késleltetéssel rendelkezik, mint a P2P, mivel az üzenet redundánsan, a címzett által kiválasztott kiszolgálók segítségével több kiszolgálón keresztül párhuzamosan továbbítható. A P2P-hálózatokban az üzenet O(log N) csomóponton halad át szekvenciálisan, az algoritmus által kiválasztott csomópontok segítségével.", - "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P-hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenessel sem, és csak az üzenetekhez használ ideiglenes, páros azonosítókat, ami jobb névtelenséget és metaadatvédelmet biztosít.", + "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében auditálta a SimpleX protokollok kriptográfiai és hálózati komponenseit. További információk.", + "simplex-network-overlay-card-1-li-1": "A P2P hálózatok az üzenetek továbbítására a DHT valamelyik változatát használják. A DHT kialakításakor egyensúlyt kell teremteni a kézbesítési garancia és a késleltetés között. A SimpleX jobb kézbesítési garanciával és alacsonyabb késleltetéssel rendelkezik, mint a P2P, mivel az üzenet redundánsan, a címzett által kiválasztott kiszolgálók segítségével több kiszolgálón keresztül párhuzamosan továbbítható. A P2P hálózatokban az üzenet O(log N) csomóponton halad át szekvenciálisan, az algoritmus által kiválasztott csomópontok segítségével.", + "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenessel sem, és csak az üzenetekhez használ ideiglenes, páros azonosítókat, ami jobb névtelenséget és metaadatvédelmet biztosít.", "simplex-network-overlay-card-1-li-3": "A P2P nem oldja meg a MITM-támadás problémát, és a legtöbb létező implementáció nem használ sávon kívüli üzeneteket a kezdeti kulcscseréhez. A SimpleX a kezdeti kulcscseréhez sávon kívüli üzeneteket, vagy bizonyos esetekben már meglévő biztonságos és megbízható kapcsolatokat használ.", - "simplex-network-overlay-card-1-li-6": "A P2P-hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX-kliensek csak az ismert kapcsolatból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", - "simplex-network-overlay-card-1-li-5": "Minden ismert P2P-hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (pl.: tracker), vagy egy drága tanúsítvány. A SimpleX-hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", + "simplex-network-overlay-card-1-li-6": "A P2P hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX kliensek csak az ismert kapcsolatból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", + "simplex-network-overlay-card-1-li-5": "Minden ismert P2P hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (pl.: tracker), vagy egy drága tanúsítvány. A SimpleX hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", "privacy-matters-overlay-card-1-p-1": "Sok nagyvállalat arra használja fel az önnel kapcsolatban álló személyek adatait, hogy megbecsülje az ön jövedelmét, hogy olyan termékeket adjon el önnek, amelyekre valójában nincs is szüksége, és hogy meghatározza az árakat.", "privacy-matters-overlay-card-1-p-2": "Az online kiskereskedők tudják, hogy az alacsonyabb jövedelműek nagyobb valószínűséggel vásárolnak azonnal, ezért magasabb árakat számíthatnak fel, vagy eltörölhetik a kedvezményeket.", "privacy-matters-overlay-card-1-p-3": "Egyes pénzügyi és biztosítótársaságok szociális grafikonokat használnak a kamatlábak és a díjak meghatározásához. Ez gyakran arra készteti az alacsonyabb jövedelmű embereket, hogy többet fizessenek — ez az úgynevezett „szegénységi prémium”.", - "privacy-matters-overlay-card-1-p-4": "A SimpleX-hálózat minden alternatívánál jobban védi a kapcsolatainak adatait, teljes mértékben megakadályozva, hogy az ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által előre beállított kiszolgálókat is használják, sem az alkalmazások, sem a kiszolgálók üzemeltetői nem ismerik, sem felhasználók számát, sem a kapcsolataikat.", + "privacy-matters-overlay-card-1-p-4": "A SimpleX hálózat minden alternatívánál jobban védi a kapcsolatainak adatait, teljes mértékben megakadályozva, hogy az ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által előre beállított kiszolgálókat is használják, sem az alkalmazások, sem a kiszolgálók üzemeltetői nem ismerik, sem felhasználók számát, sem a kapcsolataikat.", "privacy-matters-overlay-card-2-p-1": "Nem is olyan régen megfigyelhettük, hogy a nagy választásokat manipulálta egy neves tanácsadó cég, amely az ismeretségi-háló segítségével eltorzította a valós világról alkotott képünket, és manipulálta a szavazatainkat.", "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha privát kommunikációs hálózatot használ, amely nem fér hozzá az ismeretségi-hálójához.", "privacy-matters-overlay-card-2-p-3": "A SimpleX az első olyan hálózat, amely eleve nem rendelkezik felhasználói azonosítókkal, így jobban védi az ismeretségi-hálóját, mint bármely ismert alternatíva.", @@ -112,24 +112,24 @@ "privacy-matters-overlay-card-3-p-2": "Az egyik legmegdöbbentőbb a Mohamedou Ould Salahi memoárjában leírt és az „A mauritániai” c. filmben bemutatott történet. Őt bírósági tárgyalás nélkül a guantánamói táborba zárták, és ott kínozták 15 éven át, miután egy afganisztáni rokonát telefonon felhívta, akit azzal gyanúsítottak a hatóságok, hogy köze van a 9/11-es merényletekhez, holott Salahi az előző 10 évben Németországban élt.", "privacy-matters-overlay-card-3-p-3": "Átlagos embereket letartóztatnak azért, amit online megosztanak, még „névtelen” fiókjaikon keresztül is, még demokratikus országokban is.", "privacy-matters-overlay-card-3-p-4": "Nem elég csak egy végpontok között titkosított üzenetváltó-alkalmazást használnunk, mindannyiunknak olyan üzenetváltó-alkalmazásokat kell használnunk, amelyek védik a személyes partnereink magánéletét — akikkel kapcsolatban állunk.", - "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő hálózatoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — a SimpleX-kiszolgálók üzemeltetői nem tudják, hogy hányan használják a kiszolgálóikat.", + "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő hálózatoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — a SimpleX kiszolgálók üzemeltetői nem tudják, hogy hányan használják a kiszolgálóikat.", "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenet várakoztatást használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül.", - "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás megvédi annak titkosságát, hogy kivel kommunikál, elrejtve azt a SimpleX-hálózat kiszolgálói és a megfigyelők elől. IP-címének a kiszolgálók elől való elrejtéséhez azt teheti meg, hogy Toron keresztül kapcsolódik a SimpleX-kiszolgálókhoz.", - "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX-hálózaton, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás védi partnerének adatait, elrejtve azt a SimpleX hálózat kiszolgálói és a külső megfigyelők elől. Az IP-címe elrejtésének érdekében aTor hálózaton keresztül is kapcsolódhat a SimpleX kiszolgálókhoz.", + "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX hálózaton, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-overlay-card-2-p-2": "Még a felhasználói cím használata esetén is, aminek használata nem kötelező – ugyanakkor ez a kéretlen kapcsolatkérelmek küldésére is használható – módosíthatja vagy teljesen törölheti anélkül, hogy elveszítené a meglévő kapcsolatait.", "simplex-unique-overlay-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban, amely exportálható és átvihető bármely más támogatott eszközre.", "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", - "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX-kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", + "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", "simplex-unique-overlay-card-3-p-4": "A küldött és a fogadott kiszolgálóforgalom között nincsenek közös azonosítók vagy titkosított szövegek — ha bárki megfigyeli, nem tudja könnyen megállapítani, hogy ki kivel kommunikál, még akkor sem, ha a TLS-t kompromittálják.", "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleXet a saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az előre beállított kiszolgálókat használják az alkalmazásban.", - "simplex-unique-overlay-card-4-p-2": "A SimpleX-hálózat nyitott protokollt használ és SDK-t biztosít a chatbotok létrehozásához, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", - "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX-hálózatra való fejlesztést fontolgatja, például a SimpleX-alkalmazások felhasználóinak szánt chatbotot, vagy a SimpleX Chat könyvtárbotjának integrálását más mobilalkalmazásba, lépjen velünk kapcsolatba, ha bármilyen tanácsot vagy támogatást szeretne kapni.", - "simplex-unique-card-1-p-1": "A SimpleX megvédi a profilhoz tartozó kapcsolatokat és metaadatokat, elrejtve azokat a SimpleX-hálózat kiszolgálói és a megfigyelők elől.", + "simplex-unique-overlay-card-4-p-2": "A SimpleX hálózat nyitott protokollt használ és SDK-t biztosít a csevegési botok létrehozásához, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", + "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX hálózatra való fejlesztést fontolgatja, például a SimpleX alkalmazások felhasználóinak szánt csevegési botot, vagy a SimpleX Chat könyvtárbotjának integrálását más mobilalkalmazásba, lépjen velünk kapcsolatba, ha bármilyen tanácsot vagy támogatást szeretne kapni.", + "simplex-unique-card-1-p-1": "A SimpleX megvédi a profilhoz tartozó kapcsolatokat és metaadatokat, elrejtve azokat a SimpleX hálózat kiszolgálói és a megfigyelők elől.", "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő hálózattól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", - "simplex-unique-card-2-p-1": "Mivel a SimpleX-hálózaton nincs azonosítója vagy állandó címe, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-card-2-p-1": "Mivel a SimpleX hálózaton nincs azonosítója vagy állandó címe, senki sem tud kapcsolatba lépni Önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", "simplex-unique-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban —, amely exportálható és átvihető bármely más támogatott eszközre.", "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX továbbítókiszolgálóin tartózkodnak, amíg be nem érkeznek a címzetthez, majd automatikusan véglegesen törlődnek onnan.", - "simplex-unique-card-4-p-1": "A SimpleX-hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más hálózattól, kivéve az internetet.", + "simplex-unique-card-4-p-1": "A SimpleX hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más hálózattól, kivéve az internetet.", "simplex-unique-card-4-p-2": "Használhatja a SimpleXet a saját kiszolgálóival vagy az általunk biztosított kiszolgálókkal, és továbbra is kapcsolódhat bármely felhasználóhoz.", "join": "Csatlakozzon a közösségeinkhez", "we-invite-you-to-join-the-conversation": "Meghívjuk Önt, hogy csatlakozzon a beszélgetésekhez", @@ -139,7 +139,7 @@ "sign-up-to-receive-our-updates": "Regisztráljon a hírleveleinkre, hogy ne maradjon le semmiről", "enter-your-email-address": "Adja meg az e-mail-címét", "get-simplex": "A SimpleX számítógépes alkalmazásának letöltése", - "why-simplex-is-unique": "Mitől egyedülálló a SimpleX", + "why-simplex-is-unique": "A SimpleX mitől egyedülálló", "learn-more": "Tudjon meg többet", "more-info": "További információ", "hide-info": "Információ elrejtése", @@ -164,15 +164,15 @@ "simplex-chat-for-the-terminal": "SimpleX Chat a terminálhoz", "copy-the-command-below-text": "másolja be az alábbi parancsot, és használja a csevegésben:", "privacy-matters-section-header": "Miért számít az adatvédelem", - "privacy-matters-section-subheader": "A metaadatok védelmének megőrzése — kivel beszélget — megvédi a következőktől:", + "privacy-matters-section-subheader": "A metaadatok — pédául, hogy kivel beszélget — védelmének megőrzése biztonságot nyújt a következők ellen:", "privacy-matters-section-label": "Győződjön meg arról, hogy az üzenetváltó-alkalmazás amit használ nem fér hozzá az adataihoz!", "simplex-private-section-header": "Mitől lesz a SimpleX privát", - "simplex-network-section-header": "SimpleX-hálózat", + "simplex-network-section-header": "SimpleXhálózat", "simplex-network-section-desc": "A Simplex Chat a P2P- és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", "simplex-network-1-desc": "Minden üzenet a kiszolgálókon keresztül kerül elküldésre, ami jobb metaadat-védelmet és megbízható aszinkron üzenetkézbesítést biztosít, miközben elkerülhető a sok", "simplex-network-2-header": "A föderált hálózatokkal ellentétben", "simplex-network-2-desc": "A SimpleX továbbítókiszolgálói NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM kapcsolódnak egymáshoz, és NINCS kiszolgálókönyvtár.", - "simplex-network-3-header": "SimpleX-hálózat", + "simplex-network-3-header": "SimpleX hálózat", "simplex-network-3-desc": "a kiszolgálók egyirányú üzenet várakoztatásokat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", "comparison-section-header": "Összehasonlítás más protokollokkal", "protocol-1-text": "Signal, nagy platformok", @@ -195,18 +195,18 @@ "comparison-section-list-point-4": "Ha az üzemeltetett kiszolgálók veszélybe kerülnek. Hitelesítse a biztonsági kódot a Signal vagy más biztonságos üzenetküldő alkalmazás segítségével a támadások veszélyeinek csökkentésére", "comparison-section-list-point-5": "Nem védi a felhasználók metaadatait", "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált — egyetlen hálózatként működnek", - "comparison-section-list-point-7": "A P2P-hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", + "comparison-section-list-point-7": "A P2P hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", "see-here": "tekintse meg itt", "guide-dropdown-1": "Gyors indítás", "guide-dropdown-2": "Üzenetek küldése", "guide-dropdown-3": "Titkos csoportok", "guide-dropdown-4": "Csevegési profilok", "guide-dropdown-5": "Adatkezelés", - "guide-dropdown-6": "Hang- és videó hívások", + "guide-dropdown-6": "Hang- és videóhívások", "guide-dropdown-7": "Adatvédelem és biztonság", - "guide-dropdown-8": "Alkalmazás beállításai", + "guide-dropdown-8": "Alkalmazásbeállítások", "guide": "Útmutató", - "docs-dropdown-1": "SimpleX-hálózat", + "docs-dropdown-1": "SimpleX hálózat", "docs-dropdown-2": "Android fájlok elérése", "docs-dropdown-3": "Hozzáférés a csevegési adatbázishoz", "docs-dropdown-8": "SimpleX jegyzékszolgáltatás", @@ -223,8 +223,8 @@ "contact-hero-header": "Kapott egy meghívót a SimpleX Chaten való beszélgetéshez", "invitation-hero-header": "Kapott egy egyszer használható meghívót a SimpleX Chaten való beszélgetéshez", "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független — a szabványos webes protokollokon, például WebSocketsen keresztül is működik.", - "simplex-private-card-4-point-2": "A SimpleX Tor-on keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", - "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ, hogy meghiúsítsa az üzenetméret ellen irányuló támadásokat.", + "simplex-private-card-4-point-2": "A SimpleX, Tor hálózaton keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxyt (vagy a VPN-t az iOS-ban).", + "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ az üzenetméretre irányuló támadások meghiúsítása érdekében.", "simplex-private-card-5-point-2": "A kiszolgálók és a hálózatot megfigyelők számára a különböző méretű üzenetek egyformának tűnnek.", "privacy-matters-1-title": "Hirdetés és árdiszkrimináció", "hero-overlay-card-1-p-3": "Ön határozza meg, hogy melyik kiszolgáló(ka)t használja az üzenetek fogadására, a kapcsolatokhoz — azokat a kiszolgálókat, amelyeket az üzenetek küldésére használ. Minden beszélgetés két különböző kiszolgálót használ.", @@ -232,9 +232,9 @@ "chat-bot-example": "Példa csevegési botra", "simplex-private-3-title": "Biztonságos, hitelesített
TLS adatátvitel", "github-repository": "GitHub tárolójában", - "tap-to-close": "Koppintson a bezáráshoz", - "simplex-network-1-header": "A P2P-hálózatokkal ellentétben", - "simplex-network-1-overlay-linktext": "a P2P-hálózatok problémái", + "tap-to-close": "Bezárás", + "simplex-network-1-header": "A P2P hálózatokkal ellentétben", + "simplex-network-1-overlay-linktext": "a P2P hálózatok problémái", "comparison-point-3-text": "Függés a DNS-től", "yes": "Igen", "guide-dropdown-9": "Kapcsolatok létrehozása", @@ -254,6 +254,6 @@ "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", "simplex-chat-repo": "A SimpleX Chat tárolója", "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók", - "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában felülvizsgálta a SimpleX hálózati protokollok kriptográfiai felépítését. Tudjon meg többet.", + "hero-overlay-card-3-p-3": "A Trail of Bits 2024 júliusában ismét auditálta a SimpleX protokollok kriptográfiai és hálózati komponenseit. További információk.", "docs-dropdown-14": "SimpleX üzleti célra" } diff --git a/website/langs/ja.json b/website/langs/ja.json index 05365e2271..1f71aad624 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -8,10 +8,10 @@ "back-to-top": "トップへ戻る", "glossary": "用語集", "simplex-unique-card-3-p-1": "SimpleXはクライアント端末上の全てのユーザデータを ポータブルで暗号化されたデータベースフォーマットで保管します—別の端末へ移行することができます。", - "simplex-unique-card-2-p-1": "あなたは識別子や固定されたアドレスをSimpleXプラットフォーム上で持たないため、あなたがQRコードやリンクといった一度のみ使用可能もしくは一時的なユーザアドレスを共有しない限り、誰もあなたへ連絡することができません。", - "simplex-unique-card-1-p-2": "その他の既存のメッセージプラットフォームと異なり、SimpleXはユーザへ識別子を割り当てません — ランダムな番号さえありません。", + "simplex-unique-card-2-p-1": "あなたは識別子や固定されたアドレスをSimpleXネットワーク上で持たないため、あなたがQRコードやリンクといった一度のみ使用可能もしくは一時的なユーザアドレスを共有しない限り、誰もあなたへ連絡することができません。", + "simplex-unique-card-1-p-2": "その他の既存のメッセージネットワークと異なり、SimpleXはユーザへ識別子を割り当てません — ランダムな番号さえありません。", "simplex-unique-card-4-p-2": "あなたは私たちの提供するサーバや 自分自身のサーバでSimpleXを使う ことができます — そして別のユーザとつながることができます。", - "simplex-unique-card-4-p-1": "SimpleXネットワークは、インターネット以外のいかなる暗号通貨やプラットフォームから独立しており、完全に分散化されています。", + "simplex-unique-card-4-p-1": "SimpleXネットワークは、インターネット以外のいかなる暗号通貨やネットワークから独立しており、完全に分散化されています。", "simplex-unique-card-3-p-2": "エンドツーエンドで暗号化されたメッセージは、SimpleXのリレーサーバ上で受信されるまで一時的に保管され、その後永久的に削除されます。", "guide-dropdown-1": "クイックスタート", "guide-dropdown-2": "メッセージを送る", @@ -23,10 +23,10 @@ "guide-dropdown-7": "プライバシーとセキュリティ", "guide-dropdown-8": "アプリ設定", "menu": "メニュー", - "simplex-unique-card-1-p-1": "SimpleXは、SimpleXプラットフォームのサーバやその他の観察者から隠すことで、あなたのプロフィール、連絡先やメタデータのプライバシーを守ります。", - "simplex-unique-overlay-card-4-p-3": "例えば、SimpleXアプリユーザへのチャットボットやSimpleX Chatライブラリーの携帯アプリへの統合など、SimpleXプラットフォームに関する開発を検討してくださっているようでしたら、どのようなアドバイスや支援のことでもご連絡ください 。", - "simplex-unique-overlay-card-4-p-2": "SimpleXプラットフォームは、SimpleX Chatアプリを介してユーザが交流するサービスを実装させつつオープンプロトコルを使い、チャットボットを作成するためにSDKを提供します—私たちはあなた達がどのようなSimpleXのサービスを築くか本当に楽しみです。", - "simplex-unique-overlay-card-4-p-1": "あなたが、自分自身のサーバでSimpleXを使っても、私たちが提供する事前に構築されたサーバを使う方々と連絡を取ることができます。", + "simplex-unique-card-1-p-1": "SimpleXは、SimpleXネットワークのサーバやその他の観察者から隠すことで、あなたのプロフィール、連絡先やメタデータのプライバシーを守ります。", + "simplex-unique-overlay-card-4-p-3": "例えば、SimpleXアプリユーザへのチャットボットやSimpleX Chatライブラリーの携帯アプリへの統合など、SimpleXネットワークに関する開発を検討してくださっているようでしたら、どのようなアドバイスや支援のことでもご連絡ください 。", + "simplex-unique-overlay-card-4-p-2": "SimpleXネットワークは、SimpleX Chatアプリを介してユーザが交流するサービスを実装させつつオープンプロトコルを使い、チャットボットを作成するためにSDKを提供します—私たちはあなた達がどのようなSimpleXのサービスを築くか本当に楽しみです。", + "simplex-unique-overlay-card-4-p-1": "あなたが、自分自身のサーバでSimpleXを使っても、アプリで予め設定されたサーバを使う方々と連絡を取ることができます。", "reference": "参考文献", "simplex-explained-tab-1-text": "1. ユーザーが経験すること", "simplex-explained-tab-1-p-2": "ユーザー プロファイル識別子なしで単方向キューをどのように処理できるのでしょうか?", @@ -72,7 +72,7 @@ "installing-simplex-chat-to-terminal": "SimpleX チャットをターミナルにインストールする", "use-this-command": "次のコマンドを使用してください:", "to-make-a-connection": "接続するには:", - "comparison-section-list-point-6": "P2P は分散されていますが、統合されておらず、単一のネットワークとして動作します", + "comparison-section-list-point-6": "P2Pは分散されていますが、フェデレーションされていません — 単一のネットワークとして動作します", "simplex-chat-via-f-droid": "F-Droid 経由の SimpleX チャット", "privacy-matters-overlay-card-1-p-1": "多くの大企業は、あなたの収入を見積もり、本当に必要のない製品を販売し、価格を決定するために、あなたのつながりに関する情報を使用します。", "privacy-matters-1-overlay-1-title": "プライバシーの保護はコストを削減します", @@ -85,16 +85,16 @@ "guide-dropdown-9": "コネクションを作る", "simplex-unique-1-overlay-1-title": "ID、プロフィール、連絡先、メタデータの完全なプライバシー", "hero-overlay-card-2-p-4": "SimpleX は、その設計にユーザー ID を持たないことで、これらの攻撃から保護します。 また、シークレット モードを使用すると、連絡先ごとに異なる表示名が付けられ、連絡先間でデータが共有されることがなくなります。", - "privacy-matters-overlay-card-2-p-2": "客観的であり、独立した意思決定を行うには、情報空間を制御する必要があります。 これは、ソーシャル グラフにアクセスできないプライベート コミュニケーション プラットフォームを使用している場合にのみ可能です。", + "privacy-matters-overlay-card-2-p-2": "客観的であり、独立した意思決定を行うには、情報空間を制御する必要があります。 これは、ソーシャル グラフにアクセスできないプライベートコミュニケーションネットワークを使用している場合にのみ可能です。", "hero-overlay-card-2-p-1": "ユーザーが永続的な ID を持っている場合、それがセッション ID などの単なる乱数であっても、プロバイダーや攻撃者がユーザーの接続方法や送信するメッセージの数を監視できるリスクがあります。", "feature-3-title": "分散型シークレットグループ —
ユーザーのみがその存在を知っています", "simplex-network-overlay-1-title": "P2Pメッセージングプロトコルとの比較", "comparison-section-list-point-7": "P2Pネットワークには中央当局が存在するか、ネットワーク全体が侵害される可能性がある", - "docs-dropdown-1": "SimpleXプラットフォーム", + "docs-dropdown-1": "SimpleXネットワーク", "hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。", "simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット", "simplex-network-overlay-card-1-li-3": "P2P は MITM 攻撃 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", - "the-instructions--source-code": "ソース コードからダウンロードまたはコンパイルする方法を説明します。", + "the-instructions--source-code": "ソースコードからダウンロードまたはコンパイルする方法を説明します。", "simplex-network-section-desc": "Simplex Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。", "privacy-matters-section-subheader": "メタデータのプライバシーを保護する — 話す相手 — 以下のことからあなたを守ります:", "if-you-already-installed": "すでにインストールしている場合", @@ -116,7 +116,7 @@ "comparison-section-header": "他のプロトコルとの比較", "invitation-hero-header": "SimpleX Chatで接続するための使い捨てのリンクを受信しました", "no-secure": "いいえ - 安全", - "hero-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、接続ごとに個別のメッセージ キューの一時的な匿名ペア識別子を使用します — 長期的な識別子はありません。", + "hero-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は、他のすべてのネットワークで使用されるユーザー ID の代わりに、接続ごとに個別のメッセージ キューの一時的な匿名ペア識別子を使用します — 長期的な識別子はありません。", "simplex-network-1-header": "P2Pネットワークとは異なります", "simplex-private-card-7-point-2": "メッセージが追加、削除、または変更されると、受信者に警告が表示されます。", "simplex-unique-3-title": "データを管理するのはあなたです", @@ -167,9 +167,9 @@ "docs-dropdown-7": "SimpleX チャットを翻訳する", "simplex-network-1-desc": "すべてのメッセージはサーバー経由で送信され、メタデータのプライバシーが向上し、信頼性の高い非同期メッセージ配信が提供されると同時に、多くが回避されます", "simplex-chat-repo": "SimpleX チャット リポジトリ", - "simplex-private-card-6-point-1": "多くの通信プラットフォームは、サーバーやネットワーク プロバイダーによる MITM 攻撃に対して脆弱です。", + "simplex-private-card-6-point-1": "多くの通信ネットワークは、サーバーやネットワーク プロバイダーによるMITM 攻撃に対して脆弱です。", "privacy-matters-3-overlay-1-linkText": "プライバシーはあなたの自由を守ります", - "simplex-unique-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は一方向メッセージ キューのペアワイズ匿名アドレスを使用し、受信メッセージと送信メッセージに分けて、通常は異なるサーバーを経由します。 SimpleX を使用することは、別の「バーナー」 を使用するようなものです。 連絡先ごとにメールまたは電話を使用できるため、管理に手間がかかりません。", + "simplex-unique-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は一方向メッセージ キューのペアワイズ匿名アドレスを使用し、受信メッセージと送信メッセージに分けて、通常は異なるサーバーを経由します。", "simplex-unique-overlay-card-3-p-4": "送受信されるサーバー トラフィックの間に共通の識別子や暗号文はありません。 — 誰かがそれを観察している場合、たとえ TLS が侵害されたとしても、誰が誰と通信しているのかを簡単に判断することはできません。", "docs-dropdown-2": "Android ファイルへのアクセス", "get-simplex": "SimpleXを入手する desktop app", @@ -204,12 +204,12 @@ "simplex-unique-1-title": "プライバシーが完全に守られます", "protocol-2-text": "XMPP、Matrix", "guide": "ガイド", - "simplex-network-overlay-card-1-li-4": "P2P の実装は、一部のインターネット プロバイダー (BitTorrent など) によってブロックされる場合があります。 SimpleX はトランスポートに依存しません- WebSocketのような標準的な Web プロトコル上で動作します。", + "simplex-network-overlay-card-1-li-4": "P2P の実装は、一部のインターネット プロバイダー (BitTorrent など) によってブロックされる場合があります。 SimpleX はトランスポートに依存しません — WebSocketのような標準的な Web プロトコル上で動作します。", "hero-overlay-2-title": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", "docs-dropdown-4": "ホストSMPサーバー", "feature-4-title": "E2E暗号化された音声メッセージ", "privacy-matters-overlay-card-2-p-1": "つい最近まで、私たちは主要な選挙が 評判の高いコンサルティング会社によって操作されているのを観察しました。 ソーシャルグラフは私たちの現実世界の見方を歪め、私たちの投票を操作します。", - "privacy-matters-overlay-card-2-p-3": "SimpleX は、設計上ユーザー識別子を持たない最初のプラットフォームであり、この方法で既知の代替手段よりも接続グラフを保護します。", + "privacy-matters-overlay-card-2-p-3": "SimpleX は、設計上ユーザー識別子を持たない最初のネットワークであり、この方法で既知の代替手段よりも接続グラフを保護します。", "learn-more": "さらに詳しく", "simplex-private-8-title": "メッセージのミキシング
相関性を減らす", "scan-qr-code-from-mobile-app": "モバイルアプリからQRコードをスキャン", @@ -223,15 +223,15 @@ "docs-dropdown-8": "SimpleX ディレクトリ サービス", "simplex-private-card-1-point-1": "ダブルラチェットプロトコル —
完全な前方秘匿性と侵入回復機能を備えたOTRメッセージング。", "simplex-private-card-8-point-1": "SimpleX サーバーは、低遅延の混合ノードとして機能します — 受信メッセージと送信メッセージの順序が異なります。", - "simplex-unique-overlay-card-2-p-1": "SimpleX プラットフォームには識別子がないため、ワンタイムまたは一時的なユーザー アドレスを QR コードまたはリンクとして共有しない限り、誰もあなたに連絡することはできません。", + "simplex-unique-overlay-card-2-p-1": "SimpleXネットワークには識別子がないため、ワンタイムまたは一時的なユーザー アドレスを QR コードまたはリンクとして共有しない限り、誰もあなたに連絡することはできません。", "sign-up-to-receive-our-updates": "最新情報を受け取る", "simplex-private-section-header": "SimpleX をプライベートにするもの", "we-invite-you-to-join-the-conversation": "ぜひ会話にご参加ください", "feature-2-title": "E2E暗号化された
画像とファイル", "simplex-private-9-title": "単方向
メッセージキュー", - "simplex-unique-overlay-card-1-p-3": "この設計により、通信相手のプライバシーが保護され、SimpleX プラットフォーム サーバーや監視者からプライバシーが隠されます。 IP アドレスをサーバーから隠すには、Tor 経由で SimpleX サーバーに接続します。", + "simplex-unique-overlay-card-1-p-3": "この設計により、通信相手のプライバシーが保護され、SimpleX ネットワークサーバや監視者からプライバシーが隠されます。 IP アドレスをサーバから隠すには、Tor 経由で SimpleX サーバーに接続します。", "simplex-private-7-title": "メッセージの整合性
検証", - "privacy-matters-overlay-card-1-p-4": "SimpleX プラットフォームは、他のどのプラットフォームよりも接続のプライバシーを保護し、ソーシャル グラフが企業や組織に利用されることを完全に防ぎます。 SimpleX Chat が提供するサーバーを使用している場合でも、ユーザーの数や接続数はわかりません。", + "privacy-matters-overlay-card-1-p-4": "SimpleXネットワークは、他のどのプラットフォームよりも接続のプライバシーを保護し、ソーシャル グラフが企業や組織に利用されることを完全に防ぎます。 SimpleX Chatアプリに予め設定されたサーバを利用している場合でも、サーバオペレータはユーザーの数や接続数を知ることはできません。", "hero-overlay-card-1-p-6": "詳細については、SimpleX ホワイトペーパーをご覧ください。", "simplex-network-overlay-card-1-p-1": "P2P メッセージング プロトコルとアプリには、SimpleX よりも信頼性が低く、分析がより複雑になるさまざまな問題があり、 いくつかの種類の攻撃に対して脆弱です。", "simplex-network-overlay-card-1-li-1": "P2P ネットワークは、メッセージをルーティングするために DHT の一部の変種に依存します。 DHT の設計では、配信保証と遅延のバランスを取る必要があります。 SimpleX は、受信者が選択したサーバーを使用して、メッセージを複数のサーバーを介して並行して冗長的に渡すことができるため、P2P よりも優れた配信保証と低い遅延の両方を備えています。 P2P ネットワークでは、メッセージはアルゴリズムによって選択されたノードを使用して、O(log N) 個のノードを順番に通過します。", @@ -240,14 +240,14 @@ "simplex-network-3-desc": "サーバーはユーザーを接続するための一方向キューを提供しますが、ネットワーク接続グラフは表示されません— ユーザーだけがそうします。", "simplex-private-card-3-point-1": "クライアント/サーバー接続には、強力なアルゴリズムを備えた TLS 1.2/1.3 のみが使用されます。", "hero-overlay-card-1-p-3": "メッセージの受信に使用するサーバー、連絡先を定義します —メッセージを送信するために使用するサーバー。 すべての会話では 2 つの異なるサーバーが使用される可能性があります。", - "simplex-unique-overlay-card-1-p-1": "他のメッセージング プラットフォームとは異なり、SimpleX にはユーザーに割り当てられる識別子がありません。 ユーザーを識別するために、電話番号、ドメインベースのアドレス (電子メールや XMPP など)、ユーザー名、公開キー、さらには乱数にも依存しません。 —我々もSimpleX サーバーを何人が使用しているかはわかりません。", + "simplex-unique-overlay-card-1-p-1": "他のメッセージングネットワークとは異なり、SimpleX にはユーザーに割り当てられる識別子がありません。 ユーザーを識別するために、電話番号、ドメインベースのアドレス (電子メールや XMPP など)、ユーザー名、公開キー、さらには乱数にも依存しません。 — サーバオペレータはどれだけの人が利用しているかも知ることはありません。", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat と F-Droid.org リポジトリは、異なるキーを使用してビルドに署名します。 切り替えるには、チャット データベースをエクスポートし、アプリを再インストールしてください。", "simplex-private-5-title": "何レイヤーもの
コンテンツパディング", "hero-overlay-card-3-p-1": "Trail of Bitsは、大手ハイテク企業、政府機関、主要なブロックチェーン・プロジェクトなどを顧客に持つ、セキュリティとテクノロジーの大手コンサルタント会社です。", "jobs": "チームに参加する", "hero-overlay-3-textlink": "セキュリティ評価", "hero-overlay-3-title": "セキュリティ評価", - "hero-overlay-card-3-p-2": "Trail of Bitsは2022年11月にSimpleXプラットフォームの暗号とネットワークのコンポーネントを検証しました。詳しくは お知らせをご覧ください。", + "hero-overlay-card-3-p-2": "Trail of Bitsは2022年11月にSimpleXプラットフォームの暗号とネットワークのコンポーネントを検証しました。詳しくはお知らせをご覧ください。", "docs-dropdown-9": "ダウンロード", "please-enable-javascript": "QRコードを表示するためにJavaScriptを有効にしてください。", "please-use-link-in-mobile-app": "このリンクをモバイルアプリで使用してください", @@ -255,5 +255,5 @@ "docs-dropdown-11": "よくある質問", "docs-dropdown-12": "セキュリティ", "docs-dropdown-14": "ビジネス向けSimpleX", - "hero-overlay-card-3-p-3": "Trail of Bits は 2024 年 7 月に SimpleX ネットワーク プロトコルの暗号設計をレビューしました。" + "hero-overlay-card-3-p-3": "Trail of Bits は 2024年7月に SimpleX ネットワーク プロトコルの暗号設計をレビューしました。続きを読む。" } diff --git a/website/langs/nl.json b/website/langs/nl.json index 30e751c9f9..a6a0a740ae 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -48,7 +48,7 @@ "simplex-private-card-3-point-3": "Connectie herstel is uitgeschakeld om sessie aanvallen te voorkomen.", "simplex-private-card-5-point-1": "SimpleX gebruikt content opvulling voor elke coderingslaag om aanvallen op bericht grootte te verstoren.", "simplex-private-card-5-point-2": "Het zorgt ervoor dat berichten van verschillende grootte er hetzelfde uitzien voor de servers en netwerk waarnemers.", - "simplex-private-card-6-point-1": "Veel communicatie platforms zijn kwetsbaar voor MITM aanvallen door servers of netwerk providers.", + "simplex-private-card-6-point-1": "Veel communicatienetwerken zijn kwetsbaar voor MITM-aanvallen door servers of netwerkaanbieders.", "simplex-private-card-7-point-2": "Als een bericht wordt toegevoegd, verwijderd of gewijzigd, wordt de ontvanger gewaarschuwd.", "simplex-private-card-8-point-1": "SimpleX servers fungeren als mix nodes met lage latentie — de inkomende en uitgaande berichten hebben een andere volgorde.", "simplex-private-card-9-point-2": "Het vermindert de aanvals mogenlijkheden, in vergelijking met traditionele berichten diensten en de beschikbare metadata.", @@ -96,19 +96,19 @@ "privacy-matters-3-overlay-1-linkText": "Privacy beschermt uw vrijheid", "simplex-unique-2-overlay-1-title": "De beste bescherming tegen spam en misbruik", "hero-overlay-card-1-p-1": "Veel gebruikers vroegen: als SimpleX geen gebruiker-ID's heeft, hoe kan het dan weten waar berichten moeten worden afgeleverd?", - "hero-overlay-card-1-p-2": "Om berichten te bezorgen, gebruikt SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, tijdelijke anonieme paarsgewijze identifiers van berichten wachtrijen, gescheiden voor elk van uw verbindingen — er zijn geen identificatiegegevens op lange termijn.", + "hero-overlay-card-1-p-2": "Om berichten te bezorgen gebruikt SimpleX in plaats van de gebruikers-ID's die door alle andere netwerken worden gebruikt, tijdelijke, anonieme, paarsgewijze identificatiecodes van berichtenwachtrijen, die voor elk van uw verbindingen afzonderlijk zijn. Er zijn geen langetermijnidentificatiecodes.", "hero-overlay-card-2-p-3": "Zelfs met de meest privé apps die Tor v3-services gebruiken, als je met twee verschillende contacten via hetzelfde profiel praat, kunnen ze bewijzen dat ze met dezelfde persoon verbonden zijn.", "hero-overlay-card-2-p-4": "SimpleX beschermt tegen deze aanvallen door geen gebruikers-ID's in het ontwerp te hebben. En als u de incognitomodus gebruikt, heeft u voor elk contact een andere weergavenaam, waardoor gedeelde gegevens tussen hen worden vermeden.", "simplex-network-overlay-card-1-p-1": "P2P-berichtenprotocollen en -apps hebben verschillende problemen waardoor ze minder betrouwbaar zijn dan SimpleX, complexer om te analyseren en kwetsbaarder voor verschillende soorten aanvallen.", "simplex-network-overlay-card-1-li-1": "P2P netwerken vertrouwen op een variant van DHT om berichten te routeren. DHT ontwerpen moeten een balans vinden tussen leveringsgarantie en latentie. SimpleX heeft zowel een betere leveringsgarantie als een lagere latentie dan P2P, omdat het bericht redundant via meerdere servers parallel kan worden doorgegeven, met behulp van de servers die door de ontvanger zijn gekozen. In P2P netwerken wordt het bericht achtereenvolgens door O(log N)-knooppunten geleid, met behulp van door het algoritme gekozen knooppunten.", "simplex-network-overlay-card-1-li-2": "SimpleX ontwerp heeft, in tegenstelling tot de meeste P2P netwerken, geen globale gebruikers-ID's van welke soort dan ook, zelfs niet tijdelijk en gebruikt alleen tijdelijke paarsgewijze ID's wat een betere anonimiteit en metadata bescherming biedt.", "simplex-network-overlay-card-1-li-3": "P2P lost het probleem MITM-aanval niet op en de meeste bestaande implementaties gebruiken geen out-of-band berichten voor de initiële sleuteluitwisseling. SimpleX gebruikt out-of-band berichten of, in sommige gevallen, reeds bestaande veilige en vertrouwde verbindingen voor de eerste sleuteluitwisseling.", - "simplex-network-overlay-card-1-li-4": "P2P implementaties kunnen door sommige internetproviders worden geblokkeerd (zoals BitTorrent). SimpleX is transport-agnostisch - het kan werken via standaard webprotocollen, b.v. WebSockets.", + "simplex-network-overlay-card-1-li-4": "P2P-implementaties kunnen door sommige internetproviders worden geblokkeerd (zoals BitTorrent). SimpleX is transport-agnostisch – het kan werken via standaard webprotocollen, zoals WebSockets.", "simplex-network-overlay-card-1-li-5": "Alle bekende P2P netwerken kunnen kwetsbaar zijn voor Sybil-aanval, omdat elk knooppunt vindbaar is en het netwerk als een geheel opereert. Bekende maatregelen om het te verminderen, vereisen een gecentraliseerde component of duur bewijs van werk . Het SimpleX-netwerk kan niet worden gedetecteerd door de server, het is gefragmenteerd en werkt als meerdere geïsoleerde subnetwerken, waardoor netwerkbrede aanvallen onmogelijk zijn.", - "simplex-unique-card-2-p-1": "Omdat u geen identificator of vast adres op het SimpleX platform heeft, kan niemand contact met u opnemen, tenzij u een eenmalig of tijdelijk gebruikers adres deelt, als een QR-code of een link.", + "simplex-unique-card-2-p-1": "Omdat u geen identificatiecode of vast adres hebt op het SimpleX-netwerk, kan niemand contact met u opnemen, tenzij u een eenmalig of tijdelijk gebruikersadres deelt in de vorm van een QR-code of een link.", "simplex-unique-card-3-p-1": "SimpleX slaat alle gebruikersgegevens op client apparaten op in een draagbaar gecodeerd databaseformaat — en kan worden overgedragen naar een ander apparaat.", "simplex-unique-card-3-p-2": "De end-to-end versleutelde berichten worden tijdelijk op SimpleX relayservers bewaard totdat ze zijn ontvangen, waarna ze permanent worden verwijderd.", - "simplex-unique-card-4-p-1": "Het SimpleX netwerk is volledig gedecentraliseerd en onafhankelijk van enige cryptovaluta of enig ander platform, behalve internet.", + "simplex-unique-card-4-p-1": "Het SimpleX-netwerk is volledig gedecentraliseerd en onafhankelijk van welke cryptovaluta dan ook of enig ander netwerk, met uitzondering van het internet.", "simplex-unique-card-4-p-2": "U kunt SimpleX gebruiken met uw eigen servers of met de servers die door ons worden geleverd — en toch verbinding maken met elke gebruiker.", "join": "Word lid van", "we-invite-you-to-join-the-conversation": "Wij nodigen u uit om deel te nemen aan de conversatie", @@ -136,7 +136,7 @@ "use-this-command": "Gebruik deze opdracht:", "see-simplex-chat": "Zie SimpleX chat", "github-repository": "GitHub repository", - "the-instructions--source-code": "de instructies voor het downloaden of compileren van de broncode.", + "the-instructions--source-code": "voor instructies over hoe u deze kunt downloaden of compileren vanuit de broncode.", "if-you-already-installed-simplex-chat-for-the-terminal": "Als u SimpleX Chat al voor de terminal hebt geïnstalleerd", "if-you-already-installed": "Als u al hebt geïnstalleerd", "simplex-chat-for-the-terminal": "SimpleX Chat voor de terminal", @@ -174,24 +174,24 @@ "privacy-matters-overlay-card-1-p-1": "Veel grote bedrijven gebruiken informatie over met wie u verbonden bent om uw inkomen te schatten, u de producten te verkopen die u niet echt nodig heeft en om de prijzen te bepalen.", "privacy-matters-overlay-card-1-p-2": "Webwinkels weten dat mensen met lagere inkomens eerder geneigd zijn om dringende aankopen te doen, dus kunnen ze hogere prijzen vragen of kortingen intrekken.", "privacy-matters-overlay-card-1-p-3": "Sommige financiële en verzekeringsmaatschappijen gebruiken sociale grafieken om rentetarieven en premies te bepalen. Het zorgt er vaak voor dat mensen met lagere inkomens meer betalen — het staat bekend als 'armoedepremie'.", - "privacy-matters-overlay-card-1-p-4": "Het SimpleX platform beschermt de privacy van uw verbindingen beter dan welk alternatief dan ook, waardoor volledig wordt voorkomen dat uw sociale grafiek beschikbaar wordt voor bedrijven of organisaties. Zelfs wanneer mensen servers van SimpleX Chat gebruiken, weten we niet hoeveel gebruikers of hun verbindingen zijn.", - "privacy-matters-overlay-card-2-p-2": "Om objectief te zijn en onafhankelijke beslissingen te kunnen nemen, moet u de controle hebben over uw informatie. Het is alleen mogelijk als u een privé communicatieplatform gebruikt dat geen toegang heeft tot uw sociale grafiek.", - "privacy-matters-overlay-card-2-p-3": "SimpleX is het eerste platform dat standaard geen gebruikers-ID's heeft, waardoor uw verbindingsgrafiek beter wordt beschermd dan welk alternatief dan ook.", + "privacy-matters-overlay-card-1-p-4": "SimpleX Network beschermt de privacy van uw verbindingen beter dan welk alternatief dan ook en voorkomt dat uw sociale netwerk beschikbaar wordt voor bedrijven of organisaties. Zelfs wanneer gebruikers servers gebruiken die vooraf zijn geconfigureerd in SimpleX Chat-apps, weten serverbeheerders niet hoeveel gebruikers er zijn of hoeveel verbindingen ze hebben.", + "privacy-matters-overlay-card-2-p-2": "Om objectief te zijn en onafhankelijke beslissingen te nemen, moet je de controle hebben over je informatieruimte. Dit is alleen mogelijk als je gebruikmaakt van een privécommunicatienetwerk dat geen toegang heeft tot je sociale media-account.", + "privacy-matters-overlay-card-2-p-3": "SimpleX is het eerste netwerk dat standaard geen gebruikers-ID's gebruikt. Hierdoor is uw verbindingsgrafiek beter beschermd dan elk ander bekend alternatief.", "privacy-matters-overlay-card-3-p-1": "Iedereen zou zich zorgen moeten maken over de privacy en veiligheid van hun communicatie — onschuldige gesprekken kunnen je in gevaar brengen, zelfs als je niets te verbergen hebt.", - "simplex-unique-overlay-card-1-p-1": "In tegenstelling tot andere berichten platforms heeft SimpleX geen identifiers toegewezen aan de gebruikers . Het vertrouwt niet op telefoonnummers, domein gebaseerde adressen (zoals e-mail of XMPP), gebruikersnamen, openbare sleutels of zelfs willekeurige nummers om zijn gebruikers te identificeren — we weten niet ' hoeveel mensen onze SimpleX servers gebruiken.", + "simplex-unique-overlay-card-1-p-1": "In tegenstelling tot andere berichtennetwerken heeft SimpleX geen identificatiegegevens toegewezen aan de gebruikers. Het maakt geen gebruik van telefoonnummers, domeingebaseerde adressen (zoals e-mail of XMPP), gebruikersnamen, openbare sleutels of zelfs willekeurige getallen om gebruikers te identificeren – SimpleX-serverbeheerders weten niet hoeveel mensen hun servers gebruiken.", "privacy-matters-overlay-card-3-p-2": "Een van de meest schokkende verhalen is de ervaring van Mohamedou Ould Salahi beschreven in zijn memoires en getoond in The Mauritanian movie. Hij werd zonder proces in het Guantanamo kamp geplaatst en werd daar 15 jaar lang gemarteld na een telefoontje naar zijn familielid in Afghanistan, op verdenking van betrokkenheid bij aanslagen van 9/11, ook al woonde hij de afgelopen 10 jaar in Duitsland.", "privacy-matters-overlay-card-3-p-3": "Gewone mensen worden gearresteerd voor wat ze online delen, zelfs via hun 'anonieme' accounts, zelfs in democratische landen.", "privacy-matters-overlay-card-3-p-4": "Het is niet genoeg om een end-to-end versleutelde messenger te gebruiken, we zouden allemaal de messengers moeten gebruiken die de privacy van onze persoonlijke netwerken beschermen — met wie we verbonden zijn.", "simplex-unique-overlay-card-1-p-2": "Om berichten af te leveren gebruikt SimpleX paarsgewijze anonieme adressen van unidirectionele berichten wachtrijen, gescheiden voor ontvangen en verzonden berichten, meestal via verschillende servers.", - "simplex-unique-overlay-card-1-p-3": "Dit ontwerp beschermt de privacy van met wie u communiceert en verbergt deze voor SimpleX platform servers en voor waarnemers. Om uw IP-adres voor de servers te verbergen, kunt u verbinding maken met SimpleX servers via Tor .", - "simplex-unique-overlay-card-2-p-1": "Omdat je geen identificatie hebt op het SimpleX platform, kan niemand contact met je opnemen, tenzij je een eenmalig of tijdelijk gebruikers adres deelt, als een QR-code of een link.", + "simplex-unique-overlay-card-1-p-3": "Dit ontwerp beschermt de privacy van uw communicatiepartner door deze te verbergen voor SimpleX-netwerkservers en andere buitenstaanders. Om uw IP-adres voor de servers te verbergen, kunt u via Tor verbinding maken met SimpleX-servers.", + "simplex-unique-overlay-card-2-p-1": "Omdat u geen identificatiegegevens hebt op het SimpleX-netwerk, kan niemand contact met u opnemen, tenzij u een eenmalig of tijdelijk gebruikersadres deelt in de vorm van een QR-code of een link.", "simplex-unique-overlay-card-2-p-2": "Zelfs met het optionele gebruikers adres, hoewel het kan worden gebruikt om spam-contact verzoeken te verzenden, kunt u het wijzigen of volledig verwijderen zonder uw verbindingen te verliezen.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat slaat alle gebruikersgegevens alleen op client apparaten op met behulp van een draagbaar gecodeerd databaseformaat dat kan worden geëxporteerd en overgebracht naar elk ondersteund apparaat.", "simplex-unique-overlay-card-3-p-2": "De end-to-end versleutelde berichten worden tijdelijk op SimpleX relayservers bewaard totdat ze zijn ontvangen, waarna ze permanent worden verwijderd.", "simplex-unique-overlay-card-3-p-3": "In tegenstelling tot gefedereerde netwerkservers (e-mail, XMPP of Matrix), slaan SimpleX servers geen gebruikers account op, ze sturen alleen berichten door, waardoor de privacy van beide partijen wordt beschermd.", - "simplex-unique-overlay-card-4-p-1": "U kunt SimpleX gebruiken met uw eigen servers en nog steeds communiceren met mensen die de door ons geleverde vooraf geconfigureerde servers gebruiken.", - "simplex-unique-overlay-card-4-p-3": "Als u overweegt om voor het SimpleX platform te ontwikkelen, bijvoorbeeld de chatbot voor gebruikers van de SimpleX app, of de integratie van de SimpleX Chat bibliotheek in uw mobiele apps, get in touch for any advice en ondersteuning.", - "simplex-unique-card-1-p-1": "SimpleX beschermt de privacy van uw profiel, contacten en metadata en verbergt deze voor SimpleX platformservers en eventuele waarnemers.", + "simplex-unique-overlay-card-4-p-1": "U kunt SimpleX gebruiken met uw eigen servers en toch communiceren met mensen die de servers gebruiken die vooraf in de apps zijn geconfigureerd.", + "simplex-unique-overlay-card-4-p-3": "Als u overweegt om voor het SimpleX-netwerk te ontwikkelen, bijvoorbeeld de chatbot voor SimpleX-appgebruikers of de integratie van de SimpleX Chat-bibliotheek in uw mobiele apps, neem dan contact op voor advies en ondersteuning.", + "simplex-unique-card-1-p-1": "SimpleX beschermt de privacy van uw profiel, contacten en metagegevens door deze te verbergen voor de netwerkservers van SimpleX en andere buitenstaanders.", "enter-your-email-address": "Voer uw e-mail adres in", "get-simplex": "Verkrijg SimpleX desktop app", "tap-to-close": "Tik om te sluiten", @@ -199,13 +199,13 @@ "invitation-hero-header": "Je hebt een eenmalige link ontvangen om verbinding te maken met SimpleX Chat", "privacy-matters-overlay-card-2-p-1": "Nog niet zo lang geleden zagen we dat de grote verkiezingen werden gemanipuleerd door een gerenommeerd adviesbureau dat onze sociale grafieken gebruikte om ons beeld van de echte wereld te vervormen en onze stemmen te manipuleren.", "simplex-unique-overlay-card-3-p-4": "Er zijn geen identifiers of gecodeerde tekst tussen verzonden en ontvangen serververkeer — als iemand het waarneemt, kunnen ze niet gemakkelijk bepalen wie met wie communiceert, zelfs als TLS is gecompromitteerd.", - "simplex-unique-overlay-card-4-p-2": "Het SimpleX platform gebruikt een open protocol en biedt SDK om chatbots te maken, waardoor services kunnen worden geïmplementeerd waarmee gebruikers kunnen communiceren via SimpleX Chat apps — waar we' erg naar uitkijken om te zien welke SimpleX services u kunt bouwen.", - "simplex-unique-card-1-p-2": "In tegenstelling tot elk ander bestaand berichten platform, heeft SimpleX geen identificatiecodes toegewezen aan de gebruikers — zelfs geen willekeurige nummers .", + "simplex-unique-overlay-card-4-p-2": "SimpleX Network maakt gebruik van een open protocol en biedt SDK om chatbots te maken. Dit maakt de implementatie van services mogelijk waarmee gebruikers via SimpleX Chat-apps kunnen communiceren. We zijn erg benieuwd welke SimpleX-services jullie gaan bouwen.", + "simplex-unique-card-1-p-2": "In tegenstelling tot andere bestaande berichtennetwerken worden bij SimpleX geen identificatiegegevens aan de gebruikers toegewezen – zelfs geen willekeurige getallen.", "comparison-section-list-point-1": "Meestal op basis van een telefoonnummer, in sommige gevallen op gebruikersnamen", "comparison-section-list-point-2": "Op DNS gebaseerde adressen", "comparison-section-list-point-4": "Als de servers van de operator zijn gecompromitteerd. Controleer de beveiligingscode in Signal en enkele andere apps om dit te beperken", "comparison-section-list-point-5": "Beschermt de privacy van metagegevens van gebruikers niet", - "comparison-section-list-point-6": "Hoewel P2P wordt gedistribueerd, zijn ze niet federatief - ze werken als een enkel netwerk", + "comparison-section-list-point-6": "Hoewel P2P gedistribueerd is, is het niet gefedereerd — het functioneert als één enkel netwerk.", "see-here": "kijk hier", "comparison-section-list-point-7": "P2P netwerken hebben een centrale autoriteit of het hele netwerk kan worden aangetast", "comparison-section-list-point-3": "Publieke sleutel of een andere globale unieke ID", @@ -216,7 +216,7 @@ "guide-dropdown-8": "App instellingen", "guide-dropdown-9": "Connecties maken", "guide": "Handleiding", - "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-1": "SimpleX-netwerk", "docs-dropdown-3": "Toegang tot de chatdatabase", "docs-dropdown-2": "Toegang tot Android bestanden", "docs-dropdown-4": "Host SMP Server", diff --git a/website/langs/ru.json b/website/langs/ru.json index 335b19ab11..ea7766db1b 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -149,7 +149,7 @@ "back-to-top": "Вернуться к началу", "simplex-network-1-desc": "Все сообщения отправляются через серверы, что обеспечивает лучшую конфиденциальность метаданных и надежную асинхронную доставку сообщений, избегая при этом многих", "simplex-chat-repo": "Репозиторий SimpleX Chat", - "simplex-private-card-6-point-1": "Многие коммуникационные платформы уязвимы для MITM-атак со стороны серверов или сетевых провайдеров.", + "simplex-private-card-6-point-1": "Многие коммуникационные сети уязвимы для MITM-атак со стороны серверов или сетевых провайдеров.", "privacy-matters-3-overlay-1-linkText": "Конфиденциальность защищает Вашу свободу", "simplex-unique-overlay-card-1-p-2": "Для доставки сообщений SimpleX использует попарные, анонимные адреса однонаправленных очередей сообщений, раздельные для полученных и отправленных сообщений, обычно через разные серверы.", "simplex-unique-overlay-card-3-p-4": "Со стороны не видно разницы между отправлением или получением сообщений — если кто-то наблюдает за этим, он не cможет легко определить, кто с кем общается, даже если протокол TLS будет скомпрометирован.", @@ -218,7 +218,7 @@ "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для DRDoS атаки, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для создания трафика во всей сети.", "if-you-already-installed-simplex-chat-for-the-terminal": "Если Вы уже установили SimpleX Chat для терминала", "docs-dropdown-8": "Служба Каталогов SimpleX", - "simplex-private-card-1-point-1": "Протокол двойного обновления ключей —
\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома", + "simplex-private-card-1-point-1": "Протокол двойного обновления ключей —
\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома.", "simplex-private-card-8-point-1": "Серверы SimpleX действуют как узлы-миксеры с низкой задержкой — входящие и исходящие сообщения имеют разный порядок.", "simplex-unique-overlay-card-2-p-1": "Поскольку у Вас нет идентификатора в сети SimpleX, никто не сможет связаться с Вами, если Вы сами не предоставите одноразовый или временный адрес в виде QR-кода или ссылки.", "sign-up-to-receive-our-updates": "Подпишитесь на нашу рассылку новостей", diff --git a/website/langs/zh_Hant.json b/website/langs/zh_Hant.json index 324360cb19..1896fa3478 100644 --- a/website/langs/zh_Hant.json +++ b/website/langs/zh_Hant.json @@ -1,10 +1,10 @@ { - "home": "开始", + "home": "主頁", "developers": "開發人員", "reference": "參考", "blog": "部落格", "features": "特徵", - "why-simplex": "為什麼選擇SimpleX", + "why-simplex": "為什麼選擇 SimpleX", "simplex-privacy": "SimpleX 隱私", "simplex-network": "SimpleX 網路", "simplex-explained": "SimpleX 解釋", @@ -21,8 +21,8 @@ "chat-protocol": "聊天協定", "copyright-label": "© 2020-2025 SimpleX |開源專案", "donate": "捐助", - "simplex-explained-tab-1-p-1": "您可以創建聯繫人和群組,並進行雙向對話,就像在任何其他 Messenger 中一樣。", - "simplex-explained-tab-1-p-2": "它如何在沒有使用者配置檔標識符的情況下使用單向佇列?", + "simplex-explained-tab-1-p-1": "你可以建立聯絡人和群組,並進行雙向對話,就像在任何其他即時通訊軟件中一樣。", + "simplex-explained-tab-1-p-2": "它如何在沒有使用者個人檔案識別符的情況下使用單向佇列?", "simplex-explained-tab-3-p-1": "伺服器對每個佇列都有單獨的匿名憑證,並且不知道它們屬於哪些使用者。", "chat-bot-example": "聊天機器人示例", "simplex-chat-protocol": "SimpleX Chat 協定", @@ -103,7 +103,7 @@ "simplex-network-overlay-1-title": "與點對點訊息傳輸協定的比較", "simplex-private-1-title": "雙層端到端加密", "simplex-private-3-title": "經安全鑑權的TLS 傳送", - "simplex-private-4-title": "可選的經由Tor訪問", + "simplex-private-4-title": "選擇性
經由 Tor 訪問", "simplex-private-5-title": "多層內容填充", "feature-8-title": "隱身模式—SimpleX獨有", "simplex-private-6-title": "頻帶外密鑰交換", diff --git a/website/src/_includes/footer.html b/website/src/_includes/footer.html index 0cc1b16f8d..340473ab3e 100644 --- a/website/src/_includes/footer.html +++ b/website/src/_includes/footer.html @@ -24,7 +24,7 @@ class="text-grey-black dark:text-white text-[14px] font-medium leading-[28px] tracking-[0.01em] mb-3 flex items-center gap-1"> {{ "terms-and-privacy-policy" | i18n({}, lang ) | safe }} - {{ "donate" | i18n({}, lang ) | safe }} diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 6e69c559b0..fc7a80458c 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -125,7 +125,7 @@ - {{ "donate" | i18n({}, lang ) | safe }} @@ -316,4 +316,4 @@ const changeHeaderBg = ()=>{ } window.addEventListener('scroll',changeHeaderBg); - \ No newline at end of file + diff --git a/website/src/_includes/sections/join_simplex.html b/website/src/_includes/sections/join_simplex.html index 776ec33d45..ea59fd5b20 100644 --- a/website/src/_includes/sections/join_simplex.html +++ b/website/src/_includes/sections/join_simplex.html @@ -9,7 +9,7 @@

{{ "sign-up-to-receive-our-updates" | i18n({}, lang ) | safe }}

@@ -35,4 +35,4 @@ - \ No newline at end of file +