diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26979df834..9b0d5ad4b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,26 +99,43 @@ jobs: # ========================= build-linux: - name: "ubuntu-${{ matrix.os }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" + name: "ubuntu-${{ matrix.os }}-${{ matrix.arch }} (CLI,Desktop), GHC: ${{ matrix.ghc }}" needs: [maybe-release, variables] - runs-on: ubuntu-${{ matrix.os }} + runs-on: ${{ matrix.runner }} strategy: fail-fast: false matrix: include: - os: 22.04 + os_underscore: 22_04 + arch: x86_64 + runner: "ubuntu-22.04" ghc: "8.10.7" should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }} - os: 22.04 - ghc: ${{ needs.variables.outputs.GHC_VER }} - cli_asset_name: simplex-chat-ubuntu-22_04-x86-64 - desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb + os_underscore: 22_04 + arch: x86_64 + runner: "ubuntu-22.04" should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} - os: 24.04 - ghc: ${{ needs.variables.outputs.GHC_VER }} - cli_asset_name: simplex-chat-ubuntu-24_04-x86-64 - desktop_asset_name: simplex-desktop-ubuntu-24_04-x86_64.deb + os_underscore: 24_04 + arch: x86_64 + runner: "ubuntu-24.04" should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} + - os: 22.04 + os_underscore: 22_04 + arch: aarch64 + runner: "ubuntu-22.04-arm" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} + - os: 24.04 + os_underscore: 24_04 + arch: aarch64 + runner: "ubuntu-24.04-arm" + should_run: true + ghc: ${{ needs.variables.outputs.GHC_VER }} steps: - name: Checkout Code if: matrix.should_run == true @@ -143,7 +160,7 @@ jobs: path: | ~/.cabal/store dist-newstyle - key: ubuntu-${{ matrix.os }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} + key: ubuntu-${{ matrix.os }}-${{ matrix.arch }}-ghc${{ matrix.ghc }}-${{ hashFiles('cabal.project', 'simplex-chat.cabal') }} - name: Set up Docker Buildx if: matrix.should_run == true @@ -215,17 +232,17 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true shell: bash run: | - docker cp builder:/out/simplex-chat ./${{ matrix.cli_asset_name }} - path="${{ github.workspace }}/${{ matrix.cli_asset_name }}" + docker cp builder:/out/simplex-chat ./simplex-chat-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }} + path="${{ github.workspace }}/simplex-chat-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}" echo "bin_path=$path" >> $GITHUB_OUTPUT - echo "bin_hash=$(echo SHA2-256\(${{ matrix.cli_asset_name }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-256\(simplex-chat-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Upload CLI if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true uses: ./.github/actions/prepare-release with: bin_path: ${{ steps.linux_cli_prepare.outputs.bin_path }} - bin_name: ${{ matrix.cli_asset_name }} + bin_name: simplex-chat-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }} bin_hash: ${{ steps.linux_cli_prepare.outputs.bin_hash }} github_ref: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -241,16 +258,16 @@ jobs: 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_${{ matrix.arch }}.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 + echo "package_hash=$(echo SHA2-256\(simplex-desktop-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}.deb\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Upload Desktop uses: ./.github/actions/prepare-release if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true with: bin_path: ${{ steps.linux_desktop_build.outputs.package_path }} - bin_name: ${{ matrix.desktop_asset_name }} + bin_name: simplex-desktop-ubuntu-${{ matrix.os_underscore }}-${{ matrix.arch }}.deb bin_hash: ${{ steps.linux_desktop_build.outputs.package_hash }} github_ref: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} @@ -268,14 +285,14 @@ jobs: run: | path=$(echo ${{ github.workspace }}/apps/multiplatform/release/main/*imple*.AppImage) echo "appimage_path=$path" >> $GITHUB_OUTPUT - echo "appimage_hash=$(echo SHA2-256\(simplex-desktop-x86_64.AppImage\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + echo "appimage_hash=$(echo SHA2-256\(simplex-desktop-${{ matrix.arch }}.AppImage\)= $(openssl sha256 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Upload AppImage if: startsWith(github.ref, 'refs/tags/v') && matrix.os == '22.04' && matrix.should_run == true uses: ./.github/actions/prepare-release with: bin_path: ${{ steps.linux_appimage_build.outputs.appimage_path }} - bin_name: "simplex-desktop-x86_64.AppImage" + bin_name: "simplex-desktop-${{ matrix.arch }}.AppImage" bin_hash: ${{ steps.linux_appimage_build.outputs.appimage_hash }} github_ref: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 14aca94538..1aa3b649a0 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Jul 29, 2025 SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more.](./blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md) + [Jul 3, 2025 SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) [Mar 8, 2025. SimpleX Chat v6.3: new user experience and safety in public groups](./blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 91fa6c4280..2055a0ab99 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -18,6 +18,7 @@ enum ChatCommand: ChatCmdProtocol { case setAllContactReceipts(enable: Bool) case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiSetUserAutoAcceptMemberContacts(userId: Int64, enable: Bool) case apiHideUser(userId: Int64, viewPwd: String) case apiUnhideUser(userId: Int64, viewPwd: String) case apiMuteUser(userId: Int64) @@ -83,6 +84,7 @@ enum ChatCommand: ChatCmdProtocol { case apiAddGroupShortLink(groupId: Int64) case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) + case apiAcceptMemberContact(contactId: Int64) case apiTestProtoServer(userId: Int64, server: String) case apiGetServerOperators case apiSetServerOperators(operators: [ServerOperator]) @@ -198,6 +200,8 @@ enum ChatCommand: ChatCmdProtocol { case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): let umrs = userMsgReceiptSettings return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiSetUserAutoAcceptMemberContacts(userId, enable): + return "/_set accept member contacts \(userId) \(onOff(enable))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" case let .apiMuteUser(userId): return "/_mute user \(userId)" @@ -273,6 +277,7 @@ enum ChatCommand: ChatCmdProtocol { 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 .apiAcceptMemberContact(contactId): return "/_accept member contact @\(contactId)" case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" case .apiGetServerOperators: return "/_operators" case let .apiSetServerOperators(operators): return "/_operators \(encodeJSON(operators))" @@ -391,6 +396,7 @@ enum ChatCommand: ChatCmdProtocol { case .setAllContactReceipts: return "setAllContactReceipts" case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" + case .apiSetUserAutoAcceptMemberContacts: return "apiSetUserAutoAcceptMemberContacts" case .apiHideUser: return "apiHideUser" case .apiUnhideUser: return "apiUnhideUser" case .apiMuteUser: return "apiMuteUser" @@ -457,6 +463,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiAddGroupShortLink: return "apiAddGroupShortLink" case .apiCreateMemberContact: return "apiCreateMemberContact" case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" + case .apiAcceptMemberContact: return "apiAcceptMemberContact" case .apiTestProtoServer: return "apiTestProtoServer" case .apiGetServerOperators: return "apiGetServerOperators" case .apiSetServerOperators: return "apiSetServerOperators" @@ -912,6 +919,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + case memberContactAccepted(user: UserRef, contact: Contact) // receiving file responses case rcvFileAccepted(user: UserRef, chatItem: AChatItem) case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) @@ -960,6 +968,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case .groupLinkDeleted: "groupLinkDeleted" case .newMemberContact: "newMemberContact" case .newMemberContactSentInv: "newMemberContactSentInv" + case .memberContactAccepted: "memberContactAccepted" case .rcvFileAccepted: "rcvFileAccepted" case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled" case .standaloneFileInfo: "standaloneFileInfo" @@ -1004,6 +1013,7 @@ enum ChatResponse2: Decodable, ChatAPIResult { case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .memberContactAccepted(u, contact): return withUser(u, "contact: \(contact)") case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) case .rcvFileAcceptedSndCancelled: return noDetails case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index be1bea4469..ae9f21e34b 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -289,6 +289,10 @@ func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgRec try await sendCommandOkResp(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) } +func apiSetUserAutoAcceptMemberContacts(_ userId: Int64, enable: Bool) async throws { + try await sendCommandOkResp(.apiSetUserAutoAcceptMemberContacts(userId: userId, enable: enable)) +} + func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd)) } @@ -1916,6 +1920,33 @@ func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async throw r.unexpected } +func apiAcceptMemberContact(contactId: Int64) async -> Contact? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptMemberContact(contactId: contactId)) + if case let .result(.memberContactAccepted(_, contact)) = r { return contact } + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + return nil +} + +func acceptMemberContact(contactId: Int64, inProgress: Binding? = nil) async { + await MainActor.run { inProgress?.wrappedValue = true } + if let contact = await apiAcceptMemberContact(contactId: contactId) { + await MainActor.run { + ChatModel.shared.updateContact(contact) + NetworkModel.shared.setContactNetworkStatus(contact, .connected) + inProgress?.wrappedValue = false + } + if contact.sndReady { + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } + } + } else { + await MainActor.run { inProgress?.wrappedValue = false } + } +} + func apiGetVersion() throws -> CoreVersionInfo { let r: ChatResponse2 = try chatSendCmdSync(.showVersion) if case let .versionInfo(info, _, _) = r { return info } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 42a14c2395..77c1db341a 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -111,6 +111,7 @@ struct ChatInfoView: View { @State private var sendReceiptsUserDefault = true @State private var progressIndicator = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @State private var showSecrets: Set = [] enum ChatInfoViewAlert: Identifiable { case clearChatAlert @@ -397,10 +398,11 @@ struct ChatInfoView: View { .padding(.bottom, 2) } if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { - Text(descr) - .font(.subheadline) + let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) .multilineTextAlignment(.center) .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .center) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index e04584dfff..30ed3fa1a4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -120,13 +120,14 @@ struct MsgContentView: View { } } -func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding>? = nil) -> some View { +func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding>? = nil, centered: Bool = false, smallFont: Bool = false) -> some View { t.if(r.hasSecrets, transform: hiddenSecretsView) - .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets)) } + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, centered: centered, smallFont: smallFont)) } } +// smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body @inline(__always) -private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil) -> some View { +private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil, centered: Bool, smallFont: Bool) -> some View { return GeometryReader { g in Rectangle() .fill(Color.clear) @@ -135,17 +136,29 @@ private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding 100 { return } let framesetter = CTFramesetterCreateWithAttributedString(s as CFAttributedString) - let path = CGPath(rect: CGRect(origin: .zero, size: g.size), transform: nil) + let paddedSize = smallFont ? CGSize(width: g.size.width, height: g.size.height + 1.0) : g.size + let path = CGPath(rect: CGRect(origin: .zero, size: paddedSize), transform: nil) let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, s.length), path, nil) let point = CGPoint(x: event.location.x, y: g.size.height - event.location.y) // Flip y for UIKit var index: CFIndex? if let lines = CTFrameGetLines(frame) as? [CTLine] { var origins = [CGPoint](repeating: .zero, count: lines.count) CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins) + var maxWidth: CGFloat = 0 + if centered { + for line in lines { + let bounds = CTLineGetBoundsWithOptions(line, .useOpticalBounds) + if bounds.width > maxWidth { + maxWidth = bounds.width + } + } + } for i in 0 ..< lines.count { let bounds = CTLineGetBoundsWithOptions(lines[i], .useOpticalBounds) - if bounds.offsetBy(dx: origins[i].x, dy: origins[i].y).contains(point) { - index = CTLineGetStringIndexForPosition(lines[i], point) + let offsetX = centered ? (maxWidth - bounds.width) / 2 : 0 + if bounds.offsetBy(dx: origins[i].x + offsetX, dy: origins[i].y).contains(point) { + let relativePoint = centered ? CGPoint(x: point.x - origins[i].x - offsetX, y: point.y - origins[i].y) : point + index = CTLineGetStringIndexForPosition(lines[i], relativePoint) break } } @@ -207,6 +220,31 @@ private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) +@inline(__always) +func markdownText( + _ s: String, + textStyle: UIFont.TextStyle = .body, + sender: String? = nil, + preview: Bool = false, + mentions: [String: CIMention]? = nil, + userMemberId: String? = nil, + showSecrets: Set? = nil, + backgroundColor: Color +) -> MsgTextResult { + messageText( + s, + parseSimpleXMarkdown(s), + textStyle: textStyle, + sender: sender, + preview: preview, + mentions: mentions, + userMemberId: userMemberId, + showSecrets: showSecrets, + backgroundColor: UIColor(backgroundColor) + ) +} + + func messageText( _ text: String, _ formattedText: [FormattedText]?, @@ -335,6 +373,7 @@ func messageText( attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: "")) handleTaps = true } + case .unknown: () case .none: () } res.append(NSAttributedString(string: t, attributes: attrs)) diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index cd75d1b0cd..87c6ba92f8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -230,7 +230,7 @@ struct ChatItemInfoView: View { private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View { let backgroundColor = chatItemFrameColor(ci, theme) return VStack(alignment: .leading, spacing: 4) { - textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: UIColor(backgroundColor)) + textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil, backgroundColor: backgroundColor) .padding(.horizontal, 12) .padding(.vertical, 6) .background(backgroundColor) @@ -258,7 +258,7 @@ struct ChatItemInfoView: View { .frame(maxWidth: maxWidth, alignment: .leading) } - @ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: UIColor) -> some View { + @ViewBuilder private func textBubble(_ text: String, _ formattedText: [FormattedText]?, _ sender: String? = nil, backgroundColor: Color) -> some View { if text != "" { TextBubble(text: text, formattedText: formattedText, sender: sender, mentions: ci.mentions, userMemberId: userMemberId, backgroundColor: backgroundColor) } else { @@ -275,11 +275,11 @@ struct ChatItemInfoView: View { var sender: String? = nil var mentions: [String: CIMention]? var userMemberId: String? - var backgroundColor: UIColor + var backgroundColor: Color @State private var showSecrets: Set = [] var body: some View { - let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: backgroundColor) + let r = messageText(text, formattedText, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: UIColor(backgroundColor)) return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) } } @@ -305,7 +305,7 @@ struct ChatItemInfoView: View { private func quotedMsgView(_ qi: CIQuote, _ maxWidth: CGFloat) -> some View { let backgroundColor = quotedMsgFrameColor(qi, theme) return VStack(alignment: .leading, spacing: 4) { - textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: UIColor(backgroundColor)) + textBubble(qi.text, qi.formattedText, qi.getSender(nil), backgroundColor: backgroundColor) .padding(.horizontal, 12) .padding(.vertical, 6) .background(quotedMsgFrameColor(qi, theme)) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 4a4efb9adc..712a88114f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -403,7 +403,7 @@ struct ChatView: View { private func connectInProgressView(_ s: String) -> some View { VStack(spacing: 0) { Divider() - + HStack(spacing: 12) { ProgressView() Text(s) @@ -733,7 +733,7 @@ struct ChatView: View { return Group { if case .chatBanner = ci.content { VStack { - ChatBannerView(chat: chat) + ChatBannerView(chat: $chat) .padding(.bottom, 90) .padding(.top, 8) @@ -822,7 +822,8 @@ struct ChatView: View { struct ChatBannerView: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness - @ObservedObject var chat: Chat + @Binding @ObservedObject var chat: Chat + @State private var showSecrets: Set = [] var body: some View { let v = VStack(spacing: 8) { @@ -846,8 +847,8 @@ struct ChatView: View { } if let shortDescr = chat.chatInfo.shortDescr { - Text(shortDescr) - .font(.subheadline) + let r = markdownText(shortDescr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) .multilineTextAlignment(.center) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) @@ -957,6 +958,8 @@ struct ChatView: View { if !contact.sndReady && contact.active && !contact.sendMsgToConnect && !contact.nextAcceptContactRequest { contact.preparedContact?.uiConnLinkType == .con ? "contact should accept…" + : contact.contactGroupMemberId != nil + ? "contact should accept…" : "connecting…" } else { nil diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 53ce9f9bfc..876761a588 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -453,6 +453,8 @@ struct ComposeView: View { } } else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId { ContextContactRequestActionsView(contactRequestId: crId) + } else if let ct = contact, ct.nextAcceptContactRequest, let groupDirectInv = ct.groupDirectInv { + ContextMemberContactActionsView(contact: ct, groupDirectInv: groupDirectInv) } else { HStack (alignment: .center) { attachmentButton() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift new file mode 100644 index 0000000000..9a73b2b5d4 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift @@ -0,0 +1,110 @@ +// +// ContextMemberContactActionsView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 31.07.2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContextMemberContactActionsView: View { + @EnvironmentObject var theme: AppTheme + var contact: Contact + var groupDirectInv: GroupDirectInvitation + @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @State private var inProgress = false + @State private var progressByTimeout = false + + var body: some View { + VStack { + if groupDirectInv.memberRemoved { + Label("Member is deleted - can't accept request", systemImage: "info.circle") + .foregroundColor(theme.colors.secondary) + .font(.subheadline) + .padding(.horizontal) + .frame(maxWidth: .infinity, minHeight: 60) + } else { + HStack(spacing: 0) { + Button(role: .destructive, action: { showRejectMemberContactRequestAlert(contact) }) { + Label("Reject", systemImage: "multiply") + } + .frame(maxWidth: .infinity, minHeight: 60) + + Button { + acceptMemberContactRequest(contact, inProgress: $inProgress) + } label: { + Label("Accept", systemImage: "checkmark") + } + .frame(maxWidth: .infinity, minHeight: 60) + } + } + } + .disabled(inProgress || groupDirectInv.memberRemoved) + .frame(maxWidth: .infinity) + .background(ToolbarMaterial.material(toolbarMaterial)) + .opacity(progressByTimeout ? 0.4 : 1) + .overlay { + if progressByTimeout { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } + } +} + +func showRejectMemberContactRequestAlert(_ contact: Contact) { + showAlert( + NSLocalizedString("Reject contact request", comment: "alert title"), + message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in + deleteContact(contact) + }, + cancelAlertAction + ]} + ) +} + +private func deleteContact(_ contact: Contact) { + Task { + do { + _ = try await apiDeleteContact(id: contact.contactId, chatDeleteMode: .full(notify: false)) + await MainActor.run { + ChatModel.shared.removeChat(contact.id) + ChatModel.shared.chatId = nil + } + } catch let error { + logger.error("apiDeleteContact: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error deleting chat!", comment: "alert title"), + message: responseError(error) + ) + } + } + } +} + +func acceptMemberContactRequest(_ contact: Contact, inProgress: Binding? = nil) { + Task { + await acceptMemberContact(contactId: contact.contactId, inProgress: inProgress) + } +} + +#Preview { + ContextMemberContactActionsView( + contact: Contact.sampleData, + groupDirectInv: GroupDirectInvitation.sampleData + ) +} diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 376e83c2d8..872e65c7a3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -34,6 +34,7 @@ struct GroupChatInfoView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var searchText: String = "" @FocusState private var searchFocussed + @State private var showSecrets: Set = [] enum GroupChatInfoViewAlert: Identifiable { case deleteGroupAlert @@ -253,10 +254,11 @@ struct GroupChatInfoView: View { .padding(.bottom, 2) } if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { - Text(descr) - .font(.subheadline) + let r = markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, centered: true, smallFont: true) .multilineTextAlignment(.center) .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) } } .frame(maxWidth: .infinity, alignment: .center) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 5e7b8b9329..f58f2c213d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -59,7 +59,7 @@ struct GroupWelcomeView: View { } private func textPreview() -> some View { - let r = messageText(welcomeText, parseSimpleXMarkdown(welcomeText), sender: nil, mentions: nil, userMemberId: nil, showSecrets: showSecrets, backgroundColor: UIColor(theme.colors.background)) + let r = markdownText(welcomeText, showSecrets: showSecrets, backgroundColor: theme.colors.background) return msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index fc151d4889..4937bca20e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -130,26 +130,54 @@ struct ChatListNavLink: View { } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { - if contact.nextAcceptContactRequest, - let contactRequestId = contact.contactRequestId { - Button { - Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } - } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } - .tint(theme.colors.primary) - if !ChatModel.shared.addressShortLinkDataSet { + if contact.nextAcceptContactRequest { + if let contactRequestId = contact.contactRequestId { Button { - Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } - } label: { - SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } + } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } + .tint(theme.colors.primary) + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } + } label: { + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + } + .tint(.indigo) } - .tint(.indigo) + Button { + AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId)) + } label: { + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) + } + .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + deleteLabel + } + .tint(.red) } - Button { - AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId)) - } label: { - SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) - } - .tint(.red) } else { tagChatButton(chat) if !chat.chatItems.isEmpty { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 01c62ca34f..79f72e539a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -167,7 +167,7 @@ struct ChatPreviewView: View { let color = deleting ? theme.colors.secondary - : contact.nextAcceptContactRequest || contact.sendMsgToConnect + : (contact.nextAcceptContactRequest && !(contact.groupDirectInv?.memberRemoved ?? false)) || contact.sendMsgToConnect ? theme.colors.primary : !contact.sndReady ? theme.colors.secondary @@ -274,7 +274,7 @@ struct ChatPreviewView: View { private func messageDraft(_ draft: ComposeState) -> (Text, Bool) { let msg = draft.message - let r = messageText(msg, parseSimpleXMarkdown(msg), sender: nil, preview: true, mentions: draft.mentions, userMemberId: nil, showSecrets: nil, backgroundColor: UIColor(theme.colors.background)) + let r = markdownText(msg, preview: true, mentions: draft.mentions, backgroundColor: theme.colors.background) return (image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary) + attachment() + Text(AttributedString(r.string)), diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 253dca67c5..fcfcde2c07 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -81,14 +81,16 @@ struct ContactListNavLink: View { ItemsModel.shared.loadOpenChat(contact.id) } } label: { - contactRequestPreview() + contactRequestPreview(color: contact.groupDirectInv?.memberRemoved == true ? theme.colors.secondary : theme.colors.primary) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if let contactRequestId = contact.contactRequestId { Button { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } - } label: { Label("Accept", systemImage: "checkmark") } - .tint(theme.colors.primary) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) if !ChatModel.shared.addressShortLinkDataSet { Button { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } @@ -103,6 +105,33 @@ struct ContactListNavLink: View { Label("Reject", systemImage: "multiply") } .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) } } } @@ -254,7 +283,7 @@ struct ContactListNavLink: View { Button { showContactRequestDialog = true } label: { - contactRequestPreview() + contactRequestPreview(color: theme.colors.primary) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { @@ -285,12 +314,12 @@ struct ContactListNavLink: View { } } - func contactRequestPreview() -> some View { + func contactRequestPreview(color: Color) -> some View { HStack{ ProfileImage(imageStr: chat.chatInfo.image, size: 30) Text(chat.chatInfo.chatViewName) - .foregroundColor(.accentColor) + .foregroundColor(color) .lineLimit(1) Spacer() @@ -299,7 +328,7 @@ struct ContactListNavLink: View { .resizable() .scaledToFill() .frame(width: 14, height: 14) - .foregroundColor(.accentColor) + .foregroundColor(color) } } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 5a9ffe5c8b..06fe20a3fd 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -32,6 +32,8 @@ struct PrivacySettings: View { @State private var groupReceiptsReset = false @State private var groupReceiptsOverrides = 0 @State private var groupReceiptsDialogue = false + @State private var autoAcceptMemberContacts = false + @State private var autoAcceptMemberContactsReset = false @State private var alert: PrivacySettingsViewAlert? enum PrivacySettingsViewAlert: Identifiable { @@ -149,6 +151,18 @@ struct PrivacySettings: View { } } + Section { + settingsRow("checkmark", color: theme.colors.secondary) { + Toggle("Auto-accept", isOn: $autoAcceptMemberContacts) + } + } header: { + Text("Contact requests from groups") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("This setting is for your current profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + } + Section { settingsRow("person", color: theme.colors.secondary) { Toggle("Contacts", isOn: $contactReceipts) @@ -207,6 +221,13 @@ struct PrivacySettings: View { setOrAskSendReceiptsGroups(groupReceipts) } } + .onChange(of: autoAcceptMemberContacts) { _ in + if autoAcceptMemberContactsReset { + autoAcceptMemberContactsReset = false + } else { + setAutoAcceptGrpDirectInvs(autoAcceptMemberContacts) + } + } .onAppear { if let u = m.currentUser { if contactReceipts != u.sendRcptsContacts { @@ -217,6 +238,10 @@ struct PrivacySettings: View { groupReceiptsReset = true groupReceipts = u.sendRcptsSmallGroups } + if autoAcceptMemberContacts != u.autoAcceptMemberContacts { + autoAcceptMemberContactsReset = true + autoAcceptMemberContacts = u.autoAcceptMemberContacts + } } } .alert(item: $alert) { alert in @@ -333,6 +358,23 @@ struct PrivacySettings: View { } } + private func setAutoAcceptGrpDirectInvs(_ enable: Bool) { + Task { + do { + if let currentUser = m.currentUser { + try await apiSetUserAutoAcceptMemberContacts(currentUser.userId, enable: enable) + await MainActor.run { + var updatedUser = currentUser + updatedUser.autoAcceptMemberContacts = enable + m.updateUser(updatedUser) + } + } + } catch let error { + alert = .error(title: "Error setting auto-accept", error: "Error: \(responseError(error))") + } + } + } + private func simplexLockRow(_ value: LocalizedStringKey) -> some View { HStack { Text("SimpleX Lock") @@ -445,7 +487,7 @@ struct SimplexLockView: View { Toggle("Allow sharing", isOn: $allowShareExtension) } } - + if performLA && laMode == .passcode { Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) { Toggle(isOn: $selfDestruct) { 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 30c7de4167..c4f75e1989 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -465,6 +465,7 @@ time interval 1 year + 1 година delete after time @@ -565,10 +566,12 @@ swipe action Accept as member + Приеми като член alert action Accept as observer + Приеми като наблюдател alert action @@ -583,6 +586,7 @@ swipe action Accept contact request + Приеми заявка за контакт alert title @@ -1993,6 +1997,10 @@ This is your own one-time link! Настройки за контакт No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -3179,7 +3187,7 @@ chat item action Error deleting chat! Грешка при изтриването на чата! - No comment provided by engineer. + alert title Error deleting connection @@ -3354,6 +3362,10 @@ chat item action Грешка при изпращане на съобщение No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Грешка при настройването на потвърждениeто за доставка!! @@ -4617,6 +4629,10 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7716,6 +7732,10 @@ It can happen because of some bug or when the connection is compromised.Тази настройка се прилага за съобщения в текущия ви профил **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9115,11 +9135,6 @@ marked deleted chat item preview text свързан No comment provided by engineer. - - connected directly - свързан директно - rcv group event chat item - connecting свързване @@ -9685,6 +9700,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title 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 7b3cb6efc2..0ae8e0a70a 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1900,6 +1900,10 @@ This is your own one-time link! Předvolby kontaktů No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -3054,7 +3058,7 @@ chat item action Error deleting chat! Chyba při mazání chatu! - No comment provided by engineer. + alert title Error deleting connection @@ -3225,6 +3229,10 @@ chat item action Chyba při odesílání zprávy No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Chyba nastavování potvrzení o doručení! @@ -4448,6 +4456,10 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7470,6 +7482,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -8815,11 +8831,6 @@ marked deleted chat item preview text připojeno No comment provided by engineer. - - connected directly - připojeno přímo - rcv group event chat item - connecting připojování @@ -9377,6 +9388,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title 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 5a4f641db4..cda7e1ead0 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2085,6 +2085,10 @@ Das ist Ihr eigener Einmal-Link! Kontakt-Präferenzen No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden! @@ -3338,7 +3342,7 @@ chat item action Error deleting chat! Fehler beim Löschen des Chats! - No comment provided by engineer. + alert title Error deleting connection @@ -3525,6 +3529,10 @@ chat item action Fehler beim Senden der Nachricht No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Fehler beim Setzen von Empfangsbestätigungen! @@ -4862,6 +4870,10 @@ Das ist Ihr Link für die Gruppe %@! Mitglied inaktiv item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Mitglieder-Meldungen @@ -8224,6 +8236,10 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt. @@ -9708,11 +9724,6 @@ marked deleted chat item preview text Verbunden No comment provided by engineer. - - connected directly - Direkt miteinander verbunden - rcv group event chat item - connecting verbinde @@ -10302,6 +10313,14 @@ time to disappear Beitrittsanfrage abgelehnt No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect Zur Verbindung aufgefordert 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 6ce80012e0..947f0a2d6c 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2085,6 +2085,11 @@ This is your own one-time link! Contact preferences No comment provided by engineer. + + Contact requests from groups + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Contact will be deleted - this cannot be undone! @@ -3338,7 +3343,7 @@ chat item action Error deleting chat! Error deleting chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3525,6 +3530,11 @@ chat item action Error sending message No comment provided by engineer. + + Error setting auto-accept + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Error setting delivery receipts! @@ -4862,6 +4872,11 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Member reports @@ -8224,6 +8239,11 @@ It can happen because of some bug or when the connection is compromised.This setting applies to messages in your current chat profile **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Time to disappear is set only for new contacts. @@ -9708,11 +9728,6 @@ marked deleted chat item preview text connected No comment provided by engineer. - - connected directly - connected directly - rcv group event chat item - connecting connecting @@ -10302,6 +10317,16 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + requested connection + rcv group event chat item + + + requested connection from group %@ + requested connection from group %@ + rcv direct event chat item + requested to connect requested to connect 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 e6cdc17392..b58f716f8e 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1124,7 +1124,7 @@ swipe action Audio & video calls - Llamadas y videollamadas + Llamadas y Videollamadas No comment provided by engineer. @@ -2085,6 +2085,10 @@ This is your own one-time link! Preferencias de contacto No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! El contacto será eliminado. ¡No puede deshacerse! @@ -3017,7 +3021,7 @@ chat item action Enable disappearing messages by default. - Activa por defecto los mensajes temporaes. + Activa por defecto los mensajes temporales. No comment provided by engineer. @@ -3338,7 +3342,7 @@ chat item action Error deleting chat! ¡Error al eliminar chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3525,6 +3529,10 @@ chat item action Error al enviar mensaje No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! ¡Error al configurar confirmaciones de entrega! @@ -4167,7 +4175,7 @@ Error: %2$@ Hide: - Ocultar: + Oculta: No comment provided by engineer. @@ -4862,6 +4870,10 @@ This is your link for group %@! Miembro inactivo item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Informes de miembros @@ -8001,7 +8013,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. The address will be short, and your profile will be shared via the address. - La dirección será corta y tu perfil se compartirá mediante la dirección. + La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. alert message @@ -8224,6 +8236,10 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Esta configuración se aplica a los mensajes del perfil actual **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Mensajes temporales activados sólo para los contactos nuevos. @@ -9708,11 +9724,6 @@ marked deleted chat item preview text conectado No comment provided by engineer. - - connected directly - conectado directamente - rcv group event chat item - connecting conectando... @@ -10302,6 +10313,14 @@ time to disappear petición para unirse rechazada No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect solicitado para conectar 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 06beeaab5b..b5ba7bb864 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1871,6 +1871,10 @@ This is your own one-time link! Kontaktin asetukset No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -3023,7 +3027,7 @@ chat item action Error deleting chat! Virhe keskutelun poistamisessa! - No comment provided by engineer. + alert title Error deleting connection @@ -3193,6 +3197,10 @@ chat item action Virhe viestin lähettämisessä No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Virhe toimituskuittauksien asettamisessa! @@ -4416,6 +4424,10 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7434,6 +7446,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -8778,10 +8794,6 @@ marked deleted chat item preview text yhdistetty No comment provided by engineer. - - connected directly - rcv group event chat item - connecting yhdistää @@ -9339,6 +9351,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title 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 0be650389e..8072b5784b 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2069,6 +2069,10 @@ Il s'agit de votre propre lien unique ! Préférences de contact No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Le contact sera supprimé - il n'est pas possible de revenir en arrière ! @@ -3313,7 +3317,7 @@ chat item action Error deleting chat! Erreur lors de la suppression du chat ! - No comment provided by engineer. + alert title Error deleting connection @@ -3498,6 +3502,10 @@ chat item action Erreur lors de l'envoi du message No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Erreur lors de la configuration des accusés de réception ! @@ -4814,6 +4822,10 @@ Voici votre lien pour le groupe %@ ! Membre inactif item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -8082,6 +8094,10 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9538,11 +9554,6 @@ marked deleted chat item preview text connecté No comment provided by engineer. - - connected directly - s'est connecté.e de manière directe - rcv group event chat item - connecting connexion @@ -10116,6 +10127,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect demande à se connecter diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 494fa7304e..1339f0ce74 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -588,12 +588,12 @@ swipe action Accept contact request - Partnerkérés elfogadása + Partneri kapcsolatkérés elfogadása alert title Accept contact request from %@? - Elfogadja %@ partnerkérését? + Elfogadja %@ partneri kapcsolatkérését? notification body @@ -1029,7 +1029,7 @@ swipe action App passcode - Alkalmazás jelkód + Alkalmazásjelkód No comment provided by engineer. @@ -1169,7 +1169,7 @@ swipe action Auto-accept contact requests - Partnerkérések automatikus elfogadása + Partneri kapcsolatkérések automatikus elfogadása No comment provided by engineer. @@ -2085,6 +2085,10 @@ Ez a saját egyszer használható meghívója! Partnerbeállítások No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! A partner törölve lesz – ez a művelet nem vonható vissza! @@ -3217,7 +3221,7 @@ chat item action Error accepting contact request - Hiba történt a partnerkérés elfogadásakor + Hiba történt a partneri kapcsolatkérés elfogadásakor No comment provided by engineer. @@ -3338,7 +3342,7 @@ chat item action Error deleting chat! Hiba történt a csevegés törlésekor! - No comment provided by engineer. + alert title Error deleting connection @@ -3447,7 +3451,7 @@ chat item action Error rejecting contact request - Hiba történt a partnerkérés elutasításakor + Hiba történt a partneri kapcsolatkérés elutasításakor alert title @@ -3525,6 +3529,10 @@ chat item action Hiba történt az üzenet elküldésekor No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Hiba történt a kézbesítési jelentések beállításakor! @@ -4862,6 +4870,10 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Inaktív tag item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Tagok jelentései @@ -5289,7 +5301,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! New contact request - Új partnerkérés + Új partneri kapcsolatkérés notification @@ -6484,7 +6496,7 @@ swipe action Reject contact request - Elutasítás + Partneri kapcsolatkérés elutasítása alert title @@ -7025,7 +7037,7 @@ chat item action Send contact request? - Elküldi a partnerkérést? + Elküldi a partneri kapcsolatkérést? No comment provided by engineer. @@ -8224,6 +8236,10 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Az üzeneteltűnési idő csak az új partnerekre vonatkozik. @@ -9708,11 +9724,6 @@ marked deleted chat item preview text kapcsolódott No comment provided by engineer. - - connected directly - közvetlenül kapcsolódott - rcv group event chat item - connecting kapcsolódás @@ -10302,6 +10313,14 @@ time to disappear csatlakozási kérés elutasítva No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect függőben lévő kapcsolat 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 4839c81917..4b51e9dae3 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2085,6 +2085,10 @@ Questo è il tuo link una tantum! Preferenze del contatto No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Il contatto verrà eliminato - non è reversibile! @@ -3338,7 +3342,7 @@ chat item action Error deleting chat! Errore nell'eliminazione della chat! - No comment provided by engineer. + alert title Error deleting connection @@ -3525,6 +3529,10 @@ chat item action Errore nell'invio del messaggio No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Errore nell'impostazione delle ricevute di consegna! @@ -4862,6 +4870,10 @@ Questo è il tuo link per il gruppo %@! Membro inattivo item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Segnalazioni dei membri @@ -8224,6 +8236,10 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Il tempo di scomparsa è impostato solo per i contatti nuovi. @@ -9708,11 +9724,6 @@ marked deleted chat item preview text connesso/a No comment provided by engineer. - - connected directly - si è connesso/a direttamente - rcv group event chat item - connecting in connessione @@ -10302,6 +10313,14 @@ time to disappear richiesta di entrare rifiutata No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect richiesto di connettersi 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 ea67e54f1d..743c5392e6 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1938,6 +1938,10 @@ This is your own one-time link! 連絡先の設定 No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -3097,7 +3101,7 @@ chat item action Error deleting chat! チャット削除にエラー発生! - No comment provided by engineer. + alert title Error deleting connection @@ -3267,6 +3271,10 @@ chat item action メッセージ送信にエラー発生 No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! No comment provided by engineer. @@ -4489,6 +4497,10 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7504,6 +7516,10 @@ It can happen because of some bug or when the connection is compromised.この設定は現在のチャットプロフィール **%@** のメッセージに適用されます。 No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -8849,10 +8865,6 @@ marked deleted chat item preview text 接続中 No comment provided by engineer. - - connected directly - rcv group event chat item - connecting 接続待ち @@ -9410,6 +9422,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title 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 e10e0ea533..6d65bd070f 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2077,6 +2077,10 @@ Dit is uw eigen eenmalige link! Contact voorkeuren No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt! @@ -3324,7 +3328,7 @@ chat item action Error deleting chat! Fout bij verwijderen gesprek! - No comment provided by engineer. + alert title Error deleting connection @@ -3509,6 +3513,10 @@ chat item action Fout bij verzenden van bericht No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Fout bij het instellen van ontvangst bevestiging! @@ -4842,6 +4850,10 @@ Dit is jouw link voor groep %@! Lid inactief item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Ledenrapporten @@ -8173,6 +8185,10 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Deze instelling is van toepassing op berichten in je huidige chatprofiel **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9642,11 +9658,6 @@ marked deleted chat item preview text verbonden No comment provided by engineer. - - connected directly - direct verbonden - rcv group event chat item - connecting Verbinden @@ -10233,6 +10244,14 @@ time to disappear verzoek tot toetreding afgewezen No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect verzocht om verbinding te maken diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 6b0f71a191..5dd679dfff 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2050,6 +2050,10 @@ To jest twój jednorazowy link! Preferencje kontaktu No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Kontakt zostanie usunięty – nie można tego cofnąć! @@ -3268,7 +3272,7 @@ chat item action Error deleting chat! Błąd usuwania czatu! - No comment provided by engineer. + alert title Error deleting connection @@ -3448,6 +3452,10 @@ chat item action Błąd wysyłania wiadomości No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Błąd ustawiania potwierdzeń dostawy! @@ -4743,6 +4751,10 @@ To jest twój link do grupy %@! Członek nieaktywny item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7965,6 +7977,10 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9404,11 +9420,6 @@ marked deleted chat item preview text połączony No comment provided by engineer. - - connected directly - połącz bezpośrednio - rcv group event chat item - connecting łączenie @@ -9982,6 +9993,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 2053bf9ffe..156c341f8a 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2085,6 +2085,11 @@ This is your own one-time link! Предпочтения контакта No comment provided by engineer. + + Contact requests from groups + Запросы на соединение из групп + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Контакт будет удален — это нельзя отменить! @@ -3338,7 +3343,7 @@ chat item action Error deleting chat! Ошибка при удалении чата! - No comment provided by engineer. + alert title Error deleting connection @@ -3525,6 +3530,11 @@ chat item action Ошибка при отправке сообщения No comment provided by engineer. + + Error setting auto-accept + Ошибка при установке автоприёма запросов + No comment provided by engineer. + Error setting delivery receipts! Ошибка настроек отчётов о доставке! @@ -4861,6 +4871,11 @@ This is your link for group %@! Член неактивен item status text + + Member is deleted - can't accept request + Член группы удалён - невозможно принять запрос + No comment provided by engineer. + Member reports Сообщения о нарушениях @@ -8223,6 +8238,11 @@ It can happen because of some bug or when the connection is compromised.Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + Эта настройка применяется к Вашему текущему профилю чата **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. Время удаления устанавливается только для новых контактов. @@ -9707,11 +9727,6 @@ marked deleted chat item preview text соединение установлено No comment provided by engineer. - - connected directly - соединен(а) напрямую - rcv group event chat item - connecting соединяется @@ -10301,6 +10316,16 @@ time to disappear запрос на вступление отклонён No comment provided by engineer. + + requested connection + запрос на соединение + rcv group event chat item + + + requested connection from group %@ + запрос на соединение из группы %@ + rcv direct event chat item + requested to connect запрошено соединение 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 d4b377dcd7..b7fbf073bd 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1862,6 +1862,10 @@ This is your own one-time link! การกําหนดลักษณะการติดต่อ No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! No comment provided by engineer. @@ -3008,7 +3012,7 @@ chat item action Error deleting chat! เกิดข้อผิดพลาดในการลบแชท! - No comment provided by engineer. + alert title Error deleting connection @@ -3178,6 +3182,10 @@ chat item action เกิดข้อผิดพลาดในการส่งข้อความ No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง! @@ -4399,6 +4407,10 @@ This is your link for group %@! Member inactive item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports chat feature @@ -7406,6 +7418,10 @@ It can happen because of some bug or when the connection is compromised.การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@** No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -8746,10 +8762,6 @@ marked deleted chat item preview text เชื่อมต่อสำเร็จ No comment provided by engineer. - - connected directly - rcv group event chat item - connecting กำลังเชื่อมต่อ @@ -9306,6 +9318,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title 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 548ce346e0..cd07746337 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2085,6 +2085,10 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi tercihleri No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Kişiler silinecek - bu geri alınamaz ! @@ -3336,7 +3340,7 @@ chat item action Error deleting chat! Sohbet silinirken hata oluştu! - No comment provided by engineer. + alert title Error deleting connection @@ -3523,6 +3527,10 @@ chat item action Mesaj gönderilirken hata oluştu No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Görüldü ayarlanırken hata oluştu! @@ -4859,6 +4867,10 @@ Bu senin grup için bağlantın %@! Üye inaktif item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports Üye raporları @@ -8214,6 +8226,10 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9689,11 +9705,6 @@ marked deleted chat item preview text bağlanıldı No comment provided by engineer. - - connected directly - doğrudan bağlandı - rcv group event chat item - connecting bağlanılıyor @@ -10283,6 +10294,14 @@ time to disappear katılma isteği reddedildi No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect bağlanma isteği gönderildi 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 df7a91c234..9b6ff96324 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -588,6 +588,7 @@ swipe action Accept contact request + Прийняти запит на контакт alert title @@ -648,6 +649,7 @@ swipe action Add message + Додати повідомлення placeholder for sending contact request @@ -1252,10 +1254,12 @@ swipe action Bio + Біо No comment provided by engineer. Bio too large + Біографія занадто велика alert title @@ -1350,6 +1354,7 @@ swipe action Business connection + Бізнес-зв'язок No comment provided by engineer. @@ -1403,6 +1408,7 @@ swipe action Can't change profile + Не вдається змінити профіль alert title @@ -1630,6 +1636,7 @@ set passcode view Chat with members before they join. + Спілкуйтеся з учасниками до того, як вони приєднаються. No comment provided by engineer. @@ -1869,6 +1876,7 @@ set passcode view Connect faster! 🚀 + Підключайтеся швидше! 🚀 No comment provided by engineer. @@ -2077,6 +2085,10 @@ This is your own one-time link! Налаштування контактів No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! Контакт буде видалено - це неможливо скасувати! @@ -2194,6 +2206,7 @@ This is your own one-time link! Create your address + Створіть свою адресу No comment provided by engineer. @@ -2657,6 +2670,7 @@ swipe action Description too large + Опис занадто великий alert title @@ -2967,6 +2981,7 @@ chat item action Empty message! + Порожнє повідомлення! No comment provided by engineer. @@ -3006,6 +3021,7 @@ chat item action Enable disappearing messages by default. + Увімкнути зникаючі повідомлення за замовчуванням. No comment provided by engineer. @@ -3225,6 +3241,7 @@ chat item action Error adding short link + Помилка додавання короткого посилання No comment provided by engineer. @@ -3234,6 +3251,7 @@ chat item action Error changing chat profile + Помилка зміни профілю чату alert title @@ -3324,7 +3342,7 @@ chat item action Error deleting chat! Помилка видалення чату! - No comment provided by engineer. + alert title Error deleting connection @@ -3408,6 +3426,7 @@ chat item action Error opening group + Помилка відкриття групи No comment provided by engineer. @@ -3432,6 +3451,7 @@ chat item action Error rejecting contact request + Помилка відхилення запиту на контакт alert title @@ -3509,6 +3529,10 @@ chat item action Помилка надсилання повідомлення No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! Помилка встановлення підтвердження доставлення! @@ -3711,6 +3735,8 @@ snd error text File is blocked by server operator: %@. + Файл заблоковано оператором сервера: +%@. file error text @@ -4084,6 +4110,7 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. + Профіль групи було змінено. Якщо ви збережете його, оновлений профіль буде надіслано учасникам групи. alert message @@ -4635,6 +4662,7 @@ This is your link for group %@! Keep your chats clean + Підтримуйте чистоту в чатах No comment provided by engineer. @@ -4694,6 +4722,7 @@ This is your link for group %@! Less traffic on mobile networks. + Менше трафіку в мобільних мережах. No comment provided by engineer. @@ -4738,6 +4767,7 @@ This is your link for group %@! List name... + Ім'я в списку... No comment provided by engineer. @@ -4752,6 +4782,7 @@ This is your link for group %@! Loading profile… + Завантаження профілю… in progress text @@ -4831,6 +4862,7 @@ This is your link for group %@! Member admission + Прийом членів No comment provided by engineer. @@ -4838,8 +4870,13 @@ This is your link for group %@! Користувач неактивний item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports + Повідомлення учасників chat feature @@ -4869,6 +4906,7 @@ This is your link for group %@! Member will join the group, accept member? + Учасник приєднається до групи, прийняти учасника? alert message @@ -4883,6 +4921,7 @@ This is your link for group %@! Members can report messsages to moderators. + Учасники можуть повідомляти повідомлення модераторам. No comment provided by engineer. @@ -4912,6 +4951,7 @@ This is your link for group %@! Mention members 👋 + Згадуйте учасників 👋 No comment provided by engineer. @@ -4946,6 +4986,7 @@ This is your link for group %@! Message instantly once you tap Connect. + Миттєве повідомлення, щойно ви натиснете "Підключитися". No comment provided by engineer. @@ -5025,6 +5066,7 @@ This is your link for group %@! Messages are protected by **end-to-end encryption**. + Повідомлення захищені **наскрізним шифруванням**. No comment provided by engineer. @@ -5034,6 +5076,7 @@ This is your link for group %@! Messages in this chat will never be deleted. + Повідомлення в цьому чаті ніколи не будуть видалені. alert message @@ -5138,6 +5181,7 @@ This is your link for group %@! More + Більше swipe action @@ -5172,6 +5216,7 @@ This is your link for group %@! Mute all + Вимкнути звук для всіх notification label action @@ -5226,6 +5271,7 @@ This is your link for group %@! New + Новий token status text @@ -5280,6 +5326,7 @@ This is your link for group %@! New group role: Moderator + Нова роль у групі: Модератор No comment provided by engineer. @@ -5299,6 +5346,7 @@ This is your link for group %@! New member wants to join the group. + Новий учасник хоче приєднатися до групи. rcv group event chat item @@ -5328,18 +5376,22 @@ This is your link for group %@! No chats + Без чатів No comment provided by engineer. No chats found + Чати не знайдено No comment provided by engineer. No chats in list %@ + Немає чатів у списку %@ No comment provided by engineer. No chats with members + Ніяких чатів з учасниками No comment provided by engineer. @@ -5394,6 +5446,7 @@ This is your link for group %@! No message + Немає повідомлення No comment provided by engineer. @@ -5423,6 +5476,7 @@ This is your link for group %@! No private routing session + Немає приватного сеансу маршрутизації alert title @@ -5457,10 +5511,12 @@ This is your link for group %@! No token! + Немає токена! alert title No unread chats + Немає непрочитаних чатів No comment provided by engineer. @@ -5475,6 +5531,7 @@ This is your link for group %@! Notes + Нотатки No comment provided by engineer. @@ -5499,6 +5556,7 @@ This is your link for group %@! Notifications error + Помилка сповіщень alert title @@ -5508,6 +5566,7 @@ This is your link for group %@! Notifications status + Статус сповіщень alert title @@ -5597,10 +5656,12 @@ Requires compatible VPN. Only sender and moderators see it + Тільки відправник і модератори бачать це No comment provided by engineer. Only you and moderators see it + Тільки ви та модератори бачать це No comment provided by engineer. @@ -5690,6 +5751,7 @@ Requires compatible VPN. Open link? + Відкрите посилання? alert title @@ -5699,22 +5761,27 @@ Requires compatible VPN. Open new chat + Відкрити новий чат new chat action Open new group + Відкрити нову групу new chat action Open to accept + Відкрити для прийняття No comment provided by engineer. Open to connect + Відкрито для підключення No comment provided by engineer. Open to join + Відкрито для приєднання No comment provided by engineer. @@ -5764,6 +5831,7 @@ Requires compatible VPN. Organize chats into lists + Організовуйте чати в списки No comment provided by engineer. @@ -5959,18 +6027,22 @@ 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 Please wait for token to be registered. + Будь ласка, зачекайте, поки токен буде зареєстровано. token info @@ -6025,6 +6097,7 @@ Error: %@ Privacy policy and conditions of use. + Політика конфіденційності та умови використання. No comment provided by engineer. @@ -6034,6 +6107,7 @@ Error: %@ Private chats, groups and your contacts are not accessible to server operators. + Приватні чати, групи та ваші контакти недоступні для операторів сервера. No comment provided by engineer. @@ -6043,6 +6117,7 @@ Error: %@ Private media file names. + Приватні імена медіа-файлів. No comment provided by engineer. @@ -6072,6 +6147,7 @@ Error: %@ Private routing timeout + Тайм-аут приватної маршрутизації alert title @@ -6126,6 +6202,7 @@ Error: %@ Prohibit reporting messages to moderators. + Заборонити повідомлення модераторам. No comment provided by engineer. @@ -6177,6 +6254,7 @@ Enable in *Network & servers* settings. Protocol background timeout + Фоновий тайм-аут протоколу No comment provided by engineer. @@ -6391,14 +6469,17 @@ Enable in *Network & servers* settings. Register + Зареєструватися No comment provided by engineer. Register notification token? + Зареєструвати токен сповіщення? token info Registered + Зареєстровано token status text @@ -6420,6 +6501,7 @@ swipe action Reject member? + Відхилити учасника? alert title @@ -6464,6 +6546,7 @@ swipe action Removes messages and blocks members. + Видаляє повідомлення та блокує користувачів. No comment provided by engineer. @@ -6503,46 +6586,57 @@ swipe action Report + Повідомити chat item action Report content: only group moderators will see it. + Повідомити про контент: тільки модератори групи побачать це. report reason Report member profile: only group moderators will see it. + Повідомити про профіль учасника: тільки модератори групи побачать це. report reason Report other: only group moderators will see it. + Повідомити інше: тільки модератори групи побачать це. report reason Report reason? + Причина повідомлення? No comment provided by engineer. Report sent to moderators + Повідомлення надіслано модераторам alert title Report spam: only group moderators will see it. + Повідомити про спам: тільки модератори групи побачать це. report reason Report violation: only group moderators will see it. + Повідомити про порушення: тільки модератори групи побачать це. report reason Report: %@ + Повідомити: %@ report in notification Reporting messages to moderators is prohibited. + Повідомляти про повідомлення модераторам заборонено. No comment provided by engineer. Reports + Звіти No comment provided by engineer. @@ -6637,14 +6731,17 @@ swipe action Review group members + Учасники групи оглядів No comment provided by engineer. Review members + Схвалювати учасників admission stage Review members before admitting ("knocking"). + Перевірка учасників перед тим, як їх прийняти («стукіт»). admission stage description @@ -6705,10 +6802,12 @@ chat item action Save (and notify members) + Зберегти (і повідомити учасникам) alert button Save admission settings? + Зберегти налаштування входу? alert title @@ -6738,10 +6837,12 @@ chat item action Save group profile? + Зберегти профіль групи? alert title Save list + Зберегти список No comment provided by engineer. @@ -6936,6 +7037,7 @@ chat item action Send contact request? + Надіслати запит на контакт? No comment provided by engineer. @@ -6990,6 +7092,7 @@ chat item action Send private reports + Надсилайте приватні звіти No comment provided by engineer. @@ -7004,10 +7107,12 @@ chat item action Send request + Надіслати запит No comment provided by engineer. Send request without message + Надіслати запит без повідомлення No comment provided by engineer. @@ -7022,6 +7127,7 @@ chat item action Send your private feedback to groups. + Надсилайте свої приватні відгуки до груп. No comment provided by engineer. @@ -7221,6 +7327,7 @@ chat item action Set chat name… + Назвати чат… No comment provided by engineer. @@ -7245,10 +7352,12 @@ chat item action Set member admission + Встановити прийом учасників No comment provided by engineer. Set message expiration in chats. + Встановлюйте термін придатності повідомлень у чатах. No comment provided by engineer. @@ -7268,6 +7377,7 @@ chat item action Set profile bio and welcome message. + Налаштуйте біографію профілю та вітальне повідомлення. No comment provided by engineer. @@ -7343,10 +7453,12 @@ chat item action Share old address + Поділіться старою адресою alert button Share old link + Поділіться старим посиланням alert button @@ -7371,18 +7483,22 @@ chat item action Share your address + Поділіться своєю адресою No comment provided by engineer. Short SimpleX address + Коротка адреса SimpleX No comment provided by engineer. Short description + Короткий опис No comment provided by engineer. Short link + Коротке посилання No comment provided by engineer. @@ -7492,6 +7608,7 @@ chat item action SimpleX channel link + Посилання на канал SimpleX simplex link type @@ -7598,6 +7715,7 @@ chat item action Spam + Спам blocking reason report reason @@ -7688,6 +7806,7 @@ report reason Storage + Зберігання No comment provided by engineer. @@ -7747,6 +7866,7 @@ report reason TCP connection bg timeout + Таймаут TCP-з'єднання bg No comment provided by engineer. @@ -7756,6 +7876,7 @@ report reason TCP port for messaging + TCP-порт для повідомлень No comment provided by engineer. @@ -7785,10 +7906,12 @@ report reason Tap Connect to chat + Натисніть Підключитися до чату No comment provided by engineer. Tap Connect to send request + Натисніть Підключитися, щоб відправити запит No comment provided by engineer. @@ -7798,6 +7921,7 @@ report reason Tap Join group + Натисніть Приєднатися до групи No comment provided by engineer. @@ -7847,6 +7971,7 @@ report reason Test notifications + Тестові сповіщення No comment provided by engineer. @@ -7888,6 +8013,7 @@ It can happen because of some bug or when the connection is compromised. The address will be short, and your profile will be shared via the address. + Адреса буде короткою, і ваш профіль буде доступний за цією адресою. alert message @@ -7952,6 +8078,7 @@ It can happen because of some bug or when the connection is compromised. The link will be short, and group profile will be shared via the link. + Посилання буде коротким, а профіль групи буде поширюватися за посиланням. alert message @@ -8051,6 +8178,7 @@ It can happen because of some bug or when the connection is compromised. This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted. + Цю дію не можна скасувати — повідомлення, надіслані та отримані в цьому чаті раніше за обраний час, будуть видалені. alert message @@ -8090,6 +8218,7 @@ It can happen because of some bug or when the connection is compromised. 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. @@ -8099,6 +8228,7 @@ It can happen because of some bug or when the connection is compromised. This message was deleted or not received yet. + Це повідомлення було видалено або ще не отримано. No comment provided by engineer. @@ -8106,8 +8236,13 @@ It can happen because of some bug or when the connection is compromised.Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**. No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. + Час зникнення встановлюється тільки для нових контактів. No comment provided by engineer. @@ -8199,6 +8334,7 @@ You will be prompted to complete authentication before this feature is enabled.< To use another profile after connection attempt, delete the chat and use the link again. + Щоб використовувати інший профіль після спроби з'єднання, видаліть чат і скористайтеся посиланням знову. alert message @@ -8223,6 +8359,7 @@ You will be prompted to complete authentication before this feature is enabled.< Token status: %@. + Статус токена: %@. token status @@ -8399,6 +8536,7 @@ To connect, please ask your contact to create another connection link and check Unsupported connection link + Несумісне посилання для підключення No comment provided by engineer. @@ -8428,6 +8566,7 @@ To connect, please ask your contact to create another connection link and check Updated conditions + Оновлені умови No comment provided by engineer. @@ -8437,14 +8576,17 @@ To connect, please ask your contact to create another connection link and check Upgrade + Оновлення alert button Upgrade address + Адреса оновлення No comment provided by engineer. Upgrade address? + Змінити адресу? alert message @@ -8454,14 +8596,17 @@ To connect, please ask your contact to create another connection link and check Upgrade group link? + Оновити посилання на групу? alert message Upgrade link + Посилання для оновлення No comment provided by engineer. Upgrade your address + Поновіть свою адресу No comment provided by engineer. @@ -8516,10 +8661,12 @@ To connect, please ask your contact to create another connection link and check 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. @@ -8559,6 +8706,7 @@ To connect, please ask your contact to create another connection link and check Use incognito profile + Використовуйте профіль інкогніто No comment provided by engineer. @@ -8603,6 +8751,7 @@ To connect, please ask your contact to create another connection link and check Use web port + Використовувати веб-порт No comment provided by engineer. @@ -8797,6 +8946,7 @@ To connect, please ask your contact to create another connection link and check Welcome your contacts 👋 + Вітаємо ваші контакти 👋 No comment provided by engineer. @@ -9068,6 +9218,7 @@ Repeat join request? You can view your reports in Chat with admins. + Ви можете переглянути свої звіти у чаті з адміністраторами. alert message @@ -9149,10 +9300,12 @@ Repeat connection request? You should receive notifications. + Ви повинні отримувати сповіщення. token info You will be able to send messages **only after your request is accepted**. + Ви зможете надсилати повідомлення **тільки після того, як ваш запит буде прийнято**. No comment provided by engineer. @@ -9222,6 +9375,7 @@ Repeat connection request? Your business contact + Ваш діловий контакт No comment provided by engineer. @@ -9251,6 +9405,7 @@ Repeat connection request? Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + Ваш чат було переміщено на %@, але при перенаправленні на профіль сталася несподівана помилка. alert message @@ -9260,6 +9415,7 @@ Repeat connection request? Your contact + Ваш контакт No comment provided by engineer. @@ -9294,6 +9450,7 @@ Repeat connection request? Your group + Ваша група No comment provided by engineer. @@ -9383,6 +9540,7 @@ Repeat connection request? accepted %@ + прийнято %@ rcv group event chat item @@ -9397,6 +9555,7 @@ Repeat connection request? accepted you + прийняв(ла) вас rcv group event chat item @@ -9421,6 +9580,7 @@ Repeat connection request? all + усі member criteria value @@ -9440,6 +9600,7 @@ Repeat connection request? archived report + архівование повідомлення No comment provided by engineer. @@ -9510,6 +9671,7 @@ marked deleted chat item preview text can't send messages + не можна надсилати No comment provided by engineer. @@ -9562,11 +9724,6 @@ marked deleted chat item preview text з'єднаний No comment provided by engineer. - - connected directly - з'єднані безпосередньо - rcv group event chat item - connecting з'єднання @@ -9619,10 +9776,12 @@ marked deleted chat item preview text contact deleted + контакт видалено No comment provided by engineer. contact disabled + контакт вимкнено No comment provided by engineer. @@ -9637,10 +9796,12 @@ marked deleted chat item preview text contact not ready + контакт не готовий No comment provided by engineer. contact should accept… + контакт повинен прийняти… No comment provided by engineer. @@ -9811,6 +9972,7 @@ pref value group + група shown on group welcome message @@ -9820,6 +9982,7 @@ pref value group is deleted + групу видалено No comment provided by engineer. @@ -9944,6 +10107,7 @@ pref value member has old version + учасник використовує застарілу версію No comment provided by engineer. @@ -9978,6 +10142,7 @@ pref value moderator + модератор member role @@ -10012,6 +10177,7 @@ pref value not synchronized + не синхронізовано No comment provided by engineer. @@ -10069,14 +10235,17 @@ time to disappear pending + очікує No comment provided by engineer. pending approval + очікує на схвалення No comment provided by engineer. pending review + очікує на схвалення No comment provided by engineer. @@ -10096,6 +10265,7 @@ time to disappear rejected + відхилено No comment provided by engineer. @@ -10120,6 +10290,7 @@ time to disappear removed from group + видалено з групи No comment provided by engineer. @@ -10134,12 +10305,22 @@ time to disappear request is sent + запит відправлено No comment provided by engineer. request to join rejected + запит на приєднання відхилено No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect запит на підключення @@ -10147,10 +10328,12 @@ time to disappear review + перегляд No comment provided by engineer. reviewed by admins + схвалено адміністраторами No comment provided by engineer. @@ -10339,6 +10522,7 @@ last received msg: %2$@ you accepted this member + ви прийняли цього учасника snd group event chat item @@ -10479,6 +10663,7 @@ last received msg: %2$@ From %d chat(s) + З %d чату(ів) notification body diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 84b93d60e4..fce4207e84 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 @@ -2070,6 +2070,10 @@ This is your own one-time link! 联系人偏好设置 No comment provided by engineer. + + Contact requests from groups + No comment provided by engineer. + Contact will be deleted - this cannot be undone! 联系人将被删除-这是无法撤消的! @@ -3313,7 +3317,7 @@ chat item action Error deleting chat! 删除聊天错误! - No comment provided by engineer. + alert title Error deleting connection @@ -3498,6 +3502,10 @@ chat item action 发送消息错误 No comment provided by engineer. + + Error setting auto-accept + No comment provided by engineer. + Error setting delivery receipts! 设置送达回执出错! @@ -4830,6 +4838,10 @@ This is your link for group %@! 成员不活跃 item status text + + Member is deleted - can't accept request + No comment provided by engineer. + Member reports 成员举报 @@ -8089,6 +8101,10 @@ It can happen because of some bug or when the connection is compromised.此设置适用于您当前聊天资料 **%@** 中的消息。 No comment provided by engineer. + + This setting is for your current profile **%@**. + No comment provided by engineer. + Time to disappear is set only for new contacts. No comment provided by engineer. @@ -9519,11 +9535,6 @@ marked deleted chat item preview text 已连接 No comment provided by engineer. - - connected directly - 已直连 - rcv group event chat item - connecting 连接中 @@ -10097,6 +10108,14 @@ time to disappear request to join rejected No comment provided by engineer. + + requested connection + rcv group event chat item + + + requested connection from group %@ + rcv direct event chat item + requested to connect chat list item title diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings index ceace71e34..6dd5248aeb 100644 --- a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/uk.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.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 5fb83b24b4..a46384d99b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 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.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.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 */; }; @@ -189,6 +189,7 @@ 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; + 64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */; }; 8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C01E9C02C8EFC33008A4B0A /* objc.m */; }; 8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C01E9BF2C8EFBB6008A4B0A /* objc.h */; }; 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; @@ -543,8 +544,8 @@ 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.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.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 = ""; }; @@ -555,6 +556,7 @@ 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = ""; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; + 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMemberContactActionsView.swift; sourceTree = ""; }; 8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = ""; }; 8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = ""; }; 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; @@ -704,8 +706,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -790,8 +792,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.1.2-6lmVvH3zwUh1WnLIod6T9y.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a */, ); path = Libraries; sourceTree = ""; @@ -1090,6 +1092,7 @@ 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */, 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */, 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */, + 64FC8F9C2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift */, ); path = ComposeMessage; sourceTree = ""; @@ -1521,6 +1524,7 @@ 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */, 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */, 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */, + 64FC8F9D2E3B6DEF0068F384 /* ContextMemberContactActionsView.swift in Sources */, 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */, 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */, 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */, @@ -1995,7 +1999,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2020,7 +2024,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2045,7 +2049,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2070,7 +2074,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2087,11 +2091,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2107,11 +2111,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2132,7 +2136,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2147,7 +2151,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2169,7 +2173,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2184,7 +2188,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2206,7 +2210,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2232,7 +2236,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2257,7 +2261,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2283,7 +2287,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2308,7 +2312,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2323,7 +2327,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2342,7 +2346,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 292; + CURRENT_PROJECT_VERSION = 293; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2357,7 +2361,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.4.1; + MARKETING_VERSION = 6.4.2; 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 9b7f4ac5ee..db370efdc1 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -39,6 +39,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var showNtfs: Bool public var sendRcptsContacts: Bool public var sendRcptsSmallGroups: Bool + public var autoAcceptMemberContacts: Bool public var viewPwdHash: UserPwdHash? public var uiThemes: ThemeModeOverrides? @@ -65,7 +66,8 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { activeOrder: 0, showNtfs: true, sendRcptsContacts: true, - sendRcptsSmallGroups: false + sendRcptsSmallGroups: false, + autoAcceptMemberContacts: false ) } @@ -1759,8 +1761,9 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { var chatTs: Date? public var preparedContact: PreparedContact? public var contactRequestId: Int64? - var contactGroupMemberId: Int64? + public var contactGroupMemberId: Int64? var contactGrpInvSent: Bool + public var groupDirectInv: GroupDirectInvitation? public var chatTags: [Int64] public var chatItemTTL: Int64? public var uiThemes: ThemeModeOverrides? @@ -1774,7 +1777,11 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var nextConnectPrepared: Bool { active && preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) } public var profileChangeProhibited: Bool { activeConn != nil } - public var nextAcceptContactRequest: Bool { active && contactRequestId != nil && (activeConn == nil || activeConn?.connStatus == .new) } + public var nextAcceptContactRequest: Bool { + active && + (contactRequestId != nil || groupDirectInv != nil) && + (activeConn == nil || activeConn?.connStatus == .new || activeConn?.connStatus == .prepared) + } public var sendMsgToConnect: Bool { nextSendGrpInv || nextConnectPrepared } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } @@ -1843,6 +1850,26 @@ public struct PreparedContact: Decodable, Hashable { public var uiConnLinkType: ConnectionMode } +public struct GroupDirectInvitation: Decodable, Hashable { + public var groupDirectInvLink: String + public var fromGroupId_: Int64? + public var fromGroupMemberId_: Int64? + public var fromGroupMemberConnId_: Int64? + public var groupDirectInvStartedConnection: Bool + + public var memberRemoved: Bool { + fromGroupId_ == nil || fromGroupMemberId_ == nil || fromGroupMemberConnId_ == nil + } + + public static let sampleData = GroupDirectInvitation( + groupDirectInvLink: "simplex_link", + fromGroupId_: 1, + fromGroupMemberId_: 1, + fromGroupMemberConnId_: 1, + groupDirectInvStartedConnection: false + ) +} + public enum ConnectionMode: String, Decodable, Hashable { case inv case con @@ -2895,6 +2922,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { switch rcvDirectEvent { case .contactDeleted: return false case .profileUpdated: return false + case .groupInvLinkReceived: return true } case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent): switch rcvGroupEvent { @@ -4421,6 +4449,7 @@ public enum Format: Decodable, Equatable, Hashable { case mention(memberName: String) case email case phone + case unknown public var isSimplexLink: Bool { get { @@ -4702,11 +4731,14 @@ public struct E2EEInfo: Decodable, Hashable { public enum RcvDirectEvent: Decodable, Hashable { case contactDeleted case profileUpdated(fromProfile: Profile, toProfile: Profile) + case groupInvLinkReceived(groupProfile: Profile) var text: String { switch self { case .contactDeleted: return NSLocalizedString("deleted contact", comment: "rcv direct event chat item") case let .profileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile) + case let .groupInvLinkReceived(groupProfile): + return String.localizedStringWithFormat(NSLocalizedString("requested connection from group %@", comment: "rcv direct event chat item"), groupProfile.displayName) } } @@ -4771,7 +4803,7 @@ public enum RcvGroupEvent: Decodable, Hashable { case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item") case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") 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 .memberCreatedContact: return NSLocalizedString("requested connection", 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") } diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 049eccd96d..a5fa71198b 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -283,6 +283,9 @@ time interval */ time interval */ "1 week" = "1 седмица"; +/* delete after time */ +"1 year" = "1 година"; + /* No comment provided by engineer. */ "1-time link" = "Еднократен линк"; @@ -337,12 +340,21 @@ alert action swipe action */ "Accept" = "Приеми"; +/* alert action */ +"Accept as member" = "Приеми като член"; + +/* alert action */ +"Accept as observer" = "Приеми като наблюдател"; + /* No comment provided by engineer. */ "Accept conditions" = "Приеми условията"; /* No comment provided by engineer. */ "Accept connection request?" = "Приемане на заявка за връзка?"; +/* alert title */ +"Accept contact request" = "Приеми заявка за контакт"; + /* notification body */ "Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; @@ -1026,9 +1038,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Свързано настолно устройство"; -/* rcv group event chat item */ -"connected directly" = "свързан директно"; - /* No comment provided by engineer. */ "Connected to desktop" = "Свързан с настолно устройство"; @@ -1746,7 +1755,7 @@ chat item action */ /* alert title */ "Error deleting chat database" = "Грешка при изтриване на базата данни"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Грешка при изтриването на чата!"; /* No comment provided by engineer. */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index 74b883c9dd..cd90568881 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -746,9 +746,6 @@ set passcode view */ /* No comment provided by engineer. */ "connected" = "připojeno"; -/* rcv group event chat item */ -"connected directly" = "připojeno přímo"; - /* No comment provided by engineer. */ "connecting" = "připojování"; @@ -1354,7 +1351,7 @@ swipe action */ /* alert title */ "Error deleting chat database" = "Chyba při mazání databáze chatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Chyba při mazání chatu!"; /* No comment provided by engineer. */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 43631652a5..350729e01c 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Verbundener Desktop"; -/* rcv group event chat item */ -"connected directly" = "Direkt miteinander verbunden"; - /* No comment provided by engineer. */ "Connected servers" = "Verbundene Server"; @@ -2256,7 +2253,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Fehler beim Löschen des Chats mit dem Mitglied"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Fehler beim Löschen des Chats!"; /* No comment provided by engineer. */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 3ea27e1674..80c3d7cc93 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -723,7 +723,7 @@ swipe action */ "attempts" = "intentos"; /* No comment provided by engineer. */ -"Audio & video calls" = "Llamadas y videollamadas"; +"Audio & video calls" = "Llamadas y Videollamadas"; /* No comment provided by engineer. */ "Audio and video calls" = "Llamadas y videollamadas"; @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Ordenador conectado"; -/* rcv group event chat item */ -"connected directly" = "conectado directamente"; - /* No comment provided by engineer. */ "Connected servers" = "Servidores conectados"; @@ -2014,7 +2011,7 @@ chat item action */ "Enable camera access" = "Permitir acceso a la cámara"; /* No comment provided by engineer. */ -"Enable disappearing messages by default." = "Activa por defecto los mensajes temporaes."; +"Enable disappearing messages by default." = "Activa por defecto los mensajes temporales."; /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Habilitar Flux en la configuración de Red y servidores para mejorar la privacidad de los metadatos."; @@ -2256,7 +2253,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Error al eliminar el chat con el miembro"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "¡Error al eliminar chat!"; /* No comment provided by engineer. */ @@ -2769,7 +2766,7 @@ snd error text */ "Hide profile" = "Ocultar perfil"; /* No comment provided by engineer. */ -"Hide:" = "Ocultar:"; +"Hide:" = "Oculta:"; /* No comment provided by engineer. */ "History" = "Historial"; @@ -5293,7 +5290,7 @@ report reason */ "Thanks to the users – contribute via Weblate!" = "¡Agradecimiento a los colaboradores! Puedes contribuir a través de Weblate"; /* alert message */ -"The address will be short, and your profile will be shared via the address." = "La dirección será corta y tu perfil se compartirá mediante la dirección."; +"The address will be short, and your profile will be shared via the address." = "La dirección pasará a ser corta y tu perfil será compartido mediante la dirección."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index e22baa5caf..b1a21bfc69 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -1264,7 +1264,7 @@ swipe action */ /* alert title */ "Error deleting chat database" = "Virhe keskustelujen tietokannan poistamisessa"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Virhe keskutelun poistamisessa!"; /* No comment provided by engineer. */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 37b34e7107..4502e4a292 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1212,9 +1212,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Bureau connecté"; -/* rcv group event chat item */ -"connected directly" = "s'est connecté.e de manière directe"; - /* No comment provided by engineer. */ "Connected servers" = "Serveurs connectés"; @@ -2154,7 +2151,7 @@ chat item action */ /* alert title */ "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!" = "Erreur lors de la suppression du chat !"; /* No comment provided by engineer. */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 0ea847a632..6b902b5850 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -359,10 +359,10 @@ swipe action */ "Accept connection request?" = "Elfogadja a kapcsolódási kérést?"; /* alert title */ -"Accept contact request" = "Partnerkérés elfogadása"; +"Accept contact request" = "Partneri kapcsolatkérés elfogadása"; /* notification body */ -"Accept contact request from %@?" = "Elfogadja %@ partnerkérését?"; +"Accept contact request from %@?" = "Elfogadja %@ partneri kapcsolatkérését?"; /* alert action swipe action */ @@ -660,7 +660,7 @@ swipe action */ "App icon" = "Alkalmazásikon"; /* No comment provided by engineer. */ -"App passcode" = "Alkalmazás jelkód"; +"App passcode" = "Alkalmazásjelkód"; /* No comment provided by engineer. */ "App passcode is replaced with self-destruct passcode." = "Az alkalmazásjelkód helyettesítve lesz egy önmegsemmisítő jelkóddal."; @@ -756,7 +756,7 @@ swipe action */ "Auto-accept" = "Automatikus elfogadás"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Partnerkérések automatikus elfogadása"; +"Auto-accept contact requests" = "Partneri kapcsolatkérések automatikus elfogadása"; /* No comment provided by engineer. */ "Auto-accept images" = "Képek automatikus elfogadása"; @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Társított számítógép"; -/* rcv group event chat item */ -"connected directly" = "közvetlenül kapcsolódott"; - /* No comment provided by engineer. */ "Connected servers" = "Kapcsolódott kiszolgálók"; @@ -2185,7 +2182,7 @@ chat item action */ "Error accepting conditions" = "Hiba történt a feltételek elfogadásakor"; /* No comment provided by engineer. */ -"Error accepting contact request" = "Hiba történt a partnerkérés elfogadásakor"; +"Error accepting contact request" = "Hiba történt a partneri kapcsolatkérés elfogadásakor"; /* alert title */ "Error accepting member" = "Hiba a tag befogadásakor"; @@ -2256,7 +2253,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Hiba a taggal való csevegés törlésekor"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Hiba történt a csevegés törlésekor!"; /* No comment provided by engineer. */ @@ -2323,7 +2320,7 @@ chat item action */ "Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor"; /* alert title */ -"Error rejecting contact request" = "Hiba történt a partnerkérés elutasításakor"; +"Error rejecting contact request" = "Hiba történt a partneri kapcsolatkérés elutasításakor"; /* alert title */ "Error removing member" = "Hiba történt a tag eltávolításakor"; @@ -3525,7 +3522,7 @@ snd error text */ "New chat experience 🎉" = "Új csevegési élmény 🎉"; /* notification */ -"New contact request" = "Új partnerkérés"; +"New contact request" = "Új partneri kapcsolatkérés"; /* notification */ "New contact:" = "Új kapcsolat:"; @@ -4309,7 +4306,7 @@ swipe action */ "Reject (sender NOT notified)" = "Elutasítás (a feladó NEM kap értesítést)"; /* alert title */ -"Reject contact request" = "Elutasítás"; +"Reject contact request" = "Partneri kapcsolatkérés elutasítása"; /* alert title */ "Reject member?" = "Elutasítja a tagot?"; @@ -4688,7 +4685,7 @@ chat item action */ "Send a live message - it will update for the recipient(s) as you type it" = "Élő üzenet küldése – az üzenet a címzett(ek) számára valós időben frissül, ahogy Ön beírja az üzenetet"; /* No comment provided by engineer. */ -"Send contact request?" = "Elküldi a partnerkérést?"; +"Send contact request?" = "Elküldi a partneri kapcsolatkérést?"; /* No comment provided by engineer. */ "Send delivery receipts to" = "A kézbesítési jelentéseket a következő címre kell küldeni"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 33dcc1e6bf..7165a63668 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Desktop connesso"; -/* rcv group event chat item */ -"connected directly" = "si è connesso/a direttamente"; - /* No comment provided by engineer. */ "Connected servers" = "Server connessi"; @@ -2256,7 +2253,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Errore di eliminazione della chat con il membro"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Errore nell'eliminazione della chat!"; /* No comment provided by engineer. */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index d0639caf95..2c93438a8f 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1474,7 +1474,7 @@ swipe action */ /* alert title */ "Error deleting chat database" = "チャットデータベース削除にエラー発生"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "チャット削除にエラー発生!"; /* No comment provided by engineer. */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index ca488c7291..c5dedbd0d4 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1251,9 +1251,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Verbonden desktop"; -/* rcv group event chat item */ -"connected directly" = "direct verbonden"; - /* No comment provided by engineer. */ "Connected servers" = "Verbonden servers"; @@ -2211,7 +2208,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Fout bij het verwijderen van chat met lid"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Fout bij verwijderen gesprek!"; /* No comment provided by engineer. */ diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 338df147db..4e8a26d05c 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1176,9 +1176,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Połączony komputer"; -/* rcv group event chat item */ -"connected directly" = "połącz bezpośrednio"; - /* No comment provided by engineer. */ "Connected servers" = "Połączone serwery"; @@ -2025,7 +2022,7 @@ chat item action */ /* alert title */ "Error deleting chat database" = "Błąd usuwania bazy danych czatu"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Błąd usuwania czatu!"; /* No comment provided by engineer. */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 56f7f5e48c..d797ebfbbd 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Подключенный компьютер"; -/* rcv group event chat item */ -"connected directly" = "соединен(а) напрямую"; - /* No comment provided by engineer. */ "Connected servers" = "Подключенные серверы"; @@ -1413,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Предпочтения контакта"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Запросы на соединение из групп"; + /* No comment provided by engineer. */ "contact should accept…" = "контакт должен принять…"; @@ -2256,7 +2256,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Ошибка при удалении чата с членом группы"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Ошибка при удалении чата!"; /* No comment provided by engineer. */ @@ -2370,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Ошибка при отправке сообщения"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Ошибка при установке автоприёма запросов"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Ошибка настроек отчётов о доставке!"; @@ -3251,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Член неактивен"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Член группы удалён - невозможно принять запрос"; + /* chat feature */ "Member reports" = "Сообщения о нарушениях"; @@ -4425,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "запрос на вступление отклонён"; +/* rcv group event chat item */ +"requested connection" = "запрос на соединение"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "запрос на соединение из группы %@"; + /* chat list item title */ "requested to connect" = "запрошено соединение"; @@ -5433,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Эта настройка применяется к Вашему текущему профилю чата **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Время удаления устанавливается только для новых контактов."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index bfd127665e..03c1bdfed1 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -1216,7 +1216,7 @@ swipe action */ /* alert title */ "Error deleting chat database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "เกิดข้อผิดพลาดในการลบแชท!"; /* No comment provided by engineer. */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 22b7cd0c3a..e93e824921 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1275,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Bilgisayara bağlandı"; -/* rcv group event chat item */ -"connected directly" = "doğrudan bağlandı"; - /* No comment provided by engineer. */ "Connected servers" = "Bağlı sunucular"; @@ -2250,7 +2247,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Üye ile sohbet silme hatası"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Sohbet silinirken hata oluştu!"; /* No comment provided by engineer. */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 5231a82b1f..6c4cdf1510 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -358,6 +358,9 @@ swipe action */ /* No comment provided by engineer. */ "Accept connection request?" = "Прийняти запит на підключення?"; +/* alert title */ +"Accept contact request" = "Прийняти запит на контакт"; + /* notification body */ "Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; @@ -368,6 +371,9 @@ swipe action */ /* alert title */ "Accept member" = "Прийняти учасника"; +/* rcv group event chat item */ +"accepted %@" = "прийнято %@"; + /* call status */ "accepted call" = "прийнято виклик"; @@ -377,6 +383,9 @@ swipe action */ /* chat list item title */ "accepted invitation" = "прийняте запрошення"; +/* rcv group event chat item */ +"accepted you" = "прийняв(ла) вас"; + /* No comment provided by engineer. */ "Acknowledged" = "Визнано"; @@ -398,6 +407,9 @@ swipe action */ /* No comment provided by engineer. */ "Add list" = "Додати список"; +/* placeholder for sending contact request */ +"Add message" = "Додати повідомлення"; + /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; @@ -473,6 +485,9 @@ swipe action */ /* chat item text */ "agreeing encryption…" = "узгодження шифрування…"; +/* member criteria value */ +"all" = "усі"; + /* No comment provided by engineer. */ "All" = "Всі"; @@ -695,6 +710,9 @@ swipe action */ /* No comment provided by engineer. */ "Archived contacts" = "Архівні контакти"; +/* No comment provided by engineer. */ +"archived report" = "архівование повідомлення"; + /* No comment provided by engineer. */ "Archiving database" = "Архівування бази даних"; @@ -794,6 +812,12 @@ swipe action */ /* No comment provided by engineer. */ "Better user experience" = "Покращений користувацький досвід"; +/* No comment provided by engineer. */ +"Bio" = "Біо"; + +/* alert title */ +"Bio too large" = "Біографія занадто велика"; + /* No comment provided by engineer. */ "Black" = "Чорний"; @@ -861,6 +885,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Business chats" = "Ділові чати"; +/* No comment provided by engineer. */ +"Business connection" = "Бізнес-зв'язок"; + /* No comment provided by engineer. */ "Businesses" = "Бізнеси"; @@ -900,6 +927,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't call member" = "Не вдається зателефонувати користувачеві"; +/* alert title */ +"Can't change profile" = "Не вдається змінити профіль"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Не вдається запросити контакт!"; @@ -909,6 +939,9 @@ 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 new chat action */ @@ -1053,6 +1086,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat with member" = "Чат з учасником"; +/* No comment provided by engineer. */ +"Chat with members before they join." = "Спілкуйтеся з учасниками до того, як вони приєднаються."; + /* No comment provided by engineer. */ "Chats" = "Чати"; @@ -1200,6 +1236,9 @@ set passcode view */ /* No comment provided by engineer. */ "Connect automatically" = "Підключення автоматично"; +/* No comment provided by engineer. */ +"Connect faster! 🚀" = "Підключайтеся швидше! 🚀"; + /* No comment provided by engineer. */ "Connect to desktop" = "Підключення до комп'ютера"; @@ -1236,9 +1275,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "Підключений робочий стіл"; -/* rcv group event chat item */ -"connected directly" = "з'єднані безпосередньо"; - /* No comment provided by engineer. */ "Connected servers" = "Підключені сервери"; @@ -1341,9 +1377,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"; @@ -1362,9 +1404,15 @@ 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" = "Налаштування контактів"; +/* No comment provided by engineer. */ +"contact should accept…" = "контакт повинен прийняти…"; + /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Контакт буде видалено - це неможливо скасувати!"; @@ -1434,6 +1482,9 @@ set passcode view */ /* No comment provided by engineer. */ "Create SimpleX address" = "Створіть адресу SimpleX"; +/* No comment provided by engineer. */ +"Create your address" = "Створіть свою адресу"; + /* No comment provided by engineer. */ "Create your profile" = "Створіть свій профіль"; @@ -1739,6 +1790,9 @@ swipe action */ /* No comment provided by engineer. */ "Description" = "Опис"; +/* alert title */ +"Description too large" = "Опис занадто великий"; + /* No comment provided by engineer. */ "Desktop address" = "Адреса робочого столу"; @@ -1941,6 +1995,9 @@ chat item action */ /* No comment provided by engineer. */ "Edit group profile" = "Редагування профілю групи"; +/* No comment provided by engineer. */ +"Empty message!" = "Порожнє повідомлення!"; + /* No comment provided by engineer. */ "Enable" = "Увімкнути"; @@ -1953,6 +2010,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "Увімкніть доступ до камери"; +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "Увімкнути зникаючі повідомлення за замовчуванням."; + /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Увімкніть Flux у налаштуваннях мережі та серверів для кращої конфіденційності метаданих."; @@ -2133,9 +2193,15 @@ chat item action */ /* alert title */ "Error adding server" = "Помилка додавання сервера"; +/* No comment provided by engineer. */ +"Error adding short link" = "Помилка додавання короткого посилання"; + /* No comment provided by engineer. */ "Error changing address" = "Помилка зміни адреси"; +/* alert title */ +"Error changing chat profile" = "Помилка зміни профілю чату"; + /* No comment provided by engineer. */ "Error changing connection profile" = "Помилка при зміні профілю з'єднання"; @@ -2187,7 +2253,7 @@ chat item action */ /* alert title */ "Error deleting chat with member" = "Помилка при видаленні чату з учасником"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "Помилка видалення чату!"; /* No comment provided by engineer. */ @@ -2238,6 +2304,9 @@ chat item action */ /* No comment provided by engineer. */ "Error opening chat" = "Помилка відкриття чату"; +/* No comment provided by engineer. */ +"Error opening group" = "Помилка відкриття групи"; + /* alert title */ "Error receiving file" = "Помилка отримання файлу"; @@ -2250,6 +2319,9 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Помилка під час реєстрації для отримання сповіщень"; +/* alert title */ +"Error rejecting contact request" = "Помилка відхилення запиту на контакт"; + /* alert title */ "Error removing member" = "Помилка видалення учасника"; @@ -2417,6 +2489,9 @@ snd error text */ /* alert message */ "File errors:\n%@" = "Помилки файлів:\n%@"; +/* file error text */ +"File is blocked by server operator:\n%@." = "Файл заблоковано оператором сервера:\n%@."; + /* file error text */ "File not found - most likely file was deleted or cancelled." = "Файл не знайдено - найімовірніше, файл було видалено або скасовано."; @@ -2591,6 +2666,9 @@ snd error text */ /* message preview */ "Good morning!" = "Доброго ранку!"; +/* shown on group welcome message */ +"group" = "група"; + /* No comment provided by engineer. */ "Group" = "Група"; @@ -2621,6 +2699,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" = "Посилання на групу"; @@ -2645,6 +2726,9 @@ snd error text */ /* snd group event chat item */ "group profile updated" = "оновлено профіль групи"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Профіль групи було змінено. Якщо ви збережете його, оновлений профіль буде надіслано учасникам групи."; + /* No comment provided by engineer. */ "Group welcome message" = "Привітальне повідомлення групи"; @@ -3020,6 +3104,9 @@ snd error text */ /* alert title */ "Keep unused invitation?" = "Зберігати невикористані запрошення?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Підтримуйте чистоту в чатах"; + /* No comment provided by engineer. */ "Keep your connections" = "Зберігайте свої зв'язки"; @@ -3053,6 +3140,9 @@ snd error text */ /* rcv group event chat item */ "left" = "ліворуч"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Менше трафіку в мобільних мережах."; + /* email subject */ "Let's talk in SimpleX Chat" = "Поговоримо в чаті SimpleX"; @@ -3077,6 +3167,9 @@ snd error text */ /* No comment provided by engineer. */ "List name and emoji should be different for all lists." = "Назва списку та емодзі повинні бути різними для всіх списків."; +/* No comment provided by engineer. */ +"List name..." = "Ім'я в списку..."; + /* No comment provided by engineer. */ "LIVE" = "НАЖИВО"; @@ -3086,6 +3179,9 @@ snd error text */ /* No comment provided by engineer. */ "Live messages" = "Живі повідомлення"; +/* in progress text */ +"Loading profile…" = "Завантаження профілю…"; + /* No comment provided by engineer. */ "Local name" = "Місцева назва"; @@ -3140,12 +3236,21 @@ snd error text */ /* 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." = "Роль учасника буде змінено на \"%@\". Усі учасники чату отримають сповіщення."; @@ -3161,12 +3266,18 @@ snd error text */ /* 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." = "Учасники групи можуть надсилати прямі повідомлення."; @@ -3182,6 +3293,9 @@ snd error text */ /* No comment provided by engineer. */ "Members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; +/* No comment provided by engineer. */ +"Mention members 👋" = "Згадуйте учасників 👋"; + /* No comment provided by engineer. */ "Menus" = "Меню"; @@ -3203,6 +3317,9 @@ snd error text */ /* item status text */ "Message forwarded" = "Повідомлення переслано"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Миттєве повідомлення, щойно ви натиснете \"Підключитися\"."; + /* item status description */ "Message may be delivered later if member becomes active." = "Повідомлення може бути доставлене пізніше, якщо користувач стане активним."; @@ -3251,9 +3368,15 @@ snd error text */ /* No comment provided by engineer. */ "Messages & files" = "Повідомлення та файли"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Повідомлення захищені **наскрізним шифруванням**."; + /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Повідомлення від %@ будуть показані!"; +/* alert message */ +"Messages in this chat will never be deleted." = "Повідомлення в цьому чаті ніколи не будуть видалені."; + /* No comment provided by engineer. */ "Messages received" = "Отримані повідомлення"; @@ -3326,9 +3449,15 @@ snd error text */ /* marked deleted chat item preview text */ "moderated by %@" = "модерується %@"; +/* member role */ +"moderator" = "модератор"; + /* time unit */ "months" = "місяців"; +/* swipe action */ +"More" = "Більше"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Незабаром буде ще більше покращень!"; @@ -3347,6 +3476,9 @@ snd error text */ /* notification label action */ "Mute" = "Вимкнути звук"; +/* notification label action */ +"Mute all" = "Вимкнути звук для всіх"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Вимкнено, коли неактивний!"; @@ -3380,6 +3512,9 @@ snd error text */ /* delete after time */ "never" = "ніколи"; +/* token status text */ +"New" = "Новий"; + /* No comment provided by engineer. */ "New chat" = "Новий чат"; @@ -3401,6 +3536,9 @@ snd error text */ /* notification */ "New events" = "Нові події"; +/* No comment provided by engineer. */ +"New group role: Moderator" = "Нова роль у групі: Модератор"; + /* No comment provided by engineer. */ "New in %@" = "Нове в %@"; @@ -3410,6 +3548,9 @@ snd error text */ /* No comment provided by engineer. */ "New member role" = "Нова роль учасника"; +/* rcv group event chat item */ +"New member wants to join the group." = "Новий учасник хоче приєднатися до групи."; + /* notification */ "new message" = "нове повідомлення"; @@ -3440,6 +3581,18 @@ snd error text */ /* Authentication unavailable */ "No app password" = "Немає пароля програми"; +/* No comment provided by engineer. */ +"No chats" = "Без чатів"; + +/* No comment provided by engineer. */ +"No chats found" = "Чати не знайдено"; + +/* 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" = "Не вибрано жодного контакту"; @@ -3473,6 +3626,9 @@ snd error text */ /* servers error */ "No media & file servers." = "Ніяких медіа та файлових серверів."; +/* No comment provided by engineer. */ +"No message" = "Немає повідомлення"; + /* servers error */ "No message servers." = "Ніяких серверів повідомлень."; @@ -3488,6 +3644,9 @@ snd error text */ /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; +/* alert title */ +"No private routing session" = "Немає приватного сеансу маршрутизації"; + /* No comment provided by engineer. */ "No push server" = "Локально"; @@ -3509,12 +3668,24 @@ snd error text */ /* copied message info in history */ "no text" = "без тексту"; +/* alert title */ +"No token!" = "Немає токена!"; + +/* No comment provided by engineer. */ +"No unread chats" = "Немає непрочитаних чатів"; + /* No comment provided by engineer. */ "No user identifiers." = "Ніяких ідентифікаторів користувачів."; /* No comment provided by engineer. */ "Not compatible!" = "Не сумісні!"; +/* No comment provided by engineer. */ +"not synchronized" = "не синхронізовано"; + +/* No comment provided by engineer. */ +"Notes" = "Нотатки"; + /* No comment provided by engineer. */ "Nothing selected" = "Нічого не вибрано"; @@ -3527,9 +3698,15 @@ snd error text */ /* No comment provided by engineer. */ "Notifications are disabled!" = "Сповіщення вимкнено!"; +/* alert title */ +"Notifications error" = "Помилка сповіщень"; + /* No comment provided by engineer. */ "Notifications privacy" = "Сповіщення про приватність"; +/* alert title */ +"Notifications status" = "Статус сповіщень"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Тепер адміністратори можуть\n- видаляти повідомлення користувачів.\n- відключати користувачів (роль \"спостерігач\")"; @@ -3595,6 +3772,12 @@ new chat action */ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Тільки власники груп можуть вмикати голосові повідомлення."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Тільки відправник і модератори бачать це"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Тільки ви та модератори бачать це"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Тільки ви можете додавати реакції на повідомлення."; @@ -3643,12 +3826,30 @@ new chat action */ /* new chat action */ "Open group" = "Відкрита група"; +/* alert title */ +"Open link?" = "Відкрите посилання?"; + /* authentication reason */ "Open migration to another device" = "Відкрита міграція на інший пристрій"; +/* new chat action */ +"Open new chat" = "Відкрити новий чат"; + +/* new chat action */ +"Open new group" = "Відкрити нову групу"; + /* No comment provided by engineer. */ "Open Settings" = "Відкрийте Налаштування"; +/* No comment provided by engineer. */ +"Open to accept" = "Відкрити для прийняття"; + +/* No comment provided by engineer. */ +"Open to connect" = "Відкрито для підключення"; + +/* No comment provided by engineer. */ +"Open to join" = "Відкрито для приєднання"; + /* No comment provided by engineer. */ "Opening app…" = "Відкриваємо програму…"; @@ -3676,6 +3877,9 @@ new chat action */ /* No comment provided by engineer. */ "Or to share privately" = "Або поділитися приватно"; +/* No comment provided by engineer. */ +"Organize chats into lists" = "Організовуйте чати в списки"; + /* No comment provided by engineer. */ "other" = "інший"; @@ -3733,9 +3937,18 @@ new chat action */ /* No comment provided by engineer. */ "peer-to-peer" = "одноранговий"; +/* No comment provided by engineer. */ +"pending" = "очікує"; + /* No comment provided by engineer. */ "Pending" = "В очікуванні"; +/* No comment provided by engineer. */ +"pending approval" = "очікує на схвалення"; + +/* No comment provided by engineer. */ +"pending review" = "очікує на схвалення"; + /* No comment provided by engineer. */ "Periodic" = "Періодично"; @@ -3802,6 +4015,18 @@ new chat action */ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите."; +/* 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." = "Будь ласка, дочекайтеся завершення активації токену."; + +/* token info */ +"Please wait for token to be registered." = "Будь ласка, зачекайте, поки токен буде зареєстровано."; + /* No comment provided by engineer. */ "Polish interface" = "Польський інтерфейс"; @@ -3832,12 +4057,21 @@ new chat action */ /* No comment provided by engineer. */ "Privacy for your customers." = "Конфіденційність для ваших клієнтів."; +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Політика конфіденційності та умови використання."; + /* No comment provided by engineer. */ "Privacy redefined" = "Конфіденційність переглянута"; +/* No comment provided by engineer. */ +"Private chats, groups and your contacts are not accessible to server operators." = "Приватні чати, групи та ваші контакти недоступні для операторів сервера."; + /* No comment provided by engineer. */ "Private filenames" = "Приватні імена файлів"; +/* No comment provided by engineer. */ +"Private media file names." = "Приватні імена медіа-файлів."; + /* No comment provided by engineer. */ "Private message routing" = "Маршрутизація приватних повідомлень"; @@ -3853,6 +4087,9 @@ new chat action */ /* alert title */ "Private routing error" = "Помилка приватної маршрутизації"; +/* alert title */ +"Private routing timeout" = "Тайм-аут приватної маршрутизації"; + /* No comment provided by engineer. */ "Profile and server connections" = "З'єднання профілю та сервера"; @@ -3883,6 +4120,9 @@ new chat action */ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Заборонити реакції на повідомлення."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Заборонити повідомлення модераторам."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Заборонити надсилати прямі повідомлення учасникам."; @@ -3910,6 +4150,9 @@ new chat action */ /* No comment provided by engineer. */ "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами.\nУвімкніть у налаштуваннях *Мережа та сервери*."; +/* No comment provided by engineer. */ +"Protocol background timeout" = "Фоновий тайм-аут протоколу"; + /* No comment provided by engineer. */ "Protocol timeout" = "Тайм-аут протоколу"; @@ -4045,6 +4288,15 @@ new chat action */ /* No comment provided by engineer. */ "Reduced battery usage" = "Зменшення використання акумулятора"; +/* No comment provided by engineer. */ +"Register" = "Зареєструватися"; + +/* token info */ +"Register notification token?" = "Зареєструвати токен сповіщення?"; + +/* token status text */ +"Registered" = "Зареєстровано"; + /* alert action reject incoming call via notification swipe action */ @@ -4056,6 +4308,12 @@ swipe action */ /* alert title */ "Reject contact request" = "Відхилити запит на контакт"; +/* alert title */ +"Reject member?" = "Відхилити учасника?"; + +/* No comment provided by engineer. */ +"rejected" = "відхилено"; + /* call status */ "rejected call" = "відхилений виклик"; @@ -4092,12 +4350,18 @@ 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" = "видалено зображення профілю"; /* rcv group event chat item */ "removed you" = "прибрали вас"; +/* No comment provided by engineer. */ +"Removes messages and blocks members." = "Видаляє повідомлення та блокує користувачів."; + /* No comment provided by engineer. */ "Renegotiate" = "Переузгодьте"; @@ -4119,6 +4383,45 @@ swipe action */ /* chat item action */ "Reply" = "Відповісти"; +/* chat item action */ +"Report" = "Повідомити"; + +/* report reason */ +"Report content: only group moderators will see it." = "Повідомити про контент: тільки модератори групи побачать це."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Повідомити про профіль учасника: тільки модератори групи побачать це."; + +/* report reason */ +"Report other: only group moderators will see it." = "Повідомити інше: тільки модератори групи побачать це."; + +/* No comment provided by engineer. */ +"Report reason?" = "Причина повідомлення?"; + +/* alert title */ +"Report sent to moderators" = "Повідомлення надіслано модераторам"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Повідомити про спам: тільки модератори групи побачать це."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Повідомити про порушення: тільки модератори групи побачать це."; + +/* report in notification */ +"Report: %@" = "Повідомити: %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Повідомляти про повідомлення модераторам заборонено."; + +/* No comment provided by engineer. */ +"Reports" = "Звіти"; + +/* No comment provided by engineer. */ +"request is sent" = "запит відправлено"; + +/* No comment provided by engineer. */ +"request to join rejected" = "запит на приєднання відхилено"; + /* chat list item title */ "requested to connect" = "запит на підключення"; @@ -4173,9 +4476,24 @@ swipe action */ /* chat item action */ "Reveal" = "Показувати"; +/* No comment provided by engineer. */ +"review" = "перегляд"; + /* No comment provided by engineer. */ "Review conditions" = "Умови перегляду"; +/* No comment provided by engineer. */ +"Review group members" = "Учасники групи оглядів"; + +/* 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" = "Відкликати"; @@ -4204,6 +4522,12 @@ chat item action */ /* alert button */ "Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; +/* alert button */ +"Save (and notify members)" = "Зберегти (і повідомити учасникам)"; + +/* alert title */ +"Save admission settings?" = "Зберегти налаштування входу?"; + /* alert button */ "Save and notify contact" = "Зберегти та повідомити контакт"; @@ -4219,6 +4543,12 @@ chat item action */ /* No comment provided by engineer. */ "Save group profile" = "Зберегти профіль групи"; +/* alert title */ +"Save group profile?" = "Зберегти профіль групи?"; + +/* No comment provided by engineer. */ +"Save list" = "Зберегти список"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Збережіть пароль і відкрийте чат"; @@ -4354,6 +4684,9 @@ chat item action */ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите"; +/* No comment provided by engineer. */ +"Send contact request?" = "Надіслати запит на контакт?"; + /* No comment provided by engineer. */ "Send delivery receipts to" = "Надсилання звітів про доставку"; @@ -4384,18 +4717,30 @@ chat item action */ /* No comment provided by engineer. */ "Send notifications" = "Надсилати сповіщення"; +/* No comment provided by engineer. */ +"Send private reports" = "Надсилайте приватні звіти"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Надсилайте запитання та ідеї"; /* No comment provided by engineer. */ "Send receipts" = "Надіслати підтвердження"; +/* No comment provided by engineer. */ +"Send request" = "Надіслати запит"; + +/* No comment provided by engineer. */ +"Send request without message" = "Надіслати запит без повідомлення"; + /* 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 останніх повідомлень новим користувачам."; +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Надсилайте свої приватні відгуки до груп."; + /* alert message */ "Sender cancelled file transfer." = "Відправник скасував передачу файлу."; @@ -4516,6 +4861,9 @@ chat item action */ /* No comment provided by engineer. */ "Set 1 day" = "Встановити 1 день"; +/* No comment provided by engineer. */ +"Set chat name…" = "Назвати чат…"; + /* No comment provided by engineer. */ "Set contact name…" = "Встановити ім'я контакту…"; @@ -4528,6 +4876,12 @@ 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." = "Встановлюйте термін придатності повідомлень у чатах."; + /* profile update event chat item */ "set new contact address" = "встановити нову контактну адресу"; @@ -4543,6 +4897,9 @@ chat item action */ /* No comment provided by engineer. */ "Set passphrase to export" = "Встановити ключову фразу для експорту"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Налаштуйте біографію профілю та вітальне повідомлення."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Налаштуйте повідомлення, яке показуватиметься новим користувачам!"; @@ -4583,6 +4940,12 @@ chat item action */ /* No comment provided by engineer. */ "Share link" = "Поділіться посиланням"; +/* alert button */ +"Share old address" = "Поділіться старою адресою"; + +/* alert button */ +"Share old link" = "Поділіться старим посиланням"; + /* No comment provided by engineer. */ "Share profile" = "Поділіться профілем"; @@ -4598,6 +4961,18 @@ chat item action */ /* No comment provided by engineer. */ "Share with contacts" = "Поділіться з контактами"; +/* No comment provided by engineer. */ +"Share your address" = "Поділіться своєю адресою"; + +/* No comment provided by engineer. */ +"Short description" = "Короткий опис"; + +/* No comment provided by engineer. */ +"Short link" = "Коротке посилання"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Коротка адреса SimpleX"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Показувати → у повідомленнях, надісланих через приватну маршрутизацію."; @@ -4643,6 +5018,9 @@ chat item action */ /* alert title */ "SimpleX address settings" = "Автоприйняття налаштувань"; +/* 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, у додаток."; @@ -4727,6 +5105,10 @@ chat item action */ /* notification title */ "Somebody" = "Хтось"; +/* blocking reason +report reason */ +"Spam" = "Спам"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Квадрат, коло або щось середнє між ними."; @@ -4784,6 +5166,9 @@ chat item action */ /* No comment provided by engineer. */ "Stopping chat" = "Зупинка чату"; +/* No comment provided by engineer. */ +"Storage" = "Зберігання"; + /* No comment provided by engineer. */ "strike" = "закреслено"; @@ -4826,9 +5211,18 @@ chat item action */ /* No comment provided by engineer. */ "Tap button " = "Натисніть кнопку "; +/* No comment provided by engineer. */ +"Tap Connect to chat" = "Натисніть Підключитися до чату"; + +/* No comment provided by engineer. */ +"Tap Connect to send request" = "Натисніть Підключитися, щоб відправити запит"; + /* No comment provided by engineer. */ "Tap Create SimpleX address in the menu to create it later." = "Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше."; +/* No comment provided by engineer. */ +"Tap Join group" = "Натисніть Приєднатися до групи"; + /* No comment provided by engineer. */ "Tap to activate profile." = "Натисніть, щоб активувати профіль."; @@ -4850,9 +5244,15 @@ chat item action */ /* No comment provided by engineer. */ "TCP connection" = "TCP-з'єднання"; +/* No comment provided by engineer. */ +"TCP connection bg timeout" = "Таймаут TCP-з'єднання bg"; + /* No comment provided by engineer. */ "TCP connection timeout" = "Тайм-аут TCP-з'єднання"; +/* No comment provided by engineer. */ +"TCP port for messaging" = "TCP-порт для повідомлень"; + /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -4868,6 +5268,9 @@ chat item action */ /* server test failure */ "Test failed at step %@." = "Тест завершився невдало на кроці %@."; +/* No comment provided by engineer. */ +"Test notifications" = "Тестові сповіщення"; + /* No comment provided by engineer. */ "Test server" = "Тестовий сервер"; @@ -4886,6 +5289,9 @@ chat item action */ /* No comment provided by engineer. */ "Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; +/* alert message */ +"The address will be short, and your profile will be shared via the address." = "Адреса буде короткою, і ваш профіль буде доступний за цією адресою."; + /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; @@ -4925,6 +5331,9 @@ chat item action */ /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому).\nЦе може статися через помилку або коли з'єднання скомпрометовано."; +/* alert message */ +"The link will be short, and group profile will be shared via the link." = "Посилання буде коротким, а профіль групи буде поширюватися за посиланням."; + /* No comment provided by engineer. */ "The message will be deleted for all members." = "Повідомлення буде видалено для всіх учасників."; @@ -4982,6 +5391,9 @@ chat item action */ /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин."; +/* alert message */ +"This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Цю дію не можна скасувати — повідомлення, надіслані та отримані в цьому чаті раніше за обраний час, будуть видалені."; + /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені."; @@ -5006,12 +5418,21 @@ chat item action */ /* No comment provided by engineer. */ "This group no longer exists." = "Цієї групи більше не існує."; +/* 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. */ "This link was used with another mobile device, please create a new link on the desktop." = "Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі."; +/* No comment provided by engineer. */ +"This message was deleted or not received yet." = "Це повідомлення було видалено або ще не отримано."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**."; +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Час зникнення встановлюється тільки для нових контактів."; + /* No comment provided by engineer. */ "Title" = "Заголовок"; @@ -5063,6 +5484,9 @@ chat item action */ /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; +/* alert message */ +"To use another profile after connection attempt, delete the chat and use the link again." = "Щоб використовувати інший профіль після спроби з'єднання, видаліть чат і скористайтеся посиланням знову."; + /* No comment provided by engineer. */ "To use the servers of **%@**, accept conditions of use." = "Щоб користуватися серверами **%@**, прийміть умови використання."; @@ -5075,6 +5499,9 @@ chat item action */ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Увімкніть інкогніто при підключенні."; +/* token status */ +"Token status: %@." = "Статус токена: %@."; + /* No comment provided by engineer. */ "Toolbar opacity" = "Непрозорість панелі інструментів"; @@ -5192,6 +5619,9 @@ chat item action */ /* 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 останніх повідомлень."; @@ -5207,6 +5637,9 @@ chat item action */ /* No comment provided by engineer. */ "Update settings?" = "Оновити налаштування?"; +/* No comment provided by engineer. */ +"Updated conditions" = "Оновлені умови"; + /* rcv group event chat item */ "updated group profile" = "оновлений профіль групи"; @@ -5216,9 +5649,27 @@ chat item action */ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; +/* alert button */ +"Upgrade" = "Оновлення"; + +/* No comment provided by engineer. */ +"Upgrade address" = "Адреса оновлення"; + +/* alert message */ +"Upgrade address?" = "Змінити адресу?"; + /* No comment provided by engineer. */ "Upgrade and open chat" = "Оновлення та відкритий чат"; +/* alert message */ +"Upgrade group link?" = "Оновити посилання на групу?"; + +/* No comment provided by engineer. */ +"Upgrade link" = "Посилання для оновлення"; + +/* No comment provided by engineer. */ +"Upgrade your address" = "Поновіть свою адресу"; + /* No comment provided by engineer. */ "Upload errors" = "Помилки завантаження"; @@ -5261,6 +5712,9 @@ chat item action */ /* No comment provided by engineer. */ "Use from desktop" = "Використання з робочого столу"; +/* No comment provided by engineer. */ +"Use incognito profile" = "Використовуйте профіль інкогніто"; + /* No comment provided by engineer. */ "Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; @@ -5288,12 +5742,21 @@ chat item action */ /* No comment provided by engineer. */ "Use SOCKS proxy" = "Використовуйте SOCKS проксі"; +/* 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." = "Використовуйте додаток під час розмови."; /* No comment provided by engineer. */ "Use the app with one hand." = "Використовуйте додаток однією рукою."; +/* No comment provided by engineer. */ +"Use web port" = "Використовувати веб-порт"; + /* No comment provided by engineer. */ "User selection" = "Вибір користувача"; @@ -5444,6 +5907,9 @@ chat item action */ /* No comment provided by engineer. */ "Welcome message is too long" = "Привітальне повідомлення занадто довге"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Вітаємо ваші контакти 👋"; + /* No comment provided by engineer. */ "What's new" = "Що нового"; @@ -5513,6 +5979,9 @@ chat item action */ /* No comment provided by engineer. */ "You accepted connection" = "Ви прийняли підключення"; +/* snd group event chat item */ +"you accepted this member" = "ви прийняли цього учасника"; + /* No comment provided by engineer. */ "You allow" = "Ви дозволяєте"; @@ -5618,6 +6087,9 @@ chat item action */ /* 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!" = "Ви не можете надсилати повідомлення!"; @@ -5687,9 +6159,15 @@ chat item action */ /* chat list item description */ "you shared one-time link incognito" = "ви поділилися одноразовим посиланням інкогніто"; +/* token info */ +"You should receive notifications." = "Ви повинні отримувати сповіщення."; + /* snd group event chat item */ "you unblocked %@" = "ви розблокували %@"; +/* No comment provided by engineer. */ +"You will be able to send messages **only after your request is accepted**." = "Ви зможете надсилати повідомлення **тільки після того, як ваш запит буде прийнято**."; + /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!"; @@ -5726,6 +6204,9 @@ chat item action */ /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено"; +/* No comment provided by engineer. */ +"Your business contact" = "Ваш діловий контакт"; + /* No comment provided by engineer. */ "Your calls" = "Твої дзвінки"; @@ -5741,9 +6222,15 @@ chat item action */ /* No comment provided by engineer. */ "Your chat profiles" = "Ваші профілі чату"; +/* alert message */ +"Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile." = "Ваш чат було переміщено на %@, але при перенаправленні на профіль сталася несподівана помилка."; + /* No comment provided by engineer. */ "Your connection was moved to %@ but an error happened when switching profile." = "Ваше з'єднання було переміщено на %@, але під час перенаправлення на профіль сталася несподівана помилка."; +/* No comment provided by engineer. */ +"Your contact" = "Ваш контакт"; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; @@ -5762,6 +6249,9 @@ chat item action */ /* No comment provided by engineer. */ "Your current profile" = "Ваш поточний профіль"; +/* No comment provided by engineer. */ +"Your group" = "Ваша група"; + /* No comment provided by engineer. */ "Your ICE servers" = "Ваші сервери ICE"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 4a05f60226..5ba26b8363 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1215,9 +1215,6 @@ set passcode view */ /* No comment provided by engineer. */ "Connected desktop" = "已连接的桌面"; -/* rcv group event chat item */ -"connected directly" = "已直连"; - /* No comment provided by engineer. */ "Connected servers" = "已连接的服务器"; @@ -2154,7 +2151,7 @@ chat item action */ /* alert title */ "Error deleting chat database" = "删除聊天数据库错误"; -/* No comment provided by engineer. */ +/* alert title */ "Error deleting chat!" = "删除聊天错误!"; /* No comment provided by engineer. */ diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 320a8e876a..47506d9532 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -159,11 +159,11 @@ fun AppearanceScope.AppearanceLayout( } } -private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> +private fun findEnabledIcon(): AppIcon = AppIcon.values().firstOrNull { icon -> androidAppContext.packageManager.getComponentEnabledSetting( ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") ).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED } -} +} ?: AppIcon.DEFAULT @Preview @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index a7ede11160..d61e44f528 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 @@ -1228,6 +1228,7 @@ data class User( override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, + val autoAcceptMemberContacts: Boolean, val viewPwdHash: UserPwdHash?, val uiThemes: ThemeModeOverrides? = null, ): NamedChat, UserLike { @@ -1257,6 +1258,7 @@ data class User( showNtfs = true, sendRcptsContacts = true, sendRcptsSmallGroups = false, + autoAcceptMemberContacts = false, viewPwdHash = null, uiThemes = null, ) @@ -1730,6 +1732,7 @@ data class Contact( val contactRequestId: Long?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, + val groupDirectInv: GroupDirectInvitation? = null, val chatTags: List, val chatItemTTL: Long?, override val chatDeleted: Boolean, @@ -1745,7 +1748,10 @@ data class Contact( val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent override val nextConnectPrepared get() = active && preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) override val profileChangeProhibited get() = activeConn != null - val nextAcceptContactRequest get() = active && contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New) + val nextAcceptContactRequest get() = + active && + (contactRequestId != null || groupDirectInv != null) && + (activeConn == null || activeConn.connStatus == ConnStatus.New || activeConn.connStatus == ConnStatus.Prepared) val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -1836,6 +1842,18 @@ data class PreparedContact ( val uiConnLinkType: ConnectionMode ) +@Serializable +data class GroupDirectInvitation ( + val groupDirectInvLink: String, + val fromGroupId_: Long?, + val fromGroupMemberId_: Long?, + val fromGroupMemberConnId_: Long?, + val groupDirectInvStartedConnection: Boolean +) { + val memberRemoved: Boolean + get() = fromGroupId_ == null || fromGroupMemberId_ == null || fromGroupMemberConnId_ == null +} + @Serializable enum class ConnectionMode { @SerialName("inv") Inv, @@ -2843,6 +2861,7 @@ data class ChatItem ( is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { is RcvDirectEvent.ContactDeleted -> false is RcvDirectEvent.ProfileUpdated -> false + is RcvDirectEvent.GroupInvLinkReceived -> true } is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { is RcvGroupEvent.MemberAdded -> false @@ -4301,7 +4320,6 @@ sealed class MsgChatLink { @Serializable class FormattedText(val text: String, val format: Format? = null) { - // TODO make it dependent on simplexLinkMode preference fun link(mode: SimplexLinkMode): String? = when (format) { is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text" is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri @@ -4310,7 +4328,6 @@ class FormattedText(val text: String, val format: Format? = null) { else -> null } - // TODO make it dependent on simplexLinkMode preference fun viewText(mode: SimplexLinkMode): String = if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text @@ -4335,6 +4352,7 @@ sealed class Format { @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() + @Serializable @SerialName("unknown") class Unknown: Format() val style: SpanStyle @Composable get() = when (this) { is Bold -> SpanStyle(fontWeight = FontWeight.Bold) @@ -4348,6 +4366,7 @@ sealed class Format { is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle + is Unknown -> SpanStyle() } val isSimplexLink = this is SimplexLink @@ -4511,10 +4530,12 @@ sealed class MsgErrorType() { sealed class RcvDirectEvent() { @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent() @Serializable @SerialName("profileUpdated") class ProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvDirectEvent() + @Serializable @SerialName("groupInvLinkReceived") class GroupInvLinkReceived(val groupProfile: GroupProfile): RcvDirectEvent() val text: String get() = when (this) { is ContactDeleted -> generalGetString(MR.strings.rcv_direct_event_contact_deleted) is ProfileUpdated -> profileUpdatedText(fromProfile, toProfile) + is GroupInvLinkReceived -> generalGetString(MR.strings.rcv_direct_event_group_inv_link_received).format(groupProfile.displayName) } private fun profileUpdatedText(from: Profile, to: Profile): String = diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 839e90fad0..9e10d249c0 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 @@ -857,6 +857,12 @@ object ChatController { throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}") } + suspend fun apiSetUserAutoAcceptMemberContacts(u: User, enable: Boolean) { + val r = sendCmd(u.remoteHostId, CC.ApiSetUserAutoAcceptMemberContacts(u.userId, enable)) + if (r.result is CR.CmdOk) return + throw Exception("failed to set auto-accept ${r.responseType} ${r.details}") + } + suspend fun apiHideUser(u: User, viewPwd: String): User = setUserPrivacy(u.remoteHostId, CC.ApiHideUser(u.userId, viewPwd)) @@ -2207,6 +2213,16 @@ object ChatController { return null } + suspend fun apiAcceptMemberContact(rh: Long?, contactId: Long): Contact? { + val r = sendCmdWithRetry(rh, CC.APIAcceptMemberContact(contactId)) + if (r is API.Result && r.res is CR.MemberContactAccepted) return r.res.contact + if (r != null) { + Log.e(TAG, "apiAcceptMemberContact bad response: ${r.responseType} ${r.details}") + apiConnectResponseAlert(r) + } + return null + } + suspend fun allowFeatureToContact(rh: Long?, contact: Contact, feature: ChatFeature, param: Int? = null) { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) @@ -3510,6 +3526,7 @@ sealed class CC { class SetAllContactReceipts(val enable: Boolean): CC() class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() + class ApiSetUserAutoAcceptMemberContacts(val userId: Long, val enable: Boolean): CC() class ApiHideUser(val userId: Long, val viewPwd: String): CC() class ApiUnhideUser(val userId: Long, val viewPwd: String): CC() class ApiMuteUser(val userId: Long): CC() @@ -3567,6 +3584,7 @@ sealed class 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 APIAcceptMemberContact(val contactId: Long): CC() class APITestProtoServer(val userId: Long, val server: String): CC() class ApiGetServerOperators(): CC() class ApiSetServerOperators(val operators: List): CC() @@ -3687,6 +3705,7 @@ sealed class CC { val mrs = userMsgReceiptSettings "/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" } + is ApiSetUserAutoAcceptMemberContacts -> "/_set accept member contacts $userId ${onOff(enable)}" is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}" is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}" is ApiMuteUser -> "/_mute user $userId" @@ -3762,6 +3781,7 @@ sealed class CC { is ApiAddGroupShortLink -> "/_short link #$groupId" is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" + is APIAcceptMemberContact -> "/_accept member contact @$contactId" is APITestProtoServer -> "/_server test $userId $server" is ApiGetServerOperators -> "/_operators" is ApiSetServerOperators -> "/_operators ${json.encodeToString(operators)}" @@ -3879,6 +3899,7 @@ sealed class CC { is SetAllContactReceipts -> "setAllContactReceipts" is ApiSetUserContactReceipts -> "apiSetUserContactReceipts" is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts" + is ApiSetUserAutoAcceptMemberContacts -> "apiSetUserAutoAcceptMemberContacts" is ApiHideUser -> "apiHideUser" is ApiUnhideUser -> "apiUnhideUser" is ApiMuteUser -> "apiMuteUser" @@ -3935,6 +3956,7 @@ sealed class CC { is ApiAddGroupShortLink -> "apiAddGroupShortLink" is APICreateMemberContact -> "apiCreateMemberContact" is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" + is APIAcceptMemberContact -> "apiAcceptMemberContact" is APITestProtoServer -> "testProtoServer" is ApiGetServerOperators -> "apiGetServerOperators" is ApiSetServerOperators -> "apiSetServerOperators" @@ -6127,6 +6149,7 @@ sealed class CR { @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("memberContactAccepted") class MemberContactAccepted(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("newMemberContactReceivedInv") class NewMemberContactReceivedInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() // receiving file events @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR() @@ -6310,6 +6333,7 @@ sealed class CR { is GroupLinkDeleted -> "groupLinkDeleted" is NewMemberContact -> "newMemberContact" is NewMemberContactSentInv -> "newMemberContactSentInv" + is MemberContactAccepted -> "memberContactAccepted" is NewMemberContactReceivedInv -> "newMemberContactReceivedInv" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" is StandaloneFileInfo -> "standaloneFileInfo" @@ -6486,6 +6510,7 @@ sealed class CR { is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") + is MemberContactAccepted -> withUser(user, "contact: $contact") is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) is StandaloneFileInfo -> json.encodeToString(fileMeta) 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 05d9e6564c..c7b8cb2c81 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 @@ -41,6 +41,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.common.views.chat.group.ChatTTLOption +import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.chatlist.updateChatSettings import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @@ -759,16 +760,16 @@ fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard: } val descr = c.shortDescr?.trim() if (descr != null && descr != "") { - val copyDescr = { copyNameToClipboard(descr) } - Text( + MarkdownText( descr, - style = MaterialTheme.typography.body2, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center, + parseToMarkdown(descr), + toggleSecrets = true, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), maxLines = 4, overflow = TextOverflow.Ellipsis, - lineHeight = 21.sp, - modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).combinedClickable(onClick = copyDescr, onLongClick = copyDescr).onRightClick(copyDescr) + uriHandler = LocalUriHandler.current, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), + linkMode = chatModel.simplexLinkMode.value ) } } 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 074609c8f7..d19c19b83a 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 @@ -770,6 +770,8 @@ private fun connectingText(chatInfo: ChatInfo): String? { ) { if (chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { generalGetString(MR.strings.contact_should_accept) + } else if (chatInfo.contact.contactGroupMemberId != null) { + generalGetString(MR.strings.contact_should_accept) } else { generalGetString(MR.strings.contact_connection_pending) } @@ -1851,16 +1853,16 @@ fun BoxScope.ChatItemsList( val descr = chatInfo.shortDescr?.trim() if (descr != null && descr != "") { - Text( + MarkdownText( descr, - style = MaterialTheme.typography.body2, - color = MaterialTheme.colors.onBackground, - textAlign = TextAlign.Center, + parseToMarkdown(descr), + toggleSecrets = true, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), maxLines = 4, overflow = TextOverflow.Ellipsis, - lineHeight = 21.sp, - modifier = Modifier - .padding(top = DEFAULT_PADDING_HALF) + uriHandler = LocalUriHandler.current, + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), + linkMode = linkMode ) } @@ -1869,6 +1871,7 @@ fun BoxScope.ChatItemsList( Text( contextStr, style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary, modifier = Modifier.padding(top = DEFAULT_PADDING) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt new file mode 100644 index 0000000000..5c8ac3bc5d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt @@ -0,0 +1,172 @@ +package chat.simplex.common.views.chat + +import TextIconSpaced +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +@Composable +fun ComposeContextMemberContactActionsView( + rhId: Long?, + contact: Contact, + groupDirectInv: GroupDirectInvitation +) { + val inProgress = rememberSaveable { mutableStateOf(false) } + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + KeyChangeEffect(chatModel.chatId.value) { + if (inProgress.value) { + inProgress.value = false + progressByTimeout = false + } + } + + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(1000) + inProgress.value + } else { + false + } + } + + Box( + Modifier.height(60.dp), + contentAlignment = Alignment.Center + ) { + Column( + Modifier + .background(MaterialTheme.colors.surface) + .alpha(if (progressByTimeout) 0.6f else 1f) + ) { + Divider() + + if (groupDirectInv.memberRemoved) { + Row( + Modifier + .fillMaxSize() + .padding(horizontal = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) + ) { + Icon(painterResource(MR.images.ic_info), contentDescription = null, tint = MaterialTheme.colors.secondary) + Text(generalGetString(MR.strings.member_is_deleted_cant_accept_request), color = MaterialTheme.colors.secondary) + } + } else { + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + var rejectButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + rejectButtonModifier = + if (inProgress.value) rejectButtonModifier + else rejectButtonModifier.clickable { showRejectMemberContactRequestAlert(rhId, contact) } + Row( + rejectButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_close), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.reject_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else Color.Red + ) + } + var acceptButtonModifier = Modifier.fillMaxWidth().fillMaxHeight().weight(1F) + acceptButtonModifier = + if (inProgress.value) acceptButtonModifier + else acceptButtonModifier.clickable { acceptMemberContact(rhId, contact.contactId, inProgress = inProgress) } + Row( + acceptButtonModifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + ) + TextIconSpaced(false) + Text( + stringResource(MR.strings.accept_contact_button), + color = if (inProgress.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + } + } + + if (progressByTimeout) { + ComposeProgressIndicator() + } + } +} + +fun showRejectMemberContactRequestAlert(rhId: Long?, contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_contact_request), + text = generalGetString(MR.strings.the_sender_will_not_be_notified), + confirmText = generalGetString(MR.strings.reject_contact_button), + onConfirm = { + AlertManager.shared.hideAlert() + deleteMemberContact(rhId, contact) + }, + destructive = true, + hostDevice = hostDevice(rhId), + ) +} + +private fun deleteMemberContact(rhId: Long?, contact: Contact) { + withBGApi { + chatModel.controller.apiDeleteContact(rhId, contact.contactId, chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false)) + withContext(Dispatchers.Main) { + chatModel.chatsContext.removeChat(rhId, contact.id) + chatModel.chatId.value = null + } + } +} + +fun acceptMemberContact( + rhId: Long?, + contactId: Long, + close: ((chat: Chat) -> Unit)? = null, + inProgress: MutableState? = null +) { + withBGApi { + inProgress?.value = true + val contact = chatModel.controller.apiAcceptMemberContact(rhId, contactId) + if (contact != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateContact(rhId, contact) + inProgress?.value = false + } + chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) + val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) + close?.invoke(chat) + } else { + inProgress?.value = false + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index f1b370a796..ce55c62ae2 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 @@ -1248,6 +1248,7 @@ fun ComposeView( SimpleButtonIconEnded( text = stringResource(MR.strings.compose_view_connect), icon = painterResource(icon), + style = MaterialTheme.typography.body2, color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, disabled = composeState.value.inProgress, click = { withApi { sendRequest() } } @@ -1467,6 +1468,16 @@ fun ComposeView( rhId = rhId, contactRequestId = chat.chatInfo.contact.contactRequestId ) + } else if ( + chat.chatInfo is ChatInfo.Direct + && chat.chatInfo.contact.nextAcceptContactRequest + && chat.chatInfo.contact.groupDirectInv != null + ) { + ComposeContextMemberContactActionsView( + rhId = rhId, + contact = chat.chatInfo.contact, + groupDirectInv = chat.chatInfo.contact.groupDirectInv + ) } else { Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { AttachmentButton() 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 d3e7f60f62..e5f0b708e7 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 @@ -34,7 +34,7 @@ import chat.simplex.common.views.newchat.* import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.* -import chat.simplex.common.views.chatlist.openLoadedChat +import chat.simplex.common.views.chatlist.openDirectChat import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock @@ -57,6 +57,7 @@ fun GroupMemberInfoView( val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() fun syncMemberConnection() { withBGApi { @@ -86,12 +87,10 @@ fun GroupMemberInfoView( developerTools, connectionCode, getContactChat = { chatModel.getContactChat(it) }, - openDirectChat = { - withBGApi { - apiLoadMessages(chatModel.chatsContext, rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) - if (chatModel.getContactChat(it) != null) { - closeAll() - } + openDirectChat = { contactId -> + scope.launch { + openDirectChat(rhId, contactId) + closeAll() } }, createMemberContact = { @@ -104,7 +103,7 @@ fun GroupMemberInfoView( withContext(Dispatchers.Main) { chatModel.chatsContext.addChat(memberChat) } - openLoadedChat(memberChat) + openDirectChat(rhId, memberContact.contactId) closeAll() chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } 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 325f6181c9..2128d1991b 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 @@ -271,8 +271,14 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo @Composable fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { - if (contact.nextAcceptContactRequest && contact.contactRequestId != null) { - ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + if (contact.nextAcceptContactRequest) { + if (contact.contactRequestId != null) { + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + } else if (contact.groupDirectInv != null && !contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems(chat.remoteHostId, contact, showMenu) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } else { if (contact.activeConn != null) { if (showMarkRead) { @@ -545,6 +551,28 @@ fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: Chat ) } +@Composable +fun MemberContactRequestMenuItems(rhId: Long?, contact: Contact, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { + ItemAction( + stringResource(MR.strings.accept_contact_button), + painterResource(MR.images.ic_check), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptMemberContact(rhId, contact.contactId, onSuccess) + showMenu.value = false + } + ) + ItemAction( + stringResource(MR.strings.reject_contact_button), + painterResource(MR.images.ic_close), + onClick = { + showRejectMemberContactRequestAlert(rhId, contact) + showMenu.value = false + }, + color = Color.Red + ) +} + @Composable fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState) { ItemAction( 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 385a24cd50..e6c74b7558 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 @@ -144,7 +144,7 @@ fun ChatPreviewView( } val color = if (deleting) MaterialTheme.colors.secondary - else if (cInfo.contact.nextAcceptContactRequest || cInfo.contact.sendMsgToConnect) { + else if ((cInfo.contact.nextAcceptContactRequest && cInfo.contact.groupDirectInv?.memberRemoved != true) || cInfo.contact.sendMsgToConnect) { MaterialTheme.colors.primary } else if (!cInfo.contact.sndReady) { MaterialTheme.colors.secondary diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt index 83df302064..371e9072ea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -73,14 +73,25 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel }, dropdownMenuItems = { tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { - if (contactType == ContactType.CONTACT_WITH_REQUEST && chat.chatInfo.contact.contactRequestId != null) { - ContactRequestMenuItems( - rhId = chat.remoteHostId, - contactRequestId = chat.chatInfo.contact.contactRequestId, - chatModel = chatModel, - showMenu = showMenu, - onSuccess = { onRequestAccepted(it) } - ) + if (contactType == ContactType.CONTACT_WITH_REQUEST) { + if (chat.chatInfo.contact.contactRequestId != null) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + contactRequestId = chat.chatInfo.contact.contactRequestId, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else if (chat.chatInfo.contact.groupDirectInv != null && !chat.chatInfo.contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems( + rhId = chat.remoteHostId, + contact = chat.chatInfo.contact, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } else { DeleteContactAction(chat, chatModel, showMenu) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt index b86f6d7a3e..636887275c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -31,19 +31,23 @@ fun ContactPreviewView( Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } + + val textColor = when { + deleting -> MaterialTheme.colors.secondary + contactType == ContactType.CARD -> MaterialTheme.colors.primary + contactType == ContactType.CONTACT_WITH_REQUEST -> + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.groupDirectInv?.memberRemoved == true) + MaterialTheme.colors.secondary + else + MaterialTheme.colors.primary + contactType == ContactType.REQUEST -> MaterialTheme.colors.primary + contactType == ContactType.RECENT -> if (chat.chatInfo.nextConnect) MaterialTheme.colors.primary else Color.Unspecified + else -> Color.Unspecified + } + @Composable fun chatPreviewTitle() { - val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } - - val textColor = when { - deleting -> MaterialTheme.colors.secondary - contactType == ContactType.CARD -> MaterialTheme.colors.primary - contactType == ContactType.CONTACT_WITH_REQUEST -> MaterialTheme.colors.primary - contactType == ContactType.REQUEST -> MaterialTheme.colors.primary - contactType == ContactType.RECENT -> if (chat.chatInfo.nextConnect) MaterialTheme.colors.primary else Color.Unspecified - else -> Color.Unspecified - } - when (cInfo) { is ChatInfo.Direct -> Row(verticalAlignment = Alignment.CenterVertically) { @@ -90,7 +94,7 @@ fun ContactPreviewView( Icon( painterResource(MR.images.ic_check), contentDescription = null, - tint = MaterialTheme.colors.primary, + tint = textColor, modifier = Modifier .size(23.dp) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 7ed91adbd9..893ff5a467 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -46,7 +46,7 @@ fun ExposedDropDownSetting( horizontalArrangement = Arrangement.End ) { Text( - values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""), + (values.firstOrNull { it.first == selection.value }?.second ?: "") + (if (label != null) " $label" else ""), Modifier.widthIn(max = maxWidth), maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -120,8 +120,10 @@ fun ExposedDropDownSettingWithIcon( ), contentAlignment = Alignment.Center ) { - val choice = values.first { it.first == selection.value } - Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor) + val choice = values.firstOrNull { it.first == selection.value } + if (choice != null) { + Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor) + } } DefaultExposedDropdownMenu( modifier = Modifier.widthIn(min = minWidth), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 0d188bb73c..7ee52af784 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -91,7 +91,7 @@ fun SectionViewSelectable( } } } - SectionTextFooter(values.first { it.value == currentValue.value }.description) + SectionTextFooter(values.firstOrNull { it.value == currentValue.value }?.description ?: AnnotatedString("")) } @Composable @@ -221,7 +221,7 @@ fun SectionItemWithValue( horizontalArrangement = Arrangement.End ) { Text( - values.first { it.value == currentValue.value }.title + (if (label != null) " $label" else ""), + (values.firstOrNull { it.value == currentValue.value }?.title ?: "") + (if (label != null) " $label" else ""), maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary 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 d4161ee3e5..e8084e055a 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 @@ -25,6 +25,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* import chat.simplex.common.views.* import chat.simplex.common.views.chat.group.GroupLinkView +import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -44,8 +45,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c if (groupInfo != null) { withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo) - chatModel.chatsContext.chatItems.clearAndNotify() - chatModel.chatId.value = groupInfo.id + openGroupChat(rhId, groupInfo.groupId) } setGroupMembers(rhId, groupInfo, chatModel) closeAll.invoke() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 5af5d5fb90..59b9d596f1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -62,7 +62,7 @@ fun NotificationsSettingsLayout( if (appPlatform == AppPlatform.ANDROID) { SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { Text( - modes.first { it.value == notificationsMode.value }.title, + modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary @@ -71,7 +71,7 @@ fun NotificationsSettingsLayout( } SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { Text( - previewModes.first { it.value == notificationPreviewMode.value }.title, + previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index ab4c41ca93..915119fa64 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -161,7 +161,22 @@ fun PrivacySettingsView( } } + fun setAutoAcceptGrpDirectInvs(enable: Boolean) { + withApi { + chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) + chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + } + } + if (!chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced(maxTopPadding = true) + ContacRequestsFromGroupsSection( + currentUser = currentUser, + setAutoAcceptGrpDirectInvs = { enable -> + setAutoAcceptGrpDirectInvs(enable) + } + ) + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, @@ -275,6 +290,34 @@ expect fun PrivacyDeviceSection( setPerformLA: (Boolean) -> Unit, ) +@Composable +private fun ContacRequestsFromGroupsSection( + currentUser: User, + setAutoAcceptGrpDirectInvs: (Boolean) -> Unit +) { + SectionView(stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) { + SettingsActionItemWithContent(painterResource(MR.images.ic_check), stringResource(MR.strings.auto_accept_contact)) { + DefaultSwitch( + checked = currentUser.autoAcceptMemberContacts, + onCheckedChange = { enable -> + setAutoAcceptGrpDirectInvs(enable) + } + ) + } + } + SectionTextFooter( + remember(currentUser.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.this_setting_is_for_your_current_profile) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser.displayName) + } + append(".") + } + } + ) +} + @Composable private fun DeliveryReceiptsSection( currentUser: User, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 98f671ddc4..26ecf151ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -594,7 +594,7 @@ fun UseOnionHosts( onSelected = {} ) } - SectionTextFooter(values.first { it.value == onionHosts.value }.description) + SectionTextFooter(values.firstOrNull { it.value == onionHosts.value }?.description ?: AnnotatedString("")) } } 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 3e3a02aef6..36d6333d94 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1365,7 +1365,7 @@ وضع التخفي اصبح أسهل فعّل وضع التخفي عند الاتصال. أرسل لاتصال - متصل مباشرةً + طُلب اتصال جارٍ الاتصال بالفعل! مجموعات أفضل و%d أحداث أخرى @@ -2496,4 +2496,8 @@ شارك الرابط القديم سيكون الرابط قصيراً، وسيتم مشاركة الملف التعريفي للمجموعة عبر الرابط. رقِّ رابط المجموعة + طلبات الاتصال من المجموعات + حُذف العضو - لا يمكن قبول الطلب + طُلب اتصال من المجموعة %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 9e87b35294..e962c3a646 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -732,6 +732,9 @@ Reject contact request The sender will NOT be notified. + + Member is deleted - can\'t accept request + Clear chat? Clear private notes? @@ -1395,6 +1398,7 @@ An empty chat profile with the provided name is created, and the app opens as usual. If you enter this passcode when opening the app, all app data will be irreversibly removed! Set passcode + This setting is for your current profile These settings are for your current profile They can be overridden in contact and group settings. Contacts @@ -1438,6 +1442,7 @@ CHATS FILES SEND DELIVERY RECEIPTS TO + CONTACT REQUESTS FROM GROUPS Restart Shutdown Developer tools @@ -1646,6 +1651,7 @@ deleted contact + requested connection from group %1$s invited %1$s @@ -1662,7 +1668,7 @@ deleted group updated group profile invited via your group link - connected directly + requested connection New member wants to join the group. you changed role of %s to %s you changed role for yourself to %s diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index d897f6583d..ebd4b05e71 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -788,7 +788,7 @@ Маркирай като проверено %s не е потвърдено %s е потвърдено - Оценете приложението + Оцени приложението Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани. Мрежа и сървъри Разширени настройки @@ -1006,7 +1006,7 @@ Сканирай код Сканирайте кода за сигурност от приложението на вашия контакт. Код за сигурност - Изпращайте въпроси и идеи + Изпрати въпроси и идеи Запази сървърите\? Запазените WebRTC ICE сървъри ще бъдат премахнати. Запази @@ -1056,7 +1056,7 @@ Нулиране Изпрати Започни нов чат - Изпратете ни имейл + Изпрати ни мейл Някои сървъри не минаха теста: Изключване\? таен @@ -1083,7 +1083,7 @@ Отзови файл Изпращането на файла ще бъде спряно. Запази сървърите - Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете + Изпрати съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете Тестът на сървъра е неуспешен! Оценка на сигурността Сподели файл… @@ -1532,7 +1532,7 @@ Докосни за сканиране Запази Докосни за поставяне на линк за връзка - Търсене или поставяне на SimpleX линк + Търси или постави SimpleX линк Стартирай чата? Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново. Настолното устройство има грешен код за връзка @@ -2065,7 +2065,7 @@ Покани Звукът е заглушен Панели на приложението - Изпратете съобщение за да се активират обажданията. + Изпрати съобщение за да се активират обажданията. търсене видео Контактът е изтрит! @@ -2290,9 +2290,9 @@ Приеми заявка за контакт %1$s приет бяхте приети - Приемете член + Приеми член Добави съобщение - Добави кратък линк + Обнови адрес всички Всички нови съобщения от тези членове ще бъдат скрити! Позволи докладването на съобщения на модераторите. @@ -2420,7 +2420,7 @@ Време за изчакване на поверително рутиране Време за изчакване на протокола във фонов режим Времето на изчакване за установяване на TCP връзка във фонов режим - Профилът ще бъде споделен с адреса. + Адресът ще бъде кратък и вашият профил ще бъде споделен чрез него. Забранете докладването на съобщения на модератори. Отхвърляне Отхвърляне на заявка за контакт @@ -2453,15 +2453,15 @@ Изпращане на лични доклади за нарушения Изпрати заявка Изпрати заявка без съобщение - Изпратете личната си обратна връзка до групи. + Изпрати личната си обратна връзка до групи. Изпратено до вашия контакт след осъществяване на връзка. Задаване на име на чат… Задаване на достъп за членове Задаване на срок на валидност на съобщенията в чатовете. Задайте биография на профила и съобщение при посрещанее. - Споделяне на групов профил чрез линк - Сподели профил - Сподели профил с адрес + Обнови групов линк? + Обнови + Обнови адрес? Споделете адреса си Кратко описание: Кратък линк @@ -2500,4 +2500,8 @@ Вашият контакт Вашата група Вашият профил + Сподели стар адрес + Сподели стар линк + Линкът ще бъде кратък и профилът на групата ще бъде споделен чрез него. + Обнови групов линк 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 b312b345c9..57891501f8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -2400,7 +2400,7 @@ Suprimir el xat amb membre? %d xats amb membres tots(es) - Afegir enllaç curt + Actualitzar l\'adreça Acceptar la sol·licitud de contacte Afegir un missatge No es pot canviar el perfil @@ -2419,16 +2419,16 @@ Obrir per acceptar Obrir per connectar Obrir per entrar - El perfil es compartirà amb l\'adreça. + L\'adreça serà curta i el vostre perfil es compartirà a través d\'ella. Rebutjar la sol·licitud de contacte sol·licitud enviada Enviar sol·licitud de contacte? Enviar sol·licitud Enviar sol·licitud sense missatge Enviat al contacte després de la connexió. - Compartir el perfil del grup mitjançant un enllaç - Compartir el perfil - Compartir el perfil amb l\'adreça + Actualitzar l\'enllaç del grup? + Actualitzar + Actualitzar l\'adreça? L\'emissor(a) NO serà notificat(da). Per utilitzar un altre perfil després d\'un intent de connexió, suprimiu el xat i torneu a utilitzar l\'enllaç. Missatge de benvinguda @@ -2473,4 +2473,8 @@ Actualitzar la vostra adreça Utilitzar el perfil d\'incògnit Doneu la benvinguda als vostres contactes 👋 + Compartir l\'adreça antiga + Compartir l\'enllaç antic + L\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell. + Actualitzar l\'enllaç del grup 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 29704b0f66..2421337ce8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -1454,7 +1454,7 @@ Inkognito beim Verbinden einschalten. - Verbindung mit dem Directory-Service (BETA)!\n- Empfangsbestätigungen (für bis zu 20 Mitglieder).\n- Schneller und stabiler. Zum Verbinden senden - Direkt miteinander verbunden + Angefragte Verbindung Erweitern Verbindungsanfrage wiederholen? Gelöschter Kontakt @@ -2586,4 +2586,8 @@ Alten Link teilen Der Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt. Gruppen-Link aktualisieren + KONTAKTANFRAGEN VON GRUPPEN + Mitglied ist gelöscht - Anfrage kann nicht angenommen werden + Angefragte Verbindung von Gruppe %1$s + Diese Einstellung gilt für Ihr aktuelles Profil diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 179c7fec52..2d27bb3592 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -395,4 +395,4 @@ Καλύτερο για τη ζωή της μπαταρίας . Θα λαμβάνετε ειδοποιήσεις μόνο όταν εκτελείται η εφαρμογή (ΧΩΡΙΣ υπηρεσία παρασκηνίου).]]> Beta Καλύτερες κλήσεις - \ No newline at end of file + diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 10763c7655..18458e8ebc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -44,7 +44,7 @@ Aceptar llamada (sin cifrar) llamada - Llamadas y videollamadas + Llamadas y Videollamadas Audio desactivado Audio activado ID de mensaje erróneo @@ -52,7 +52,7 @@ Se eliminarán todos los chats y mensajes. ¡No puede deshacerse! Aceptar Se permiten mensajes temporales. - Android Keystore se usará para almacenar la frase de contraseña de forma segura - permite que el servicio de notificación funcione. + Android Keystore se usará para almacenar la frase de contraseña de forma segura - permite que el servicio de notificaciones funcione. Añadir perfil Color Permites a tus contactos eliminar irreversiblemente los mensajes enviados. (24 horas) @@ -108,7 +108,7 @@ Llamada con cifrado de extremo a extremo cifrado de extremo a extremo mensaje duplicado - Herramientas para desarrolladores + Herramientas de desarrollo Eliminar los archivos de todos los perfiles Activar ¡Base de datos cifrada! @@ -996,7 +996,7 @@ El archivo se recibirá cuando el contacto termine de subirlo. La imagen se recibirá cuando el contacto termine de subirla. Mostrar opciones para desarrolladores - Ocultar: + Oculta: Muestra: Eliminar perfil Contraseña del perfil @@ -1380,7 +1380,7 @@ \n- confirmaciones de entrega (hasta 20 miembros). \n- mayor rapidez y estabilidad. envia para conectar - conectado directamente + conexión solicitada Expandir Error de renegociación de cifrado contacto eliminado @@ -2459,7 +2459,7 @@ Abrir para conectar Abrir para unirte Timeout enrutamiento privado - La dirección será corta y tu perfil se compartirá mediante la dirección. + La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. Timeout protocolo en segundo plano Rechazar solicitud del contacto Elimina mensajes y bloquea miembros. @@ -2500,7 +2500,7 @@ 4 idiomas nuevos Catalán, Indonesio, Rumano y Vietnamita - gracias a los colaboradores! Crea tu dirección - Activa por defecto los mensajes temporaes + Activa por defecto los mensajes temporales Mantén los chats limpios Añade mensaje de bienvenida y biografía del perfil Comparte tu dirección @@ -2511,4 +2511,8 @@ Compartir enlace antiguo El enlace será corto y el perfil del grupo se compartirá mediante el enlace. Actualizar enlace de grupo + SOLICITUDES DE CONTACTO EN GRUPOS + conexión solicitada desde el grupo %1$s + Esta configuración se aplica al perfil actual + Miembro eliminado, no puede aceptar solicitudes 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 1e0ce6f894..c6f89dd3c0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -90,7 +90,7 @@ titkosítás elfogadása… Nem lehet meghívni a partnert! hibás az üzenet azonosítója - Partnerkérések automatikus elfogadása + Partneri kapcsolatkérések automatikus elfogadása Megjegyzés: NEM fogja tudni helyreállítani, vagy módosítani a jelmondatot abban az esetben, ha elveszíti.]]> hívás… További másodlagos szín @@ -142,7 +142,7 @@ Hibás a számítógép címe Profil hozzáadása Mellékelés - Alkalmazás jelkód + Alkalmazásjelkód Felkérték a kép fogadására Kamera Nem érhető el a Keystore az adatbázis jelszavának mentéséhez @@ -193,7 +193,7 @@ Kapcsolódás Közvetlenül kapcsolódik? Kapcsolódás - közvetlenül kapcsolódott + partnerkapcsolatot kért kapcsolat %1$d a partner e2e titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. @@ -583,7 +583,7 @@ Hiba történt a cím módosításának megszakításakor Hiba történt a fájl fogadásakor titkosítása rendben van - Hiba történt a partnerkérés törlésekor + Hiba történt a partneri kapcsolatkérés törlésekor Engedélyezi a kézbesítési jelentéseket a csoportok számára? Partner általi javítás nem támogatott Fájl nem található @@ -608,7 +608,7 @@ Hiba történt a cím létrehozásakor engedélyezve Hiba történt a részletek betöltésekor - Hiba történt a partnerkérés elfogadásakor + Hiba történt a partneri kapcsolatkérés elfogadásakor a titkosítás újraegyeztetése engedélyezve van %s számára a titkosítás újraegyeztetése szükséges Rejtett csevegési profilok @@ -737,13 +737,13 @@ Az onion kiszolgálók nem lesznek használva. perc Tudjon meg többet - Új partnerkérés + Új partneri kapcsolatkérés Csatlakozás a csoporthoz Társított számítógép beállítások meghíva a saját csoporthivatkozásán keresztül elhagyta a csoportot Társított számítógépek - Nincs alkalmazás jelkód + Nincs alkalmazásjelkód Némítás, ha inaktív! A meghívó lejárt! (csak a csoporttagok tárolják) @@ -1488,7 +1488,7 @@ A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél. Elindítja a csevegést? Látható előzmények - Alkalmazás jelkód + Alkalmazásjelkód Partner hozzáadása Koppintson ide a QR-kód beolvasásához Koppintson ide a hivatkozás beillesztéséhez @@ -2406,8 +2406,8 @@ Csevegés megnyitása Új csevegés megnyitása Új csoport megnyitása - végpontok közötti titkosítással vannak védve.]]> - Hiba történt a partnerkérés elutasításakor + végpontok közötti titkosítással vannak védve.]]> + Hiba történt a partneri kapcsolatkérés elutasításakor Hiba a csevegés megnyitásakor Hiba a csoport megnyitásakor Hiba a profil módosításakor @@ -2418,13 +2418,13 @@ Csatlakozás a csoporthoz Üzenet hozzáadása Kapcsolódás - Elküldi a partnerkérést? + Elküldi a partneri kapcsolatkérést? miután a kérését elfogadták.]]> Kérés küldése üzenet nélkül Kérés küldése kérés elküldve - Partnerkérés elfogadása - Elutasítás + Partneri kapcsolatkérés elfogadása + Partneri kapcsolatkérés elutasítása A feladó NEM lesz értesítve. Saját profil Elküldés a partnernek a kapcsolódást követően. @@ -2454,7 +2454,7 @@ Bemutatkozás: A bemutatkozás túl hosszú A leírás túl hosszú - Partnerkérés elfogadása + Partneri kapcsolatkérés elfogadása Üzleti kapcsolat Csoport Koppintson a „Csatlakozás a csoporthoz” gombra @@ -2474,9 +2474,13 @@ Cím frissítése Üdvözölje a partnereit 👋 4 új kezelőfelületi nyelv - Katalán, indonéz, román és vietnami - köszönjük felhasználóinknak! + Katalán, indonéz, román és vietnami – köszönjük felhasználóinknak! Csoporthivatkozás frissítése A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül. Régi cím megosztása Régi hivatkozás megosztása + PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL + A tag törölve lett – nem lehet elfogadni a kérést + a(z) %1$s nevű csoportból partnerkapcsolatot kért + Ez a beállítás a jelenlegi profiljára vonatkozik 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 9bd858e4f8..c67e843d07 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -16,13 +16,13 @@ Accent suplimentar Suplimentar secundar Acceptă - 6 limbi noi pentru interfață + 6 noi limbi de interfață %1$d mesaje nu au putut fi decriptate. %1$d mesaj(e) omis(e) %1$s MEMBRI 1 zi 1 minut - Link unic + Link de unică folosință 5 minute Despre SimpleX Despre adresa SimpleX @@ -234,7 +234,7 @@ Repetă Trimiteți previzualizări de linkuri Setați frază de acces - Partajează adresă + Partajează adresa Trimis la Secundar Mesaj trimis @@ -286,7 +286,7 @@ Nume contact… Trimite mesaj Trimiteți un mesaj care dispare - (scanează sau lipește din Copiate) + (scanează sau lipește din clipboard) Afișează codul QR Testul serverului a eșuat! Afișează: @@ -322,19 +322,19 @@ Serviciul SimpleX Chat Adresă SimpleX Oprește - Link-uri SimpleX - Link-urile SimpleX sunt interzise. + Linkuri SimpleX + Linkurile 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 + Linkurile SimpleX nu sunt permise Echipa SimpleX Opriți? Adresă de contact SimpleX Link pentru grup SimpleX - Link-uri SimpleX + Linkuri SimpleX Invitație de unică folosință SimpleX Logo SimpleX Grupuri mici (max 20) @@ -520,7 +520,7 @@ Te conectezi la tine? Verifică conexiunea la internet și încearcă din nou Culori conversație - Aspectul discuției + Temă pentru chat Bun pentru baterie. Aplicația verifică mesajele la fiecare 10 minute. Pot fi pierdute apeluri sau mesaje urgente.]]> Negru Blocați membrul pentru toți? @@ -530,7 +530,7 @@ Nu se poate trimite mesajul Șterge Confirmați fișiere de la servere necunoscute. - schimbat adresa pentru dumneavoastră + am schimbat adresa pentru tine Confirmați parola nouă… Conectare conexiune %1$d @@ -613,7 +613,7 @@ Șterge pentru mine %dd șters - Șterge contact + Șterge contactul Șters la Versiunea aplicației desktop %s nu este compatibilă cu această aplicație. Ștergeți mesajul membrului? @@ -624,7 +624,7 @@ %d contact(e) selectat(e) Șters la: %s Ștergi profilul de conversație? - Ștergeți profilul + Șterge profilul implicit (%s) Confirmări de livrare! Confirmările de livrare sunt dezactivate! @@ -636,12 +636,12 @@ Șterge după Ștergeți pentru toată lumea Descriere - Șterge fișier - Ștergeți coada + Șterge fișierul + Șterge coada Ștergeți contactul? Șterge Șterge - Ștergi fișiere și media? + Ștergi fișierele și conținutul media? %d zile %d zi Șterge adresa @@ -767,8 +767,8 @@ 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.]]> + criptare end-to-end, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> + criptare end-to-end rezistentă la computere cuantice, cu confidențialitate perfectă în avans, nerepudiere și recuperare în caz de compromitere.]]> Acest chat este protejat prin criptare end-to-end. Link complet incognito printr-un link de unică folosință @@ -883,7 +883,7 @@ Invitația a expirat! Grup Invitați în grup - Deconectați telefoanele mobile + Deconectează dispozitivele mobile Nume afișat nevalid! Eroare la schimbarea profilului! Fără servere media și de fișiere. @@ -938,7 +938,7 @@ Eroare la schimbarea adresei Eroare la sincronizarea conexiunii Eroare - Deconectați-vă + Deconectează Execuția funcției durează prea mult: %1$d secunde: %2$s Ascunde Introduceți parola @@ -973,7 +973,7 @@ 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 să o utilizezi + Cum se utilizează 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. @@ -992,9 +992,9 @@ Instalat cu succes Dezactivează Opțiuni dezvoltator - Notificările vor înceta să funcționeze până când nu redeschideți aplicația. + Notificările vor înceta să funcționeze până când nu redeschideți aplicația Eroare la salvarea setărilor - Ștergeți mesajele după + Șterge mesajele după %d sec Email Eroare la salvarea parolei utilizatorului @@ -1010,7 +1010,7 @@ Găsește conversații mai rapid Luminos Nu - Deschide linkul web? + Deschizi link-ul web? MESAJE ȘI FIȘIERE moderator Rol inițial @@ -1038,7 +1038,7 @@ 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 + Deschide setările Acordare în setări Instant Cum afectează bateria @@ -1116,7 +1116,7 @@ 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? + Deconectezi desktopul? Nu activați Eroare la primirea fișierului Eroare la ștergerea solicitării de contact @@ -1157,7 +1157,7 @@ Arhivă veche a bazei de date Deschideți folderul bazei de date implicit (%s) - Activați ștergerea automată a mesajelor? + Activezi ș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ă @@ -1231,9 +1231,9 @@ Eroare Versiune incompatibilă Dispozitiv mobil nou - Descoperibil prin rețeaua locală - Descoperiți prin intermediul rețelei locale - Deschideți portul în firewall + Detectabil prin rețeaua locală + Detectează prin rețeaua locală + Deschide portul în firewall Desktopul este inactiv Desktopul a fost deconectat Desktopul are codul de invitație greșit @@ -1308,7 +1308,7 @@ Î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 + Deschide setările serverului criptare ok pentru %s profilul grupului a fost actualizat proprietar @@ -1316,7 +1316,7 @@ ieșit Remedierea nu este suportată de contact Erori - Deschideți grupul + Deschide grupul Mesajele vor fi șterse - acest lucru nu poate fi anulat! Eroare la criptarea bazei de date Fișiere @@ -1334,7 +1334,7 @@ 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 + Deschide 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 @@ -1381,7 +1381,7 @@ Servere media și fișiere Alte servere XFTP NU utilizați rutare privată. - Activați apelurile de pe ecranul de blocare prin Setări. + Activează apelurile de pe ecranul de blocare prin Setări. Dezactivează (păstrează suprascrierile) Dacă introduceți parola de autodistrugere în timp ce deschideți aplicația: Mediu @@ -1419,7 +1419,7 @@ Instalați update Nu crea adresă 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. + Deschide Setările Safari / Site-uri web / Microfon, apoi selectează Permite pentru localhost. Apel ratat Renegocierea criptării a eșuat. Oprit @@ -1449,7 +1449,7 @@ 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 + Descoperă și alătură-te 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ă @@ -1547,7 +1547,7 @@ Linkuri de grup Ștergere ireversibilă a mesajului Măriți dimensiunea fontului. - Deconectați-vă + Deconectează Deconectat din motivul: %s Eroare la ștergerea bazei de date Eroare la încărcarea arhivei @@ -1608,7 +1608,7 @@ 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. + Te rog să verifici dacă linkul SimpleX este corect. Vă rugăm să contactați administratorul grupului. Se pregătește încărcarea Acces refuzat! @@ -1667,7 +1667,7 @@ Parolă Servere presetate Lipiți adresa desktopului - Este posibil ca amprenta certificatului din adresa serverului să fie incorectă. + Este posibil ca amprenta certificatului din adresa serverului să fie incorectă Interval PING-uri Expirare protocol per KB Notificări private @@ -1678,7 +1678,7 @@ 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 + Mesaj primit Note private Bara de instrumente de chat accesibilă Temă de profil @@ -1718,7 +1718,7 @@ În așteptare confirmare primită… 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. + Te rog să verifici dacă ai folosit linkul corect sau cere contactului tău să îți trimită unul nou. Eroare de rutare privată respins Răspuns primit @@ -1763,7 +1763,7 @@ Primit total Primiți erori Reconectați serverul pentru a forța livrarea mesajului. Consumă trafic suplimentar. - Resetați toate statisticile + Resetează toate statisticile Trimis prin proxy Trimis direct revizuit de administratori @@ -1789,7 +1789,7 @@ Informații despre servere Reconectați serverele? Statisticile serverelor vor fi resetate - această acțiune nu poate fi anulată! - Reconectați toate serverele + Reconectează toate serverele Reconectați serverul? Trimis total Preferințele de chat selectate interzic acest mesaj. @@ -1810,7 +1810,7 @@ Trimite confirmări de citire Setați 1 zi Parolă de autodistrugere - Reconectați toate serverele conectate pentru a forța livrarea mesajelor. Aceasta utilizează trafic suplimentar. + Reconectează toate serverele conectate pentru a forța livrarea mesajelor. Aceasta folosește trafic suplimentar. Se primesc mesaje… Motivul raportării? respins @@ -2025,7 +2025,7 @@ Atingeți pentru a activa profilul. Sistem Mulțumim utilizatorilor – contribuiți prin Weblate! - Verificați securitatea conexiunii + Verifică 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. @@ -2041,12 +2041,12 @@ 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. + 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 + Verifică parola bazei de date Ethernet prin cablu Abonamente ignorate Mărime @@ -2154,7 +2154,7 @@ Aceasta este propria ta adresă SimpleX! Se încarcă arhiva %s a fost încărcat - Verificați parola + Verifică parola Blocarea SimpleX nu este activă! Autentificare sistem Baza de date nu funcționează corect. Atingeți pentru a afla mai multe. @@ -2192,7 +2192,7 @@ Mesajele vocale sunt interzise. Protocoalele SimpleX analizate de Trail of Bits. Acest link a fost utilizat cu un alt dispozitiv mobil, vă rugăm să creați un link nou pe desktop. - Verificați conexiunile + Verifică 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 @@ -2416,4 +2416,16 @@ contactul ar trebui să accepte… Descriere prea lungă Eroare la modificarea profilului + Folosește profil incognito + Deschide conversația + Eroare la respingerea cererii de contact + 4 noi limbi de interfață + Catalană, Indoneziană, Română și Vietnameză - mulțumită utilizatorilor noștri! + criptare end-to-end.]]> + Deschide conversație nouă + Creează un grup nou + Deschide pentru a accepta + Deschide pentru conectare + Deschide pentru a te alătura + Timp de așteptare depășit pentru rutarea privată 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 ece356ec46..a13106edaf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -1333,6 +1333,7 @@ Нет истории Отправка отчётов о доставке включена для %d контактов. Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата. + Установка для Вашего активного профиля Установки для Вашего активного профиля Отправка отчётов о доставке выключена для %d контактов. Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения! @@ -1359,6 +1360,7 @@ Включить (кроме исключений) Выключить отчёты о доставке\? ОТПРАВКА ОТЧЁТОВ О ДОСТАВКЕ + ЗАПРОСЫ НА СОЕДИНЕНИЕ ИЗ ГРУПП шифрование согласовано шифрование согласовано для %s шифрование работает @@ -1467,6 +1469,7 @@ Повторить запрос на соединение? Ошибка нового соглашения о шифровании удалил(а) контакт + запрос на соединение из группы %1$s Ошибка Создайте группу, используя случайный профиль. Создать группу @@ -2561,6 +2564,7 @@ Цель: Фоновый таймаут TCP-соединения Отправитель не будет уведомлён. + Член группы удалён - невозможно принять запрос Чтобы использовать другой профиль после попытки соединения, удалите чат и используйте ссылку снова. Приветственное сообщение О Вас: 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 4c342076ba..ddd54b717f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -200,7 +200,7 @@ редаговано помилка відправки непрочитане - приєднатися як %s + Приєднуйтесь як %s Скасувати попередній перегляд зображення Скасувати попередній перегляд файлу Очікування на зображення @@ -571,7 +571,7 @@ Вітаємо, %1$s! Вітаємо! Цей текст доступний у налаштуваннях - вас запрошено в групу + Запрошуємо вас до групи Поділитися повідомленням… Поділитися медіа… Поділитися файлом… @@ -586,7 +586,7 @@ Відео Ваш контакт відправив файл, розмір якого більший, ніж поточно підтримуваний максимальний розмір (%1$s). Поточно максимально підтримуваний розмір файлу - %1$s. - Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з\'явиться в мережі. + Адреса отримання буде змінена на інший сервер. Зміна адреси буде завершена після того, як відправник з\'явиться в мережі. Перевірити код безпеки Надіслати повідомлення Записати голосове повідомлення @@ -1031,7 +1031,7 @@ Зупинити поділ Введіть текст привітання... (необов\'язково) Зберегти налаштування\? - Зберегти налаштування автоприйому + Зберегти налаштування адреси SimpleX Привіт! \nПриєднуйтесь до мене через SimpleX Chat: %s Запросити друзів @@ -1426,7 +1426,7 @@ \n- швидше та надійніше. Ключова фраза зберігається в налаштуваннях як звичайний текст. Ви вже подали запит на підключення за цією адресою! - надіслати приватне повідомлення + відправити для підключення Показувати консоль в новому вікні Усі нові повідомлення від %s будуть приховані! підключив(лась) безпосередньо @@ -2058,7 +2058,7 @@ Видалити архів? Поділитися профілем Завантажений архів бази даних буде остаточно видалено з серверів. - Підключення було перенесено до %s, але під час перенаправлення на профіль сталася непередбачена помилка. + Ваше з\'єднання було переміщено на %s, але при перемиканні профілю сталася помилка. Режим системи Не використовуйте облікові дані з проксі. Аутентифікація проксі @@ -2088,7 +2088,7 @@ Повідомлення були видалені після того, як ви їх вибрали. Помилка при пересиланні повідомлень Звук вимкнено - Помилка ініціалізації WebView. Переконайтеся, що WebView встановлено, і його підтримувана архітектура — arm64. \nПомилка: %s + Помилка під час ініціалізації WebView. Переконайтеся, що WebView встановлено і що його архітектура підтримується arm64.\nПомилка: %s Хвіст Кут Форма повідомлення @@ -2419,7 +2419,7 @@ Схвалювати учасників вимкнено Схвалювати учасників для вступу до групи. - Додати коротке посилання + Адреса оновлення не синхронізовано схвалено адміністраторами очікує на схвалення @@ -2436,4 +2436,80 @@ Відхилити Відхилити учасника? ви вийшли + 4 нові мови інтерфейсу + Прийняти запит на контакт + Прийняти запит на контакт + Додати повідомлення + Біо: + Біографія занадто велика + Бізнес-зв\'язок + Не вдається змінити профіль + Каталонська, індонезійська, румунська та в\'єтнамська - завдяки нашим користувачам! + наскрізним шифруванням.]]> + лише після того, як ваш запит буде прийнятий.]]> + Чат з адміністраторами + Спілкуйтеся з учасниками до того, як вони приєднаються. + Підключіться + Підключайтеся швидше! 🚀 + контакт повинен прийняти… + Створіть свою адресу + Опис занадто великий + Увімкнути зникаючі повідомлення за замовчуванням. + Помилка зміни профілю + Помилка відкриття чату + Помилка відкриття групи + Помилка відхилення запиту на контакт + Група + Приєднуйтесь до групи + Підтримуйте чистоту в чатах + Менше трафіку в мобільних мережах. + Завантаження профілю… + Миттєве повідомлення, щойно ви натиснете \"Підключитися\". + Нова роль у групі: Модератор + Немає приватного сеансу маршрутизації + Відкритий чат + Відкрити новий чат + Відкрити нову групу + Відкрити для прийняття + Відкрито для підключення + Відкрито для приєднання + Тайм-аут приватної маршрутизації + Фоновий тайм-аут протоколу + Відхилити запит на контакт + Видаляє повідомлення та блокує користувачів. + запит відправлено + Учасники групи оглядів + Надіслати запит на контакт? + Надіслати запит + Надіслати запит без повідомлення + Надсилайте свої приватні відгуки до груп. + Відправлено вашому контакту після з\'єднання. + Налаштуйте біографію профілю та вітальне повідомлення. + Поділіться старою адресою + Поділіться старим посиланням + Поділіться своєю адресою + Короткий опис: + Коротка адреса SimpleX + Натисніть Підключитися до чату + Натисніть Підключитися, щоб відправити запит + Натисніть Приєднатися до групи + Таймаут TCP-з\'єднання bg + Адреса буде короткою, і ваш профіль буде доступний за цією адресою. + Посилання буде коротким, а профіль групи буде поширюватися за посиланням. + Відправник НЕ буде повідомлений. + Час зникнення встановлюється тільки для нових контактів. + Щоб використовувати інший профіль після спроби з\'єднання, видаліть чат і скористайтеся посиланням знову. + Оновіть свою адресу + Оновлення + Змінити адресу? + Оновити посилання на групу + Оновити посилання на групу? + Використовуйте профіль інкогніто + Вітальне повідомлення + Вітаємо ваші контакти 👋 + Твоя біографія: + Ваш діловий контакт + Ваш контакт + Ваша група + Ваш профіль 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 79bab6d3b0..fa781773c9 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 @@ -183,7 +183,7 @@ 点击以加入隐身聊天 你的聊天资料将被发送给群成员 你正在尝试邀请与你共享隐身个人资料的联系人加入你使用主要个人资料的群 - 隐身模式通过为每个联系人使用新的随机配置文件来保护你的隐私。 + 隐身模式通过为每个联系人使用新的随机个人资料来保护你的隐私。 你正在为该群使用隐身个人资料——为防止共享你的主要个人资料,不允许邀请联系人 通过一次性链接隐身 只有群主可以启用语音信息。 @@ -460,7 +460,7 @@ 无效的服务器地址! 邀请成员 离开群 - 仅本地配置文件数据 + 仅本地个人资料数据 即时通知 即时通知! 使用你的凭据登录 @@ -853,7 +853,7 @@ SimpleX 团队 %1$s 名成员 - 你将在组主设备上线时连接到该群,请稍等或稍后再检查! + 你将在群主设备上线时连接到该群,请稍等或稍后再检查! 当你启动应用或在应用程序驻留后台超过30 秒后,你将需要进行身份验证。 连接到 SimpleX Chat 开发者提出任何问题并接收更新 。]]> 你已接受连接 @@ -885,7 +885,7 @@ 你将 %s 的角色更改为 %s 你将自己的角色更改为 %s 你已更改地址 - 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该组的成员。 + 你可以共享链接或二维码——任何人都可以加入该群。如果你稍后将其删除,你不会失去该群的成员。 间接(%1$s) 在移动应用程序中打开按钮。]]> SimpleX @@ -905,7 +905,7 @@ %d 天 %dw 你被邀请加入群。 加入以与群成员联系。 - 你加入了这个群。连接到邀请组成员。 + 你加入了这个群。连接到邀请群成员。 你更改了 %s 的地址 你已离开 已选择 %d 名联系人 @@ -939,7 +939,7 @@ 隐藏 将个人资料设置为私密! 静音 - 保存并更新组配置文件 + 保存并更新群资料 不再显示 不活跃时静音! 语音和视频通话 @@ -969,8 +969,8 @@ 感谢用户——通过 Weblate 做出贡献! 解除静音 欢迎消息 - 当静音配置文件处于活动状态时,你仍会收到来自静音配置文件的电话和通知。 - 你可以隐藏或静音用户配置文件——长按以显示菜单。 + 当静音个人资料处于活动状态时,你仍会收到来自静音个人资料的电话和通知。 + 你可以隐藏或静音用户个人资料——长按以显示菜单。 欢迎消息 确认数据库升级 实验性 @@ -1268,7 +1268,7 @@ 你的个人资料 %1$s 将被共享。 将为所有联系人启用送达回执功能。 打开应用程序设置 - 为所有组启用 + 为所有群启用 %s、%s 和 %d 其他成员已连接 已为 %d 联系人启用送达回执功能 已同意 %s 的加密 @@ -1277,7 +1277,7 @@ 使用新的隐身个人资料 过滤未读和收藏的聊天记录。 已更改安全密码 - 将为所有可见聊天配置文件中的所有联系人启用送达回执功能。 + 将为所有可见聊天个人资料中的所有联系人启用送达回执功能。 应用电池使用情况 / 无限制。]]> %s 在 %s 禁用回执? @@ -1285,12 +1285,12 @@ 可以在联系人和群设置中覆盖它们。 对所有联系人关闭 随机密码以明文形式存储在设置中。 \n你可以稍后更改。 - 已禁用 %d 组的送达回执功能 + 已禁用 %d 群的送达回执功能 需要为 %s 重新协商加密 SimpleX 无法在后台运行。只有在应用程序运行时,你才会收到通知。 启用(保留覆盖) 即将更新数据库加密密码并将其存储在设置中。 - 使用当前配置文件 + 使用当前个人资料 从设置中删除密码? 同意加密 启用回执? @@ -1309,12 +1309,12 @@ 为群禁用回执吗? %s、%s 和 %s 已连接 修复群成员不支持的问题 - 已为 %d 组启用送达回执功能 + 已为 %d 群启用送达回执功能 重新协商 禁用(保留覆盖) 设置数据库密码 已禁用 %d 联系人的送达回执功能 - 启用(保留组覆盖) + 启用(保留群覆盖) 应用电池使用情况 / 无限制。]]> 送达回执已禁用 打开数据库文件夹 @@ -1333,7 +1333,7 @@ 没有选中的聊天 可以加密 重新协商加密 - 禁用(保留组覆盖) + 禁用(保留群覆盖) 为群启用回执吗? 修复联系人不支持的问题 对 %s 加密正常 @@ -1342,10 +1342,10 @@ 禁用通知 回复 不启用 - 连接请求将发送给该组成员。 + 连接请求将发送给该群成员。 密码以明文形式存储在设置中。 同步连接时出错 - 这些设置适用于你当前的配置文件 + 这些设置适用于你当前的个人资料 允许为 %s 重新协商加密 为所有人启用 需要重新协商加密 @@ -1366,7 +1366,7 @@ 创建成员联系人时出错 发送私信来连接 发送私信来连接 - 已直连 + 已请求连接 展开 重复连接请求吗? 已删除联系人 @@ -1852,7 +1852,7 @@ 显示百分比 不活跃 缩放 - 所有配置文件 + 所有个人资料 文件 没有信息,试试重新加载 服务器信息 @@ -1930,7 +1930,7 @@ 订阅被忽略 已配置的 SMP 服务器 已配置的 XFTP 服务器 - 当前配置文件 + 当前个人资料 传输会话 已上传 已停用 @@ -2046,16 +2046,16 @@ 上传的数据库存档将永久性从服务器被删除。 确保代理配置正确 消息将被删除 - 此操作无法撤销! - 你的连接被移动到 %s,但在切换配置文件时发生了错误。 + 你的连接被移动到 %s,但在切换个人资料时发生了错误。 代理不使用身份验证凭据 - 切换配置文件出错 + 切换个人资料出错 代理身份验证 删除存档? - 选择聊天配置文件 + 选择聊天个人资料 保存代理出错 密码 每个连接使用不同的代理身份验证凭据。 - 每个配置文件使用不同的代理身份验证。 + 每一个人资料使用不同的代理身份验证。 你的凭据可能以未经加密的方式被发送。 使用随机凭据 用户名 @@ -2095,7 +2095,7 @@ 一次转发最多20条消息。 Trail of Bits 审核了 SimpleX 协议。 通话期间切换音频和视频。 - 对一次性邀请切换聊天配置文件。 + 对一次性邀请切换聊天个人资料。 更佳的通话 允许自行删除或管理员移除最多200条消息。 保存服务器出错 @@ -2339,7 +2339,7 @@ 更快地删除群。 更快发送消息。 被提及时收到通知。 - 帮助管理员管理群组。 + 帮助管理员管理群。 将聊天组织到列表 私密媒体文件名。 发送私下举报 @@ -2363,7 +2363,7 @@ 隐私政策和使用条款。 接受 使用 SimpleX Chat 代表您同意:\n- 在公开群中只发送合法内容\n- 尊重其他用户 – 没有垃圾信息。 - 服务器运营方无法访问私密聊天、群组和你的联系人。 + 服务器运营方无法访问私密聊天、群和你的联系人。 配置服务器运营方 不支持的连接链接 SimpleX 频道链接 @@ -2499,4 +2499,8 @@ 升级群链接 使用隐身个人资料 欢迎联系人👋 + 来自%1$s群的连接请求 + 此设置用于当前个人资料 + 来自群的联络请求 + 成员被删除——无法接受请求 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt index 03c8e51c55..f1103bc516 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.desktop.kt @@ -31,6 +31,7 @@ actual fun CustomTimePicker( mutableStateOf(res) } val values = remember(unit.value) { + // TODO replace with firstOrNull val limit = timeUnitsLimits.first { it.timeUnit == unit.value } val res = ArrayList>() for (i in limit.minValue..limit.maxValue) { diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index d352366e35..d92e8f465e 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.4.1 -android.version_code=307 +android.version_name=6.4.2 +android.version_code=309 android.bundle=false -desktop.version_name=6.4.1 -desktop.version_code=114 +desktop.version_name=6.4.2 +desktop.version_code=115 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 aa2374f919..290742ce03 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -124,13 +124,13 @@ data DirectoryCmdTag (r :: DirectoryRole) where DCDeleteGroup_ :: DirectoryCmdTag 'DRUser DCMemberRole_ :: DirectoryCmdTag 'DRUser DCGroupFilter_ :: DirectoryCmdTag 'DRUser + DCShowUpgradeGroupLink_ :: DirectoryCmdTag 'DRUser DCApproveGroup_ :: DirectoryCmdTag 'DRAdmin DCRejectGroup_ :: DirectoryCmdTag 'DRAdmin DCSuspendGroup_ :: DirectoryCmdTag 'DRAdmin DCResumeGroup_ :: DirectoryCmdTag 'DRAdmin DCListLastGroups_ :: DirectoryCmdTag 'DRAdmin DCListPendingGroups_ :: DirectoryCmdTag 'DRAdmin - DCShowGroupLink_ :: DirectoryCmdTag 'DRAdmin DCSendToGroupOwner_ :: DirectoryCmdTag 'DRAdmin DCInviteOwnerToGroup_ :: DirectoryCmdTag 'DRAdmin -- DCAddBlockedWord_ :: DirectoryCmdTag 'DRAdmin @@ -156,13 +156,13 @@ data DirectoryCmd (r :: DirectoryRole) where DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser DCMemberRole :: UserGroupRegId -> Maybe GroupName -> Maybe GroupMemberRole -> DirectoryCmd 'DRUser DCGroupFilter :: UserGroupRegId -> Maybe GroupName -> Maybe DirectoryMemberAcceptance -> DirectoryCmd 'DRUser + DCShowUpgradeGroupLink :: GroupId -> Maybe GroupName -> DirectoryCmd 'DRUser DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRAdmin DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCListLastGroups :: Int -> DirectoryCmd 'DRAdmin DCListPendingGroups :: Int -> DirectoryCmd 'DRAdmin - DCShowGroupLink :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin DCSendToGroupOwner :: GroupId -> GroupName -> Text -> DirectoryCmd 'DRAdmin DCInviteOwnerToGroup :: GroupId -> GroupName -> DirectoryCmd 'DRAdmin -- DCAddBlockedWord :: Text -> DirectoryCmd 'DRAdmin @@ -200,13 +200,13 @@ directoryCmdP = "delete" -> u DCDeleteGroup_ "role" -> u DCMemberRole_ "filter" -> u DCGroupFilter_ + "link" -> u DCShowUpgradeGroupLink_ "approve" -> au DCApproveGroup_ "reject" -> au DCRejectGroup_ "suspend" -> au DCSuspendGroup_ "resume" -> au DCResumeGroup_ "last" -> au DCListLastGroups_ "pending" -> au DCListPendingGroups_ - "link" -> au DCShowGroupLink_ "owner" -> au DCSendToGroupOwner_ "invite" -> au DCInviteOwnerToGroup_ -- "block_word" -> au DCAddBlockedWord_ @@ -266,6 +266,7 @@ directoryCmdP = "=all" $> PCAll <|> ("=noimage" <|> "=no_image" <|> "=no-image") $> PCNoImage <|> pure PCAll + DCShowUpgradeGroupLink_ -> gc_ DCShowUpgradeGroupLink DCApproveGroup_ -> do (groupId, displayName) <- gc (,) groupApprovalId <- A.space *> A.decimal @@ -275,7 +276,6 @@ directoryCmdP = DCResumeGroup_ -> gc DCResumeGroup DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) DCListPendingGroups_ -> DCListPendingGroups <$> (A.space *> A.decimal <|> pure 10) - DCShowGroupLink_ -> gc DCShowGroupLink DCSendToGroupOwner_ -> do (groupId, displayName) <- gc (,) msg <- A.space *> A.takeText @@ -299,17 +299,17 @@ directoryCmdTag = \case DCRecentGroups -> "new" DCSubmitGroup _ -> "submit" DCConfirmDuplicateGroup {} -> "confirm" - DCListUserGroups -> "list" + DCListUserGroups -> "list" DCDeleteGroup {} -> "delete" DCApproveGroup {} -> "approve" DCMemberRole {} -> "role" DCGroupFilter {} -> "filter" + DCShowUpgradeGroupLink {} -> "link" DCRejectGroup {} -> "reject" DCSuspendGroup {} -> "suspend" DCResumeGroup {} -> "resume" DCListLastGroups _ -> "last" DCListPendingGroups _ -> "pending" - DCShowGroupLink {} -> "link" DCSendToGroupOwner {} -> "owner" DCInviteOwnerToGroup {} -> "invite" -- DCAddBlockedWord _ -> "block_word" diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 7b533a56cc..1cbf419f41 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -29,7 +29,7 @@ import Control.Monad.IO.Class import Data.List (find, intercalate) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe, isJust, maybeToList) +import Data.Maybe (fromMaybe, isJust, isNothing, maybeToList) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) @@ -72,7 +72,12 @@ import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) import System.Directory (getAppUserDataDirectory) import System.Process (readProcess) -data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError +data GroupProfileUpdate + = GPNoServiceLink + | GPServiceLinkAdded {linkNow :: Text} + | GPServiceLinkRemoved + | GPHasServiceLink {linkBefore :: Text, linkNow :: Text} + | GPServiceLinkError data DuplicateGroup = DGUnique -- display name or full name is unique @@ -223,6 +228,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName SDRSuperUser -> deSuperUserCommand ct ciId cmd DELogChatResponse r -> logInfo r where + groupLinkText (CCLink cReq sLnk_) = maybe (strEncodeTxt $ simplexChatContact cReq) strEncodeTxt sLnk_ withAdminUsers action = void . forkIO $ do forM_ superUsers $ \KnownContact {contactId} -> action contactId forM_ adminUsers $ \KnownContact {contactId} -> action contactId @@ -354,14 +360,14 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case - Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = CCLink gLink _}} -> do + Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = gLink}} -> do setGroupStatus st gr GRSPendingUpdate notifyOwner gr "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" - notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink) + notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> groupLinkText gLink Left (ChatError e) -> case e of CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." @@ -386,20 +392,20 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName groupProfileUpdate >>= \case GPNoServiceLink -> notifyOwner gr $ "The profile updated for " <> userGroupRef <> byMember <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded -> groupLinkAdded gr byMember + GPServiceLinkAdded _ -> groupLinkAdded gr byMember GPServiceLinkRemoved -> notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message" <> byMember <> ", please add it." - GPHasServiceLink -> groupLinkAdded gr byMember + GPHasServiceLink {} -> groupLinkAdded gr byMember GPServiceLinkError -> do notifyOwner gr $ ("Error: " <> serviceName <> " has no group link for " <> userGroupRef) <> " after profile was updated" <> byMember <> ". Please report the error to the developers." logError $ "Error: no group link for " <> userGroupRef - GRSPendingApproval n -> processProfileChange gr byMember $ n + 1 - GRSActive -> processProfileChange gr byMember 1 - GRSSuspended -> processProfileChange gr byMember 1 - GRSSuspendedBadRoles -> processProfileChange gr byMember 1 + GRSPendingApproval n -> processProfileChange gr byMember False $ n + 1 + GRSActive -> processProfileChange gr byMember True 1 + GRSSuspended -> processProfileChange gr byMember False 1 + GRSSuspendedBadRoles -> processProfileChange gr byMember False 1 GRSRemoved -> pure () where GroupInfo {groupId, groupProfile = p} = fromGroup @@ -407,7 +413,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName sameProfile GroupProfile {displayName = n, fullName = fn, shortDescr = sd, image = i, description = d} GroupProfile {displayName = n', fullName = fn', shortDescr = sd', image = i', description = d'} = - n == n' && fn == fn' && i == i' && sd == sd' && d == d' + n == n' && fn == fn' && i == i' && sd == sd' && (T.words <$> d) == (T.words <$> d') groupLinkAdded gr byMember = do getDuplicateGroup toGroup >>= \case Nothing -> notifyOwner gr "Error: getDuplicateGroup. Please notify the developers." @@ -419,53 +425,65 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName ("Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message" <> byMember) <> ".\nYou will be notified once the group is added to the directory - it may take up to 48 hours." checkRolesSendToApprove gr gaId - processProfileChange gr byMember n' = do - setGroupStatus st gr GRSPendingUpdate + processProfileChange gr byMember isActive n' = do let userGroupRef = userGroupReference gr toGroup groupRef = groupReference toGroup groupProfileUpdate >>= \case GPNoServiceLink -> do + setGroupStatus st gr GRSPendingUpdate notifyOwner gr $ ("The group profile is updated for " <> userGroupRef <> byMember <> ", but no link is added to the welcome message.\n\n") <> "The group will remain hidden from the directory until the group link is added and the group is re-approved." GPServiceLinkRemoved -> do + setGroupStatus st gr GRSPendingUpdate notifyOwner gr $ ("The group link for " <> userGroupRef <> " is removed from the welcome message" <> byMember) <> ".\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." notifyAdminUsers $ "The group link is removed from " <> groupRef <> ", de-listed." - GPServiceLinkAdded -> do + GPServiceLinkAdded _ -> do setGroupStatus st gr $ GRSPendingApproval n' notifyOwner gr $ ("The group link is added to " <> userGroupRef <> byMember) <> "!\nIt is hidden from the directory until approved." notifyAdminUsers $ "The group link is added to " <> groupRef <> byMember <> "." checkRolesSendToApprove gr n' - GPHasServiceLink -> do - setGroupStatus st gr $ GRSPendingApproval n' - notifyOwner gr $ - ("The group " <> userGroupRef <> " is updated" <> byMember) - <> "!\nIt is hidden from the directory until approved." - notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> "." - checkRolesSendToApprove gr n' + GPHasServiceLink {linkBefore, linkNow} + | isActive && onlyLinkChanged p p' -> do + notifyOwner gr $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nThe group is listed in directory." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> " - only link or whitespace changes.\nThe group remained listed in directory." + | otherwise -> do + setGroupStatus st gr $ GRSPendingApproval n' + notifyOwner gr $ + ("The group " <> userGroupRef <> " is updated" <> byMember) + <> "!\nIt is hidden from the directory until approved." + notifyAdminUsers $ "The group " <> groupRef <> " is updated" <> byMember <> "." + checkRolesSendToApprove gr n' + where + onlyLinkChanged + GroupProfile {displayName = dn, fullName = fn, shortDescr = sd, image = i, description = d} + GroupProfile {displayName = dn', fullName = fn', shortDescr = sd', image = i', description = d'} = + dn == dn' && fn == fn' && i == i' && sd == sd' && (T.words . T.replace linkBefore "" <$> d) == (T.words . T.replace linkNow "" <$> d') GPServiceLinkError -> logError $ "Error: no group link for " <> groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) where profileUpdate = \case Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cr sl_}} -> - let hadLinkBefore = profileHasGroupLink fromGroup - hasLinkNow = profileHasGroupLink toGroup - profileHasGroupLink GroupInfo {groupProfile = gp} = - maybe False (any ftHasLink) $ parseMaybeMarkdownList =<< description gp + let linkBefore_ = profileGroupLinkText fromGroup + linkNow_ = profileGroupLinkText toGroup + profileGroupLinkText GroupInfo {groupProfile = gp} = + maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< description gp ftHasLink = \case FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of CLFull cr' -> sameConnReqContact cr' cr CLShort sl' -> maybe False (sameShortLinkContact sl') sl_ _ -> False - in if - | hadLinkBefore && hasLinkNow -> GPHasServiceLink - | hadLinkBefore -> GPServiceLinkRemoved - | hasLinkNow -> GPServiceLinkAdded - | otherwise -> GPNoServiceLink + in case (linkBefore_, linkNow_) of + (Just linkBefore, Just linkNow) -> GPHasServiceLink linkBefore linkNow + (Just _, Nothing) -> GPServiceLinkRemoved + (Nothing, Just linkNow) -> GPServiceLinkAdded linkNow + (Nothing, Nothing) -> GPNoServiceLink _ -> GPServiceLinkError checkRolesSendToApprove gr gaId = do (badRolesMsg <$$> getGroupRolesStatus toGroup gr) >>= \case @@ -706,8 +724,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName DCListUserGroups -> getUserGroupRegs st (contactId' ct) >>= \grs -> do sendReply $ tshow (length grs) <> " registered group(s)" - void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> - sendGroupInfo ct gr userGroupRegId Nothing + void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {dbGroupId, userGroupRegId} -> + let useGroupId = if isAdmin then dbGroupId else userGroupRegId + in sendGroupInfo ct gr useGroupId Nothing DCDeleteGroup gId gName -> (if isAdmin then withGroupAndReg sendReply else withUserGroupReg) gId gName $ \GroupInfo {groupProfile = GroupProfile {displayName}} gr -> do delGroupReg st gr @@ -718,7 +737,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName case mRole_ of Nothing -> getGroupLink' cc user g >>= \case - Just GroupLink {connLinkContact = CCLink gLink _, acceptMemberRole} -> do + Just GroupLink {connLinkContact = gLink, acceptMemberRole} -> do let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver sendReply $ initialRole n acceptMemberRole @@ -731,7 +750,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated." where initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n" - onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> strEncodeTxt (simplexChatContact gLink) + onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink DCGroupFilter gId gName_ acceptance_ -> (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g @@ -760,6 +779,50 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Nothing -> "_disabled_" Just PCAll -> "_enabled_" Just PCNoImage -> "_enabled for profiles without image_" + DCShowUpgradeGroupLink gId gName_ -> + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, localDisplayName = gName} _ -> do + let groupRef = groupReference' gId gName + withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ + \GroupLink {connLinkContact = gLink@(CCLink _ sLnk_), acceptMemberRole, shortLinkDataSet, shortLinkLargeDataSet = BoolDef slLargeDataSet} -> do + let shouldBeUpgraded = isNothing sLnk_ || not shortLinkDataSet || not slLargeDataSet + sendReply $ T.unlines $ + [ "The link to join the group " <> groupRef <> ":", + groupLinkText gLink, + "New member role: " <> strEncodeTxt acceptMemberRole + ] + <> ["The link is being upgraded..." | shouldBeUpgraded] + when shouldBeUpgraded $ do + let send = sendComposedMessage cc ct Nothing . MCText . T.unlines + withGroupLinkResult groupRef (sendChatCmd cc $ APIAddGroupShortLink groupId) $ + \GroupLink {connLinkContact = CCLink _ sLnk_'} -> case (sLnk_, sLnk_') of + (Just _, Just _) -> + send ["The group link is upgraded for: " <> groupRef, "No changes to group needed."] + (Nothing, Just sLnk) -> + sendComposedMessages cc (SRDirect $ contactId' ct) + [ MCText $ T.unlines + [ "Please replace the old link in welcome message of your group " <> groupRef, + "If this is the only change, the group will remain listed in directory without re-approval.", + "", + "The new link:" + ], + MCText $ strEncodeTxt sLnk + ] + (_, Nothing) -> + send ["The short link is not created for " <> groupRef, "Please report it to the developers."] + where + withGroupLinkResult groupRef a cb = + a >>= \case + Right CRGroupLink {groupLink} -> cb groupLink + Left (ChatErrorStore (SEGroupLinkNotFound _)) -> + sendReply $ "The group " <> groupRef <> " has no public link." + Right r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r + sendReply $ "Unexpected error:\n" <> resp + Left e -> do + let resp = T.pack $ serializeChatError True (config cc) e + sendReply $ "Unexpected error:\n" <> resp DCUnknownCommand -> sendReply "Unknown command" DCCommandError tag -> sendReply $ "Command error: " <> tshow tag where @@ -894,26 +957,6 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." DCListLastGroups count -> listGroups count False DCListPendingGroups count -> listGroups count True - DCShowGroupLink groupId gName -> do - let groupRef = groupReference' groupId gName - withGroupAndReg sendReply groupId gName $ \_ _ -> - sendChatCmd cc (APIGetGroupLink groupId) >>= \case - Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cReq _, acceptMemberRole}} -> - sendReply $ T.unlines - [ "The link to join the group " <> groupRef <> ":", - strEncodeTxt $ simplexChatContact cReq, - "New member role: " <> strEncodeTxt acceptMemberRole - ] - Left (ChatErrorStore (SEGroupLinkNotFound _)) -> - sendReply $ "The group " <> groupRef <> " has no public link." - Right r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r - sendReply $ "Unexpected error:\n" <> resp - Left e -> do - let resp = T.pack $ serializeChatError True (config cc) e - sendReply $ "Unexpected error:\n" <> resp DCSendToGroupOwner groupId gName msg -> do let groupRef = groupReference' groupId gName withGroupAndReg sendReply groupId gName $ \_ gr@GroupReg {dbContactId} -> do @@ -1054,11 +1097,11 @@ getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Maybe GroupLink) getGroupLink' cc user gInfo = withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo -setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact) +setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe CreatedLinkContact) setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) where resp = \case - Right (CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink gLink _sLnk}}) -> Just gLink + Right (CRGroupLink {groupLink = GroupLink {connLinkContact}}) -> Just connLinkContact _ -> Nothing unexpectedError :: Text -> Text diff --git a/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md b/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md index a5c498878e..7a227d4667 100644 --- a/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md +++ b/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md @@ -2,26 +2,119 @@ layout: layouts/article.html title: "SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more." date: 2025-07-29 -# previewBody: blog_previews/20250308.html -# image: images/20250308-captcha.png -# imageBottom: true -draft: true +previewBody: blog_previews/20250729.html +image: images/20250729-join2.png +imageBottom: true permalink: "/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html" --- # SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more. -**Will be published:** Jul 29, 2025 - -This is a placeholder for upcoming v6.4.1 release announcement. +**Published:** Jul 29, 2025 **What's new in v6.4.1**: -TODO +- [welcome your contacts](#welcome-your-contacts-the-new-experience-of-making-connections): set your profile bio and welcome message. +- [protect your communities](#protect-your-groups) from spam and abuse: + - review new members ("knocking"), + - moderator role to delegate message moderation to trusted members, + - receive direct feedback from your group members. +- [other improvements](#other-improvements): set default time to delete messages for new contacts. +- [improved app integrity](#improved-app-integrity). + +Also, we added 3 new interface languages to Android and desktop apps: Indonesian, Romanian and Vietnamese. + +Huge thanks to our users who [contributed translations](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). ## What's new in v6.4.1 -TODO +### Welcome your contacts: the new experience of making connections + + + +The new simple way to connect to your friends is fully available in this version. + +We received many compliments from our users who started using it in beta versions and in v6.4 about how it simplifies connecting with friends. We agree - this is the biggest UX revolution since the app was released. + +Instead of connecting blindly, and waiting until your contact is online, as it was before, you can now see profile and welcome message of the person you connect to, before you connect. + +When you tap Open new chat you can decide which profile to use to connect or if you want to connect incognito, and in some cases you can include a message with your connection request. + +This way, the conversation with your friends starts even before they connect to you! + +For previously created SimpleX addresses and group links you have an option to upgrade. The links will become short, and will include profile information into link data. Old long links will continue to work, so you won't lose any contacts or members during the upgrade. + +These links are now short enough to be shared in your social media profiles - they are less than 80 characters. + +And as before, it is as secure - servers cannot see you profiles, unless they have the link, and cannot modify them even if they somehow get the link. You can read more about security property and other technical details in our [post about SimpleX protocols extension](./20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) supporting this new user experience. + +Thank you for bringing your friends to SimpleX network! + +### Protect your groups + + + +**Review new members** + +Since v6.4 there are some major improvements in your ability to protect your group from spam and abuse. + +You can enable an option to review all new group members. It is also commonly called "knocking". It allows you to: +- ask prospective members any questions, +- explain the group rules, +- make sure their profile is appropriate for the group, +- decide whether to allow them joining the group, and whether they should be able to send messages in the group. + +Some small groups may enable member review permanently, while larger public groups may enable it temporarily during spam/troll attacks. + +**New role for group moderators** + +In addition to that, there is a new group role - moderator. + +This role allows: +- to approve members in review, +- moderate messages, +- block members for all. + +Unlike admins, moderators can't add new members or permanently remove members from the group. This allows you to delegate group moderation to your community members without risking that they may disrupt the group. + +**Receive direct feedback from group members** + +Your group members now can send messages to group admins. Each conversation with a group member is a mini-group where all group owners, admins and moderators can talk to a member. Reports that members can send [since v6.3](./20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) are also added to chat with member, allowing you to discuss the report. + +### Other improvements + +**Enable disappearing messages for new contacts** + +Now you can enable disappearing messages for all new contacts automatically. Tap your profile image in the corner, then tap Chat preferences and set time for messages to disappear. + +**Improved message delivery** + +We improved networking layer by increasing request timeouts for all background requests. It substantially reduces traffic on slow networks. + +### Improved app integrity + +**Supply chain security** + +The app security depends on security of its components and its build process, and many of these components are created by third parties. In this version we improved the build process to control the upgrades of these components: +- all 3rd party GitHub actions used during the build are now moved to [the forks we control](https://github.com/simplex-chat?q=action&type=fork&sort=name) - it prevents supply chain attacks via build actions. +- we now build VLC library for all platforms from the source code ourselves, in [this repository](https://github.com/simplex-chat/vlc). +- SQLCipher and [Haskell dependencies](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/dependencies/HASKELL.md) versions were already "locked" prior to this version. + +**Automatic virus scanning** + +We now run automatic daily virus scanning of all apps released via GitHub using [VirusTotal.com](https://www.virustotal.com/). + +You can see the scan results [here](https://github.com/simplex-chat/simplex-virutstotal-scan). + +**Reproducible builds** + +In addition to [server builds](https://github.com/simplex-chat/simplexmq/releases/tag/v6.4.1) that were reproducible since v6.3, the builds of Linux CLI and desktop apps are now reproducible too. You can build Linux apps from source using [this script](https://github.com/simplex-chat/simplex-chat/blob/master/scripts/simplex-chat-reproduce-builds.sh). + +*Please note*: Linux package upgrades may change the build. + +Stable builds of Linux apps are now independently reproduced and [signed by our and Flux teams](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.1) - it verifies the integrity of GitHub builds. + +Huge thanks to [Flux](https://runonflux.com/) for doing that and for providing their servers via the app. ## SimpleX network diff --git a/blog/README.md b/blog/README.md index c8b464c810..4544dc0f45 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,21 @@ # Blog +Jul 29, 2025 [SimpleX Chat v6.4.1: welcome your contacts, review members to protect groups, and more.](./20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.md) + +What's new in v6.4.1: + +- welcome your contacts: set your profile bio and welcome message. +- protect your communities from spam and abuse: + - review new members ("knocking"), + - moderator role to delegate message moderation to trusted members, + - receive direct feedback from your group members. +- set default time to delete messages for new contacts. +- improved app integrity: Linux app builds are now reproducible. + +Also, we added 3 new interface languages to Android and desktop apps, thanks to our users: Indonesian, Romanian and Vietnamese. + +--- + Jul 3, 2025 [SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) Now you can start talking to your contacts much faster, as soon as you scan the link. This technical post covers the technology that enabled this new user experience — short links and associated data of messaging queues. diff --git a/blog/images/20250729-connect1.png b/blog/images/20250729-connect1.png new file mode 100644 index 0000000000..984e4e44ef Binary files /dev/null and b/blog/images/20250729-connect1.png differ diff --git a/blog/images/20250729-connect2.png b/blog/images/20250729-connect2.png new file mode 100644 index 0000000000..784ea74e84 Binary files /dev/null and b/blog/images/20250729-connect2.png differ diff --git a/blog/images/20250729-join1.png b/blog/images/20250729-join1.png new file mode 100644 index 0000000000..6cec898eaf Binary files /dev/null and b/blog/images/20250729-join1.png differ diff --git a/blog/images/20250729-join2.png b/blog/images/20250729-join2.png new file mode 100644 index 0000000000..43fa412112 Binary files /dev/null and b/blog/images/20250729-join2.png differ diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index d3badbd598..5515181de9 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -82,6 +82,7 @@ This file is generated automatically. - [FullPreferences](#fullpreferences) - [GroupChatScope](#groupchatscope) - [GroupChatScopeInfo](#groupchatscopeinfo) +- [GroupDirectInvitation](#groupdirectinvitation) - [GroupFeature](#groupfeature) - [GroupFeatureEnabled](#groupfeatureenabled) - [GroupInfo](#groupinfo) @@ -1595,6 +1596,7 @@ Error: - contactRequestId: int64? - contactGroupMemberId: int64? - contactGrpInvSent: bool +- groupDirectInv: [GroupDirectInvitation](#groupdirectinvitation)? - chatTags: [int64] - chatItemTTL: int64? - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? @@ -2032,6 +2034,18 @@ MemberSupport: - groupMember_: [GroupMember](#groupmember)? +--- + +## GroupDirectInvitation + +**Record type**: +- groupDirectInvLink: string +- fromGroupId_: int64? +- fromGroupMemberId_: int64? +- fromGroupMemberConnId_: int64? +- groupDirectInvStartedConnection: bool + + --- ## GroupFeature @@ -2080,6 +2094,7 @@ MemberSupport: - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? - customData: JSONObject? - membersRequireAttention: int +- viaGroupLinkUri: string? --- @@ -2810,6 +2825,10 @@ ProfileUpdated: - fromProfile: [Profile](#profile) - toProfile: [Profile](#profile) +GroupInvLinkReceived: +- type: "groupInvLinkReceived" +- groupProfile: [GroupProfile](#groupprofile) + --- @@ -3601,6 +3620,7 @@ Handshake: - showNtfs: bool - sendRcptsContacts: bool - sendRcptsSmallGroups: bool +- autoAcceptMemberContacts: bool - userMemberProfileUpdatedAt: UTCTime? - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 0f8b82f392..9630ea2070 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -245,6 +245,7 @@ cliCommands = "SendImage", "SendLiveMessage", "SendMemberContactMessage", + "AcceptMemberContact", "SendMessage", "SendMessageBroadcast", "SendMessageQuote", @@ -265,6 +266,7 @@ cliCommands = "SetUserContactReceipts", "SetUserFeature", "SetUserGroupReceipts", + "SetUserAutoAcceptMemberContacts", "SetUserTimedMessages", "ShowChatItem", "ShowChatItemInfo", @@ -320,6 +322,8 @@ undocumentedCommands = "APICreateChatItems", "APICreateChatTag", "APICreateMemberContact", + "APISendMemberContactInvitation", + "APIAcceptMemberContact", "APIDeleteChatTag", "APIDeleteMemberSupportChat", "APIDeleteReceivedReports", @@ -370,7 +374,6 @@ undocumentedCommands = "APISendCallExtraInfo", "APISendCallInvitation", "APISendCallOffer", - "APISendMemberContactInvitation", "APISetAppFilePaths", "APISetChatItemTTL", "APISetChatSettings", @@ -390,6 +393,7 @@ undocumentedCommands = "APISetServerOperators", "APISetUserContactReceipts", "APISetUserGroupReceipts", + "APISetUserAutoAcceptMemberContacts", "APISetUserServers", "APISetUserUIThemes", "APIStandaloneFileInfo", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 8a4e806b59..50e9c4c14b 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -171,6 +171,7 @@ undocumentedResponses = "CRNetworkStatuses", "CRNewMemberContact", "CRNewMemberContactSentInv", + "CRMemberContactAccepted", "CRNewPreparedChat", "CRNtfConns", "CRNtfToken", diff --git a/bots/src/API/Docs/Syntax/Types.hs b/bots/src/API/Docs/Syntax/Types.hs index efb8f5e23f..211348d5a9 100644 --- a/bots/src/API/Docs/Syntax/Types.hs +++ b/bots/src/API/Docs/Syntax/Types.hs @@ -1,4 +1,5 @@ {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLists #-} module API.Docs.Syntax.Types where @@ -27,4 +28,6 @@ isConst = \case instance IsString Expr where fromString = Const -instance Semigroup Expr where sconcat = Concat +instance Semigroup Expr where + sconcat = Concat + x <> y = Concat [x, y] diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 8309ee28ba..fd8529ff52 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -255,7 +255,7 @@ chatTypesDocsData = (sti @FileProtocol, (STEnum' $ consLower "FP"), "", [], "", ""), (sti @FileStatus, STEnum, "FS", [], "", ""), (sti @FileTransferMeta, STRecord, "", [], "", ""), - (sti @Format, STUnion, "", [], "", ""), + (sti @Format, STUnion, "", ["Unknown"], "", ""), (sti @FormattedText, STRecord, "", [], "", ""), (sti @FullGroupPreferences, STRecord, "", [], "", ""), (sti @FullPreferences, STRecord, "", [], "", ""), @@ -302,6 +302,7 @@ chatTypesDocsData = (sti @PrefEnabled, STRecord, "", [], "", ""), (sti @Preferences, STRecord, "", [], "", ""), (sti @PreparedContact, STRecord, "", [], "", ""), + (sti @GroupDirectInvitation, STRecord, "", [], "", ""), (sti @PreparedGroup, STRecord, "", [], "", ""), (sti @Profile, STRecord, "", [], "", ""), (sti @ProxyClientError, STUnion, "Proxy", [], "", ""), @@ -492,6 +493,7 @@ deriving instance Generic PendingContactConnection deriving instance Generic PrefEnabled deriving instance Generic Preferences deriving instance Generic PreparedContact +deriving instance Generic GroupDirectInvitation deriving instance Generic PreparedGroup deriving instance Generic Profile deriving instance Generic ProxyClientError diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 90f9cdb186..d8efd21742 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -170,6 +170,7 @@ toTypeInfo tr = "FormatColor" -> ST "Color" [] "CustomData" -> ST "JSONObject" [] "KeyMap" -> ST "JSONObject" [] + "Value" -> ST "JSONObject" [] "CIQDirection" -> ST "CIDirection" [] "SendRef" -> ST "ChatRef" [] t diff --git a/cabal.project b/cabal.project index 3bde335ba9..c1d83da219 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: 846be50f72c1bfbbd70a98e9edf25c0a2d9d4234 + tag: db325cb81f77652471a27f4331143982739f9f10 source-repository-package type: git diff --git a/docs/rfcs/2025-02-13-super-peer-groups-mvp.md b/docs/rfcs/2025-02-13-super-peer-groups-mvp.md new file mode 100644 index 0000000000..ffc7c46e7a --- /dev/null +++ b/docs/rfcs/2025-02-13-super-peer-groups-mvp.md @@ -0,0 +1,228 @@ +# Super-peer groups MVP + +## Priorities + +1. migration to super-peer protocol for groups, min. protocol for choice of super-peer member +2. decrease number of connections, decrease traffic for senders - super-peer message delivery +3. optimize postgres client +4. availability of past messages, skipped messages - conversation pagination via requests to super-peers +5. \* comment threads (subchannels) +6. \* rework group links -> shared group link - ownership to owners, operation to super-peers? or point 10? (initially HA admin is ok) +7. \* multiple super-peers for redundancy, load balancing (decision to deliver based on number of super-peers) +8. \* more advanced management of super-peer list (create, add, choose, delete) +9. \* protect super-peers from abuse - rate limiting, banning (via client restrictions?) +10. \* discovery server (member) language and image recognition, pre-moderation of messages + > an advertised link to join the group should be controlled by discovery server (so that the group owners can't make discovery server see a content that is different from the actual group content). +11. \* features - ability to conceal member list, scheduled posts +12. \* automated moderation via members reputation score (observer -> member, image posts, more frequent posts) +13. \* authorization of admin changes (have owners and admins sign / wait for delivery from multiple super-peers) +14. \* super-peers to prioritize processing owner > admin > moderator commands/messages (priority to connections) +15. \* integrity of group state (member list > profile > messages) between super-peers + +\* not MVP + +## Design and implementation ideas + +### 1. Migration to super-peer protocol for groups, min. protocol for choice of super-peer member + +- Allow owner to assign member as super-peer. In practice many groups have single owner. +- Alternatively protocol to accept and approve role changes is necessary, as it should not be possible to unilaterally appoint some member to be a super-peer? Not necessary. +- Client to have choice to accept/reject becoming super-peer. +- Not necessary to do UI, as regular users aren't highly available clients anyway. They will ignore offer to be super-peer. +- Client to have configuration to auto-accept (or manually) super-peer offer. HA clients can be run with it - directory service. +- In practice to make transition we would recommend owners of current directory groups to choose directory service as super-peer. Another advantage of this approach is that directory service already hosts links. +- Possibly create a "super-peer" bot that can be chosen. Recommend (hardcode?) creating a link via it. +- Multi-host group links are not necessary initially - users will be joining directly to single super-peer. +- Remove ability to add contact via interface? Or for super-peer group it should share group link / introduce to super-peer, and allow all members to invite? Not necessarily MVP, maybe removing ability to add for admins is enough. Instead we could have super-peer link to be included in group profile / description. Pinned? +- Clients to delete connections (probably background job) once super-peer is appointed. Potentially disruptive but will greatly reduce subscription load. We could make warning to owner that it's experimental feature and it's irreversible, at least during beta. Also automatically post warning in group with super-peer's group link to rejoin in case of disruption. + +### 2. Decrease number of connections, decrease traffic for senders - super-peer message delivery + +- We already have group forwarding - simply have to change rules for forwarding for super-peer groups: only super-peer forwards; other admins don't forward; super-peer forwards messages always, not only for not connected members (simplifies filtering on forwarding). +- New members are never introduced to other members in order to establish connections. Instead all new members join to super-peer via its group link. +- Instead introduction (in previous sense) is replaced with sharing "member records" - profiles and member IDs. Existing members receive new "member record". +- Sub protocol for inviting new members via sharing super-peer link (same considerations as above). +- We may need more robust processing for forwarded messages - more events / edge cases. + +### 3. Optimize postgres client + +- Connection pool. +- Optimization of indexes (different from SQLite) may be required. +- Many small queries may have to be reworked into large queries in some cases. + +### 4. Availability of past messages, skipped messages - conversation pagination via requests to super-peers + +- Protocol to paginate group conversation. +- Based on shared msg id? item ts? +- Response batches messages. +- Rate limit? Probably not MVP. + +## Super-peer agreement protocol + +This protocol draft is for larger scope MVP that includes short links. + +The main idea is that public groups should have identity, that is defined via a permanent link to them, and that only owner(s) should be able to control group identity and link. This link is a short link either to SMP blob (see simplexmq rfcs on blob extensions), or to an XFTP file (requires indefinitely long storage). For clarity, to distinguish from per super-peer group links used for establishing connections with super-peers, it will be further called "short link". + +Short link points to a blob/file containing: + +- Super-peer group links, +- Owners' public keys for verifying ownership transfer and other administrative actions, +- Other group metadata as required by further clarifications to protocol. + +UX for creating a public group should be straightforward: + +1. Select operators, whose super-peers to use, or optionally custom configured super-peers. +2. Create group, which generates short link and invites super-peers to it. Connection progress is shown for each super-peer. +3. Confirm creation, once super-peers connected. Super-peer can fail to connect, at least one connected super-peer is required for confirmation. +4. Share link for members to join. + +Super-peers are pre-configured in app for preset operators. User can also add self-hosted or other known super-peers to custom configuration. Super-peer should have a SimpleX address to receive group join requests. + +Protocol for creating public group (happy path): + +1. Group owner's client (further owner) creates group record locally. +2. Owner sends contact requests to selected super-peers SimpleX addresses. These contact requests are essentially invitations to be super-peers in this group. ConnInfo sent in these contact requests (INV) contains group invitation details (XGrpInv with added fields, or new specific protocol message - TBC). +3. Super-peers receive these requests. They generate new group links specifically for this group, to serve as a point of connection to them for new members. +4. Super-peers accept requests, sending generated group links in confirmation (CONF) ConnInfos. +5. Owner packages super-peers group links and other group metadata into blob. +6. Owner uploads this blob to one of their SMP servers, creating short link. +7. Owner shares short link publicly or with selected new members. +8. New members retrieve blob via short link. +9. New members connect to super-peers via group links specified in the blob. + +``` +Owner SMP Owner Super-peers Super-peers SMP New members + | | | | | + | |1. create group| | | | + | | | | | + | |2. contact requests| | | + | | (INV, group inv.) | | | + | |------------------>| | | + | | | 3. create group | | + | | | links | | + | | |------------------>| | + | | |<------------------| | + | | | new addresses | | + | |4. accept requests | | | + | |(CONF, group links)| | | + | |<------------------| | | + | | | | | + | |5. package blob| | | | + | | | | | + | 6. upload blob | | | | + |<------------------| | | | + |------------------>| | | | + | short link | | | | + | | 7. short link (oob) | + | |~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ >| + | | | | | + | 8. retrieve blob | + |<------------------------------------------------------------------------------| + |------------------------------------------------------------------------------>| + | blob with super-peers group links | + | | | | | + | | | 9. connect via group links | + | | |<--------------------------------------| + | | | | | + * * * * * +``` + +On client side super-peer is a specific user profile, which can be created in a hosted CLI client, or in a highly available desktop app. Super-peer user is programmed to accept incoming requests to join groups as super-peer. It's hidden from regular user list and requires specific API and UI for management. + +```sql +ALTER TABLE users ADD COLUMN superpeer INTEGER NOT NULL DEFAULT 0; +``` + +In user clients super-peer is an invisible group member, that is prohibited to send its own messages, but allowed to forward all messages between group members. It's also hidden from group member list. + +```sql +ALTER TABLE group_members ADD COLUMN superpeer INTEGER NOT NULL DEFAULT 0; +``` + +If group has more than one active super-peer, owner can remove super-peer from group by removing its group link from short link's blob, which means new members will not use it to connect. Owner also should issue message to members to delete connections with removed super-peer, and to super-peer to leave group. + +Problems: + +- To prevent abuse from owner, super-peer can periodically or on each group join check short link for presence of its group link. If it's absent, it should delete group link, and after some time stop forwarding messages in group and leave it, see next point. +- If group has only one super-peer, fully removing it from group should not be possible until new super-peer \[fully?\] connects to \[all? active?\] existing group members. For this, current super-peer has to remain in group in order to introduce members to new super-peer. At the very least, current super-peer should forward owner's message with new super peer's group link to members. Better, to prevent downtimes or failures in delivery, current super-peer should wait for confirmations of connection from members. +- If group has a single super-peer, or only super-peers of select operators, nothing prevents these super-peers (operators) from effectively deleting group by destroying all connections with members. So if operators adhere to the same moderating/banning policies, group is not protected from censorship unless it uses self-hosted or other custom configured super-peers. Even then, if group used at least one super-peer of select operators, group owner is subject to potential client restrictions. + +### Removing super-peer from group + +If group has a single super-peer, owner has to add a new one before removing it. Protocol for removing single super-peer from group: + +1. Owner adds new super-peer to group and connects with it (as above). +2. New super-peer connects with current super-peer and starts to synchronize its group state, including group history and member profiles. + - TBC group state to synchronize: full or partial history, additional metadata? +3. Owner updates short link: adds new super-peer group link to it, removes or marks as disabled current super-peer. +4. Owner announces deleting current and adding new super-peer to group. + - Via both super-peers - current would forward to existing members, new would forward to newly joined. + - Separate messages or single specialized message ("replace")? +5. Members that have received this message start connecting with new super-peer. +6. Once group state is synchronized, current (removed) super-peer deletes connections with members. + - To be clarified, protocol for synchronizing group state between super-peers. +7. Members that haven't received announcement via removed super-peer (for example, they were offline), receive AUTH errors on subscription to connection with it. Knowing it is a super-peer connection to a specific group, they retrieve new super-peer group link from updated blob via short link and connect to it. + +``` +Owner SMP Owner Current super-peer New super-peer Members + | | | | | + | | 1. connect (add to group) | | + | |-------------------------------------->| | + | |<--------------------------------------| | + | | new address for this group | | + | | | | | + | | | 2. connect, | | + | | |start synchronizing| | + | | |<----------------->| | + | 3. update blob | | | | + | by short link | | | | + | (new, disabled | | | | + | super-peers) | | | | + |<------------------| | | | + |------------------>| | | | + | OK | | | | + | | 4. announce removing current and | + | | adding new super-peer | + | |---------------------------------------------------------->| + | | | | | + | | | | 5. connect | + | | | | (members who re- | + | | | |ceived announcemnt)| + | | | |<----------------->| + | | | | | + | | [ state is synchronized ] | + | | | | | + | | |6. delete connections| | | + | | | | | + | | | 7. subscribe (members who | + | | | didn't receive announcement) | + | | |<--------------------------------------| + | | |-------------------------------------->| + | | | AUTH error (from SMP) | + | 7. retrieve updated blob | + |<------------------------------------------------------------------------------| + |------------------------------------------------------------------------------>| + | blob with new super-peer group link | + | | | | 7. connect | + | | | |<----------------->| + | | | | | + * * * * * +``` + +The advantage of this approach is that current super-peer doesn't have to wait for new super-peer to connect to existing members, which can take arbitrary time if they are offline, or even never complete if they don't come online. + +If group has more than one active super-peer, owner can remove a super-peer from group immediately. + +### Member profile accounting, group statistics + +Super-peers don't broadcast all member profiles on introduction, instead they keep accounting of which member profiles were shared to which members, and when forwarding messages also send profiles to members who haven't received them before. Regular members don't see full list of member profiles, only overall number of members and list of profiles of actively participating members (who send messages). + +Even without addition of new super-peer, current super-peers can synchronize state by forwarding all messages to each other. + +Periodically super-peers send group statistics to owner: + +- Number of actively participating members - those who send messages and whose profiles were shared to other members (known via shared profile accounting). +- Number of connected members - members to whom super-peer forwards messages. +- Number of inactive members - members to whom super-peer currently doesn't forward messages due to inactivity. Super-peer considers member inactive if it received their profile from previous super-peer and new member hasn't connected, or due to QUOTA error inactivity. These reasons could be differentiated. Perhaps number of members with QUOTA error inactivity should be a sub-count of connected members. + +Owner client can show aggregated statistics and detailed statistics for each super-peer. diff --git a/docs/rfcs/2025-04-14-signing-messages.md b/docs/rfcs/2025-04-14-signing-messages.md new file mode 100644 index 0000000000..8845de0cd8 --- /dev/null +++ b/docs/rfcs/2025-04-14-signing-messages.md @@ -0,0 +1,83 @@ +# Signing some member member messages and profiles + +## Problems + +1. Authenticity of profile addresses. + +When somebody sends a profile, e.g. when connecting via one time link, it may include contact address. It may be incorrectly used to claim that the sending person is the same person as one who published the same address in some trustworthy channel, like a social media account or a website. This claim is unproven, and can potentially be misleading - the address can be simply copy-pasted, together with other attributes of the profile. The solution to that would be to sign the profile or a message including the profile with the key that has its public part included in immutable address data. If only profile is signed, it can also be copy-pasted, but if the message is signed and if it includes some connection attributes (e.g. security code), then it would be impossible to copy paste. So the latter approach is preferred. + +Additional social media addresses can be included in the account, and they can also be validated via signature by the key from the short link published on social media account (requiring access to that account). + +With Nostr that uses secp256k1 key as identity it's even better, as ownership of this identity can be validated without contacting nostr, simply by verifying signature with Nostr public key. + +2. Authenticity of owners' and admins messages delivered via chat relays (aka superpeers). + +For the next generation of groups we want to protect from attacks by chat relays who otherwise could deliver messages that they have no right to send. E.g., messages that can only be sent by owners and or by the admin. + +Group identity is established by root Ed25519 key included into immutable part of group link data, and further ownership changes are recorded as an ownership chain, as described [here](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-04-04-short-links-for-groups.md#multiple-owners-managing-queue-data). + +The same key should be used by owners to sign additions of admins and moderators, and these users would use their own keys communicated at the time they are added (and signed by owners or other admins). This chain of trust will be stored by admins, all members, and by chat relays, it should have strong consistency using some blockchain consensus algorithm, and should be re-sent on request, in case some messages were skipped or messages authenticated with missed key are delivered (in which case their processing would be postponed until the key is delivered). + +This has been previously described, and is not the subject of this document. This document focus is how to sign messages in a verifiable way, which is problematic as signatures are applied to byte-streams, and we use JSON encoding with non-deterministic ordering of keys. + +## Solution + +### Option 1. Deterministic JSON encoding. + +Include used key (or key reference), signature and some conversation binding (security code for direct chats, and, possibly, group root key for groups, as security code will not be shared between members) into the JSON being signed. + +To sign: +- JSON will be deterministically encoded without signature +- signed, +- signature is added to nullable/Maybe field of the same object (or to wrapper object). + +To verify: +- signature will be removed, +- remaining JSON re-encoded, +- signature verified. + +Pros: +- backwards compatible (in case no wrapper is used), +- single-pass JSON decoding. + +Cons: +- deterministic encodings are not widely used, and alternative implementations may fail to do it in a compatible way, +- requires re-encoding to verify signature, partially removing the advantages of the initial single-pass decoding. + +### Option 2. Multi-stage decoding and encoding + +As we need to sign the whole message anyway, and not just its part, it may be a new top-level encoding that sequentially includes: +- encoded JSON body (the order of keys is not important), +- conversation binding (security code or `group` and `member` identities defined as their keys), +- the array of tuples `(key reference, signature)`. + +Key reference could point to: +- root key of the group, +- owner key in the chain of trust with root key, +- member key that is supposed to be previously known or included in profile in the message, +- nostr key included in the profile in the message. + +To sign: +- encode JSON as usual, in non-deterministic way, +- append conversation binding, +- sign with all applicable keys, +- append `(key reference, signature)` tuples to the message, +- encode and send the message (could be JSON array or sequential SMP encoding), + +To verify: +- decode message, obtaining JSON as string, conversation binding, and `(key reference, signature)` tuples, +- compare conversation binding, if it does not match - fail, notify the sender, show the message to the user (potentially, if sender sends the replacement, then warning could be replaced with the correct message). +- decode JSON string, +- resolve key references to keys (some can be in decoded message, some can be known), +- if any keys are unknown, request them from sender, show the warning to the user, +- verify signature, in case of failure notify sender (?) and show warning message to the user, +- if all ok, process the message. + +Pros: +- more straightforward design, +- if binary encoding is used for message, then there will be no overhead of re-encoding JSON as a string (that would require escaping). The size increase can be offset by compression. + +Cons: +- two-stage decoding may be seen as a downside, but it is offset by the fact that re-encodings are avoided, and under the hood JSON is decoded in stages anyway. + +While deterministic JSON is [quite simple](https://github.com/simplex-chat/aeson/pull/4/files) for aeson implementation, the Option 2 seems more attractive overall, as it avoids questionable design of including signatures into JSON and the need to re-encode JSON to sign and to verify signatures. diff --git a/scripts/ci/linux_util_free_space.sh b/scripts/ci/linux_util_free_space.sh index ef00eb886e..841edf9ef0 100755 --- a/scripts/ci/linux_util_free_space.sh +++ b/scripts/ci/linux_util_free_space.sh @@ -89,8 +89,7 @@ sudo rm -rf /opt/hostedtoolcache/PyPy || : # 376MB sudo rm -rf /opt/hostedtoolcache/node || : # Remove Web browser packages -sudo apt purge -y \ - firefox \ - google-chrome-stable \ - microsoft-edge-stable +for pkg in firefox google-chrome-stable microsoft-edge-stable; do + sudo apt purge -y "$pkg" || echo "Failed or not installed: $pkg" +done df -h diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index 1db2755926..6b197a0b8b 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -7,7 +7,7 @@ function readlink() { } OS=linux -ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} +ARCH="$(uname -m)" GHC_VERSION=9.6.3 if [ "$ARCH" == "aarch64" ]; then diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index ffd5e13337..5978fe0cba 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -2,6 +2,9 @@ set -e + +ARCH="$(uname -m)" + function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } @@ -36,13 +39,13 @@ sed -i 's|Icon=.*|Icon=simplex|g' *imple*.desktop 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/simplex-chat/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage - chmod +x ../appimagetool-x86_64.AppImage +if [ ! -f ../appimagetool-${ARCH}.AppImage ]; then + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage -O ../appimagetool-${ARCH}.AppImage + chmod +x ../appimagetool-${ARCH}.AppImage fi -if [ ! -f ../runtime-x86_64 ]; then - 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 +if [ ! -f ../runtime-${ARCH} ]; then + wget --secure-protocol=TLSv1_3 https://github.com/simplex-chat/type2-runtime/releases/download/continuous/runtime-${ARCH} -O ../runtime-${ARCH} + chmod +x ../runtime-${ARCH} fi # Determenistic build @@ -56,7 +59,7 @@ 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 . +../appimagetool-${ARCH}.AppImage --verbose --no-appstream --runtime-file ../runtime-${ARCH} . mv *imple*.AppImage ../../ # Just a safeguard diff --git a/scripts/desktop/make-deb-linux.sh b/scripts/desktop/make-deb-linux.sh index 6f9de8a1e6..c9c4d5a81c 100755 --- a/scripts/desktop/make-deb-linux.sh +++ b/scripts/desktop/make-deb-linux.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh +ARCH="$(uname -m)" + scripts/desktop/build-lib-linux.sh cd apps/multiplatform ./gradlew packageDeb @@ -39,6 +41,6 @@ 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 +dpkg-deb --build --root-owner-group --uniform-compression ./extracted ./release/main/deb/simplex_${ARCH}.deb -strip-nondeterminism ./release/main/deb/simplex_amd64.deb +strip-nondeterminism ./release/main/deb/simplex_${ARCH}.deb diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh index a798fd6fc9..6106035d83 100755 --- a/scripts/desktop/prepare-vlc-linux.sh +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -2,16 +2,18 @@ set -e +ARCH="$(uname -m)" + function readlink() { echo "$(cd "$(dirname "$1")"; pwd -P)" } root_dir="$(dirname "$(dirname "$(readlink "$0")")")" -vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/vlc +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-${ARCH}/vlc 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" +vlc_url="https://github.com/simplex-chat/vlc/releases/download/${vlc_tag}/vlc-linux-${ARCH}.appimage" cd /tmp mkdir tmp 2>/dev/null || true diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index f9c2b48a5b..9ced131207 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,17 @@ + + https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html + +

New in v6.4.1:

+
    +
  • welcome your contacts: set profile bio and welcome message.
  • +
  • enable disappearing messages by default for new contacts.
  • +
  • short SimpleX addresses and group links now include profile images and welcome messages.
  • +
+
+
https://simplex.chat/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index cd430a6883..8c24afe716 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."846be50f72c1bfbbd70a98e9edf25c0a2d9d4234" = "1lllm9vg15pgkrgfl9vx7wic0fzpnyxpwfh9zcmjlni56l4pyvl3"; + "https://github.com/simplex-chat/simplexmq.git"."db325cb81f77652471a27f4331143982739f9f10" = "15q2zjcsmp40c65d9wgngjk7vs753nq9cxf5vd996ns1lkqq1ckb"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 4f5e8877c4..fe9c2c6b4e 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.4.1.2 +version: 6.4.2.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -112,6 +112,8 @@ library Simplex.Chat.Store.Postgres.Migrations.M20250704_groups_conn_link_prepared_connection Simplex.Chat.Store.Postgres.Migrations.M20250709_profile_short_descr Simplex.Chat.Store.Postgres.Migrations.M20250721_indexes + Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests + Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri else exposed-modules: Simplex.Chat.Archive @@ -249,6 +251,8 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250704_groups_conn_link_prepared_connection Simplex.Chat.Store.SQLite.Migrations.M20250709_profile_short_descr Simplex.Chat.Store.SQLite.Migrations.M20250721_indexes + Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests + Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 890f927e7b..b165ec135f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -265,6 +265,8 @@ data ChatCommand | SetUserContactReceipts UserMsgReceiptSettings | APISetUserGroupReceipts UserId UserMsgReceiptSettings | SetUserGroupReceipts UserMsgReceiptSettings + | APISetUserAutoAcceptMemberContacts UserId Bool + | SetUserAutoAcceptMemberContacts Bool | APIHideUser UserId UserPwd | APIUnhideUser UserId UserPwd | APIMuteUser UserId @@ -373,6 +375,7 @@ data ChatCommand | APIAddGroupShortLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} + | APIAcceptMemberContact ContactId | GetUserProtoServers AProtocolType | SetUserProtoServers AProtocolType [AProtoServerWithAuth] | APITestProtoServer UserId AProtoServerWithAuth @@ -477,6 +480,7 @@ data ChatCommand | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text + | AcceptMemberContact ContactName | SendLiveMessage ChatName Text | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text} | SendMessageBroadcast MsgContent -- UserId (not used in UI) @@ -726,6 +730,7 @@ data ChatResponse | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} | CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CRMemberContactAccepted {user :: User, contact :: Contact} | CRCallInvitations {callInvitations :: [RcvCallInvitation]} | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 4c5a7215c8..90e79d845b 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -379,6 +379,12 @@ processChatCommand vr nm = \case withFastStore' $ \db -> updateUserGroupReceipts db user' settings ok user SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserGroupReceipts userId settings + APISetUserAutoAcceptMemberContacts userId' onOff -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' Nothing + withFastStore' $ \db -> updateUserAutoAcceptMemberContacts db user' onOff + ok user + SetUserAutoAcceptMemberContacts onOff -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserAutoAcceptMemberContacts userId onOff APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -1539,7 +1545,7 @@ processChatCommand vr nm = \case m <- withFastStore $ \db -> do liftIO $ updateGroupMemberSettings db user gId gMemberId settings getGroupMember db vr user gId gMemberId - let ntfOn = showMessages $ memberSettings m + let ntfOn = not (memberBlocked m) toggleNtf m ntfOn ok user APIContactInfo contactId -> withUser $ \user@User {userId} -> do @@ -2053,6 +2059,9 @@ processChatCommand vr nm = \case Just ctId -> do let sendRef = SRDirect ctId processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + AcceptMemberContact cName -> withUser $ \user -> do + contactId <- withFastStore $ \db -> getContactIdByName db user cName + processChatCommand vr nm $ APIAcceptMemberContact contactId SendLiveMessage chatName msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg withSendRef chatRef $ \sendRef -> do @@ -2607,6 +2616,45 @@ processChatCommand vr nm = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRNewMemberContactSentInv user ct' g m _ -> throwChatError CEGroupMemberNotActive + APIAcceptMemberContact contactId -> withUser $ \user -> do + (g, mConn, ct, groupDirectInv) <- withFastStore $ \db -> getMemberContactInvited db vr user contactId + when (groupDirectInvStartedConnection groupDirectInv) $ throwCmdError "connection already started" + connectMemberContact user g mConn ct groupDirectInv `catchChatError` \e -> do + -- get updated contact, in case connection was started + ct' <- withFastStore $ \db -> getContact db vr user contactId + toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') + throwError e + -- get updated contact (groupDirectInvStartedConnection) with connection + ct' <- withFastStore $ \db -> do + liftIO $ setMemberContactStartedConnection db ct + getContact db vr user contactId + pure $ CRMemberContactAccepted user ct' + where + connectMemberContact user gInfo mConn Contact {activeConn} GroupDirectInvitation {groupDirectInvLink = cReq} = + withInvitationLock "connect" (strEncode cReq) $ do + subMode <- chatReadVar subscriptionMode + case activeConn of + Nothing -> joinNewConn subMode + Just conn@Connection {connStatus} -> case connStatus of + ConnPrepared -> joinPreparedConn subMode conn + _ -> throwChatError $ CEException "connection already started (past prepared status)" + where + joinNewConn subMode = do + -- possible improvement: use agent connRequestPQSupport to determine pqSupport here; + -- for joinPreparedConn below - same + encodeConnInfoPQ; + -- same for auto-accept on xGrpDirectInv + acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff + conn <- withStore $ \db -> do + connId <- liftIO $ createMemberContactConn db user acId Nothing gInfo mConn ConnPrepared contactId subMode + getConnectionById db vr user connId + joinPreparedConn subMode conn + joinPreparedConn subMode conn = do + -- [incognito] send membership incognito profile + let p = userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile gInfo) Nothing True + dm <- encodeConnInfo $ XInfo p + (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode + let newStatus = if sqSecured then ConnSndReady else ConnJoined + void $ withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand vr nm $ APICreateGroupLink groupId mRole @@ -2983,22 +3031,19 @@ processChatCommand vr nm = \case connectViaContact :: User -> Maybe PreparedChatEntity -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> CM ConnectViaContactResult connectViaContact user@User {userId} preparedEntity_ incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - cReqHash = ConnReqUriHash . C.sha256Hash . strEncode - cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex} - cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat} -- groupLinkId is Nothing for business chats when (isJust msg_ && isJust groupLinkId) $ throwChatError CEConnReqMessageProhibited case preparedEntity_ of Just (PCEContact ct@Contact {activeConn}) -> case activeConn of - Nothing -> connect' Nothing cReqHash1 Nothing + Nothing -> connect' Nothing Nothing Just conn@Connection {connStatus, xContactId} -> case connStatus of ConnPrepared -> joinPreparedConn' xContactId conn False _ -> pure $ CVRConnectedContact ct Just (PCEGroup _gInfo GroupMember {activeConn}) -> case activeConn of - Nothing -> connect' groupLinkId cReqHash1 Nothing + Nothing -> connect' groupLinkId Nothing Just conn@Connection {connStatus, xContactId} -> case connStatus of ConnPrepared -> joinPreparedConn' xContactId conn $ isJust groupLinkId - _ -> connect' groupLinkId cReqHash1 xContactId -- why not "already connected" for host member? + _ -> connect' groupLinkId xContactId -- why not "already connected" for host member? Nothing -> withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case Right ct@Contact {activeConn} -> case groupLinkId of @@ -3008,14 +3053,17 @@ processChatCommand vr nm = \case Just gLinkId -> -- allow repeat contact request -- TODO [short links] is this branch needed? it probably remained from the time we created host contact - connect' (Just gLinkId) cReqHash1 Nothing + connect' (Just gLinkId) Nothing Left conn_ -> case conn_ of Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn $ isJust groupLinkId -- TODO [short links] this is executed on repeat request after success -- it probably should send the second message without creating the second connection? - Just Connection {xContactId} -> connect' groupLinkId cReqHash1 xContactId - Nothing -> connect' groupLinkId cReqHash1 Nothing + Just Connection {xContactId} -> connect' groupLinkId xContactId + Nothing -> connect' groupLinkId Nothing where + cReqHash = ConnReqUriHash . C.sha256Hash . strEncode + cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex} + cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat} joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} inGroup = do when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" xContactId <- mkXContactId xContactId_ @@ -3023,7 +3071,7 @@ processChatCommand vr nm = \case let incognitoProfile = fromLocalProfile <$> localIncognitoProfile conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile - connect' groupLinkId cReqHash xContactId_ = do + connect' groupLinkId xContactId_ = do let inGroup = isJust groupLinkId pqSup = if inGroup then PQSupportOff else PQSupportOn (connId, chatV) <- prepareContact user cReq pqSup @@ -3032,7 +3080,7 @@ processChatCommand vr nm = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk - conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReq cReqHash1 sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse @@ -3047,7 +3095,7 @@ processChatCommand vr nm = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq - conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing False pqSup ct' <- withStore $ \db -> getContact db vr user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile @@ -4336,6 +4384,8 @@ chatCommandP = "/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings), "/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings), "/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings), + "/_set accept member contacts " *> (APISetUserAutoAcceptMemberContacts <$> A.decimal <* A.space <*> onOffP), + "/set accept member contacts " *> (SetUserAutoAcceptMemberContacts <$> onOffP), "/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP), "/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP), "/_mute user " *> (APIMuteUser <$> A.decimal), @@ -4569,6 +4619,7 @@ chatCommandP = "/show link #" *> (ShowGroupLink <$> displayNameP), "/_create member contact #" *> (APICreateMemberContact <$> A.decimal <* A.space <*> A.decimal), "/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)), + "/_accept member contact @" *> (APIAcceptMemberContact <$> A.decimal), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), @@ -4592,6 +4643,7 @@ chatCommandP = ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), + "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP), "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index d37a8a70d9..941ee04c9e 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -62,7 +62,6 @@ import Simplex.Chat.Operators import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store -import Simplex.Chat.Store.Connections import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 08a558bd48..320f81e497 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1943,13 +1943,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = 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_ + unless (memberBlocked 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 [] + let mentions' = if memberBlocked m' then [] else mentions (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed_ live mentions' - ci' <- blockedMember m' ci $ withStore' $ \db -> markGroupChatItemBlocked db user gInfo' ci + ci' <- blockedMemberCI gInfo' m' ci reactions <- maybe (pure []) (\sharedMsgId -> withStore' $ \db -> getGroupCIReactions db gInfo' memberId sharedMsgId) sharedMsgId_ groupMsgToView cInfo ci' {reactions} @@ -1963,14 +1963,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). -- 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 [] + mentions' = if memberBlocked m then [] else 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 cInfo ci') + updateGroupChatItem db user groupId ci content True live Nothing + ci'' <- blockedMemberCI gInfo' m' ci' + toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv cInfo ci'') pure $ Just $ toGroupForwardScope gInfo scopeInfo where content = CIRcvMsgContent mc @@ -2088,13 +2088,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" (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 + ci' <- blockedMemberCI gInfo m ci groupMsgToView cInfo ci' - blockedMember :: Monad m' => GroupMember -> ChatItem c d -> m' (ChatItem c d) -> m' (ChatItem c d) - blockedMember m ci blockedCI - | showMessages (memberSettings m) = pure ci - | otherwise = blockedCI + blockedMemberCI :: GroupInfo -> GroupMember -> ChatItem 'CTGroup 'MDRcv -> CM (ChatItem 'CTGroup 'MDRcv) + blockedMemberCI gInfo m ci + | blockedByAdmin m = + withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo ci + | not (showMessages $ memberSettings m) = + withStore' $ \db -> markGroupChatItemBlocked db user gInfo ci + | otherwise = + pure ci receiveInlineMode :: FileInvitation -> Maybe MsgContent -> Integer -> CM (Maybe InlineFileMode) receiveInlineMode FileInvitation {fileSize, fileInline, fileDescr} mc_ chSize = case (fileInline, fileDescr) of @@ -2265,7 +2269,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = dm <- encodeConnInfo $ XGrpAcpt membershipMemId connIds <- joinAgentConnectionAsync user True connRequest dm subMode withStore' $ \db -> do - setViaGroupLinkHash db groupId connId + setViaGroupLinkUri db groupId connId createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted @@ -3061,37 +3065,71 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = 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 - unless (groupFeatureMemberAllowed SGFDirectMessages m g) $ messageError "x.grp.direct.inv: direct messages not allowed" - let GroupMember {memberContactId} = m - subMode <- chatReadVar subscriptionMode - case memberContactId of - Nothing -> createNewContact subMode - Just mContactId -> do - mCt <- withStore $ \db -> getContact db vr user mContactId - let Contact {activeConn, contactGrpInvSent} = mCt - forM_ activeConn $ \Connection {connId} -> - if contactGrpInvSent - then do - ownConnReq <- withStore $ \db -> getConnReqInv db connId - -- in case both members sent x.grp.direct.inv before receiving other's for processing, - -- only the one who received greater connReq joins, the other creates items and waits for confirmation - if strEncode connReq > strEncode ownConnReq - then joinExistingContact subMode mCt - else createItems mCt m - else joinExistingContact subMode mCt + xGrpDirectInv g@GroupInfo {groupId, groupProfile = gp} m mConn@Connection {connId = mConnId} connReq mContent_ msg brokerTs + | not (groupFeatureMemberAllowed SGFDirectMessages m g) = messageError "x.grp.direct.inv: direct messages not allowed" + | memberBlocked m = messageWarning "x.grp.direct.inv: member is blocked (ignoring)" + | otherwise = do + let GroupMember {memberContactId} = m + subMode <- chatReadVar subscriptionMode + case memberContactId of + Nothing -> createNewContact subMode + Just mContactId -> do + mCt <- withStore $ \db -> getContact db vr user mContactId + let Contact {activeConn, contactGrpInvSent} = mCt + forM_ activeConn $ \Connection {connId} -> + if contactGrpInvSent + then do + ownConnReq <- withStore $ \db -> getConnReqInv db connId + -- in case both members sent x.grp.direct.inv before receiving other's for processing, + -- only the one who received greater connReq joins, the other creates items and waits for confirmation + if strEncode connReq > strEncode ownConnReq + then joinExistingContact subMode mCt + else createItems mCt m + else joinExistingContact subMode mCt where - joinExistingContact subMode mCt = do - connIds <- joinConn subMode - mCt' <- withStore $ \db -> updateMemberContactInvited db user connIds g mConn mCt subMode - createItems mCt' m - securityCodeChanged mCt' - createNewContact subMode = do - connIds <- joinConn subMode - -- [incognito] reuse membership incognito profile - (mCt', m') <- withStore' $ \db -> createMemberContactInvited db user connIds g m mConn subMode - createInternalChatItem user (CDDirectSnd mCt') CIChatBanner (Just epochStart) - createItems mCt' m' + groupDirectInv = + GroupDirectInvitation { + groupDirectInvLink = connReq, + fromGroupId_ = Just groupId, + fromGroupMemberId_ = Just (groupMemberId' m), + fromGroupMemberConnId_ = Just mConnId, + groupDirectInvStartedConnection = isTrue $ autoAcceptMemberContacts user + } + joinExistingContact subMode mCt@Contact {contactId = mContactId} + | isTrue (autoAcceptMemberContacts user) = do + (cmdId, acId) <- joinConn subMode + mCt' <- withStore $ \db -> do + updateMemberContactInvited db user mCt groupDirectInv + void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode + getContact db vr user mContactId + securityCodeChanged mCt' + createItems mCt' m + | otherwise = do + mCt' <- withStore $ \db -> do + updateMemberContactInvited db user mCt groupDirectInv + getContact db vr user mContactId + securityCodeChanged mCt' + createInternalChatItem user (CDDirectRcv mCt') (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing + createItems mCt' m + createNewContact subMode + | isTrue (autoAcceptMemberContacts user) = do + (cmdId, acId) <- joinConn subMode + -- [incognito] reuse membership incognito profile + (mCt, m') <- withStore $ \db -> do + (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv + void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode + mCt <- getContact db vr user mContactId + pure (mCt, m') + createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) + createItems mCt m' + | otherwise = do + (mCt, m') <- withStore $ \db -> do + (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv + mCt <- getContact db vr user mContactId + pure (mCt, m') + createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) + createInternalChatItem user (CDDirectRcv mCt) (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing + createItems mCt m' joinConn subMode = do -- [incognito] send membership incognito profile let p = userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing True diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 79365c8243..f368631852 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -53,6 +53,7 @@ data Format | Mention {memberName :: Text} | Email | Phone + | Unknown {json :: J.Value} deriving (Eq, Show) mentionedNames :: MarkdownList -> [Text] @@ -305,6 +306,7 @@ markdownText (FormattedText f_ t) = case f_ of Mention _ -> t Email -> t Phone -> t + Unknown _ -> t where around c = c `T.cons` t `T.snoc` c color c = case colorStr c of @@ -340,7 +342,10 @@ viewName s = if T.any isSpace s || maybe False (isPunctuation . snd) (T.unsnoc s $(JQ.deriveJSON (enumJSON $ dropPrefix "XL") ''SimplexLinkType) -$(JQ.deriveJSON (sumTypeJSON fstToLower) ''Format) +$(JQ.deriveToJSON (sumTypeJSON fstToLower) ''Format) + +instance FromJSON Format where + parseJSON v = $(JQ.mkParseJSON (sumTypeJSON fstToLower) ''Format) v <|> pure (Unknown v) $(JQ.deriveJSON defaultJSON ''FormattedText) diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 505f73d9cd..fd8f1cc41b 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -209,6 +209,7 @@ ciRequiresAttention content = case msgDirection @d of CIRcvDirectEvent rde -> case rde of RDEContactDeleted -> False RDEProfileUpdated {} -> False + RDEGroupInvLinkReceived _ -> True CIRcvGroupEvent rge -> case rge of RGEMemberAdded {} -> False RGEMemberConnected -> False @@ -328,6 +329,7 @@ rcvDirectEventToText :: RcvDirectEvent -> Text rcvDirectEventToText = \case RDEContactDeleted -> "contact deleted" RDEProfileUpdated {} -> "updated profile" + RDEGroupInvLinkReceived GroupProfile {displayName} -> "requested connection from group " <> displayName rcvGroupEventToText :: RcvGroupEvent -> Text rcvGroupEventToText = \case diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 539c1f524c..adacb06ee4 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -61,6 +61,7 @@ data SndConnEvent data RcvDirectEvent = RDEContactDeleted | RDEProfileUpdated {fromProfile :: Profile, toProfile :: Profile} -- CRContactUpdated + | RDEGroupInvLinkReceived {groupProfile :: GroupProfile} deriving (Show) -- platform-specific JSON encoding (used in API) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 4aacc7e677..634c4e7175 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -16,8 +16,7 @@ module Simplex.Chat.Store.Connections getConnectionEntityViaShortLink, getContactConnEntityByConnReqHash, getConnectionsToSubscribe, - unsetConnectionToSubscribe, - deleteConnectionRecord, + unsetConnectionToSubscribe ) where @@ -113,20 +112,22 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, - c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl + c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, + c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 |] (userId, contactId) toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, shortDescr, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) = + toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, shortDescr, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL)) = let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn preparedContact = toPreparedContact preparedContactRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + groupDirectInv = toGroupDirectInvitation groupDirectInvRow + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = do gm <- @@ -142,7 +143,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -270,7 +271,3 @@ getConnectionsToSubscribe db vr = do unsetConnectionToSubscribe :: DB.Connection -> IO () unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" - -deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO () -deleteConnectionRecord db User {userId} cId = do - DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId) diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 4de4a300f2..3f41abb29f 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -111,7 +111,8 @@ createOrUpdateContactRequest -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 6fd064d853..c9b76a0fd2 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -154,8 +154,8 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> Maybe PreparedChatEntity -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection -createConnReqConnection db userId acId preparedEntity_ cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> Maybe PreparedChatEntity -> ConnReqContact -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection +createConnReqConnection db userId acId preparedEntity_ cReq cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile let connStatus = ConnPrepared @@ -164,13 +164,13 @@ createConnReqConnection db userId acId preparedEntity_ cReqHash sLnk xContactId [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, + via_contact_uri, via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (userId, acId, connStatus, connType, BI True) - :. (cReqHash, sLnk, contactId_, groupMemberId_) + :. (cReq, cReqHash, sLnk, contactId_, groupMemberId_) :. (xContactId, customUserProfileId, BI (isJust groupLinkId), groupLinkId) :. (currentTs, currentTs, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) @@ -217,8 +217,8 @@ createConnReqConnection db userId acId preparedEntity_ cReqHash sLnk xContactId updatePreparedGroup GroupInfo {groupId, membership} customUserProfileId currentTs = do DB.execute db - "UPDATE groups SET via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ?" - (cReqHash, BI True, currentTs, groupId) + "UPDATE groups SET via_group_link_uri = ?, via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ?" + (cReq, cReqHash, BI True, currentTs, groupId) when (isJust customUserProfileId) $ DB.execute db @@ -264,7 +264,8 @@ getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, @@ -335,8 +336,8 @@ createDirectConnection_ db userId acId (CCLink cReq shortLinkInv) contactId_ pcc ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, contactId_, BI contactConnInitiated, customUserProfileId) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) - dbConnId <- insertedRowId db - pure (dbConnId, customUserProfileId, contactConnInitiated) + connId <- insertedRowId db + pure (connId, customUserProfileId, contactConnInitiated) createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 createIncognitoProfile db User {userId} p = do @@ -804,6 +805,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, + groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, @@ -854,7 +856,8 @@ getContact_ db vr user@User {userId} contactId deleted = do -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index d76554ef29..cceef17228 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -136,6 +136,9 @@ module Simplex.Chat.Store.Groups setContactGrpInvSent, createMemberContactInvited, updateMemberContactInvited, + createMemberContactConn, + getMemberContactInvited, + setMemberContactStartedConnection, resetMemberContactFields, updateMemberProfile, updateContactMemberProfile, @@ -201,8 +204,8 @@ import Database.SQLite.Simple.QQ (sql) 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 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, shortDescr, 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, shortDescr, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs)) +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, shortDescr, 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, shortDescr, 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 GroupLink @@ -370,7 +373,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc chatItemTTL = Nothing, uiThemes = Nothing, customData = Nothing, - membersRequireAttention = 0 + membersRequireAttention = 0, + viaGroupLinkUri = Nothing } -- | creates a new group record for the group the current user was invited to, or returns an existing one @@ -442,7 +446,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ chatItemTTL = Nothing, uiThemes = Nothing, customData = Nothing, - membersRequireAttention = 0 + membersRequireAttention = 0, + viaGroupLinkUri = Nothing }, groupMemberId ) @@ -741,7 +746,7 @@ createGroupViaLink' 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 membershipStatus IBUnknown customUserProfileId currentTs vr - liftIO $ setViaGroupLinkHash db groupId connId + liftIO $ setViaGroupLinkUri db groupId connId (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where insertHost_ currentTs groupId = do @@ -903,7 +908,7 @@ cleanupHostGroupLinkConn db user@User {userId} GroupInfo {groupId} = do DB.execute db [sql| - UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL + UPDATE connections SET via_contact_uri = NULL, via_contact_uri_hash = NULL, xcontact_id = NULL WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( SELECT contact_id FROM group_members @@ -951,7 +956,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, 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.short_descr, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, @@ -1908,7 +1913,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -2583,7 +2588,7 @@ createMemberContact quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do @@ -2606,23 +2611,17 @@ setContactGrpInvSent db Contact {contactId} xGrpDirectInvSent = do "UPDATE contacts SET contact_grp_inv_sent = ?, updated_at = ? WHERE contact_id = ?" (BI xGrpDirectInvSent, currentTs, contactId) -createMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> GroupMember -> Connection -> SubscriptionMode -> IO (Contact, GroupMember) +createMemberContactInvited :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupDirectInvitation -> IO (ContactId, GroupMember) createMemberContactInvited db - user@User {userId, profile = LocalProfile {preferences}} - connIds + User {userId, profile = LocalProfile {preferences}} gInfo - m@GroupMember {localDisplayName = memberLDN, memberProfile, memberContactProfileId} - mConn - subMode = do + m@GroupMember {localDisplayName = memberLDN, memberContactProfileId} + GroupDirectInvitation {groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, groupDirectInvStartedConnection} = do currentTs <- liftIO getCurrentTime let userPreferences = fromMaybe emptyChatPrefs $ incognitoMembershipProfile gInfo >> preferences contactId <- createContactUpdateMember currentTs userPreferences - ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode - let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} - m' = m {memberContactId = Just contactId} - pure (mCt', m') + pure (contactId, m {memberContactId = Just contactId}) where createContactUpdateMember :: UTCTime -> Preferences -> IO ContactId createContactUpdateMember currentTs userPreferences = do @@ -2631,10 +2630,12 @@ createMemberContactInvited [sql| INSERT INTO contacts ( user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + grp_direct_inv_link, grp_direct_inv_from_group_id, grp_direct_inv_from_group_member_id, grp_direct_inv_from_member_conn_id, grp_direct_inv_started_connection, created_at, updated_at, chat_ts - ) VALUES (?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (userId, memberLDN, memberContactProfileId, BI True, userPreferences, BI True) + :. (groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, BI groupDirectInvStartedConnection) :. (currentTs, currentTs, currentTs) ) contactId <- insertedRowId db @@ -2644,14 +2645,26 @@ createMemberContactInvited (contactId, currentTs, memberContactProfileId) pure contactId -updateMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> Connection -> Contact -> SubscriptionMode -> ExceptT StoreError IO Contact -updateMemberContactInvited _ _ _ _ _ Contact {localDisplayName, activeConn = Nothing} _ = throwError $ SEContactNotReady localDisplayName -updateMemberContactInvited db user connIds gInfo mConn ct@Contact {contactId, activeConn = Just oldContactConn} subMode = liftIO $ do - updateConnectionStatus db oldContactConn ConnDeleted - activeConn' <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode - ct' <- updateContactStatus db user ct CSActive - ct'' <- resetMemberContactFields db ct' - pure (ct'' :: Contact) {activeConn = Just activeConn'} +updateMemberContactInvited :: DB.Connection -> User -> Contact -> GroupDirectInvitation -> ExceptT StoreError IO () +updateMemberContactInvited _ _ Contact {localDisplayName, activeConn = Nothing} _ = throwError $ SEContactNotReady localDisplayName +updateMemberContactInvited db user Contact {contactId, activeConn = Just oldContactConn} groupDirectInv = liftIO $ do + deleteConnectionRecord db user (dbConnId oldContactConn) + updateMemberContactFields groupDirectInv + where + -- - reset status to active (in case contact was deleted) + -- - reset fields used for sending invitation + -- - set fields used for accepting invitation + updateMemberContactFields GroupDirectInvitation {groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, groupDirectInvStartedConnection} = + DB.execute + db + [sql| + UPDATE contacts + SET contact_status = ?, + contact_group_member_id = NULL, contact_grp_inv_sent = 0, + grp_direct_inv_link = ?, grp_direct_inv_from_group_id = ?, grp_direct_inv_from_group_member_id = ?, grp_direct_inv_from_member_conn_id = ?, grp_direct_inv_started_connection = ? + WHERE contact_id = ? + |] + (CSActive, groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, BI groupDirectInvStartedConnection, contactId) resetMemberContactFields :: DB.Connection -> Contact -> IO Contact resetMemberContactFields db ct@Contact {contactId} = do @@ -2666,13 +2679,15 @@ resetMemberContactFields db ct@Contact {contactId} = do (currentTs, contactId) pure ct {contactGroupMemberId = Nothing, contactGrpInvSent = False, updatedAt = currentTs} -createMemberContactConn_ :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> Connection -> ContactId -> SubscriptionMode -> IO Connection -createMemberContactConn_ +createMemberContactConn :: DB.Connection -> User -> ConnId -> Maybe CommandId -> GroupInfo -> Connection -> ConnStatus -> ContactId -> SubscriptionMode -> IO Int64 +createMemberContactConn db user@User {userId} - (cmdId, acId) + acId + cmdId_ gInfo - _memberConn@Connection {connLevel, connChatVersion, peerChatVRange = peerChatVRange@(VersionRange minV maxV)} + _memberConn@Connection {connLevel, connChatVersion, peerChatVRange = VersionRange minV maxV} + connStatus contactId subMode = do currentTs <- liftIO getCurrentTime @@ -2685,38 +2700,31 @@ createMemberContactConn_ conn_chat_version, peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, ConnJoined, ConnContact, contactId, customUserProfileId) + ( (userId, acId, connLevel, connStatus, ConnContact, contactId, customUserProfileId) :. (connChatVersion, minV, maxV, currentTs, currentTs, BI (subMode == SMOnlyCreate)) ) connId <- insertedRowId db - setCommandConnId db user cmdId connId - pure - Connection - { connId, - agentConnId = AgentConnId acId, - connChatVersion, - peerChatVRange, - connType = ConnContact, - contactConnInitiated = False, - entityId = Just contactId, - viaContact = Nothing, - viaUserContactLink = Nothing, - viaGroupLink = False, - groupLinkId = Nothing, - xContactId = Nothing, - customUserProfileId, - connLevel, - connStatus = ConnJoined, - localAlias = "", - createdAt = currentTs, - connectionCode = Nothing, - pqSupport = PQSupportOff, - pqEncryption = PQEncOff, - pqSndEnabled = Nothing, - pqRcvEnabled = Nothing, - authErrCounter = 0, - quotaErrCounter = 0 - } + forM_ cmdId_ $ \cmdId -> setCommandConnId db user cmdId connId + pure connId + +getMemberContactInvited :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, Connection, Contact, GroupDirectInvitation) +getMemberContactInvited db vr user contactId = do + ct@Contact {groupDirectInv = groupDirectInv_} <- getContact db vr user contactId + case groupDirectInv_ of + Just groupDirectInv@GroupDirectInvitation {fromGroupId_ = Just groupId, fromGroupMemberId_ = Just _gmId, fromGroupMemberConnId_ = Just mConnId} -> do + g <- getGroupInfo db vr user groupId + mConn <- getConnectionById db vr user mConnId + pure (g, mConn, ct, groupDirectInv) + _ -> + throwError $ SEMemberContactGroupMemberNotFound contactId + +setMemberContactStartedConnection :: DB.Connection -> Contact -> IO () +setMemberContactStartedConnection db Contact {contactId} = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE contacts SET grp_direct_inv_started_connection = ?, updated_at = ? WHERE contact_id = ?" + (BI True, currentTs, contactId) updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember updateMemberProfile db user@User {userId} m p' diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index b5b7be8adb..1fc8054702 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -13,6 +13,8 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_ import Simplex.Chat.Store.Postgres.Migrations.M20250704_groups_conn_link_prepared_connection import Simplex.Chat.Store.Postgres.Migrations.M20250709_profile_short_descr import Simplex.Chat.Store.Postgres.Migrations.M20250721_indexes +import Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests +import Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -25,7 +27,9 @@ schemaMigrations = ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete), ("20250704_groups_conn_link_prepared_connection", m20250704_groups_conn_link_prepared_connection, Just down_m20250704_groups_conn_link_prepared_connection), ("20250709_profile_short_descr", m20250709_profile_short_descr, Just down_m20250709_profile_short_descr), - ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes) + ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes), + ("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests), + ("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250729_member_contact_requests.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250729_member_contact_requests.hs new file mode 100644 index 0000000000..b886184331 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250729_member_contact_requests.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250729_member_contact_requests where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250729_member_contact_requests :: Text +m20250729_member_contact_requests = + T.pack + [r| +ALTER TABLE contacts ADD COLUMN grp_direct_inv_link BYTEA; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_group_id BIGINT REFERENCES groups(group_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_member_conn_id BIGINT REFERENCES connections(connection_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_started_connection SMALLINT NOT NULL DEFAULT 0; + +CREATE INDEX idx_contacts_grp_direct_inv_from_group_id ON contacts(grp_direct_inv_from_group_id); +CREATE INDEX idx_contacts_grp_direct_inv_from_group_member_id ON contacts(grp_direct_inv_from_group_member_id); +CREATE INDEX idx_contacts_grp_direct_inv_from_member_conn_id ON contacts(grp_direct_inv_from_member_conn_id); + +ALTER TABLE users ADD COLUMN auto_accept_member_contacts SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20250729_member_contact_requests :: Text +down_m20250729_member_contact_requests = + T.pack + [r| +ALTER TABLE users DROP COLUMN auto_accept_member_contacts; + +DROP INDEX idx_contacts_grp_direct_inv_from_group_id; +DROP INDEX idx_contacts_grp_direct_inv_from_group_member_id; +DROP INDEX idx_contacts_grp_direct_inv_from_member_conn_id; + +ALTER TABLE contacts DROP COLUMN grp_direct_inv_link; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_group_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_group_member_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_member_conn_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_started_connection; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250801_via_group_link_uri.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250801_via_group_link_uri.hs new file mode 100644 index 0000000000..db0ba00eff --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250801_via_group_link_uri.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250801_via_group_link_uri where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250801_via_group_link_uri :: Text +m20250801_via_group_link_uri = + T.pack + [r| +ALTER TABLE groups ADD COLUMN via_group_link_uri BYTEA; +ALTER TABLE connections ADD COLUMN via_contact_uri BYTEA; +|] + +down_m20250801_via_group_link_uri :: Text +down_m20250801_via_group_link_uri = + T.pack + [r| +ALTER TABLE groups DROP COLUMN via_group_link_uri; +ALTER TABLE connections DROP COLUMN via_contact_uri; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index b236093a79..e649c03c34 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -303,7 +303,8 @@ CREATE TABLE test_chat_schema.connections ( pq_rcv_enabled smallint, quota_err_counter bigint DEFAULT 0 NOT NULL, short_link_inv bytea, - via_short_link_contact bytea + via_short_link_contact bytea, + via_contact_uri bytea ); @@ -409,7 +410,12 @@ CREATE TABLE test_chat_schema.contacts ( conn_short_link_to_connect bytea, welcome_shared_msg_id bytea, request_shared_msg_id bytea, - contact_request_id bigint + contact_request_id bigint, + grp_direct_inv_link bytea, + grp_direct_inv_from_group_id bigint, + grp_direct_inv_from_group_member_id bigint, + grp_direct_inv_from_member_conn_id bigint, + grp_direct_inv_started_connection smallint DEFAULT 0 NOT NULL ); @@ -648,7 +654,8 @@ CREATE TABLE test_chat_schema.groups ( conn_link_started_connection smallint DEFAULT 0 NOT NULL, welcome_shared_msg_id bytea, request_shared_msg_id bytea, - conn_link_prepared_connection smallint DEFAULT 0 NOT NULL + conn_link_prepared_connection smallint DEFAULT 0 NOT NULL, + via_group_link_uri bytea ); @@ -1126,7 +1133,8 @@ CREATE TABLE test_chat_schema.users ( send_rcpts_small_groups smallint DEFAULT 0 NOT NULL, user_member_profile_updated_at timestamp with time zone, ui_themes text, - active_order bigint DEFAULT 0 NOT NULL + active_order bigint DEFAULT 0 NOT NULL, + auto_accept_member_contacts smallint DEFAULT 0 NOT NULL ); @@ -1808,6 +1816,18 @@ CREATE INDEX idx_contacts_contact_request_id ON test_chat_schema.contacts USING +CREATE INDEX idx_contacts_grp_direct_inv_from_group_id ON test_chat_schema.contacts USING btree (grp_direct_inv_from_group_id); + + + +CREATE INDEX idx_contacts_grp_direct_inv_from_group_member_id ON test_chat_schema.contacts USING btree (grp_direct_inv_from_group_member_id); + + + +CREATE INDEX idx_contacts_grp_direct_inv_from_member_conn_id ON test_chat_schema.contacts USING btree (grp_direct_inv_from_member_conn_id); + + + CREATE INDEX idx_contacts_via_group ON test_chat_schema.contacts USING btree (via_group); @@ -2344,6 +2364,21 @@ ALTER TABLE ONLY test_chat_schema.contacts +ALTER TABLE ONLY test_chat_schema.contacts + ADD CONSTRAINT contacts_grp_direct_inv_from_group_id_fkey FOREIGN KEY (grp_direct_inv_from_group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE SET NULL; + + + +ALTER TABLE ONLY test_chat_schema.contacts + ADD CONSTRAINT contacts_grp_direct_inv_from_group_member_id_fkey FOREIGN KEY (grp_direct_inv_from_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE SET NULL; + + + +ALTER TABLE ONLY test_chat_schema.contacts + ADD CONSTRAINT contacts_grp_direct_inv_from_member_conn_id_fkey FOREIGN KEY (grp_direct_inv_from_member_conn_id) REFERENCES test_chat_schema.connections(connection_id) ON DELETE SET NULL; + + + ALTER TABLE ONLY test_chat_schema.contacts ADD CONSTRAINT contacts_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 1cceaf8e8c..b74705fd53 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -40,6 +40,7 @@ module Simplex.Chat.Store.Profiles updateAllContactReceipts, updateUserContactReceipts, updateUserGroupReceipts, + updateUserAutoAcceptMemberContacts, updateUserProfile, setUserProfileContactLink, getUserContactProfiles, @@ -135,11 +136,12 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe let showNtfs = True sendRcptsContacts = True sendRcptsSmallGroups = True + autoAcceptMemberContacts = False order <- getNextActiveOrder db DB.execute db - "INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?)" - (auId, displayName, BI activeUser, order, BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, currentTs, currentTs) + "INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?)" + (auId, displayName, BI activeUser, order, BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, currentTs, currentTs) userId <- insertedRowId db DB.execute db @@ -156,7 +158,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser $ (userId, auId, contactId, profileId, BI activeUser, order, displayName, fullName, shortDescr, image, Nothing, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, Nothing, Nothing, Nothing, Nothing) + pure $ toUser $ (userId, auId, contactId, profileId, BI activeUser, order, displayName, fullName, shortDescr, image, Nothing, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] @@ -291,6 +293,10 @@ updateUserGroupReceipts db User {userId} UserMsgReceiptSettings {enable, clearOv DB.execute db "UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ?" (BI enable, userId) when clearOverrides $ DB.execute_ db "UPDATE groups SET send_rcpts = NULL" +updateUserAutoAcceptMemberContacts :: DB.Connection -> User -> Bool -> IO () +updateUserAutoAcceptMemberContacts db User {userId} autoAccept = + DB.execute db "UPDATE users SET auto_accept_member_contacts = ? WHERE user_id = ?" (BI autoAccept, userId) + updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User updateUserProfile db user p' | displayName == newName = liftIO $ do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 47f1f5c041..7665d5155f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -136,6 +136,8 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_ca import Simplex.Chat.Store.SQLite.Migrations.M20250704_groups_conn_link_prepared_connection import Simplex.Chat.Store.SQLite.Migrations.M20250709_profile_short_descr import Simplex.Chat.Store.SQLite.Migrations.M20250721_indexes +import Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests +import Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -271,7 +273,9 @@ schemaMigrations = ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete), ("20250704_groups_conn_link_prepared_connection", m20250704_groups_conn_link_prepared_connection, Just down_m20250704_groups_conn_link_prepared_connection), ("20250709_profile_short_descr", m20250709_profile_short_descr, Just down_m20250709_profile_short_descr), - ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes) + ("20250721_indexes", m20250721_indexes, Just down_m20250721_indexes), + ("20250729_member_contact_requests", m20250729_member_contact_requests, Just down_m20250729_member_contact_requests), + ("20250801_via_group_link_uri", m20250801_via_group_link_uri, Just down_m20250801_via_group_link_uri) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250729_member_contact_requests.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250729_member_contact_requests.hs new file mode 100644 index 0000000000..0488af2e60 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250729_member_contact_requests.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250729_member_contact_requests where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250729_member_contact_requests :: Query +m20250729_member_contact_requests = + [sql| +ALTER TABLE contacts ADD COLUMN grp_direct_inv_link BLOB; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_group_id INTEGER REFERENCES groups(group_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_from_member_conn_id INTEGER REFERENCES connections(connection_id) ON DELETE SET NULL; +ALTER TABLE contacts ADD COLUMN grp_direct_inv_started_connection INTEGER NOT NULL DEFAULT 0; + +CREATE INDEX idx_contacts_grp_direct_inv_from_group_id ON contacts(grp_direct_inv_from_group_id); +CREATE INDEX idx_contacts_grp_direct_inv_from_group_member_id ON contacts(grp_direct_inv_from_group_member_id); +CREATE INDEX idx_contacts_grp_direct_inv_from_member_conn_id ON contacts(grp_direct_inv_from_member_conn_id); + +ALTER TABLE users ADD COLUMN auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0; +|] + +down_m20250729_member_contact_requests :: Query +down_m20250729_member_contact_requests = + [sql| +ALTER TABLE users DROP COLUMN auto_accept_member_contacts; + +DROP INDEX idx_contacts_grp_direct_inv_from_group_id; +DROP INDEX idx_contacts_grp_direct_inv_from_group_member_id; +DROP INDEX idx_contacts_grp_direct_inv_from_member_conn_id; + +ALTER TABLE contacts DROP COLUMN grp_direct_inv_link; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_group_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_group_member_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_from_member_conn_id; +ALTER TABLE contacts DROP COLUMN grp_direct_inv_started_connection; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250801_via_group_link_uri.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250801_via_group_link_uri.hs new file mode 100644 index 0000000000..cc47bad7af --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250801_via_group_link_uri.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250801_via_group_link_uri where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250801_via_group_link_uri :: Query +m20250801_via_group_link_uri = + [sql| +ALTER TABLE groups ADD COLUMN via_group_link_uri BLOB; +ALTER TABLE connections ADD COLUMN via_contact_uri BLOB; +|] + +down_m20250801_via_group_link_uri :: Query +down_m20250801_via_group_link_uri = + [sql| +ALTER TABLE groups DROP COLUMN via_group_link_uri; +ALTER TABLE connections DROP COLUMN via_contact_uri; +|] 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 2ae6dc3a76..c66716d56a 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -65,7 +65,7 @@ Query: g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -198,7 +198,8 @@ Query: -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, @@ -342,8 +343,9 @@ SEARCH xftp_file_descriptions USING INTEGER PRIMARY KEY (rowid=?) Query: INSERT INTO contacts ( user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + grp_direct_inv_link, grp_direct_inv_from_group_id, grp_direct_inv_from_group_member_id, grp_direct_inv_from_member_conn_id, grp_direct_inv_started_connection, created_at, updated_at, chat_ts - ) VALUES (?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) @@ -386,7 +388,8 @@ Query: SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, - c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl + c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, + c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 @@ -936,7 +939,8 @@ Query: -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, @@ -969,7 +973,7 @@ Query: g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -1019,7 +1023,7 @@ Query: g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, 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.short_descr, pu.image, pu.contact_link, pu.local_alias, pu.preferences, mu.created_at, mu.updated_at, @@ -1427,7 +1431,7 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: - UPDATE connections SET via_contact_uri_hash = NULL, xcontact_id = NULL + UPDATE connections SET via_contact_uri = NULL, via_contact_uri_hash = NULL, xcontact_id = NULL WHERE user_id = ? AND via_group_link = 1 AND contact_id IN ( SELECT contact_id FROM group_members @@ -1439,6 +1443,16 @@ SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) LIST SUBQUERY 1 SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE contacts + SET contact_status = ?, + contact_group_member_id = NULL, contact_grp_inv_sent = 0, + grp_direct_inv_link = ?, grp_direct_inv_from_group_id = ?, grp_direct_inv_from_group_member_id = ?, grp_direct_inv_from_member_conn_id = ?, grp_direct_inv_started_connection = ? + WHERE contact_id = ? + +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contacts SET local_display_name = ?, contact_profile_id = ?, updated_at = ? @@ -1568,7 +1582,8 @@ Query: -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, + ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection 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.xcontact_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, @@ -3645,6 +3660,14 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups + SET via_group_link_uri = ?, via_group_link_uri_hash = ? + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: DELETE FROM chat_items WHERE group_scope_group_member_id = ? @@ -3720,6 +3743,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM connections WHERE connection_id IN ( @@ -3738,6 +3762,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM connections WHERE connection_id IN ( @@ -3756,6 +3781,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM connections WHERE connection_id IN ( @@ -3774,6 +3800,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM contact_profiles @@ -4171,10 +4198,10 @@ Plan: Query: INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, + via_contact_uri, via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4601,16 +4628,6 @@ Query: 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 = ?) - WHERE group_id = ? - -Plan: -SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) -SCALAR SUBQUERY 1 -SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE msg_deliveries SET delivery_status = ?, updated_at = ? @@ -4732,7 +4749,7 @@ Query: g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -4758,7 +4775,7 @@ Query: g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -5114,7 +5131,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5126,7 +5143,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5139,7 +5156,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5152,7 +5169,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5166,7 +5183,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5179,7 +5196,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5192,7 +5209,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5205,7 +5222,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5218,7 +5235,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5443,6 +5460,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM connections WHERE user_id = ? AND group_member_id = ? Plan: @@ -5451,6 +5469,7 @@ SEARCH msg_deliveries USING COVERING INDEX idx_msg_deliveries_agent_msg_id (conn SEARCH commands USING COVERING INDEX idx_commands_connection_id (connection_id=?) SEARCH messages USING COVERING INDEX idx_messages_connection_id (connection_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_connection_id (connection_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_member_conn_id (grp_direct_inv_from_member_conn_id=?) Query: DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5524,6 +5543,7 @@ SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_membe SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? @@ -5548,6 +5568,7 @@ SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_membe SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) SEARCH group_member_intros USING COVERING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) SEARCH group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id (invited_by_group_member_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_member_id (grp_direct_inv_from_group_member_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (contact_group_member_id=?) Query: DELETE FROM groups WHERE user_id = ? AND group_id = ? @@ -5564,6 +5585,7 @@ SEARCH contact_requests USING COVERING INDEX idx_contact_requests_business_group SEARCH user_contact_links USING COVERING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH files USING COVERING INDEX idx_files_group_id (group_id=?) SEARCH group_members USING COVERING INDEX sqlite_autoindex_group_members_1 (group_id=?) +SEARCH contacts USING COVERING INDEX idx_contacts_grp_direct_inv_from_group_id (grp_direct_inv_from_group_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_via_group (via_group=?) Query: DELETE FROM messages WHERE connection_id = ? @@ -5798,7 +5820,7 @@ Plan: Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?) +Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?) Plan: Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?) @@ -6050,6 +6072,10 @@ Query: SELECT user_id FROM users WHERE local_display_name = ? Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_2 (local_display_name=?) +Query: SELECT via_contact_uri, via_contact_uri_hash FROM connections WHERE connection_id = ? +Plan: +SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6142,6 +6168,10 @@ Query: UPDATE contacts SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE u Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE contacts SET grp_direct_inv_started_connection = ?, updated_at = ? WHERE contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contacts SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) @@ -6302,7 +6332,7 @@ Query: UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND g Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE groups SET via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ? +Query: UPDATE groups SET via_group_link_uri = ?, via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -6358,6 +6388,10 @@ Query: UPDATE users SET active_user = 1, active_order = ? WHERE user_id = ? Plan: SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE users SET auto_accept_member_contacts = ? WHERE user_id = ? +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE users SET contact_id = ? WHERE user_id = ? Plan: SEARCH users 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 322814385f..371f3f09ec 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -37,7 +37,8 @@ CREATE TABLE users( send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0, user_member_profile_updated_at TEXT, ui_themes TEXT, - active_order INTEGER NOT NULL DEFAULT 0, -- 1 for active user + active_order INTEGER NOT NULL DEFAULT 0, + auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE RESTRICT @@ -85,6 +86,11 @@ CREATE TABLE contacts( welcome_shared_msg_id BLOB, request_shared_msg_id BLOB, contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL, + grp_direct_inv_link BLOB, + grp_direct_inv_from_group_id INTEGER REFERENCES groups(group_id) ON DELETE SET NULL, + grp_direct_inv_from_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE SET NULL, + grp_direct_inv_from_member_conn_id INTEGER REFERENCES connections(connection_id) ON DELETE SET NULL, + grp_direct_inv_started_connection INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -148,7 +154,8 @@ CREATE TABLE groups( conn_link_started_connection INTEGER NOT NULL DEFAULT 0, welcome_shared_msg_id BLOB, request_shared_msg_id BLOB, - conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0, -- received + conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0, + via_group_link_uri BLOB, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -321,6 +328,7 @@ CREATE TABLE connections( quota_err_counter INTEGER NOT NULL DEFAULT 0, short_link_inv BLOB, via_short_link_contact BLOB, + via_contact_uri BLOB, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -1085,3 +1093,12 @@ CREATE INDEX idx_chat_items_group_scope_stats_all ON chat_items( chat_item_id, user_mention ); +CREATE INDEX idx_contacts_grp_direct_inv_from_group_id ON contacts( + grp_direct_inv_from_group_id +); +CREATE INDEX idx_contacts_grp_direct_inv_from_group_member_id ON contacts( + grp_direct_inv_from_group_member_id +); +CREATE INDEX idx_contacts_grp_direct_inv_from_member_conn_id ON contacts( + grp_direct_inv_from_member_conn_id +); diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 72282c8436..67c4eb8c1e 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -457,19 +457,22 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe SharedMsgId, Maybe SharedMsgId) -type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) +type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) + +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) type ContactRow = Only ContactId :. ContactRow' toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, shortDescr, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) = +toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, shortDescr, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, localAlias} activeConn = toMaybeConnection vr connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito preparedContact = toPreparedContact preparedContactRow - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + groupDirectInv = toGroupDirectInvitation groupDirectInvRow + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} toPreparedContact :: PreparedContactRow -> Maybe PreparedContact toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) = @@ -482,6 +485,11 @@ toACreatedConnLink_ (Just (ACR m cr)) csl = case csl of Nothing -> Just $ ACCL m $ CCLink cr Nothing Just (ACSL m' l) -> (\Refl -> ACCL m $ CCLink cr (Just l)) <$> testEquality m m' +toGroupDirectInvitation :: GroupDirectInvitationRow -> Maybe GroupDirectInvitation +toGroupDirectInvitation (Nothing, _, _, _, _) = Nothing +toGroupDirectInvitation (Just groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, BI groupDirectInvStartedConnection) = + Just $ GroupDirectInvitation {groupDirectInvLink, fromGroupId_, fromGroupMemberId_, fromGroupMemberConnId_, groupDirectInvStartedConnection} + getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = ExceptT . firstRow rowToLocalProfile (SEProfileNotFound profileId) $ @@ -506,15 +514,15 @@ userQuery :: Query userQuery = [sql| SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User -toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, shortDescr, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} +toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User +toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, shortDescr, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts = BoolDef autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} where profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences @@ -630,21 +638,21 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupMemberRow type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image) :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. 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, shortDescr, description, image, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow preparedGroup = toPreparedGroup preparedGroupRow - in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention} + in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention, viaGroupLinkUri} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case @@ -690,7 +698,7 @@ groupInfoQuery = g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, 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, + g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, -- 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, @@ -776,13 +784,23 @@ addGroupChatTags db g@GroupInfo {groupId} = do chatTags <- getGroupChatTags db groupId pure (g :: GroupInfo) {chatTags} -setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO () -setViaGroupLinkHash db groupId connId = - DB.execute - db - [sql| - UPDATE groups - SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?) - WHERE group_id = ? - |] - (connId, groupId) +setViaGroupLinkUri :: DB.Connection -> GroupId -> Int64 -> IO () +setViaGroupLinkUri db groupId connId = do + r <- + DB.query + db + "SELECT via_contact_uri, via_contact_uri_hash FROM connections WHERE connection_id = ?" + (Only connId) :: IO [(Maybe ConnReqContact, Maybe ConnReqUriHash)] + forM_ (listToMaybe r) $ \(viaContactUri, viaContactUriHash) -> + DB.execute + db + [sql| + UPDATE groups + SET via_group_link_uri = ?, via_group_link_uri_hash = ? + WHERE group_id = ? + |] + (viaContactUri, viaContactUriHash, groupId) + +deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO () +deleteConnectionRecord db User {userId} cId = do + DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index d0b1af3546..964d8f083b 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -132,6 +132,7 @@ data User = User showNtfs :: Bool, sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool, + autoAcceptMemberContacts :: BoolDef, userMemberProfileUpdatedAt :: Maybe UTCTime, uiThemes :: Maybe UIThemeEntityOverrides } @@ -191,8 +192,14 @@ data Contact = Contact chatTs :: Maybe UTCTime, preparedContact :: Maybe PreparedContact, contactRequestId :: Maybe Int64, + -- contactGroupMemberId + contactGrpInvSent are used in conjunction for making connection request + -- to a group member via direct message feature contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool, + -- groupDirectInv is used for accepting connection request made via direct message feature by a group member + -- when auto-accept is disabled - this is the opposite side of contactGroupMemberId + contactGrpInvSent + -- (there is no hidden meaning in naming inconsistency) + groupDirectInv :: Maybe GroupDirectInvitation, chatTags :: [ChatTagId], chatItemTTL :: Maybe Int64, uiThemes :: Maybe UIThemeEntityOverrides, @@ -212,6 +219,15 @@ data PreparedContact = PreparedContact } deriving (Eq, Show) +data GroupDirectInvitation = GroupDirectInvitation + { groupDirectInvLink :: ConnReqInvitation, + fromGroupId_ :: Maybe GroupId, + fromGroupMemberId_ :: Maybe GroupMemberId, + fromGroupMemberConnId_ :: Maybe Int64, + groupDirectInvStartedConnection :: Bool + } + deriving (Eq, Show) + newtype SharedMsgId = SharedMsgId ByteString deriving (Eq, Show) deriving newtype (FromField) @@ -470,7 +486,8 @@ data GroupInfo = GroupInfo chatItemTTL :: Maybe Int64, uiThemes :: Maybe UIThemeEntityOverrides, customData :: Maybe CustomData, - membersRequireAttention :: Int + membersRequireAttention :: Int, + viaGroupLinkUri :: Maybe ConnReqContact } deriving (Eq, Show) @@ -995,6 +1012,9 @@ incognitoMembershipProfile GroupInfo {membership = m@GroupMember {memberProfile} memberSecurityCode :: GroupMember -> Maybe SecurityCode memberSecurityCode GroupMember {activeConn} = connectionCode =<< activeConn +memberBlocked :: GroupMember -> Bool +memberBlocked m = blockedByAdmin m || not (showMessages $ memberSettings m) + data NewGroupMember = NewGroupMember { memInfo :: MemberInfo, memCategory :: GroupMemberCategory, @@ -1586,6 +1606,9 @@ data Connection = Connection } deriving (Eq, Show) +dbConnId :: Connection -> Int64 +dbConnId Connection {connId} = connId + connReady :: Connection -> Bool connReady Connection {connStatus} = connStatus == ConnReady || connStatus == ConnSndReady @@ -1962,8 +1985,8 @@ instance ToJSON ChatVersionRange where -- This type is needed for backward compatibility of new remote controller with old remote host. -- See CONTRIBUTING.md -newtype BoolDef = BoolDef Bool - deriving newtype (Show, ToJSON) +newtype BoolDef = BoolDef {isTrue :: Bool} + deriving newtype (Eq, Show, ToJSON) instance FromJSON BoolDef where parseJSON v = BoolDef <$> parseJSON v @@ -2069,6 +2092,8 @@ $(JQ.deriveJSON defaultJSON ''FileTransferMeta) $(JQ.deriveJSON defaultJSON ''PreparedContact) +$(JQ.deriveJSON defaultJSON ''GroupDirectInvitation) + $(JQ.deriveJSON defaultJSON ''LocalFileMeta) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FT") ''FileTransfer) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 342b7b8a6c..b376993beb 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -242,6 +242,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] + CRMemberContactAccepted u ct -> ttyUser u ["contact " <> ttyContact' ct <> " is accepted, starting connection"] CRCallInvitations _ -> [] CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"] CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)] @@ -485,7 +486,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CEvtAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] CEvtNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] - CEvtNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] + CEvtNewMemberContactReceivedInv u ct g m -> ttyUser u $ viewNewMemberContactReceivedInv u ct g m CEvtContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct' CEvtCallInvitation RcvCallInvitation {user, contact, callType, sharedKey} -> ttyUser user $ viewCallInvitation contact callType sharedKey CEvtCallOffer {user = u, contact, callType, offer, sharedKey} -> ttyUser u $ viewCallOffer contact callType offer sharedKey @@ -567,7 +568,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 _scopeInfo, CIGroupRcv m) -> groupNtf user g mention && not (blockedByAdmin m) && showMessages (memberSettings m) + (GroupChat g _scopeInfo, CIGroupRcv m) -> groupNtf user g mention && not (memberBlocked m) _ -> True contactNtf :: User -> Contact -> Bool -> Bool @@ -1411,6 +1412,16 @@ viewContactsMerged c1 c2 ct' = "use " <> ttyToContact' ct' <> highlight' "" <> " to send messages" ] +viewNewMemberContactReceivedInv :: User -> Contact -> GroupInfo -> GroupMember -> [StyledString] +viewNewMemberContactReceivedInv user ct@Contact {localDisplayName = c} g m + | isTrue (autoAcceptMemberContacts user) = + [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] + | otherwise = + [ ttyGroup' g <> " " <> ttyMember m <> " requests to create direct contact with you", + "to accept: " <> highlight ("/accept_member_contact @" <> viewName c), + "to reject: " <> highlight ("/delete @" <> viewName c) <> " (the sender will NOT be notified)" + ] + viewContactAndMemberAssociated :: Contact -> GroupInfo -> GroupMember -> Contact -> [StyledString] viewContactAndMemberAssociated ct g m ct' = [ "contact and member are merged: " <> ttyContact' ct <> ", " <> ttyGroup' g <> " " <> ttyMember m, diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 351943925f..6a4ff8f5fc 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -253,8 +253,49 @@ testSuspendResume ps = superUser #> "@SimpleX-Directory /link 1:privacy" superUser <# "SimpleX-Directory> > /link 1:privacy" superUser <## " The link to join the group ID 1 (privacy):" - superUser <##. "https://simplex.chat/contact" + superUser <##. "https://localhost/g#" superUser <## "New member role: member" + -- get and change the link to the equivalent - should not ask to re-approve + bob #> "@SimpleX-Directory /link 1" + bob <# "SimpleX-Directory> > /link 1" + bob <## " The link to join the group ID 1 (privacy):" + gLink <- getTermLine bob + gLink `shouldStartWith` "https://localhost/g#" + bob <## "New member role: member" + bob ##> "/show welcome #privacy" + bob <## "Welcome message:" + bob <## ("Link to join the group privacy: " <> gLink) + bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink <> "?same_link=true") + bob <## "welcome message changed to:" + bob <## ("Link to join the group privacy: " <> gLink <> "?same_link=true") + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <## "The group is listed in directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated - only link or whitespace changes." + superUser <## "The group remained listed in directory." + -- upgrade link + -- make it upgradeable first + superUser #> "@SimpleX-Directory /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" + superUser <# "SimpleX-Directory> > /x /sql chat UPDATE user_contact_links SET short_link_contact = NULL" + superUser <## "" + bob #> "@SimpleX-Directory /link 1" + bob <# "SimpleX-Directory> > /link 1" + bob <## " The link to join the group ID 1 (privacy):" + bob <##. "https://simplex.chat/contact#/" + bob <## "New member role: member" + bob <## "The link is being upgraded..." + bob <# "SimpleX-Directory> Please replace the old link in welcome message of your group ID 1 (privacy)" + bob <## "If this is the only change, the group will remain listed in directory without re-approval." + bob <## "" + bob <## "The new link:" + gLink' <- dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine bob + bob ##> ("/set welcome #privacy Link to join the group privacy: " <> gLink') + bob <## "welcome message changed to:" + bob <## ("Link to join the group privacy: " <> gLink') + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <## "The group is listed in directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated - only link or whitespace changes." + superUser <## "The group remained listed in directory." + -- send message to group owner superUser #> "@SimpleX-Directory /owner 1:privacy hello there" superUser <# "SimpleX-Directory> > /owner 1:privacy hello there" superUser <## " Forwarded to @bob, the owner of the group ID 1 (privacy)" @@ -324,7 +365,7 @@ testSetRole ps = cath <## "connection request sent!" cath <## "#privacy: joining the group..." cath <## "#privacy: you joined the group" - cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://simplex.chat/" + cath <#. "#privacy SimpleX-Directory> Link to join the group privacy: https://localhost/g#" cath <## "#privacy: member bob (Bob) is connected" bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" bob <## "#privacy: new member cath is connected" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 9c19923772..efcab785a9 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -603,6 +603,8 @@ xftpServerConfig = logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, + prometheusInterval = Nothing, + prometheusMetricsFile = "tests/xftp-server-metrics.txt", controlPort = Nothing, transportConfig = mkTransportServerConfig True (Just alpnSupportedXFTPhandshakes) False, responseDelay = 0 diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 1aba3a77c6..ffac4a8f6f 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -147,6 +147,7 @@ chatGroupTests = do it "share incognito profile" testMemberContactIncognito it "sends and updates profile when creating contact" testMemberContactProfileUpdate it "re-create member contact after deletion, many groups" testRecreateMemberContactManyGroups + it "manually accept contact with group member" testMemberContactAccept describe "group message forwarding" $ do it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForward it "forward reports to moderators, don't forward to members (x.msg.new, MCReport)" testGroupMsgForwardReport @@ -437,6 +438,9 @@ testNewGroupIncognito :: HasCallStack => TestParams -> IO () testNewGroupIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do + bob ##> "/set accept member contacts on" + bob <## "ok" + connectUsers alice bob -- alice creates group with incognito membership @@ -846,6 +850,9 @@ testGroupDeleteInvitedContact :: HasCallStack => TestParams -> IO () testGroupDeleteInvitedContact = testChat2 aliceProfile bobProfile $ \alice bob -> do + bob ##> "/set accept member contacts on" + bob <## "ok" + connectUsers alice bob alice ##> "/g team" alice <## "group #team is created" @@ -4144,6 +4151,11 @@ testMemberContactMessage :: HasCallStack => TestParams -> IO () testMemberContactMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + bob ##> "/set accept member contacts on" + bob <## "ok" + cath ##> "/set accept member contacts on" + cath <## "ok" + createGroup3 "team" alice bob cath -- alice and bob delete contacts, connect @@ -4211,6 +4223,9 @@ testMemberContactNoMessage :: HasCallStack => TestParams -> IO () testMemberContactNoMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + cath ##> "/set accept member contacts on" + cath <## "ok" + createGroup3 "team" alice bob cath -- bob and cath connect @@ -4245,6 +4260,9 @@ testMemberContactProhibitedRepeatInv :: HasCallStack => TestParams -> IO () testMemberContactProhibitedRepeatInv = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + cath ##> "/set accept member contacts on" + cath <## "ok" + createGroup3 "team" alice bob cath bob ##> "/_create member contact #1 3" @@ -4273,6 +4291,9 @@ testMemberContactInvitedConnectionReplaced ps = do withNewTestChat ps "alice" aliceProfile $ \alice -> do withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "cath" cathProfile $ \cath -> do + bob ##> "/set accept member contacts on" + bob <## "ok" + createGroup3 "team" alice bob cath alice ##> "/d bob" @@ -4343,6 +4364,9 @@ testMemberContactIncognito :: HasCallStack => TestParams -> IO () testMemberContactIncognito = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + cath ##> "/set accept member contacts on" + cath <## "ok" + -- create group, bob joins incognito threadDelay 100000 alice ##> "/g team" @@ -4433,6 +4457,9 @@ testMemberContactProfileUpdate :: HasCallStack => TestParams -> IO () testMemberContactProfileUpdate = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + cath ##> "/set accept member contacts on" + cath <## "ok" + createGroup3 "team" alice bob cath bob ##> "/p rob Rob" @@ -4501,6 +4528,9 @@ testRecreateMemberContactManyGroups :: HasCallStack => TestParams -> IO () testRecreateMemberContactManyGroups = testChat2 aliceProfile bobProfile $ \alice bob -> do + bob ##> "/set accept member contacts on" + bob <## "ok" + connectUsers alice bob createGroup2' "team" alice (bob, GRAdmin) False createGroup2' "club" alice (bob, GRAdmin) False @@ -4570,6 +4600,46 @@ testRecreateMemberContactManyGroups = bob <# "@alice 4" alice <# "bob> 4" +testMemberContactAccept :: HasCallStack => TestParams -> IO () +testMemberContactAccept = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + -- bob and cath connect + bob ##> "/_create member contact #1 3" + bob <## "contact for member #team cath is created" + + bob ##> "/_invite member contact @3" + bob <## "sent invitation to connect directly to member #team cath" + cath <## "#team bob requests to create direct contact with you" + cath <## "to accept: /accept_member_contact @bob" + cath <## "to reject: /delete @bob (the sender will NOT be notified)" + + cath #$> ("/_get chat @3 count=1", chat, [(0, "requested connection from group team")]) + + cath ##> "/accept_member_contact @bob" + cath <## "contact bob is accepted, starting connection" + concurrently_ + (bob <## "cath (Catherine): contact is connected") + (cath <## "bob (Bob): contact is connected") + + bob <##> cath + + -- if group is deleted, bob and cath keep contact with each other + alice ##> "/d #team" + concurrentlyN_ + [ alice <## "#team: you deleted the group", + do + bob <## "#team: alice deleted the group" + bob <## "use /d #team to delete the local copy of the group", + do + cath <## "#team: alice deleted the group" + cath <## "use /d #team to delete the local copy of the group" + ] + + bob <##> cath + testGroupMsgForward :: HasCallStack => TestParams -> IO () testGroupMsgForward = testChat3 aliceProfile bobProfile cathProfile $ diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 986cd487ce..f043e1c425 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -211,6 +211,9 @@ testMultiWordProfileNames :: HasCallStack => TestParams -> IO () testMultiWordProfileNames = testChat3 aliceProfile' bobProfile' cathProfile' $ \alice bob cath -> do + cath ##> "/set accept member contacts on" + cath <## "ok" + alice ##> "/c" inv <- getInvitation alice bob ##> ("/c " <> inv) @@ -2611,6 +2614,9 @@ testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $ testGroupPrefsDirectForRole :: HasCallStack => TestParams -> IO () testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do + dan ##> "/set accept member contacts on" + dan <## "ok" + createGroup3 "team" alice bob cath threadDelay 1000000 alice ##> "/set direct #team on owner" diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index 99157b127f..baf9dde8c1 100644 --- a/tests/JSONFixtures.hs +++ b/tests/JSONFixtures.hs @@ -17,10 +17,10 @@ activeUserExistsTagged :: LB.ByteString activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}" activeUserSwift :: LB.ByteString -activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" +activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}}}}" activeUserTagged :: LB.ByteString -activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" +activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}}}" chatStartedSwift :: LB.ByteString chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" @@ -35,7 +35,7 @@ networkStatusesTagged :: LB.ByteString networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" userJSON :: LB.ByteString -userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" +userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}" memberSubSummarySwift :: LB.ByteString memberSubSummarySwift = "{\"result\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" diff --git a/website/langs/fa.json b/website/langs/fa.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/website/langs/fa.json @@ -0,0 +1 @@ +{} diff --git a/website/src/_includes/blog_previews/20250729.html b/website/src/_includes/blog_previews/20250729.html new file mode 100644 index 0000000000..945479ddda --- /dev/null +++ b/website/src/_includes/blog_previews/20250729.html @@ -0,0 +1,10 @@ +

v6.4.1 is released:

+ +
    +
  • welcome your contacts: set your profile bio and welcome message.
  • +
  • protect your communities from spam and abuse: review new members ("knocking").
  • +
  • set default time to delete messages for new contacts.
  • +
  • improved app integrity: Linux app builds are now reproducible.
  • +
+ +

Also, we added 3 new interface languages to Android and desktop apps thanks to our users: Indonesian, Romanian and Vietnamese.