diff --git a/.gitignore b/.gitignore index 7bd3d04e59..035d24c6cd 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,10 @@ website/translations.json website/src/img/images/ website/src/images/ website/src/js/lottie.min.js -website/src/js/ethers* +website/src/js/ethers.* +website/src/js/directory.js +website/src/js/channel-preview.js +website/src/js/simplex-lib.js website/src/file-assets/ website/src/link-images/ website/src/privacy.md diff --git a/README.md b/README.md index 252fc95708..5583fad0b5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ | 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) | -SimpleX logo +SimpleX logo + +Invest in SimpleX Chat. [Learn more on Wefunder](https://wefunder.com/simplexchat). # SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design! @@ -218,12 +220,14 @@ 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 22, 2026. SimpleX Public Names — a Name Nobody Can Take From You](./blog/20260722-simplex-public-names.md) + +[Apr 30, 2026. SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech](./blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md) + +[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) - [Jan 14, 2025. SimpleX network: large groups and privacy-preserving content moderation](./blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) [Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) @@ -240,10 +244,6 @@ Recent and important updates: [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). -[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). - -[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). - [All updates](./blog) ## :zap: Quick installation of a terminal app diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index ba49c767da..22d0da3829 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -257,26 +257,33 @@ struct ContentView: View { ChatListView(activeUserPickerSheet: $chatListUserPickerSheet) .redacted(reason: appSheetState.redactionReasons(protectScreen)) .onAppear { - requestNtfAuthorization() - // Local Authentication notice is to be shown on next start after onboarding is complete - if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { - prefLANoticeShown = true - alertManager.showAlert(laNoticeAlert()) - } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - if !noticesShown { - let showWhatsNew = shouldShowWhatsNew() - let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false - noticesShown = showWhatsNew || showUpdatedConditions - if showWhatsNew || showUpdatedConditions { - noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + // Connect only after the notifications prompt is resolved: the system prompt suspends + // the app (scene .inactive), which kills an in-flight connect and makes + // getTopViewController() nil. Deferring keeps the URL until the app is active again. + let openingViaLink = pendingConnectUrl != nil + requestNtfAuthorization(showDeniedAlert: !openingViaLink) { + connectViaUrl() + } + if !openingViaLink { + // Local Authentication notice is to be shown on next start after onboarding is complete + if (!prefLANoticeShown && prefShowLANotice && chatModel.chats.count > 2) { + prefLANoticeShown = true + alertManager.showAlert(laNoticeAlert()) + } else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + if !noticesShown { + let showWhatsNew = shouldShowWhatsNew() + let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false + noticesShown = showWhatsNew || showUpdatedConditions + if showWhatsNew || showUpdatedConditions { + noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions) + } } } } + showReRegisterTokenAlert() } prefShowLANotice = true - connectViaUrl() - showReRegisterTokenAlert() } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .onChange(of: chatModel.reRegisterTknStatus) { _ in showReRegisterTokenAlert() } @@ -383,15 +390,16 @@ struct ContentView: View { } } - func requestNtfAuthorization() { + func requestNtfAuthorization(showDeniedAlert: Bool = true, whenDone: (() -> Void)? = nil) { NtfManager.shared.requestAuthorization( onDeny: { - if (!notificationAlertShown) { + if showDeniedAlert, !notificationAlertShown { notificationAlertShown = true alertManager.showAlert(notificationAlert()) } }, - onAuthorized: { notificationAlertShown = false } + onAuthorized: { notificationAlertShown = false }, + whenDone: { if let whenDone { DispatchQueue.main.async(execute: whenDone) } } ) } @@ -436,16 +444,20 @@ struct ContentView: View { } // Spec: spec/client/navigation.md#connectViaUrl + // a URL opened via link that is ready to be connected now (appOpenUrl immediately, or + // appOpenUrlLater once the app is active — see .onChange(of: scenePhase) in SimpleXApp) + private var pendingConnectUrl: URL? { + let m = ChatModel.shared + if let url = m.appOpenUrl { return url } + if let url = m.appOpenUrlLater, AppChatState.shared.value == .active, scenePhase == .active { return url } + return nil + } + func connectViaUrl() { let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil - connectViaUrl_(url) - } else if let url = m.appOpenUrlLater, AppChatState.shared.value == .active, scenePhase == .active { - // correcting branch in case .onChange(of: scenePhase) in SimpleXApp doesn't trigger and transfer appOpenUrlLater into appOpenUrl - m.appOpenUrlLater = nil - connectViaUrl_(url) - } + guard let url = pendingConnectUrl else { return } + if m.appOpenUrl != nil { m.appOpenUrl = nil } else { m.appOpenUrlLater = nil } + connectViaUrl_(url) } func connectViaUrl_(_ url: URL) { diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index a5a56174b1..40b88ec338 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -45,7 +45,7 @@ enum ChatCommand: ChatCmdProtocol { case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String) case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?) case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) - case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool, live: Bool, ttl: Int?, sign: Bool, composedMessages: [ComposedMessage]) case apiCreateChatTag(tag: ChatTagData) case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64]) case apiDeleteChatTag(tagId: Int64) @@ -63,6 +63,7 @@ enum ChatCommand: ChatCmdProtocol { case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64]) case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) case apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) + case apiShareMyAddress(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -84,6 +85,7 @@ enum ChatCommand: ChatCmdProtocol { case apiLeaveGroup(groupId: Int64) case apiListMembers(groupId: Int64) case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) + case apiSetPublicGroupAccess(groupId: Int64, access: PublicGroupAccess) case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole) case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiDeleteGroupLink(groupId: Int64) @@ -130,9 +132,9 @@ enum ChatCommand: ChatCmdProtocol { case apiAddContact(userId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) - case apiConnectPlan(userId: Int64, connLink: String, linkOwnerSig: LinkOwnerSig?) - case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) - case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) + case apiConnectPlan(userId: Int64, connLink: String, resolveMode: PlanResolveMode, linkOwnerSig: LinkOwnerSig?) + case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain?) + case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain?) case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) @@ -155,6 +157,9 @@ enum ChatCommand: ChatCmdProtocol { case apiAddMyAddressShortLink(userId: Int64) case apiSetProfileAddress(userId: Int64, on: Bool) case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings) + case apiSetUserDomain(userId: Int64, simplexDomain: String?) + case apiVerifyContactDomain(contactId: Int64) + case apiVerifyGroupDomain(groupId: Int64) case apiAcceptContact(incognito: Bool, contactReqId: Int64) case apiRejectContact(contactReqId: Int64) // WebRTC calls @@ -236,11 +241,11 @@ enum ChatCommand: ChatCmdProtocol { return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))" case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)" - case let .apiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages): + case let .apiSendMessages(type, id, scope, sendAsGroup, live, ttl, sign, composedMessages): let msgs = encodeJSON(composedMessages) let ttlStr = ttl != nil ? "\(ttl!)" : "default" let asGroup = sendAsGroup ? "(as_group=on)" : "" - return "/_send \(ref(type, id, scope: scope))\(asGroup) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + return "/_send \(ref(type, id, scope: scope))\(asGroup) live=\(onOff(live)) ttl=\(ttlStr) sign=\(onOff(sign)) json \(msgs)" case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))" case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))" case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)" @@ -266,6 +271,9 @@ enum ChatCommand: ChatCmdProtocol { case let .apiShareChatMsgContent(shareChatType, shareChatId, toChatType, toChatId, toScope, sendAsGroup): let asGroup = sendAsGroup ? "(as_group=on)" : "" return "/_share chat content \(ref(shareChatType, shareChatId, scope: nil)) \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" + case let .apiShareMyAddress(toChatType, toChatId, toScope, sendAsGroup): + let asGroup = sendAsGroup ? "(as_group=on)" : "" + return "/_share address \(ref(toChatType, toChatId, scope: toScope))\(asGroup)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -343,11 +351,12 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" - case let .apiConnectPlan(userId, connLink, linkOwnerSig): + case let .apiConnectPlan(userId, connLink, resolveMode, linkOwnerSig): + let resolveStr = resolveMode == .unknown ? "" : " resolve=\(resolveMode.rawValue)" let sigStr = if let linkOwnerSig { " sig=\(encodeJSON(linkOwnerSig))" } else { "" } - return "/_connect plan \(userId) \(connLink)\(sigStr)" - case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" - case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") direct=\(onOff(directLink)) \(encodeJSON(groupShortLinkData))" + return "/_connect plan \(userId) \(connLink)\(resolveStr)\(sigStr)" + case let .apiPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain): return "/_prepare contact \(userId) \(connLink.cmdString)\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(contactShortLinkData))" + case let .apiPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain): return "/_prepare group \(userId) \(connLink.cmdString) direct=\(onOff(directLink))\(verifiedDomain.map{ " \($0.cmdString)" } ?? "") \(encodeJSON(groupShortLinkData))" case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)" case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)" case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito))\(maybeContent(mc))" @@ -370,6 +379,10 @@ enum ChatCommand: ChatCmdProtocol { case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))" + case let .apiSetUserDomain(userId, simplexDomain): return "/_set domain \(userId)" + (simplexDomain.map { " " + $0 } ?? "") + case let .apiSetPublicGroupAccess(groupId, access): return "/_public group access #\(groupId) \(encodeJSON(access))" + case let .apiVerifyContactDomain(contactId): return "/_verify domain @\(contactId)" + case let .apiVerifyGroupDomain(groupId): return "/_verify domain #\(groupId)" case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" @@ -460,6 +473,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiPlanForwardChatItems: return "apiPlanForwardChatItems" case .apiForwardChatItems: return "apiForwardChatItems" case .apiShareChatMsgContent: return "apiShareChatMsgContent" + case .apiShareMyAddress: return "apiShareMyAddress" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" @@ -481,6 +495,7 @@ enum ChatCommand: ChatCmdProtocol { case .apiLeaveGroup: return "apiLeaveGroup" case .apiListMembers: return "apiListMembers" case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" + case .apiSetPublicGroupAccess: return "apiSetPublicGroupAccess" case .apiCreateGroupLink: return "apiCreateGroupLink" case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" case .apiDeleteGroupLink: return "apiDeleteGroupLink" @@ -551,6 +566,9 @@ enum ChatCommand: ChatCmdProtocol { case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink" case .apiSetProfileAddress: return "apiSetProfileAddress" case .apiSetAddressSettings: return "apiSetAddressSettings" + case .apiSetUserDomain: return "apiSetUserDomain" + case .apiVerifyContactDomain: return "apiVerifyContactDomain" + case .apiVerifyGroupDomain: return "apiVerifyGroupDomain" case .apiAcceptContact: return "apiAcceptContact" case .apiRejectContact: return "apiRejectContact" case .apiSendCallInvitation: return "apiSendCallInvitation" @@ -802,7 +820,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection) case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) - case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan) + case connectionPlan(user: UserRef, connLink: CreatedConnLink, planSimplexName: SimplexNameInfo?, otherSimplexName: SimplexNameInfo?, connectionPlan: ConnectionPlan) case newPreparedChat(user: UserRef, chat: ChatData) case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact) case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo) @@ -926,7 +944,7 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)") case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))") - case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") + case let .connectionPlan(u, connLink, _, _, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") case let .newPreparedChat(u, chat): return withUser(u, String(describing: chat)) case let .contactUserChanged(u, fromContact, newUser, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\nnewUserId: \(String(describing: newUser.userId))\ntoContact: \(String(describing: toContact))") case let .groupUserChanged(u, fromGroup, newUser, toGroup): return withUser(u, "fromGroup: \(String(describing: fromGroup))\nnewUserId: \(String(describing: newUser.userId))\ntoGroup: \(String(describing: toGroup))") @@ -960,6 +978,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole) case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool) case groupUpdated(user: UserRef, toGroup: GroupInfo) + case contactDomainVerified(user: UserRef, contact: Contact, verificationFailure: String?) + case groupDomainVerified(user: UserRef, groupInfo: GroupInfo, verificationFailure: String?) case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink) case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink) case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) @@ -1015,6 +1035,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case .membersRoleUser: "membersRoleUser" case .membersBlockedForAllUser: "membersBlockedForAllUser" case .groupUpdated: "groupUpdated" + case .contactDomainVerified: "contactDomainVerified" + case .groupDomainVerified: "groupDomainVerified" case .groupLinkCreated: "groupLinkCreated" case .groupLink: "groupLink" case .groupLinkDeleted: "groupLinkDeleted" @@ -1066,6 +1088,8 @@ enum ChatResponse2: Decodable, ChatAPIResult { case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)") case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)") case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) + case let .contactDomainVerified(u, contact, verificationFailure): return withUser(u, "contact: \(contact)\nverificationFailure: \(verificationFailure ?? "ok")") + case let .groupDomainVerified(u, groupInfo, verificationFailure): return withUser(u, "groupInfo: \(groupInfo)\nverificationFailure: \(verificationFailure ?? "ok")") case let .groupLinkCreated(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)") case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) @@ -1368,6 +1392,20 @@ enum OwnerVerification: Decodable, Hashable { case failed(reason: String) } +struct ConnectionPlanResult { + var connLink: CreatedConnLink + var planSimplexName: SimplexNameInfo? + var otherSimplexName: SimplexNameInfo? + var connectionPlan: ConnectionPlan +} + +// APIConnectPlan resolution scope; .never is local-store-only (no network), used for per-keystroke name search +enum PlanResolveMode: String { + case allGroups + case unknown + case never +} + enum ConnectionPlan: Decodable, Hashable { case invitationLink(invitationLinkPlan: InvitationLinkPlan) case contactAddress(contactAddressPlan: ContactAddressPlan) @@ -1766,14 +1804,24 @@ struct ServerOperator: Identifiable, Equatable, Codable { serverDomains: ["simplex.im"], conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), enabled: true, - smpRoles: ServerRoles(storage: true, proxy: true), - xftpRoles: ServerRoles(storage: true, proxy: true) + smpRoles: ServerRoles(storage: true, proxy: true, names: true), + xftpRoles: ServerRoles(storage: true, proxy: true, names: false) ) } struct ServerRoles: Equatable, Codable { var storage: Bool var proxy: Bool + var names: Bool + + // roles applied when a server matches no operator, mirrors core resolveServerRoles (Operators.hs) + static let noOperatorDefault = ServerRoles(storage: true, proxy: true, names: false) +} + +struct ServerRolesOverride: Equatable, Codable, Hashable { + var storage: Bool? + var proxy: Bool? + var names: Bool? } struct UserOperatorServers: Identifiable, Equatable, Codable { @@ -1800,8 +1848,8 @@ struct UserOperatorServers: Identifiable, Equatable, Codable { serverDomains: [], conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false), enabled: false, - smpRoles: ServerRoles(storage: true, proxy: true), - xftpRoles: ServerRoles(storage: true, proxy: true) + smpRoles: ServerRoles.noOperatorDefault, + xftpRoles: ServerRoles.noOperatorDefault ) } set { `operator` = newValue } @@ -1824,6 +1872,7 @@ struct UserOperatorServers: Identifiable, Equatable, Codable { public enum UserServersWarning: Decodable { case noChatRelays(user: UserRef?) + case noNamesServers(user: UserRef?) } enum UserServersError: Decodable { @@ -1922,11 +1971,12 @@ struct UserServer: Identifiable, Equatable, Codable, Hashable { var tested: Bool? var enabled: Bool var deleted: Bool + var roles: ServerRolesOverride = ServerRolesOverride() var createdAt = Date() static func == (l: UserServer, r: UserServer) -> Bool { l.serverId == r.serverId && l.server == r.server && l.preset == r.preset && l.tested == r.tested && - l.enabled == r.enabled && l.deleted == r.deleted + l.enabled == r.enabled && l.deleted == r.deleted && l.roles == r.roles } var id: String { "\(server) \(createdAt)" } @@ -1986,6 +2036,7 @@ struct UserServer: Identifiable, Equatable, Codable, Hashable { case tested case enabled case deleted + case roles } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index a1d28b8e22..e3a6ae30b9 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -429,6 +429,7 @@ final class ChatModel: ObservableObject { // audio recording and playback @Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source @Published var draft: ComposeState? + // chat id with chat scope, see draftChatId() - group chat and its support chats have the same chat id @Published var draftChatId: String? @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) // usage conditions @@ -812,32 +813,63 @@ final class ChatModel: ObservableObject { } func removeMemberItems(_ removedMember: GroupMember, byMember: GroupMember, _ groupInfo: GroupInfo) { + // Mirrors backend groupFeatureMemberAllowed: fullDelete may be role-gated in business groups. + let fullDeletePref = groupInfo.fullGroupPreferences.fullDelete + let fullDelete = fullDeletePref.on + && byMember.memberRole >= (fullDeletePref.role ?? .observer) if chatId == groupInfo.id { - for i in 0..= 0 { + let item = im.reversedChatItems[i] + if isRemovedMemberItem(item) { + if item.isRcvNew { + unreadCollector.changeUnreadCounter(groupInfo.id, by: -1, unreadMentions: item.meta.userMention ? -1 : 0) + } + if item.isActiveReport { + decreaseGroupReportsCounter(groupInfo.id) + } + VoiceItemState.stopVoiceInChatView(cInfo, item) + removed.append((item.id, i, item.isRcvNew)) + im.reversedChatItems.remove(at: i) + } + i -= 1 + } + if !removed.isEmpty { + im.chatState.itemsRemoved(removed.reversed(), im.reversedChatItems.reversed()) + } + } else { + for i in 0.. 0 { + let preview = chat.chatItems[0] + if isRemovedMemberItem(preview) { + if fullDelete { + chat.chatItems = [ChatItem.deletedItemDummy()] + } else if let updatedItem = markedUpdatedItem(preview) { + chat.chatItems = [updatedItem] } } - } else if let chat = getChat(groupInfo.id), - chat.chatItems.count > 0, - let updatedItem = removedUpdatedItem(chat.chatItems[0]) { - chat.chatItems = [updatedItem] } - func removedUpdatedItem(_ item: ChatItem) -> ChatItem? { - let newContent: CIContent - if case .groupSnd = item.chatDir, removedMember.groupMemberId == groupInfo.membership.groupMemberId { - newContent = .sndModerated - } else if case let .groupRcv(groupMember) = item.chatDir, groupMember.groupMemberId == removedMember.groupMemberId { - newContent = .rcvModerated - } else { - return nil + func isRemovedMemberItem(_ item: ChatItem) -> Bool { + switch item.chatDir { + case .groupSnd: return removedMember.groupMemberId == groupInfo.membership.groupMemberId + case let .groupRcv(groupMember): return groupMember.groupMemberId == removedMember.groupMemberId + default: return false } + } + + func markedUpdatedItem(_ item: ChatItem) -> ChatItem? { + guard isRemovedMemberItem(item) else { return nil } var updatedItem = item updatedItem.meta.itemDeleted = .moderated(deletedTs: Date.now, byGroupMember: byMember) - if groupInfo.fullGroupPreferences.fullDelete.on { - updatedItem.content = newContent - } if item.isActiveReport { decreaseGroupReportsCounter(groupInfo.id) } @@ -1213,6 +1245,15 @@ final class ChatModel: ObservableObject { chats.insert(chat, at: position) } + func replaceConnReqView(_ id: String, _ withId: ChatId) { + if id == showingInvitation?.pcc.id { + markShowingInvitationUsed() + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(withId) + } + } + } + func dismissConnReqView(_ id: String) { if id == showingInvitation?.pcc.id { markShowingInvitationUsed() diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index c6c6e88d8c..efd28d0ea5 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -212,16 +212,18 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } // Spec: spec/services/notifications.md#requestAuthorization - func requestAuthorization(onDeny denied: (()-> Void)? = nil, onAuthorized authorized: (()-> Void)? = nil) { + func requestAuthorization(onDeny denied: (()-> Void)? = nil, onAuthorized authorized: (()-> Void)? = nil, whenDone: (() -> Void)? = nil) { logger.debug("NtfManager.requestAuthorization") let center = UNUserNotificationCenter.current() center.getNotificationSettings { settings in switch settings.authorizationStatus { case .denied: denied?() + whenDone?() case .authorized: self.granted = true authorized?() + whenDone?() default: center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if let error = error { @@ -230,6 +232,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { self.granted = granted authorized?() } + whenDone?() } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ea2de31569..7a934fc746 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -509,6 +509,12 @@ func apiShareChatMsgContent(shareChatType: ChatType, shareChatId: Int64, toChatT throw r.unexpected } +func apiShareMyAddress(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool) async throws -> MsgContent { + let r: ChatResponse1 = try await chatSendCmd(.apiShareMyAddress(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup)) + if case let .chatMsgContent(_, mc) = r { return mc } + throw r.unexpected +} + func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, sendAsGroup: Bool = false, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? { let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, sendAsGroup: sendAsGroup, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) @@ -542,8 +548,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws { try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds)) } -func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool = false, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { - let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, sendAsGroup: sendAsGroup, live: live, ttl: ttl, composedMessages: composedMessages) +func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool = false, live: Bool = false, ttl: Int? = nil, sign: Bool = false, composedMessages: [ComposedMessage]) async -> [ChatItem]? { + let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, sendAsGroup: sendAsGroup, live: live, ttl: ttl, sign: sign, composedMessages: composedMessages) return await processSendMessageCmd(toChatType: type, cmd: cmd) } @@ -569,7 +575,7 @@ private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async return cItems } if let networkErrorAlert = networkErrorAlert(r) { - AlertManager.shared.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { sendMessageErrorAlert(r.unexpected) } @@ -1003,15 +1009,15 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo return nil } -func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) { +func apiAddContact(incognito: Bool) async -> (CreatedConnLink, PendingContactConnection)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) - if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } - let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } - return (nil, alert) + if case let .result(.invitation(_, connLinkInv, connection)) = r { return (connLinkInv, connection) } + if let r { await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { @@ -1026,94 +1032,128 @@ func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> Pendi if let r { throw r.unexpected } else { return nil } } -func apiConnectPlan(connLink: String, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { +func apiConnectPlan(connLink: String, resolveMode: PlanResolveMode = .unknown, linkOwnerSig: LinkOwnerSig? = nil, inProgress: BoxedValue) async -> ConnectionPlanResult? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectPlan: no current user") - return (nil, nil) + return nil } - let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, linkOwnerSig: linkOwnerSig), inProgress: inProgress) - if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink, resolveMode: resolveMode, linkOwnerSig: linkOwnerSig), inProgress: inProgress) + if case let .result(.connectionPlan(_, connLink, planSimplexName, otherSimplexName, connPlan)) = r { + return ConnectionPlanResult(connLink: connLink, planSimplexName: planSimplexName, otherSimplexName: otherSimplexName, connectionPlan: connPlan) + } + // a .never (typing) search that matches nothing locally is not an error to surface + if case .error(.error(.notResolvedLocally)) = r { return nil } + if let r { await apiConnectResponseAlert(r) } + return nil } func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { - let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink) - if let alert = alert { - AlertManager.shared.showAlert(alert) - return nil - } else { - return r - } -} - -func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnect: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { case let .result(.sentConfirmation(_, connection)): - return ((.invitation, connection), nil) + return (.invitation, connection) case let .result(.sentInvitation(_, connection)): - return ((.contact, connection), nil) + return (.contact, connection) case let .result(.contactAlreadyExists(_, contact)): if let c = m.getContactChat(contact.contactId) { ItemsModel.shared.loadOpenChat(c.id) } - let alert = contactAlreadyExistsAlert(contact) - return (nil, alert) + await contactAlreadyExistsAlert(contact) + return nil default: () } - let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } - return (nil, alert) + if let r { await apiConnectResponseAlert(r) } + return nil } -private func apiConnectResponseAlert(_ r: APIResult) -> Alert { - switch r.unexpected { - case .error(.invalidConnReq): - mkAlert( - title: "Invalid connection link", - message: "Please check that you used the correct link or ask your contact to send you another one." - ) - case .error(.unsupportedConnReq): - mkAlert( - title: "Unsupported connection link", - message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." - ) - case .errorAgent(.SMP(_, .AUTH)): - mkAlert( - title: "Connection error (AUTH)", - message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." - ) - case let .errorAgent(.SMP(_, .BLOCKED(info))): - Alert( - title: Text("Connection blocked"), - message: Text("Connection is blocked by server operator:\n\(info.reason.text)"), - primaryButton: .default(Text("Ok")), - secondaryButton: .default(Text("How it works")) { - DispatchQueue.main.async { - UIApplication.shared.open(contentModerationPostLink) - } - } - ) - case .errorAgent(.SMP(_, .QUOTA)): - mkAlert( - title: "Undelivered messages", - message: "The connection reached the limit of undelivered messages, your contact may be offline." - ) - case let .errorAgent(.INTERNAL(internalErr)): - if internalErr == "SEUniqueID" { - mkAlert( - title: "Already connected?", - message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))." +private func apiConnectResponseAlert(_ r: APIResult) async { + await MainActor.run { + switch r.unexpected { + case .error(.invalidConnReq): + showAlert( + NSLocalizedString("Invalid connection link", comment: ""), + message: NSLocalizedString("Please check that you used the correct link or ask your contact to send you another one.", comment: "") ) - } else { - connectionErrorAlert(r) + case .error(.unsupportedConnReq): + showAlert( + NSLocalizedString("Unsupported connection link", comment: ""), + message: NSLocalizedString("This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.", comment: "") + ) + case let .error(.simplexDomainNotReady(domain, err)): + switch err { + case .noValidLink: + showAlert( + NSLocalizedString("No valid link", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but it has no valid link.", comment: ""), domain.fullDomainName) + ) + case .unknownDomain: + showAlert( + NSLocalizedString("Unconfirmed name", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.", comment: ""), domain.fullDomainName) + ) + } + case .errorAgent(.NO_NAME_SERVERS): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: NSLocalizedString("None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.", comment: "") + ) + case .errorAgent(.SMP(_, .AUTH)): + showAlert( + NSLocalizedString("Connection link removed", comment: ""), + message: NSLocalizedString("Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link.", comment: "") + ) + case let .errorAgent(.SMP(_, .BLOCKED(info))): + showAlert( + NSLocalizedString("Connection blocked", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Connection is blocked by server operator:\n%@", comment: ""), info.reason.text), + actions: {[ + okAlertAction, + UIAlertAction(title: NSLocalizedString("How it works", comment: ""), style: .default) { _ in + DispatchQueue.main.async { + UIApplication.shared.open(contentModerationPostLink) + } + } + ]} + ) + case .errorAgent(.SMP(_, .QUOTA)): + showAlert( + NSLocalizedString("Undelivered messages", comment: ""), + message: NSLocalizedString("The connection reached the limit of undelivered messages, your contact may be offline.", comment: "") + ) + case let .errorAgent(.INTERNAL(internalErr)): + if internalErr == "SEUniqueID" { + showAlert( + NSLocalizedString("Already connected?", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("It seems like you are already connected via this link. If it is not the case, there was an error (%@).", comment: ""), internalErr) + ) + } else { + showAlert(connectionErrorAlert(r)) + } + case let .errorAgent(.SMP(serverAddress, .NAME(nameErr))): + switch nameErr { + case .NOT_FOUND: + showAlert( + NSLocalizedString("Name not found", comment: ""), + message: NSLocalizedString("This SimpleX name is not registered. Please check the name.", comment: "") + ) + case .NO_RESOLVER: + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server %@ does not support name resolution. Configure servers, or use a connection link.", comment: ""), serverAddress) + ) + case let .RESOLVER(resolverErr): + showAlert( + NSLocalizedString("SimpleX name error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Resolver error: %@", comment: ""), resolverErr) + ) + } + default: showAlert(connectionErrorAlert(r)) } - default: connectionErrorAlert(r) } } @@ -1124,48 +1164,46 @@ func connErrorText(_ e: ChatError) -> String { case .error(.unsupportedConnReq): NSLocalizedString("Unsupported connection link", comment: "conn error description") case .errorAgent(.SMP(_, .AUTH)): - NSLocalizedString("Connection error (AUTH)", comment: "conn error description") + NSLocalizedString("Connection link removed", comment: "conn error description") case let .errorAgent(.SMP(_, .BLOCKED(info))): - NSLocalizedString("Connection blocked: \(info.reason.text)", comment: "conn error description") + String.localizedStringWithFormat(NSLocalizedString("Connection blocked: %@", comment: "conn error description"), info.reason.text) case .errorAgent(.SMP(_, .QUOTA)): NSLocalizedString("The connection reached the limit of undelivered messages", comment: "conn error description") default: if getNetworkErrorAlert(e) != nil { NSLocalizedString("Network error", comment: "conn error description") } else { - "\(NSLocalizedString("Error", comment: "conn error description")): \(responseError(e))" + String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "conn error description"), responseError(e)) } } } -func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { - mkAlert( - title: "Contact already exists", - message: "You are already connected to \(contact.displayName)." - ) -} - -private func connectionErrorAlert(_ r: APIResult) -> Alert { - if let networkErrorAlert = networkErrorAlert(r) { - return networkErrorAlert - } else { - return mkAlert( - title: "Connection error", - message: "Error: \(responseError(r.unexpected))" +func contactAlreadyExistsAlert(_ contact: Contact) async { + await MainActor.run { + showAlert( + NSLocalizedString("Contact already exists", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("You are already connected to %@.", comment: ""), contact.displayName) ) } } -func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> ChatData { +private func connectionErrorAlert(_ r: APIResult) -> (title: String, message: String?) { + networkErrorAlert(r) ?? ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) +} + +func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareContact") - let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData)) + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain)) if case let .newPreparedChat(_, chat) = r { return chat } throw r.unexpected } -func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData) async throws -> ChatData { +func apiPrepareGroup(connLink: CreatedConnLink, directLink: Bool, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = nil) async throws -> ChatData { let userId = try currentUserId("apiPrepareGroup") - let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData)) + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, directLink: directLink, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain)) if case let .newPreparedChat(_, chat) = r { return chat } throw r.unexpected } @@ -1185,30 +1223,29 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws - func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> (GroupInfo, [RelayConnectionResult])? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToGroup(_, groupInfo, relayResults)) = r { return (groupInfo, relayResults) } - if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } + if let r { await apiConnectResponseAlert(r) } return nil } -func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { +func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> Contact? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectContactViaAddress: no current user") - return (nil, nil) + return nil } let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) - if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } + if case let .result(.sentInvitationToContact(_, contact, _)) = r { return contact } if let r { logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") - return (nil, connectionErrorAlert(r)) - } else { - return (nil, nil) + await MainActor.run { showAlert(connectionErrorAlert(r)) } } + return nil } func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { @@ -1329,6 +1366,43 @@ func apiSetProfileAddress(on: Bool) async throws -> User? { } } +func showSetSimplexNameError(_ r: APIResult, isChannel: Bool) async { + if case let .error(.simplexDomainNotReady(domain, .noValidLink)) = r.unexpected { + let format = isChannel + ? NSLocalizedString("The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page.", comment: "alert message") + : NSLocalizedString("The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page.", comment: "alert message") + await MainActor.run { + showAlert(NSLocalizedString("Error saving name", comment: "alert title"), message: String.localizedStringWithFormat(format, domain.fullDomainName)) + } + } else { + await apiConnectResponseAlert(r) + } +} + +func apiSetUserDomain(_ simplexDomain: String?) async throws -> User { + let userId = try currentUserId("apiSetUserDomain") + let r: APIResult = await chatApiSendCmd(.apiSetUserDomain(userId: userId, simplexDomain: simplexDomain)) + switch r { + case let .result(.userProfileUpdated(user, _, _, _)): return user + case let .result(.userProfileNoChange(user)): return user + default: + await showSetSimplexNameError(r, isChannel: false) + throw r.unexpected + } +} + +func apiVerifyContactDomain(_ contactId: Int64) async throws -> (Contact, String?) { + let r: ChatResponse2 = try await chatSendCmd(.apiVerifyContactDomain(contactId: contactId)) + if case let .contactDomainVerified(_, contact, verificationFailure) = r { return (contact, verificationFailure) } + throw r.unexpected +} + +func apiVerifyGroupDomain(_ groupId: Int64) async throws -> (GroupInfo, String?) { + let r: ChatResponse2 = try await chatSendCmd(.apiVerifyGroupDomain(groupId: groupId)) + if case let .groupDomainVerified(_, groupInfo, verificationFailure) = r { return (groupInfo, verificationFailure) } + throw r.unexpected +} + func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? { let r: ChatResponse1 = try await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences)) if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact } @@ -1429,23 +1503,22 @@ func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> User func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) - let am = AlertManager.shared if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } if case .error(.errorAgent(.SMP(_, .AUTH))) = r { - am.showAlertMsg( - title: "Connection error (AUTH)", - message: "Sender may have deleted the connection request." - ) + await MainActor.run { showAlert( + NSLocalizedString("Connection link removed", comment: ""), + message: NSLocalizedString("The sender deleted the connection request.", comment: "") + ) } } else if let r { if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) + await MainActor.run { showAlert(networkErrorAlert) } } else { logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r.unexpected))" - ) + await MainActor.run { showAlert( + NSLocalizedString("Error accepting contact request", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(r.unexpected)) + ) } } } return nil @@ -1689,11 +1762,11 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { try await sendCommandOkResp(.deleteRemoteCtrl(remoteCtrlId: rcId)) } -func networkErrorAlert(_ res: APIResult) -> Alert? { - if case let .error(e) = res, let alert = getNetworkErrorAlert(e) { - return mkAlert(title: alert.title, message: alert.message) +func networkErrorAlert(_ res: APIResult) -> (title: String, message: String?)? { + if case let .error(e) = res { + getNetworkErrorAlert(e) } else { - return nil + nil } } @@ -1995,6 +2068,13 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r.unexpected } +func apiSetPublicGroupAccess(_ groupId: Int64, access: PublicGroupAccess) async throws -> GroupInfo { + let r: APIResult = await chatApiSendCmd(.apiSetPublicGroupAccess(groupId: groupId, access: access)) + if case let .result(.groupUpdated(_, toGroup)) = r { return toGroup } + await showSetSimplexNameError(r, isChannel: true) + throw r.unexpected +} + func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? { let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } @@ -2049,7 +2129,7 @@ func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async 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)) } + if let r { await apiConnectResponseAlert(r) } return nil } @@ -2359,7 +2439,7 @@ func processReceivedMsg(_ res: ChatEvent) async { await MainActor.run { m.updateContact(contact) if let conn = contact.activeConn { - m.dismissConnReqView(conn.id) + m.replaceConnReqView(conn.id, contact.id) m.removeChat(conn.id) } if contact.id == m.chatId, let conn = contact.activeConn { @@ -2376,7 +2456,7 @@ func processReceivedMsg(_ res: ChatEvent) async { await MainActor.run { m.updateContact(contact) if let conn = contact.activeConn { - m.dismissConnReqView(conn.id) + m.replaceConnReqView(conn.id, contact.id) m.removeChat(conn.id) } } @@ -2386,7 +2466,7 @@ func processReceivedMsg(_ res: ChatEvent) async { await MainActor.run { m.updateContact(contact) if let conn = contact.activeConn { - m.dismissConnReqView(conn.id) + m.replaceConnReqView(conn.id, contact.id) m.removeChat(conn.id) } } @@ -2536,7 +2616,7 @@ func processReceivedMsg(_ res: ChatEvent) async { await MainActor.run { m.updateGroup(groupInfo) if let conn = hostContact?.activeConn { - m.dismissConnReqView(conn.id) + m.replaceConnReqView(conn.id, groupInfo.id) m.removeChat(conn.id) } } @@ -2546,7 +2626,7 @@ func processReceivedMsg(_ res: ChatEvent) async { m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, hostMember) if let hostConn = hostMember.activeConn { - m.dismissConnReqView(hostConn.id) + m.replaceConnReqView(hostConn.id, groupInfo.id) m.removeChat(hostConn.id) } } diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift index 1f98b23a1d..254d1707da 100644 --- a/apps/ios/Shared/Theme/Theme.swift +++ b/apps/ios/Shared/Theme/Theme.swift @@ -192,7 +192,7 @@ extension ThemeModeOverride { background: colors.background != tc.background ? colors.background : nil, surface: colors.surface != tc.surface ? colors.surface : nil, title: colors.title != tc.title ? colors.title : nil, - primaryVariant2: colors.primaryVariant2 != tc.primaryVariant2 ? colors.primary : nil, + primaryVariant2: colors.primaryVariant2 != tc.primaryVariant2 ? colors.primaryVariant2 : nil, sentMessage: colors.sentMessage != tc.sentMessage ? colors.sentMessage : nil, sentQuote: colors.sentQuote != tc.sentQuote ? colors.sentQuote : nil, receivedMessage: colors.receivedMessage != tc.receivedMessage ? colors.receivedMessage : nil, diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 00c8d7070b..f825dbeca7 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -131,6 +131,15 @@ public func subscriberCountStr(_ count: Int64) -> String { : String.localizedStringWithFormat(NSLocalizedString("%d subscribers", comment: "channel subscriber count"), count) } +public func ownersContributorsCountStr(_ count: Int, withContributors: Bool) -> String { + if withContributors { + return String.localizedStringWithFormat(NSLocalizedString("%d owners & contributors", comment: "channel members count"), count) + } + return count == 1 + ? String.localizedStringWithFormat(NSLocalizedString("%d owner", comment: "channel owners count"), count) + : String.localizedStringWithFormat(NSLocalizedString("%d owners", comment: "channel owners count"), count) +} + struct ChatInfoToolbar_Previews: PreviewProvider { static var previews: some View { ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index b21def7944..3d1e7ef477 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -112,7 +112,6 @@ 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 @@ -283,8 +282,9 @@ struct ChatInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -334,7 +334,7 @@ struct ChatInfoView: View { case .syncConnectionForceAlert: return syncConnectionForceAlert({ Task { - if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: true) { connectionStats = stats dismiss() } @@ -392,13 +392,8 @@ struct ChatInfoView: View { .lineLimit(3) .padding(.bottom, 2) } - if let descr = cInfo.shortDescr?.trimmingCharacters(in: .whitespacesAndNewlines), descr != "" { - 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) - } + contactSimplexNameView(contact) { contact = $0 } + ProfileDescriptionView(shortDescr: cInfo.shortDescr, description: cInfo.profileDescription) } .frame(maxWidth: .infinity, alignment: .center) } @@ -521,7 +516,7 @@ struct ChatInfoView: View { private func synchronizeConnectionButton() -> some View { Button { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats dismiss() } @@ -569,7 +564,7 @@ struct ChatInfoView: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) @@ -598,9 +593,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -616,9 +610,8 @@ struct ChatInfoView: View { } } catch let error { logger.error("abortSwitchContactAddress apiAbortSwitchContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -728,7 +721,7 @@ struct ChatTTLOption: View { } } -func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? { +func syncContactConnection(_ contact: Contact, force: Bool) async -> ConnectionStats? { do { let stats = try apiSyncContactRatchet(contact.apiId, force) await MainActor.run { @@ -737,14 +730,8 @@ func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAler return stats } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - showAlert( - SomeAlert( - alert: mkAlert(title: a.title, message: a.message), - id: "syncContactConnection error" - ) - ) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } return nil } @@ -824,7 +811,7 @@ private struct CallButton: View { message: Text("Connection requires encryption renegotiation."), primaryButton: .default(Text("Fix")) { Task { - if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) { + if let stats = await syncContactConnection(contact, force: false) { connectionStats = stats } } @@ -1177,6 +1164,7 @@ private func deleteContactOrConversationDialog( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Delete contact?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Only delete conversation")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert) @@ -1323,6 +1311,7 @@ private func deleteContactWithoutConversation( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Confirm contact deletion?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Delete and notify contact")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert) @@ -1347,6 +1336,7 @@ private func deleteNotReadyContact( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Confirm contact deletion?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Confirm")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) @@ -1358,6 +1348,150 @@ private func deleteNotReadyContact( )) } +@ViewBuilder func contactSimplexNameView(_ contact: Contact, verifiable: Bool = true, onUpdate: ((Contact) -> Void)? = nil) -> some View { + if let domain = contact.profile.contactDomain, + contact.profile.contactDomainVerified != nil || domain.proof != nil { + SimplexNameView( + simplexName: "@\(domain.domain)", + verified: contact.profile.contactDomainVerified, + verify: { + do { + let (ct, reason) = try await apiVerifyContactDomain(contact.contactId) + await MainActor.run { + ChatModel.shared.updateContact(ct) + onUpdate?(ct) + } + return (ct.profile.contactDomainVerified, reason) + } catch { + logger.error("apiVerifyContactDomain: \(responseError(error))") + return nil + } + }, + verifiable: verifiable + ) + } +} + +@ViewBuilder func groupSimplexNameView(_ groupInfo: GroupInfo, verifiable: Bool = true, onUpdate: ((GroupInfo) -> Void)? = nil) -> some View { + if groupInfo.businessChat == nil { + if let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess, + let domain = access.groupDomainClaim?.shortName, + groupInfo.groupDomainVerified != nil || access.groupDomainClaim?.proof != nil { + SimplexNameView( + simplexName: "#\(domain)", + verified: groupInfo.groupDomainVerified, + verify: { + do { + let (gInfo, reason) = try await apiVerifyGroupDomain(groupInfo.groupId) + await MainActor.run { + ChatModel.shared.updateGroup(gInfo) + onUpdate?(gInfo) + } + return (gInfo.groupDomainVerified, reason) + } catch { + logger.error("apiVerifyGroupDomain: \(responseError(error))") + return nil + } + }, + verifiable: verifiable + ) + } + } else if let claim = groupInfo.businessChat?.businessDomain, + groupInfo.groupDomainVerified != nil || claim.proof != nil { + // A business presents as a contact, so the name retains its .simplex suffix; it cannot be re-verified. + SimplexNameView( + simplexName: "@\(claim.domain)", + verified: groupInfo.groupDomainVerified, + verify: { nil }, + verifiable: false + ) + } +} + +struct SimplexNameView: View { + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) var autoVerify = false + let simplexName: String + let verified: Bool? + let verify: () async -> (Bool?, String?)? + var verifiable: Bool = true + @State private var inFlight = false + @State private var showSpinner = false + + var body: some View { + content + .padding(.bottom, 2) + .onAppear { if verifiable && autoVerify && verified == nil { runVerify(manual: false) } } + } + + private var nameText: Text { + Text(simplexName) + .font(.subheadline) + .foregroundColor(verified == true ? theme.colors.primary : theme.colors.secondary) + } + + // Size the inline check/cross to the name's cap height so it reads like a capital letter, not an oversized glyph. + private var iconFont: Font { .system(size: UIFont.preferredFont(forTextStyle: .subheadline).capHeight) } + + @ViewBuilder private var content: some View { + if showSpinner { + HStack(spacing: 6) { + nameText + ProgressView() + } + } else if verified == true { + HStack(alignment: .firstTextBaseline, spacing: 4) { + nameText + Image(systemName: "checkmark").font(iconFont).foregroundColor(theme.colors.primary) + .alignmentGuide(.firstTextBaseline) { $0[.bottom] - $0.height * 0.15 } + } + .contentShape(Rectangle()) + .onTapGesture { + UIPasteboard.general.string = simplexName + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + } + } else if !verifiable { + nameText + } else if verified == false { + HStack(alignment: .firstTextBaseline, spacing: 4) { + nameText + Image(systemName: "xmark").font(iconFont).foregroundColor(.red) + .alignmentGuide(.firstTextBaseline) { $0[.bottom] - $0.height * 0.15 } + } + .contentShape(Rectangle()) + .onTapGesture { runVerify(manual: true) } + } else { + HStack(spacing: 6) { + nameText + Button { runVerify(manual: true) } label: { + Text("Verify name").font(.subheadline).foregroundColor(theme.colors.primary) + } + } + } + } + + private func runVerify(manual: Bool) { + if inFlight { return } + inFlight = true + // delay the spinner so a fast result on appear doesn't flash it + Task { + try? await Task.sleep(nanoseconds: 300_000000) + await MainActor.run { if inFlight { showSpinner = true } } + } + Task { + let res = await verify() + await MainActor.run { + inFlight = false + showSpinner = false + // show the reason on a manual run, or on an inconclusive auto run (state stayed nil) + if let (newV, reason) = res, let reason, manual || newV == nil { + showAlert(NSLocalizedString("SimpleX name not verified", comment: "alert title"), message: reason) + } + } + } + } +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 75a5baafee..fc46669cee 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -14,8 +14,13 @@ import SimpleXChat struct CIFileView: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme + @Environment(\.showTimestamp) var showTimestamp: Bool + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + @AppStorage(DEFAULT_PRIVACY_SHOW_SIGNATURE) private var showSignature = true + @AppStorage(DEFAULT_PRIVACY_SHOW_FILE_ENCRYPTION) private var showFileEncryption = true + @ObservedObject var chat: Chat let file: CIFile? - let edited: Bool + let meta: CIMeta let senderProfile: LocalProfile? var smallViewSize: CGFloat? @@ -24,9 +29,9 @@ struct CIFileView: View { fileIndicator() .simultaneousGesture(TapGesture().onEnded(fileAction)) } else { - let metaReserve = edited - ? " " - : " " + // reserve exact space for the overlaid meta (timestamp + all icons), rendered transparently - matches MsgContentView + let encrypted: Bool? = if let fileSource = file?.fileSource { fileSource.cryptoArgs != nil } else { nil } + let metaReserve = Text(verbatim: " ") + ciMetaText(meta, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: encrypted, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp, signedFileVerified: file?.loaded, showSignature: showSignature, showFileEncryption: showFileEncryption) HStack(alignment: .bottom, spacing: 6) { fileIndicator() .padding(.top, 5) @@ -38,14 +43,14 @@ struct CIFileView: View { .lineLimit(1) .multilineTextAlignment(.leading) .foregroundColor(theme.colors.onBackground) - Text(prettyFileSize + metaReserve) + (Text(prettyFileSize) + metaReserve) .font(.caption) .lineLimit(1) .multilineTextAlignment(.leading) .foregroundColor(theme.colors.secondary) } } else { - Text(metaReserve) + metaReserve.font(.caption) } } .padding(.top, 4) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index e3bc654ac9..2984c3a286 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -23,6 +23,8 @@ struct CIMetaView: View { var invertedMaterial = false @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + @AppStorage(DEFAULT_PRIVACY_SHOW_SIGNATURE) private var showSignature = true + @AppStorage(DEFAULT_PRIVACY_SHOW_FILE_ENCRYPTION) private var showFileEncryption = true var body: some View { if chatItem.isDeletedContent { @@ -41,7 +43,10 @@ struct CIMetaView: View { showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, - showTimesamp: showTimestamp + showTimesamp: showTimestamp, + signedFileVerified: chatItem.file?.loaded, + showSignature: showSignature, + showFileEncryption: showFileEncryption ).invertedForegroundStyle(enabled: invertedMaterial) if invertedMaterial { ciMetaText( @@ -53,7 +58,10 @@ struct CIMetaView: View { showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy, - showTimesamp: showTimestamp + showTimesamp: showTimestamp, + signedFileVerified: chatItem.file?.loaded, + showSignature: showSignature, + showFileEncryption: showFileEncryption ) } } @@ -102,7 +110,10 @@ func ciMetaText( showStatus: Bool = true, showEdited: Bool = true, showViaProxy: Bool, - showTimesamp: Bool + showTimesamp: Bool, + signedFileVerified: Bool? = nil, + showSignature: Bool = true, + showFileEncryption: Bool = true ) -> Text { var r = Text("") var space: Text? = nil @@ -142,11 +153,20 @@ func ciMetaText( } space = textSpace } - if let enc = encrypted { + if let enc = encrypted, showFileEncryption { appendSpace() r = r + statusIconText(enc ? "lock" : "lock.open", resolved) space = textSpace } + if showSignature, meta.msgVerified?.verified == true && signedFileVerified != false { + appendSpace() + r = r + colored(Text(Image(systemName: "checkmark.seal")), resolved) + space = textSpace + } else if meta.msgVerified == .sigMissing { + appendSpace() + r = r + colored(Text(Image(systemName: "xmark.seal")), colorMode.resolve(.red)) + space = textSpace + } if showTimesamp { appendSpace() r = r + colored(meta.timestampText, resolved) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index ec23dc15a4..bdd38cb4df 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -185,9 +185,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } @@ -202,9 +201,8 @@ struct CIRcvDecryptionError: View { } } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 372c7df8a3..44284350dc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -99,7 +99,7 @@ struct FramedItemView: View { .background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) } .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } - if let (title, text) = chatItem.meta.itemStatus.statusInfo { + if let (title, text) = chatItem.meta.itemStatus.statusInfo ?? chatItem.meta.msgVerified?.sigMissingInfo { v.simultaneousGesture(TapGesture().onEnded { AlertManager.shared.showAlert( Alert( @@ -349,7 +349,7 @@ struct FramedItemView: View { } @ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View { - CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited, senderProfile: ciSenderProfile(chatItem, chat.chatInfo)) + CIFileView(chat: chat, file: chatItem.file, meta: chatItem.meta, senderProfile: ciSenderProfile(chatItem, chat.chatInfo)) .overlay(DetermineWidth()) if text != "" || ci.meta.isLive { ciMsgContentView (chatItem) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 11c3c4c3f4..c9d8858fa1 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -48,6 +48,7 @@ struct MsgContentView: View { @State private var phase: CGFloat = 0 @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + @AppStorage(DEFAULT_PRIVACY_SHOW_SIGNATURE) private var showSignature = true var body: some View { let v = msgContentView() @@ -131,7 +132,7 @@ struct MsgContentView: View { @inline(__always) private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? textNewLine : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) + (rightToLeft ? textNewLine : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp, showSignature: showSignature) } } @@ -140,11 +141,12 @@ func msgTextResultView( _ t: Text, showSecrets: Binding>? = nil, sendCommand: ((String) -> Void)? = nil, + openModal: ((Format) -> Void)? = 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, sendCommand: sendCommand, centered: centered, smallFont: smallFont)) } + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, openModal: openModal, 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 @@ -153,6 +155,7 @@ private func handleTextTaps( _ s: NSAttributedString, showSecrets: Binding>? = nil, sendCommand: ((String) -> Void)? = nil, + openModal: ((Format) -> Void)? = nil, centered: Bool, smallFont: Bool ) -> some View { @@ -214,8 +217,8 @@ private func handleTextTaps( var simplex: Bool = false s.enumerateAttributes(in: NSRange(location: 0, length: s.length)) { attrs, range, stop in if index >= range.location && index < range.location + range.length { - if let nameInfo = attrs[nameAttrKey] as? SimplexNameInfo { - showUnsupportedNameAlert(nameInfo) + if attrs[nameAttrKey] is SimplexNameInfo { + planAndConnect(s.attributedSubstring(from: range).string, theme: AppTheme.shared, dismiss: false) } else if let url = attrs[linkAttrKey] as? String { linkURL = url browser = attrs[webLinkAttrKey] != nil @@ -228,6 +231,8 @@ private func handleTextTaps( } } else if let sendCommand, let cmd = attrs[commandAttrKey] as? String { sendCommand(cmd) + } else if let openModal, let fmt = attrs[modalAttrKey] as? Format { + openModal(fmt) } stop.pointee = true } @@ -263,9 +268,65 @@ private let secretAttrKey = NSAttributedString.Key("chat.simplex.app.secret") private let commandAttrKey = NSAttributedString.Key("chat.simplex.app.command") private let nameAttrKey = NSAttributedString.Key("chat.simplex.app.name") +private let modalAttrKey = NSAttributedString.Key("chat.simplex.app.modal") typealias MsgTextResult = (string: NSMutableAttributedString, hasSecrets: Bool, handleTaps: Bool) +// Reusable profile bio/description header: renders the teaser and opens the full +// description in a sheet when the "Read more" (Format.modal) link is tapped. +struct ProfileDescriptionView: View { + @EnvironmentObject var theme: AppTheme + let shortDescr: String? + let description: String? + @State private var showSecrets: Set = [] + @State private var modal: ModalText? = nil + + var body: some View { + if let r = markdownProfileDescription(shortDescr: shortDescr, description: description, showSecrets: showSecrets, backgroundColor: theme.colors.background) { + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets, openModal: openModal, centered: true, smallFont: true) + .multilineTextAlignment(.center) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + .appSheet(item: $modal) { m in + FullProfileDescriptionView(description: m.text).environmentObject(theme) + } + } + } + + private func openModal(_ format: Format) { + if case let .modal(_, text) = format { modal = ModalText(text: text) } + } +} + +private struct ModalText: Identifiable { + let id = UUID() + let text: String +} + +private struct FullProfileDescriptionView: View { + @EnvironmentObject var theme: AppTheme + let description: String + @State private var showSecrets: Set = [] + + var body: some View { + List { + Text("Description") + .font(.title) + .bold() + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .listRowBackground(Color.clear) + + Section { + let r = markdownText(description, showSecrets: showSecrets, backgroundColor: theme.colors.background) + msgTextResultView(r, Text(AttributedString(r.string)), showSecrets: $showSecrets) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + } + .modifier(ThemedBackground(grouped: true)) + } +} + @inline(__always) func markdownText( _ s: String, @@ -291,6 +352,46 @@ func markdownText( ) } +// Renders a profile bio/description: the bio, a short single-line description, or a +// truncated teaser followed by a "Read more" link that opens the full description (Format.modal). +func markdownProfileDescription( + shortDescr: String?, + description: String?, + showSecrets: Set? = nil, + backgroundColor: Color +) -> MsgTextResult? { + func trimmed(_ s: String?) -> String? { + guard let t = s?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty else { return nil } + return t + } + let short = trimmed(shortDescr) + let descr = trimmed(description) + guard let descr else { + return short.map { markdownText($0, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: backgroundColor) } + } + let firstLine = String(descr.prefix(while: { $0 != "\n" })) + let truncated = firstLine.count > 100 + let multiline = descr.count > firstLine.count + if short == nil && !truncated && !multiline { + return markdownText(descr, textStyle: .subheadline, showSecrets: showSecrets, backgroundColor: backgroundColor) + } + let teaser = short ?? (truncated ? String(firstLine.prefix(100)).trimmingCharacters(in: .whitespaces) + "…" : firstLine + "…") + let readMore = NSLocalizedString("Read more", comment: "profile description teaser") + var formatted = parseSimpleXMarkdown(teaser) ?? [FormattedText(text: teaser)] + formatted.append(FormattedText(text: " ")) + formatted.append(FormattedText(text: readMore, format: .modal(modalName: Format.modalDescription, text: descr))) + return messageText( + "\(teaser) \(readMore)", + formatted, + textStyle: .subheadline, + sender: nil, + mentions: nil, + userMemberId: nil, + showSecrets: showSecrets, + backgroundColor: UIColor(backgroundColor) + ) +} + func messageText( _ text: String, @@ -455,6 +556,12 @@ func messageText( attrs[linkAttrKey] = "tel:" + t.replacingOccurrences(of: " ", with: "") handleTaps = true } + case let .modal(modalName, text): + attrs = linkAttrs() + if !preview { + attrs[modalAttrKey] = Format.modal(modalName: modalName, text: text) + handleTaps = true + } case .unknown: () case .none: () } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index bd0e549d38..2213b34586 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -162,7 +162,28 @@ struct ChatItemInfoView: View { if let deleteAt = meta.itemTimed?.deleteAt { infoRow("Disappears at", localTimestamp(deleteAt)) } + if meta.msgVerified?.verified == true { + let signedText: LocalizedStringKey = ci.chatDir.sent ? "Signed" : "Signed & verified" + HStack { + Label { + Text(signedText) + } icon: { + Text(Image(systemName: "checkmark.seal")).foregroundColor(.secondary) + } + Spacer() + } + } else if meta.msgVerified == .sigMissing { + HStack { + Label { + Text("Signature missing") + } icon: { + Text(Image(systemName: "xmark.seal")).foregroundColor(.red) + } + Spacer() + } + } if developerTools { + Divider().padding(.vertical) infoRow("Database ID", "\(meta.itemId)") infoRow("Record updated at", localTimestamp(meta.updatedAt)) let msv = infoRow("Message status", ci.meta.itemStatus.id) @@ -195,6 +216,9 @@ struct ChatItemInfoView: View { } } } + if ci.file != nil, let servers = chatItemInfo?.fileXftpServers, !servers.isEmpty { + infoRow("File servers", servers.map(serverHostname).joined(separator: "\n")) + } } } @@ -507,6 +531,13 @@ struct ChatItemInfoView: View { if let deleteAt = meta.itemTimed?.deleteAt { shareText += [String.localizedStringWithFormat(NSLocalizedString("Disappears at: %@", comment: "copied message info"), localTimestamp(deleteAt))] } + if meta.msgVerified?.verified == true { + shareText += [ci.chatDir.sent + ? NSLocalizedString("Signed", comment: "copied message info") + : NSLocalizedString("Signed & verified", comment: "copied message info")] + } else if meta.msgVerified == .sigMissing { + shareText += [NSLocalizedString("Signature missing", comment: "copied message info")] + } if developerTools { shareText += [ String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId), @@ -517,6 +548,9 @@ struct ChatItemInfoView: View { shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)] } } + if ci.file != nil, let servers = chatItemInfo?.fileXftpServers, !servers.isEmpty { + shareText += [String.localizedStringWithFormat(NSLocalizedString("File servers: %@", comment: "copied message info"), servers.map(serverHostname).joined(separator: ", "))] + } if let qi = ci.quotedItem { shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] let t = qi.text diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index efe26fdf89..bc55fc6174 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -14,6 +14,29 @@ import Combine private let memberImageSize: CGFloat = 34 +private func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { + let oldIsGroupRcv = switch older?.chatDir { + case .groupRcv: true + case .channelRcv: true + default: false + } + let sameMember = switch (older?.chatDir, current.chatDir) { + case (.groupRcv(let oldMember), .groupRcv(let member)): + oldMember.memberId == member.memberId + case (.channelRcv, .channelRcv): + true + default: + false + } + if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { + return true + } else { + return false + } +} + // Spec: spec/client/chat-view.md#ChatView struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @@ -757,7 +780,7 @@ struct ChatView: View { } updateAvailableContent() } - if chatModel.draftChatId == cInfo.id && !composeState.forwarding, + if chatModel.draftChatId == draftChatId(cInfo.id, cInfo.groupChatScope()) && !composeState.forwarding, let draft = chatModel.draft { composeState = draft } @@ -895,8 +918,15 @@ struct ChatView: View { } } else { let voiceNoFrame = voiceWithoutFrame(ci) + let channelReceived = !ci.chatDir.sent && cInfo.isChannel + // consecutive (no-avatar) received messages in channels drop the avatar-sized + // left padding (see .leading padding below), so they get the full row width here + // too — otherwise the reserved avatar inset would leave a gap on the right + let channelReceivedNoAvatar = channelReceived && !shouldShowAvatar(mergedItem.newest().item, mergedItem.oldest().nextItem) let maxWidth = cInfo.chatType == .group - ? voiceNoFrame + ? channelReceivedNoAvatar + ? g.size.width - 26 + : voiceNoFrame || channelReceived ? (g.size.width - 28) - 42 : (g.size.width - 28) * 0.84 - 42 : voiceNoFrame @@ -975,7 +1005,6 @@ struct ChatView: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_CHAT_ITEM_ROUNDNESS) private var roundness = defaultChatItemRoundness @Binding @ObservedObject var chat: Chat - @State private var showSecrets: Set = [] var body: some View { let v = VStack(spacing: 8) { @@ -998,13 +1027,16 @@ struct ChatView: View { .frame(maxWidth: 260) } - if let shortDescr = chat.chatInfo.shortDescr { - 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) - .padding(.horizontal) + ProfileDescriptionView(shortDescr: chat.chatInfo.shortDescr, description: chat.chatInfo.profileDescription) + .padding(.horizontal) + + switch chat.chatInfo { + case let .direct(contact): + contactSimplexNameView(contact, verifiable: false) + case let .group(groupInfo, _): + groupSimplexNameView(groupInfo, verifiable: false) + default: + EmptyView() } if let chatContext { @@ -1732,29 +1764,6 @@ struct ChatView: View { ) } - func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool { - let oldIsGroupRcv = switch older?.chatDir { - case .groupRcv: true - case .channelRcv: true - default: false - } - let sameMember = switch (older?.chatDir, current.chatDir) { - case (.groupRcv(let oldMember), .groupRcv(let member)): - oldMember.memberId == member.memberId - case (.channelRcv, .channelRcv): - true - default: - false - } - if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { - return true - } else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) { - return true - } else { - return false - } - } - var body: some View { let last = isLastItem ? im.reversedChatItems.last : nil let listItem = merged.newest() @@ -1978,7 +1987,7 @@ struct ChatView: View { } chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.trailing) - .padding(.leading, 10 + memberImageSize + 12) + .padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12) } .padding(.bottom, bottomPadding) } @@ -1998,7 +2007,7 @@ struct ChatView: View { let (name, role) = if ci.meta.showGroupAsSender { (groupInfo.chatViewName, NSLocalizedString("group", comment: "shown on group welcome message")) } else { - (member.chatViewName, member.memberRole.text) + (member.chatViewName, member.memberRole.text(isChannel: groupInfo.isChannel)) } Group { if #available(iOS 16.0, *) { @@ -2075,7 +2084,7 @@ struct ChatView: View { } chatItemWithMenu(ci, range, maxWidth, itemSeparation) .padding(.trailing) - .padding(.leading, 10 + memberImageSize + 12) + .padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12) } .padding(.bottom, bottomPadding) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift index 1ec46816f5..4b9169c72a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift @@ -23,6 +23,7 @@ struct ComposeFileView: View { .foregroundColor(Color(uiColor: .tertiaryLabel)) .padding(.leading, 4) Text(fileName) + .lineLimit(1) Spacer() if cancelEnabled { Button { cancelFile() } label: { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index e308a145b9..54f8597ffc 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -131,7 +131,12 @@ struct ComposeState { } var memberMentions: [String: Int64] { - self.mentions.compactMapValues { $0.memberRef?.groupMemberId } + var result: [String: Int64] = [:] + for ft in parsedMessage { + if result.count >= MAX_NUMBER_OF_MENTIONS { break } + if case let .mention(name) = ft.format, let id = mentions[name]?.memberRef?.groupMemberId { result[name] = id } + } + return result } var editing: Bool { @@ -392,38 +397,31 @@ struct ComposeView: View { } let ownerState = ownerRelayState + let subscriberState = subscriberRelayState if let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) { if gInfo.membership.memberRole == .owner { if let s = ownerState, s.relays.isEmpty || s.activeCount < s.relays.count { ownerChannelRelayBar(relays: s.relays, activeCount: s.activeCount, failedCount: s.failedCount, removedCount: s.removedCount) } - } else { - let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() - let relayMembers = chatModel.groupMembers - .filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) } - .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } + } else if let s = subscriberState { let showProgress = !gInfo.nextConnectPrepared || composeState.inProgress - let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count - let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count - let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count - let resolvedCount = connectedCount + removedCount + failedCount - let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count - if total == 0 || removedCount + failedCount > 0 || resolvedCount < total { + let resolvedCount = s.connectedCount + s.removedCount + s.failedCount + if s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total { subscriberChannelRelayBar( - hostnames: hostnames, - relayMembers: relayMembers, - connectedCount: connectedCount, - removedCount: removedCount, - failedCount: failedCount, - total: total, + hostnames: s.hostnames, + relayMembers: s.relayMembers, + connectedCount: s.connectedCount, + removedCount: s.removedCount, + failedCount: s.failedCount, + total: s.total, showProgress: showProgress ) } } } - let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: ownerState?.noActiveRelays ?? false) + let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerState?.noActiveRelays ?? subscriberState?.noActiveRelays) ?? false) let composeEnabled = ( userCantSendReason == nil || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || @@ -748,8 +746,25 @@ struct ComposeView: View { return (relays, activeCount, failedCount, removedCount, noActiveRelays) } + private var subscriberRelayState: (hostnames: [String], relayMembers: [GMember], connectedCount: Int, removedCount: Int, failedCount: Int, total: Int, noActiveRelays: Bool)? { + guard let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays, + gInfo.membership.memberRole != .owner, + ![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) + else { return nil } + let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted() + let relayMembers = chatModel.groupMembers + .filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) } + .sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") } + let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count + let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count + let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count + let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count + let noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total + return (hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays) + } + private var disabledText: LocalizedStringKey? { - chat.chatInfo.userCantSendReason(allRelaysBroken: ownerRelayState?.noActiveRelays ?? false)?.composeLabel + chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerRelayState?.noActiveRelays ?? subscriberRelayState?.noActiveRelays) ?? false)?.composeLabel } @ViewBuilder private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int) -> some View { @@ -1029,6 +1044,10 @@ struct ComposeView: View { sendMessage(ttl: ttl) resetLinkPreview() }, + sendSignedMessage: { + sendMessage(ttl: nil, sign: true) + resetLinkPreview() + }, sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, updateLiveMessage: updateLiveMessage, cancelLiveMessage: { @@ -1048,6 +1067,7 @@ struct ComposeView: View { finishVoiceMessageRecording: finishVoiceMessageRecording, allowVoiceMessagesToContact: allowVoiceMessagesToContact, timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), + showSign: chat.chatInfo.groupInfo?.useRelays == true, onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, @@ -1446,16 +1466,16 @@ struct ComposeView: View { } // Spec: spec/client/compose.md#sendMessage - private func sendMessage(ttl: Int?) { + private func sendMessage(ttl: Int?, sign: Bool = false) { logger.debug("ChatView sendMessage") Task { logger.debug("ChatView sendMessage: in Task") - _ = await sendMessageAsync(nil, live: false, ttl: ttl) + _ = await sendMessageAsync(nil, live: false, ttl: ttl, sign: sign) } } // Spec: spec/client/compose.md#sendMessageAsync - private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?) async -> ChatItem? { + private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?, sign: Bool = false) async -> ChatItem? { var sent: ChatItem? let msgText = text ?? composeState.message let liveMessage = composeState.liveMessage @@ -1468,7 +1488,7 @@ struct ComposeView: View { // Composed text is send as a reply to the last forwarded item sent = await forwardItems(chatItems, fromChatInfo, ttl).last if !composeState.message.isEmpty { - _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions) + _ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions, sign: sign) } } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) @@ -1484,13 +1504,13 @@ struct ComposeView: View { switch (composeState.preview) { case .noPreview: - sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case .linkPreview: - sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case let .chatLinkPreview(chatLink, ownerSig): let linkStr = chatLink.connLinkStr let text = msgText.isEmpty ? linkStr : msgText + "\n" + linkStr - sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions) + sent = await send(.chat(text: text, chatLink: chatLink, ownerSig: ownerSig), quoted: quoted, live: live, ttl: ttl, mentions: mentions, sign: sign) case let .mediaPreviews(media): // TODO: CHECK THIS let last = media.count - 1 @@ -1512,15 +1532,15 @@ struct ComposeView: View { if msgs.isEmpty { msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))] } - sent = await send(msgs, live: live, ttl: ttl).last + sent = await send(msgs, live: live, ttl: ttl, sign: sign).last case let .voicePreview(recordingFileName, duration): stopPlayback.toggle() let file = voiceCryptoFile(recordingFileName) - sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions) + sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions, sign: sign) case let .filePreview(_, file): if let savedFile = saveFileFromURL(file) { - sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions) + sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions, sign: sign) } } } @@ -1528,7 +1548,7 @@ struct ComposeView: View { let wasForwarding = composeState.forwarding clearState(live: live) if wasForwarding, - chatModel.draftChatId == chat.chatInfo.id, + chatModel.draftChatId == draftChatId(chat.chatInfo.id, chat.chatInfo.groupChatScope()), let draft = chatModel.draft { composeState = draft } @@ -1654,15 +1674,16 @@ struct ComposeView: View { ) } - func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? { + func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64], sign: Bool = false) async -> ChatItem? { await send( [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)], live: live, - ttl: ttl + ttl: ttl, + sign: sign ).first } - func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] { + func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?, sign: Bool = false) async -> [ChatItem] { if let chatItems = chat.chatInfo.chatType == .local ? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs) : await apiSendMessages( @@ -1672,6 +1693,7 @@ struct ComposeView: View { sendAsGroup: chat.chatInfo.sendAsGroup, live: live, ttl: ttl, + sign: sign, composedMessages: msgs ) { await MainActor.run { @@ -1829,12 +1851,12 @@ struct ComposeView: View { // Spec: spec/client/compose.md#saveCurrentDraft private func saveCurrentDraft() { chatModel.draft = composeState - chatModel.draftChatId = chat.id + chatModel.draftChatId = draftChatId(chat.id, chat.chatInfo.groupChatScope()) } // Spec: spec/client/compose.md#clearCurrentDraft private func clearCurrentDraft() { - if chatModel.draftChatId == chat.id { + if chatModel.draftChatId == draftChatId(chat.id, chat.chatInfo.groupChatScope()) { chatModel.draft = nil chatModel.draftChatId = nil } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index c5fd8e39d0..225e83c014 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -20,7 +20,7 @@ struct NativeTextEditor: UIViewRepresentable { @Binding var placeholder: String? @Binding var selectedRange: NSRange let onImagesAdded: ([UploadContent]) -> Void - + static let minHeight: CGFloat = 39 func makeUIView(context: Context) -> CustomUITextField { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 713f462c27..1d75f38715 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -19,6 +19,7 @@ struct SendMessageView: View { @EnvironmentObject var theme: AppTheme @Environment(\.isEnabled) var isEnabled var sendMessage: (Int?) -> Void + var sendSignedMessage: () -> Void = {} var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil var cancelLiveMessage: (() -> Void)? = nil @@ -32,6 +33,7 @@ struct SendMessageView: View { var finishVoiceMessageRecording: (() -> Void)? = nil var allowVoiceMessagesToContact: (() -> Void)? = nil var timedMessageAllowed: Bool = false + var showSign: Bool = false var onMediaAdded: ([UploadContent]) -> Void @State private var holdingVMR = false @Namespace var namespace @@ -46,6 +48,7 @@ struct SendMessageView: View { @State private var showCustomTimePicker = false @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() @UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false + @UserDefault(DEFAULT_SIGN_MESSAGE_ALERT_SHOWN) private var signMessageAlertShown = false var body: some View { let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) @@ -243,6 +246,14 @@ struct SendMessageView: View { Label("Disappearing message", systemImage: "stopwatch") } } + // hidden until message signing is user-facing (recipient-only stage) +// if showSign { +// Button { +// startSignedMessage() +// } label: { +// Label("Sign message", systemImage: "checkmark.seal") +// } +// } } } @@ -352,6 +363,22 @@ struct SendMessageView: View { .padding([.bottom, .horizontal], 4) } + private func startSignedMessage() { + if signMessageAlertShown { + sendSignedMessage() + } else { + AlertManager.shared.showAlert(Alert( + title: Text("Sign message"), + message: Text("Signing proves you authored this message and can't be denied later."), + primaryButton: .default(Text("Send")) { + signMessageAlertShown = true + sendSignedMessage() + }, + secondaryButton: .cancel() + )) + } + } + private func startLiveMessage(send: @escaping () async -> Void, update: @escaping () async -> Void) { if liveMessageAlertShown { start() diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index b59fd51fe8..108f4d4306 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -174,8 +174,9 @@ struct AddGroupMembersViewCommon: View { } addedMembersCb(selectedContacts) } catch { - let a = getErrorAlert(error, "Error adding member(s)") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error adding member(s)", comment: "")) + } } } } @@ -183,7 +184,7 @@ struct AddGroupMembersViewCommon: View { private func rolePicker() -> some View { Picker("New member role", selection: $selectedRole) { ForEach(GroupMemberRole.supportedRoles.filter({ $0 <= groupInfo.membership.memberRole })) { role in - Text(role.text) + Text(role.text(isChannel: groupInfo.isChannel)) } } .frame(height: 36) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift index 50144e2bc5..6b7f5b65fc 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift @@ -14,6 +14,8 @@ struct ChannelMembersView: View { var groupInfo: GroupInfo @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme + @State private var searchText: String = "" + @FocusState private var searchFocussed var body: some View { let members = chatModel.groupMembers @@ -21,22 +23,36 @@ struct ChannelMembersView: View { let s = m.wrapped.memberStatus return s != .memLeft && s != .memRemoved && m.wrapped.memberRole != .relay } + .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } + let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1) + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase if groupInfo.isOwner { - let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1) List { Section(header: Text(subscriberCountStr(subscriberCount)).foregroundColor(theme.colors.secondary)) { + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 8) memberRow(GMember(groupInfo.membership), user: true, showRole: true) - ForEach(members) { member in - memberRow(member, user: false, showRole: member.wrapped.memberRole >= .owner) + let filteredMembers = s == "" ? members : members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + ForEach(filteredMembers) { member in + memberRow(member, user: false, showRole: member.wrapped.memberRole >= .member) } } } } else { - let owners = members.filter { $0.wrapped.memberRole >= .owner } + let contributors = members.filter { $0.wrapped.memberRole >= .member && $0.wrapped.memberStatus != .memUnknown } + let contributorCount = contributors.count + (groupInfo.membership.memberRole >= .member ? 1 : 0) + let withContributors = contributors.contains { $0.wrapped.memberRole < .owner } + || groupInfo.membership.memberRole >= .member List { - Section(header: Text("Owners").foregroundColor(theme.colors.secondary)) { - ForEach(owners) { member in - memberRow(member, user: false, showRole: false) + Section(header: Text(ownersContributorsCountStr(contributorCount, withContributors: withContributors)).foregroundColor(theme.colors.secondary)) { + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 8) + if groupInfo.membership.memberRole >= .member { + memberRow(GMember(groupInfo.membership), user: true, showRole: true) + } + let filteredContributors = s == "" ? contributors : contributors.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) } + ForEach(filteredContributors) { member in + memberRow(member, user: false, showRole: member.wrapped.memberRole >= .moderator) } } } @@ -66,7 +82,7 @@ struct ChannelMembersView: View { } Spacer() if showRole { - Text(member.memberRole.text) + Text(member.memberRole.text(isChannel: groupInfo.isChannel)) .foregroundColor(theme.colors.secondary) } } diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift index 27935768e3..aa94f5b346 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelRelaysView.swift @@ -24,26 +24,24 @@ struct ChannelRelaysView: View { var body: some View { List { relaysList() - // TODO [relays] re-enable when relay management ships - // if groupInfo.isOwner { - // Section { - // Button { - // showAddRelay = true - // } label: { - // Label("Add relay", systemImage: "plus") - // } - // } - // } + if groupInfo.isOwner { + Section { + Button { + showAddRelay = true + } label: { + Label("Add relay", systemImage: "plus") + } + } + } + } + .sheet(isPresented: $showAddRelay) { + // Backend gate (APIAddGroupRelays) rejects any chatRelayId already in group_relays + // regardless of relayStatus, so all current rows must be excluded from the add list. + let existingRelayIds = Set(groupRelays.compactMap { $0.userChatRelay.chatRelayId }) + AddGroupRelayView(groupInfo: groupInfo, existingRelayIds: existingRelayIds) { + Task { await chatModel.loadGroupMembers(groupInfo) } + } } - // TODO [relays] re-enable when relay management ships - // .sheet(isPresented: $showAddRelay) { - // // Backend gate (APIAddGroupRelays) rejects any chatRelayId already in group_relays - // // regardless of relayStatus, so all current rows must be excluded from the add list. - // let existingRelayIds = Set(groupRelays.compactMap { $0.userChatRelay.chatRelayId }) - // AddGroupRelayView(groupInfo: groupInfo, existingRelayIds: existingRelayIds) { - // Task { await chatModel.loadGroupMembers(groupInfo) } - // } - // } .onAppear { Task { await chatModel.loadGroupMembers(groupInfo) @@ -82,20 +80,18 @@ struct ChannelRelaysView: View { : subscriberRelayStatusText(member.wrapped) relayMemberRow(member.wrapped, statusText: statusText) } - // TODO [relays] re-enable when relay management ships - // if groupInfo.isOwner && member.wrapped.canBeRemoved(groupInfo: groupInfo) { - // link.swipeActions(edge: .trailing) { - // Button { - // showRemoveMemberAlert(groupInfo, member.wrapped) - // } label: { - // Label("Remove relay", systemImage: "trash") - // } - // .tint(.red) - // } - // } else { - // link - // } - link + if groupInfo.isOwner && member.wrapped.canBeRemoved(groupInfo: groupInfo) { + link.swipeActions(edge: .trailing) { + Button { + showRemoveMemberAlert(groupInfo, member.wrapped) + } label: { + Label("Remove relay", systemImage: "trash") + } + .tint(.red) + } + } else { + link + } } } footer: { Text("Chat relays forward messages to channel subscribers.") diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift new file mode 100644 index 0000000000..df0867c470 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift @@ -0,0 +1,169 @@ +// +// ChannelWebAccessView.swift +// SimpleX (iOS) +// +// Created by simplex.chat on 31/05/2026. +// Copyright © 2026 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ChannelWebAccessView: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dismiss) var dismiss: DismissAction + @Binding var groupInfo: GroupInfo + @State private var webPage: String + @State private var allowEmbedding: Bool + @State private var saving = false + @State private var groupRelays: [GroupRelay] = [] + + init(groupInfo: Binding) { + _groupInfo = groupInfo + let access = groupInfo.wrappedValue.groupProfile.publicGroup?.publicGroupAccess + _webPage = State(initialValue: access?.groupWebPage ?? "") + _allowEmbedding = State(initialValue: access?.allowEmbedding ?? false) + } + + var body: some View { + List { + if let code = embedCode { + webpageInfo("Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting.") + + Section { + ScrollView { + Text(code) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + } + .frame(maxHeight: 88) + Button { + UIPasteboard.general.string = code + } label: { + Label("Copy code", systemImage: "doc.on.doc") + } + } header: { + Text("Webpage code") + } footer: { + Text("Add this code to your webpage. It will display the preview of your channel / group.") + } + } else { + webpageInfo("Used chat relays do not support webpages.") + } + + Section { + TextField("https://", text: $webPage) + .keyboardType(.URL) + .autocapitalization(.none) + .disableAutocorrection(true) + } header: { + Text("Enter webpage URL") + } footer: { + Text("It will be shown to subscribers and used to allow loading the preview.") + } + + Section { + Toggle("Allow anyone to embed", isOn: $allowEmbedding) + } footer: { + Text(allowEmbedding ? "Any webpage can show the preview." : "Only your page above can show the preview.") + } + + Section { + Button { + saveAccess() + } label: { + HStack { + Text(groupInfo.isChannel ? "Save and notify subscribers" : "Save and notify members") + if saving { Spacer(); ProgressView() } + } + } + .disabled(!hasChanges || saving) + } + } + .modifier(ThemedBackground(grouped: true)) + .onAppear { + Task { + let relays = await apiGetGroupRelays(groupInfo.groupId) + await MainActor.run { groupRelays = relays } + } + } + .onDisappear { + if hasChanges { + showAlert( + title: NSLocalizedString("Save webpage settings?", comment: "alert title"), + message: NSLocalizedString("Webpage settings were changed. If you save, the updated settings will be sent to subscribers.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save", comment: "alert button"), + buttonAction: saveAccess, + cancelButton: true + ) + } + } + } + + private func webpageInfo(_ text: LocalizedStringKey) -> some View { + Section { + Text(text).foregroundColor(theme.colors.secondary) + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16)) + } + + private var hasChanges: Bool { + let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess + let currentWebPage = access?.groupWebPage ?? "" + let currentEmbedding = access?.allowEmbedding ?? false + return webPage != currentWebPage || allowEmbedding != currentEmbedding + } + + private var relayDomains: [String] { + groupRelays.compactMap { $0.relayCap.webDomain } + } + + private var embedCode: String? { + if let pg = groupInfo.groupProfile.publicGroup, + !relayDomains.isEmpty { + """ +
+ + """ + } else { + nil + } + } + + private func saveAccess() { + saving = true + Task { + do { + var gp = groupInfo.groupProfile + if var pg = gp.publicGroup { + let trimmedPage = webPage.trimmingCharacters(in: .whitespacesAndNewlines) + let existingAccess = pg.publicGroupAccess + pg.publicGroupAccess = PublicGroupAccess( + groupWebPage: trimmedPage.isEmpty ? nil : trimmedPage, + groupDomainClaim: existingAccess?.groupDomainClaim, + domainWebPage: existingAccess?.domainWebPage ?? false, + allowEmbedding: allowEmbedding + ) + gp.publicGroup = pg + } + let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp) + await MainActor.run { + groupInfo = gInfo + ChatModel.shared.updateGroup(gInfo) + saving = false + } + } catch { + logger.error("ChannelWebAccessView apiUpdateGroup error: \(responseError(error))") + await MainActor.run { saving = false } + } + } + } +} diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index da895b325c..1be8070259 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -159,6 +159,16 @@ struct GroupChatInfoView: View { } } + if groupInfo.useRelays && groupInfo.isOwner && groupLink != nil { + Section { + channelSimplexNameButton() + } header: { + if groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName != nil { + Text("Channel SimpleX name").foregroundColor(theme.colors.secondary) + } + } + } + Section { if groupInfo.isOwner && groupInfo.businessChat == nil { editGroupButton() @@ -244,6 +254,12 @@ struct GroupChatInfoView: View { } } + if groupInfo.useRelays && groupInfo.isOwner { + Section(header: Text("Advanced options").foregroundColor(theme.colors.secondary)) { + channelWebAccessButton() + } + } + if developerTools { Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", chat.chatInfo.localDisplayName) @@ -325,6 +341,7 @@ struct GroupChatInfoView: View { .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } + groupSimplexNameView(groupInfo) { groupInfo = $0 } if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage, let url = URL(string: webPage) { Link(destination: url) { @@ -575,7 +592,7 @@ struct GroupChatInfoView: View { } else { let role = member.memberRole if [.owner, .admin, .moderator, .observer].contains(role) { - Text(member.memberRole.text) + Text(member.memberRole.text(isChannel: groupInfo.isChannel)) .foregroundColor(theme.colors.secondary) } } @@ -657,6 +674,49 @@ struct GroupChatInfoView: View { } } + private func channelWebAccessButton() -> some View { + let title: LocalizedStringKey = groupInfo.isChannel ? "Channel webpage" : "Group webpage" + return NavigationLink { + ChannelWebAccessView(groupInfo: $groupInfo) + .navigationBarTitle(title) + .navigationBarTitleDisplayMode(.large) + } label: { + Label(title, systemImage: "globe") + } + } + + private func channelSimplexNameButton() -> some View { + NavigationLink { + let domain = if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { "#\(d)" } else { "" } + SetSimplexDomainView( + title: "SimpleX name", + footer: "Let people join via name registered with this channel link.", + prompt: "#channelname.testing", + simplexName: domain, + save: { domain in + do { + var access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?? PublicGroupAccess() + access.groupDomainClaim = domain.map { SimplexDomainClaim(domain: $0) } + let gInfo = try await apiSetPublicGroupAccess(groupInfo.groupId, access: access) + await MainActor.run { + chatModel.updateGroup(gInfo) + groupInfo = gInfo + } + return true + } catch { + return false + } + } + ) + } label: { + if let d = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName { + Label("\(d)", systemImage: "number") + } else { + Label("Get SimpleX name (BETA)", systemImage: "number") + } + } + } + private func groupLinkDestinationView() -> some View { GroupLinkView( groupId: groupInfo.groupId, @@ -674,7 +734,7 @@ struct GroupChatInfoView: View { } private func channelMembersButton() -> some View { - let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners" + let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners & contributors" return NavigationLink { ChannelMembersView(chat: chat, groupInfo: groupInfo) .navigationTitle(label) @@ -845,7 +905,7 @@ struct GroupChatInfoView: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( title: Text(label), - message: deleteGroupAlertMessage(groupInfo), + message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { do { @@ -867,7 +927,7 @@ struct GroupChatInfoView: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) @@ -889,7 +949,7 @@ struct GroupChatInfoView: View { ) return Alert( title: Text(titleLabel), - message: Text(messageLabel), + message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(chat.chatInfo.apiId) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 22253c4808..8a9bcaf059 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -84,7 +84,7 @@ struct GroupLinkView: View { if !isChannel { Picker("Initial role", selection: $groupLinkMemberRole) { ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in - Text(role.text) + Text(role.text(isChannel: isChannel)) } } .frame(height: 36) @@ -155,8 +155,9 @@ struct GroupLinkView: View { do { groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole) } catch let error { - let a = getErrorAlert(error, "Error updating group link") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating group link", comment: "")) + } } } } @@ -188,8 +189,7 @@ struct GroupLinkView: View { logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error creating group link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating group link", comment: "")) } } } @@ -230,8 +230,7 @@ struct GroupLinkView: View { logger.error("apiAddGroupShortLink: \(responseError(error))") await MainActor.run { creatingLink = false - let a = getErrorAlert(error, "Error adding short link") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error adding short link", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 28693e8d8a..ebbdc6b6a3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -126,28 +126,25 @@ struct GroupMemberInfoView: View { && member.memberRole != .relay && ((groupInfo.fullGroupPreferences.support.on && member.memberRole < .moderator) || member.supportChat != nil) + let canVerifyCode = connectionCode != nil && member.memberRole != .relay + let canSyncConn = connectionStats?.ratchetSyncAllowed ?? false - if member.memberActive { + if (member.memberActive || (groupInfo.useRelays && member.memberCurrent)) + && (showMemberSupportChat || canVerifyCode || canSyncConn) { Section { if showMemberSupportChat { MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) } - if let code = connectionCode, - !(groupInfo.useRelays && member.memberRole == .relay) { + if canVerifyCode, let code = connectionCode { verifyCodeButton(code) } - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { + if canSyncConn { synchronizeConnectionButton() } // } else if developerTools { // synchronizeConnectionButtonForce() // } } - } else if groupInfo.useRelays && member.memberCurrent && showMemberSupportChat { - Section { - MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) - } } if let contactLink = member.contactLink { @@ -178,15 +175,15 @@ struct GroupMemberInfoView: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Channel" : groupInfo.businessChat == nil ? "Group" : "Chat" infoRow(label, groupInfo.displayName) - if !groupInfo.useRelays, let roles = member.canChangeRoleTo(groupInfo: groupInfo) { + if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { Picker("Change role", selection: $newRole) { ForEach(roles) { role in - Text(role.text) + Text(role.text(isChannel: groupInfo.isChannel)) } } .frame(height: 36) } else { - infoRow("Role", member.memberRole.text) + infoRow("Role", member.memberRole.text(isChannel: groupInfo.isChannel)) } if let link = member.relayLink { infoRow("Relay link", String.localizedStringWithFormat(NSLocalizedString("via %@", comment: "relay hostname"), hostFromRelayLink(link))) @@ -278,8 +275,9 @@ struct GroupMemberInfoView: View { } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") - let a = getErrorAlert(e, "Error") - await MainActor.run { alert = .error(title: a.title, error: a.message) } + await MainActor.run { + showErrorAlert(e, NSLocalizedString("Error", comment: "")) + } } } } @@ -299,7 +297,8 @@ struct GroupMemberInfoView: View { newRole = member.memberRole do { let (_, stats) = try await apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - let (mem, code) = member.memberActive ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) + let getCode = (member.memberActive || (groupInfo.useRelays && member.memberCurrent)) && member.memberRole != .relay + let (mem, code) = getCode ? try await apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) await MainActor.run { _ = chatModel.upsertGroupMember(groupInfo, mem) connectionStats = stats @@ -473,10 +472,9 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error creating member contact") await MainActor.run { progressIndicator = false - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error creating member contact", comment: "")) } } } @@ -585,12 +583,17 @@ struct GroupMemberInfoView: View { let (verified, existingCode) = r let connCode = verified ? SecurityCode(securityCode: existingCode, verifiedAt: .now) : nil connectionCode = existingCode - member.activeConn?.connectionCode = connCode + if groupInfo.useRelays { + member.memberVerifiedCode = connCode + } else { + member.activeConn?.connectionCode = connCode + } _ = chatModel.upsertGroupMember(groupInfo, member) return r } return nil - } + }, + verificationText: groupInfo.useRelays ? "To verify keys with this subscriber, compare (or scan) the code on your devices." : nil ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") @@ -633,8 +636,7 @@ struct GroupMemberInfoView: View { blockForAllButton(mem) } } - // TODO [relays] re-enable when relay management ships - if canRemove && mem.memberRole != .relay { + if canRemove { if mem.memberStatus != .memRemoved && (mem.memberStatus != .memLeft || mem.memberRole == .relay) { removeMemberButton(mem) } else if mem.memberRole != .relay { @@ -728,15 +730,17 @@ struct GroupMemberInfoView: View { private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert { Alert( - title: Text("Change member role?"), + title: Text("Change role?"), message: ( mem.memberCurrent ? ( - groupInfo.businessChat == nil - ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") - : Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.") + groupInfo.isChannel + ? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All subscribers will be notified.") + : groupInfo.businessChat == nil + ? Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All group members will be notified.") + : Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". All chat members will be notified.") ) - : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.") + : Text("Role will be changed to \"\(newRole.text(isChannel: groupInfo.isChannel))\". The member will receive a new invitation.") ), primaryButton: .default(Text("Change")) { Task { @@ -751,8 +755,9 @@ struct GroupMemberInfoView: View { } catch let error { newRole = mem.memberRole logger.error("apiMembersRole error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing role") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing role", comment: "")) + } } } }, @@ -773,9 +778,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error changing address") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error changing address", comment: "")) } } } @@ -791,9 +795,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("abortSwitchMemberAddress apiAbortSwitchGroupMember error: \(responseError(error))") - let a = getErrorAlert(error, "Error aborting address change") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error aborting address change", comment: "")) } } } @@ -810,9 +813,8 @@ struct GroupMemberInfoView: View { } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error synchronizing connection", comment: "")) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index cdbed7fe30..958bbbabb2 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -47,7 +47,7 @@ struct GroupMentionsView: View { LazyVStack(spacing: 0) { ForEach(Array(filtered.enumerated()), id: \.element.wrapped.groupMemberId) { index, member in let mentioned = mentionMemberId == member.wrapped.memberId - let disabled = composeState.mentions.count >= MAX_NUMBER_OF_MENTIONS && !mentioned + let disabled = composeState.memberMentions.count >= MAX_NUMBER_OF_MENTIONS && !mentioned ZStack(alignment: .bottom) { memberRowView(member.wrapped, mentioned) .contentShape(Rectangle()) @@ -124,14 +124,13 @@ struct GroupMentionsView: View { } private func messageChanged(_ msg: String, _ parsedMsg: [FormattedText], _ range: NSRange) { - removeUnusedMentions(parsedMsg) if let (ft, r) = selectedMarkdown(parsedMsg, range) { switch ft.format { case let .mention(name): isVisible = true mentionName = name mentionRange = r - mentionMemberId = composeState.mentions[name]?.memberId + mentionMemberId = composeState.memberMentions[name] != nil ? composeState.mentions[name]?.memberId : nil if !m.membersLoaded { Task { await m.loadGroupMembers(groupInfo) @@ -169,15 +168,6 @@ struct GroupMentionsView: View { .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } } - private func removeUnusedMentions(_ parsedMsg: [FormattedText]) { - let usedMentions: Set = Set(parsedMsg.compactMap { ft in - if case let .mention(name) = ft.format { name } else { nil } - }) - if usedMentions.count < composeState.mentions.count { - composeState = composeState.copy(mentions: composeState.mentions.filter({ usedMentions.contains($0.key) })) - } - } - private func getCharacter(_ s: String, _ pos: Int) -> (char: String.SubSequence, range: NSRange)? { if pos < 0 || pos >= s.count { return nil } let r = NSRange(location: pos, length: 1) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index cc2feef706..da15ab98a5 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -50,6 +50,8 @@ struct GroupPreferencesView: View { featureSection(.history, $preferences.history.enable) featureSection(.support, $preferences.support.enable, disabled: true) } else { + // hidden until message signing is user-facing (recipient-only stage) +// featureSection(.signMessages, $preferences.signMessages.enable) featureSection(.timedMessages, $preferences.timedMessages.enable) featureSection(.fullDelete, $preferences.fullDelete.enable) featureSection(.reactions, $preferences.reactions.enable) diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift index 0263a39a90..da9d56a699 100644 --- a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift +++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift @@ -205,7 +205,7 @@ struct MemberSupportView: View { } else if member.memberPending { return member.memberStatus.text } else { - return LocalizedStringKey(member.memberRole.text) + return LocalizedStringKey(member.memberRole.text(isChannel: groupInfo.isChannel)) } } diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 373311073a..d35ca8f3dc 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -15,6 +15,7 @@ struct VerifyCodeView: View { @State var connectionCode: String? @State var connectionVerified: Bool var verify: (String?) -> (Bool, String)? + var verificationText: LocalizedStringKey? = nil @State private var showCodeError = false var body: some View { @@ -44,7 +45,7 @@ struct VerifyCodeView: View { Text("\(displayName) is not verified").textCase(.none) } } footer: { - Text("To verify end-to-end encryption with your contact compare (or scan) the code on your devices.") + Text(verificationText ?? "To verify end-to-end encryption with your contact compare (or scan) the code on your devices.") } Section { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index b4590fc124..0ed78401b0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -532,9 +532,7 @@ struct ChatListNavLink: View { .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection) { a in - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) - }) + AlertManager.shared.showAlert(deleteContactConnectionAlert(contactConnection)) } label: { deleteLabel } @@ -568,7 +566,7 @@ struct ChatListNavLink: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( title: Text(label), - message: deleteGroupAlertMessage(groupInfo), + message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, @@ -600,7 +598,7 @@ struct ChatListNavLink: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) } }, @@ -630,7 +628,7 @@ struct ChatListNavLink: View { ) return Alert( title: Text(titleLabel), - message: Text(messageLabel), + message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(groupInfo.groupId) } }, @@ -698,13 +696,13 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert { ) } -func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { +func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), - message: - contactConnection.initiated - ? Text("The contact you shared this link with will NOT be able to connect!") - : Text("The connection you accepted will be cancelled!"), + message: Text(contactConnection.displayName + "\n\n") + + (contactConnection.initiated + ? Text("The contact you shared this link with will NOT be able to connect!") + : Text("The connection you accepted will be cancelled!")), primaryButton: .destructive(Text("Delete")) { Task { do { @@ -715,7 +713,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } } catch let error { await MainActor.run { - showError(getErrorAlert(error, "Error deleting connection")) + showErrorAlert(error, NSLocalizedString("Error deleting connection", comment: "")) } } } @@ -725,11 +723,7 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, } func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool { - let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) - if let alert = alert { - showAlert(alert) - return false - } else if let contact = contact { + if let contact = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) { await MainActor.run { ChatModel.shared.updateContact(contact) } @@ -757,8 +751,9 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { await onComplete() } catch let error { await onComplete() - let a = getErrorAlert(error, "Error joining group") - AlertManager.shared.showAlertMsg(title: a.title, message: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error joining group", comment: "")) + } } func deleteGroup() async { @@ -773,12 +768,13 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } } -func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { +func showErrorAlert(_ error: Error, _ title: String) { + let err = { String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: ""), responseError(error)) } if let r = error as? ChatError, let alert = getNetworkErrorAlert(r) { - return alert + showAlert(alert.title, message: alert.message ?? err()) } else { - return ErrorAlert(title: title, message: "Error: \(responseError(error))") + showAlert(title, message: err()) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index d90149c7dd..b05e0696e3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -66,6 +66,7 @@ enum ActiveFilter: Identifiable, Equatable { class SaveableSettings: ObservableObject { @Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: [], serverWarnings: []) + var profileSave: (() -> Void)? = nil } struct ServerSettings { @@ -135,6 +136,15 @@ struct UserPickerSheetView: View { cancelButton: true ) } + if let saveProfile = ss.profileSave { + showAlert( + title: NSLocalizedString("Save your profile?", comment: "alert title"), + message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"), + buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"), + buttonAction: saveProfile, + cancelButton: true + ) + } } .environmentObject(ss) } @@ -151,7 +161,7 @@ struct ChatListView: View { @FocusState private var searchFocussed @State private var searchText = "" @State private var searchShowingSimplexLink = false - @State private var searchChatFilteredBySimplexLink: String? = nil + @State private var searchChatFilteredBySimplexLink: Set = [] @State private var scrollToSearchBar = false @State private var userPickerShown: Bool = false @State private var sheet: SomeSheet? = nil @@ -511,8 +521,8 @@ struct ChatListView: View { // Spec: spec/client/chat-list.md#filteredChats private func filteredChats() -> [Chat] { - if let linkChatId = searchChatFilteredBySimplexLink { - return chatModel.chats.filter { $0.id == linkChatId } + if !searchChatFilteredBySimplexLink.isEmpty { + return chatModel.chats.filter { searchChatFilteredBySimplexLink.contains($0.id) } } else { let s = searchString() return s == "" @@ -626,13 +636,29 @@ struct ChatListSearchBar: View { @FocusState.Binding var searchFocussed: Bool @Binding var searchText: String @Binding var searchShowingSimplexLink: Bool - @Binding var searchChatFilteredBySimplexLink: String? + @Binding var searchChatFilteredBySimplexLink: Set @Binding var parentSheet: SomeSheet? + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @State private var ignoreSearchTextChange = false + // when the search text is a SimpleX name, the string to connect to (with @/# preserved); nil otherwise + @State private var connectNameCandidate: String? = nil + @State private var nameSearchTask: Task? = nil var body: some View { VStack(spacing: 12) { - ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } + // a typed name shows a row to connect to it (as on Android mobile): with the reachable toolbar it + // replaces the tags above the search field; in top bar mode the tags stay and it moves below (end of VStack) + if oneHandUI, let candidate = connectNameCandidate { + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: false + ) + } else { + ScrollView([.horizontal], showsIndicators: false) { TagsView(parentSheet: $parentSheet, searchText: $searchText) } + } HStack(spacing: 12) { HStack(spacing: 4) { Image(systemName: "magnifyingglass") @@ -667,6 +693,15 @@ struct ChatListSearchBar: View { toggleFilterButton() } } + if !oneHandUI, let candidate = connectNameCandidate { + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: false + ) + } } .onChange(of: searchFocussed) { sf in withAnimation { searchMode = sf } @@ -675,24 +710,50 @@ struct ChatListSearchBar: View { if ignoreSearchTextChange { ignoreSearchTextChange = false } else { - switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) { + let s = t.trimmingCharacters(in: .whitespaces) + switch strConnectTarget(s) { case let .link(text, _, linkText): + nameSearchTask?.cancel() + nameSearchTask = nil searchFocussed = false ignoreSearchTextChange = true searchText = linkText searchShowingSimplexLink = true - searchChatFilteredBySimplexLink = nil + searchChatFilteredBySimplexLink = [] + connectNameCandidate = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) - case .none: - if t != "" { + default: + // not a link: a recognized SimpleX name shows the connect-by-name row (in place of the + // list tags) and, debounced, resolves locally per keystroke to narrow the list to the + // matching known chat(s); tapping the row connects online. Clear the filter immediately so + // the list falls back to text search until the search returns. + let candidate = nameSearchCandidate(s) + connectNameCandidate = candidate + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = [] + nameSearchTask?.cancel() + nameSearchTask = nil + if let candidate = candidate { + nameSearchTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 300_000_000) + if Task.isCancelled { return } + // a bare name can be a contact or a channel: search both and keep every match + let targets = candidate.hasPrefix("@") || candidate.hasPrefix("#") ? [candidate] : ["@\(candidate)", "#\(candidate)"] + var ids: [String] = [] + for name in targets { + let plan = await apiConnectPlan(connLink: name, resolveMode: .never, inProgress: BoxedValue(false)) + if Task.isCancelled { return } + if let id = knownChatId(plan) { ids.append(id) } + } + searchChatFilteredBySimplexLink = Set(ids) + // drop the row only when every searched type is already known locally + if ids.count == targets.count { connectNameCandidate = nil } + } + } else if t != "" { searchFocussed = true } else { ConnectProgressManager.shared.cancelConnectProgress() } - searchShowingSimplexLink = false - searchChatFilteredBySimplexLink = nil } } } @@ -730,12 +791,108 @@ struct ChatListSearchBar: View { searchText = "" searchFocussed = false }, - filterKnownContact: { searchChatFilteredBySimplexLink = $0.id }, - filterKnownGroup: { searchChatFilteredBySimplexLink = $0.id } + filterKnownContact: { searchChatFilteredBySimplexLink = [$0.id] }, + filterKnownGroup: { searchChatFilteredBySimplexLink = [$0.id] } ) } } +// Row shown when the search text is a SimpleX name — in place of the list tags in the chat list, below +// the search field in the new chat sheet. The @ icon marks a contact name, the tag icon a channel/other +// name; tapping hides the keyboard, connects online, and clears the field. +struct ConnectByNameRow: View { + @EnvironmentObject var theme: AppTheme + var name: String + @Binding var searchText: String + @Binding var connectNameCandidate: String? + @FocusState.Binding var searchFocussed: Bool + var dismiss: Bool + + var body: some View { + HStack(spacing: 4) { + Image(systemName: name.hasPrefix("@") ? "at" : "number") + .foregroundColor(theme.colors.primary) + Text(String.localizedStringWithFormat(NSLocalizedString("Connect to %@", comment: "new chat action"), name)) + .foregroundColor(theme.colors.primary) + Spacer() + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { + searchFocussed = false + planAndConnect( + name, + theme: theme, + dismiss: dismiss, + cleanup: { + searchText = "" + connectNameCandidate = nil + } + ) + } + } +} + +// Default top-level part used to complete a bare name typed in the search field (search field only; +// the message parser and the wire format are unchanged). +private let DEFAULT_NAME_TLD = "testing" +// Shortest name that offers the button, so it is discoverable but does not flash on short prefixes. +private let MIN_NAME_LENGTH = 5 + +private func isNameLabel(_ s: String) -> Bool { + s.count >= 1 && s.count <= 63 && s.range(of: "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*$", options: .regularExpression) != nil +} + +// On-device candidate for connecting by SimpleX name: the string sent to the core to resolve it. +// The chat id a local (.never) search resolved to — a contact, business, or channel — or nil on a miss. +// A name-resolved chat may be prepared in the store but not yet listed, so add it so the filter can surface it. +@MainActor +func knownChatId(_ result: ConnectionPlanResult?) -> String? { + guard let plan = result?.connectionPlan else { return nil } + let m = ChatModel.shared + switch plan { + case let .contactAddress(contactAddressPlan): + if case let .known(contact) = contactAddressPlan { + if m.getContactChat(contact.contactId) == nil { + m.addChat(Chat(chatInfo: .direct(contact: contact), chatItems: [])) + } + return contact.id + } + return nil + case let .groupLink(groupLinkPlan): + switch groupLinkPlan { + case .known(let groupInfo), .ownLink(let groupInfo): + if m.getGroupChat(groupInfo.groupId) == nil { + m.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil), chatItems: [])) + } + return groupInfo.id + default: + return nil + } + default: + return nil + } +} + +// Mirrors the domain grammar (nameLabelP/mkDomain in SimplexName.hs): an optional @/# prefix, then +// dot-separated ASCII labels; a dotless word is completed with the default top-level part. Returns +// the string to send (keeping @/# so the type is preserved), or nil when the text is not a name. +func nameSearchCandidate(_ str: String) -> String? { + let text = str.trimmingCharacters(in: .whitespaces) + let prefix: Character? = text.first.flatMap { $0 == "@" || $0 == "#" ? $0 : nil } + let core = prefix != nil ? String(text.dropFirst()) : text + if core.isEmpty { return nil } + let labels = core.split(separator: ".", omittingEmptySubsequences: false) + if labels.contains(where: { !isNameLabel(String($0)) }) { return nil } + if labels.count > 1 { + return text // already has a top-level part + } else if core.count >= MIN_NAME_LENGTH { + return "\(prefix.map(String.init) ?? "")\(core).\(DEFAULT_NAME_TLD)" + } else { + return nil + } +} + struct TagsView: View { @EnvironmentObject var chatTagsModel: ChatTagsModel @EnvironmentObject var chatModel: ChatModel diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index a6e7fc5870..59b92a265b 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -438,7 +438,7 @@ struct ChatPreviewView: View { } case .file: smallContentPreviewFile(size: dynamicMediaSize) { - CIFileView(file: ci.file, edited: ci.meta.itemEdited, senderProfile: ciSenderProfile(ci, chat.chatInfo), smallViewSize: dynamicMediaSize) + CIFileView(chat: chat, file: ci.file, meta: ci.meta, senderProfile: ciSenderProfile(ci, chat.chatInfo), smallViewSize: dynamicMediaSize) } case let .chat(_, chatLink, ownerSig): smallContentPreview(size: dynamicMediaSize, borderColor: chatLink.image != nil ? .secondary : .clear) { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 124c5ee7ba..c777eea80c 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -103,11 +103,7 @@ struct ContactConnectionInfo: View { .alert(item: $alert) { _alert in switch _alert { case .deleteInvitationAlert: - return deleteContactConnectionAlert(contactConnection) { a in - alert = .error(title: a.title, error: a.message) - } success: { - dismiss() - } + return deleteContactConnectionAlert(contactConnection, success: { dismiss() }) case let .error(title, error): return mkAlert(title: title, message: error) } } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index d5d70abaea..051ed9f037 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -110,33 +110,88 @@ struct DatabaseView: View { } Section { - settingsRow( - stopped ? "exclamationmark.octagon.fill" : "play.fill", - color: stopped ? .red : .green - ) { - Toggle( - stopped ? "Chat is stopped" : "Chat is running", - isOn: $runChat - ) - .onChange(of: runChat) { _ in - if runChat { - DatabaseView.startChat($runChat, $progressIndicator) - } else if !stoppingChat { - stoppingChat = false - alert = .stopChat - } - } - } - } header: { - Text("Run chat") - .foregroundColor(theme.colors.secondary) - } footer: { - if case .documents = dbContainer { - Text("Database will be migrated when the app restarts") - .foregroundColor(theme.colors.secondary) - } + NavigationLink("Database passphrase & export", destination: databaseManagementView) } + Section { + Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { + alert = .deleteFilesAndMedia + } + .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) + } header: { + Text("Files & media") + .foregroundColor(theme.colors.secondary) + } footer: { + if let (fileCount, size) = appFilesCountAndSize { + if fileCount == 0 { + Text("No received or sent files") + .foregroundColor(theme.colors.secondary) + } else { + Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") + .foregroundColor(theme.colors.secondary) + } + } + } + } + .onAppear { + runChat = m.chatRunning ?? true + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + currentChatItemTTL = chatItemTTL + } + .onChange(of: chatItemTTL) { ttl in + if ttl < currentChatItemTTL { + alert = .setChatItemTTL(ttl: ttl) + } else if ttl != currentChatItemTTL { + setCiTTL(ttl) + } + } + .alert(item: $alert) { item in databaseAlert(item) } + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.zip], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + importedArchivePath = fileURL + alert = .importArchive + } + } + } + + private func runChatToggleView() -> some View { + Section { + let stopped = m.chatRunning == false + settingsRow( + stopped ? "exclamationmark.octagon.fill" : "play.fill", + color: stopped ? .red : .green + ) { + Toggle( + stopped ? "Chat is stopped" : "Chat is running", + isOn: $runChat + ) + .onChange(of: runChat) { _ in + if runChat { + DatabaseView.startChat($runChat, $progressIndicator) + } else if !stoppingChat { + stoppingChat = false + alert = .stopChat + } + } + } + } header: { + Text("Run chat") + .foregroundColor(theme.colors.secondary) + } footer: { + if case .documents = dbContainer { + Text("Database will be migrated when the app restarts") + .foregroundColor(theme.colors.secondary) + } + } + } + + private func databaseManagementView() -> some View { + List { + let stopped = m.chatRunning == false Section { let unencrypted = m.chatDbEncrypted == false let color: Color = unencrypted ? .orange : theme.colors.secondary @@ -194,47 +249,12 @@ struct DatabaseView: View { } } - Section { - Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { - alert = .deleteFilesAndMedia - } - .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) - } header: { - Text("Files & media") - .foregroundColor(theme.colors.secondary) - } footer: { - if let (fileCount, size) = appFilesCountAndSize { - if fileCount == 0 { - Text("No received or sent files") - .foregroundColor(theme.colors.secondary) - } else { - Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") - .foregroundColor(theme.colors.secondary) - } - } - } + runChatToggleView() } - .onAppear { - runChat = m.chatRunning ?? true - appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) - currentChatItemTTL = chatItemTTL - } - .onChange(of: chatItemTTL) { ttl in - if ttl < currentChatItemTTL { - alert = .setChatItemTTL(ttl: ttl) - } else if ttl != currentChatItemTTL { - setCiTTL(ttl) - } - } - .alert(item: $alert) { item in databaseAlert(item) } - .fileImporter( - isPresented: $showFileImporter, - allowedContentTypes: [.zip], - allowsMultipleSelection: false - ) { result in - if case let .success(files) = result, let fileURL = files.first { - importedArchivePath = fileURL - alert = .importArchive + .modifier(ThemedBackground(grouped: true)) + .overlay { + if progressIndicator { + ProgressView().scaleEffect(2) } } } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 82d17cd2b1..670cc7cae0 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -54,6 +54,10 @@ func showAlert( } } +func showAlert(_ a: (title: String, message: String?)) { + showAlert(a.title, message: a.message) +} + func showAlert( _ title: String, message: String? = nil, @@ -140,8 +144,10 @@ class OpenChatAlertViewController: UIViewController { private let information: String? private let cancelTitle: String private let confirmTitle: String? + private let secondTitle: String? private let onCancel: () -> Void private let onConfirm: (() -> Void)? + private let onSecond: (() -> Void)? init( profileName: String, @@ -152,8 +158,10 @@ class OpenChatAlertViewController: UIViewController { information: String? = nil, cancelTitle: String = "Cancel", confirmTitle: String? = "Open", + secondTitle: String? = nil, onCancel: @escaping () -> Void = {}, - onConfirm: (() -> Void)? = nil + onConfirm: (() -> Void)? = nil, + onSecond: (() -> Void)? = nil ) { self.profileName = profileName self.profileFullName = profileFullName @@ -163,8 +171,10 @@ class OpenChatAlertViewController: UIViewController { self.information = information self.cancelTitle = cancelTitle self.confirmTitle = confirmTitle + self.secondTitle = secondTitle self.onCancel = onCancel self.onConfirm = onConfirm + self.onSecond = onSecond super.init(nibName: nil, bundle: nil) modalPresentationStyle = .overFullScreen @@ -273,7 +283,38 @@ class OpenChatAlertViewController: UIViewController { let buttonStack: UIStackView var buttonDividerConstraints: [NSLayoutConstraint] = [] - if let confirmTitle { + if let confirmTitle, let secondTitle { + // Three buttons (a sibling action is present) — always vertical + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + + let secondButton = UIButton(type: .system) + secondButton.setTitle(secondTitle, for: .normal) + secondButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) + secondButton.addTarget(self, action: #selector(secondTapped), for: .touchUpInside) + + buttonStack = UIStackView(arrangedSubviews: [confirmButton, secondButton, cancelButton]) + buttonStack.axis = .vertical + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * 3).isActive = true + + for button in [secondButton, cancelButton] { + let divider = UIView() + divider.backgroundColor = UIColor.separator + divider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(divider) + buttonDividerConstraints += [ + divider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + divider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + divider.bottomAnchor.constraint(equalTo: button.topAnchor), + divider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale) + ] + } + } else if let confirmTitle { let confirmButton = UIButton(type: .system) confirmButton.setTitle(confirmTitle, for: .normal) confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body) @@ -368,6 +409,12 @@ class OpenChatAlertViewController: UIViewController { self.onConfirm?() } } + + @objc private func secondTapped() { + dismiss(animated: true) { + self.onSecond?() + } + } } @@ -381,8 +428,10 @@ func showOpenChatAlert( information: String? = nil, cancelTitle: String = "Cancel", confirmTitle: String? = "Open", + secondTitle: String? = nil, onCancel: @escaping () -> Void = {}, - onConfirm: (() -> Void)? = nil + onConfirm: (() -> Void)? = nil, + onSecond: (() -> Void)? = nil ) { let themedView = profileImage.environmentObject(theme) let hostingController = UIHostingController(rootView: themedView) @@ -399,8 +448,10 @@ func showOpenChatAlert( information: information, cancelTitle: cancelTitle, confirmTitle: confirmTitle, + secondTitle: secondTitle, onCancel: onCancel, - onConfirm: onConfirm + onConfirm: onConfirm, + onSecond: onSecond ) topVC.present(alertVC, animated: true) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index f99b03086e..416dc32308 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -41,6 +41,8 @@ struct NewChatSheet: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil + // when the search text is a SimpleX name, the string to connect to (with @/# preserved); nil otherwise + @State private var connectNameCandidate: String? = nil @State private var alert: SomeAlert? // Sheet height management @@ -81,15 +83,25 @@ struct NewChatSheet: View { private func viewBody(_ showArchive: Bool) -> some View { List { - HStack { + VStack(spacing: 12) { ContactsListSearchBar( searchMode: $searchMode, searchFocussed: $searchFocussed, searchText: $searchText, searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + connectNameCandidate: $connectNameCandidate ) .frame(maxWidth: .infinity) + if let candidate = connectNameCandidate { + ConnectByNameRow( + name: candidate, + searchText: $searchText, + connectNameCandidate: $connectNameCandidate, + searchFocussed: $searchFocussed, + dismiss: true + ) + } } .listRowSeparator(.hidden) .listRowBackground(Color.clear) @@ -129,7 +141,7 @@ struct NewChatSheet: View { .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - Label("Create public channel (BETA)", systemImage: "antenna.radiowaves.left.and.right") + Label("Create public channel", systemImage: "antenna.radiowaves.left.and.right") } } @@ -327,6 +339,7 @@ struct ContactsListSearchBar: View { @Binding var searchText: String @Binding var searchShowingSimplexLink: Bool @Binding var searchChatFilteredBySimplexLink: String? + @Binding var connectNameCandidate: String? @State private var ignoreSearchTextChange = false @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @@ -381,24 +394,32 @@ struct ContactsListSearchBar: View { if ignoreSearchTextChange { ignoreSearchTextChange = false } else { - switch strConnectTarget(t.trimmingCharacters(in: .whitespaces)) { + let s = t.trimmingCharacters(in: .whitespaces) + switch strConnectTarget(s) { case let .link(text, _, linkText): searchFocussed = false ignoreSearchTextChange = true searchText = linkText searchShowingSimplexLink = true searchChatFilteredBySimplexLink = nil + connectNameCandidate = nil connect(text) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) - case .none: - if t != "" { - searchFocussed = true - } else { - connectProgressManager.cancelConnectProgress() + default: + // A name is resolved only when its "Connect to …" row is tapped, not on every keystroke. + // The simplex-name filter is chat-list only: this contacts/deleted view is a scoped + // subset, so a resolved chat id (channel, business, unlisted or active-only contact) + // may not be present in it. + let candidate = nameSearchCandidate(s) + connectNameCandidate = candidate + if candidate == nil { + if t != "" { + searchFocussed = true + } else { + connectProgressManager.cancelConnectProgress() + } + searchShowingSimplexLink = false + searchChatFilteredBySimplexLink = nil } - searchShowingSimplexLink = false - searchChatFilteredBySimplexLink = nil } } } @@ -440,7 +461,9 @@ struct DeletedChats: View { @State private var searchText = "" @State private var searchShowingSimplexLink = false @State private var searchChatFilteredBySimplexLink: String? = nil - + // deleted contacts are not connected to by name, so this candidate only stops per-keystroke resolution + @State private var connectNameCandidate: String? = nil + var body: some View { List { ContactsListSearchBar( @@ -448,7 +471,8 @@ struct DeletedChats: View { searchFocussed: $searchFocussed, searchText: $searchText, searchShowingSimplexLink: $searchShowingSimplexLink, - searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + connectNameCandidate: $connectNameCandidate ) .listRowSeparator(.hidden) .listRowBackground(Color.clear) diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 67fd353ebc..51746766bd 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -204,8 +204,7 @@ struct NewChatView: View { creatingConnReq = true Task { _ = try? await Task.sleep(nanoseconds: 250_000000) - let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get()) - if let (connLink, pcc) = r { + if let (connLink, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { await MainActor.run { m.updateContactConnection(pcc) m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) @@ -215,9 +214,6 @@ struct NewChatView: View { } else { await MainActor.run { creatingConnReq = false - if let apiAlert = apiAlert { - alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) - } } } } @@ -434,15 +430,9 @@ private struct ActiveProfilePicker: View { profileSwitchStatus = .idle incognitoEnabled = !incognito logger.error("apiSetConnectionIncognito error: \(responseError(error))") - let err = getErrorAlert(error, "Error changing to incognito!") - - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "setConnectionIncognitoError" - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error changing to incognito!", comment: "")) + } } } } @@ -494,14 +484,7 @@ private struct ActiveProfilePicker: View { if let currentUser = chatModel.currentUser { selectedProfile = currentUser } - let err = getErrorAlert(error, "Error changing connection profile") - alert = SomeAlert( - alert: Alert( - title: Text(err.title), - message: Text(err.message ?? "Error: \(responseError(error))") - ), - id: "changeConnectionUserError" - ) + showErrorAlert(error, NSLocalizedString("Error changing connection profile", comment: "")) } } } @@ -669,8 +652,9 @@ private struct ConnectView: View { case let .link(text, _, _): pastedLink = text connect(pastedLink) - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) + case let .name(text, _): + pastedLink = text + connect(pastedLink) case .none: alert = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."), @@ -869,37 +853,23 @@ func strIsSimplexLink(_ str: String) -> Bool { enum ConnectTarget { case link(text: String, linkType: SimplexLinkType, linkText: String) - case name(SimplexNameInfo) + case name(text: String, nameInfo: SimplexNameInfo) } func strConnectTarget(_ str: String) -> ConnectTarget? { let parsedMd = parseSimpleXMarkdown(str) let links = parsedMd?.filter { $0.format?.isSimplexLink ?? false } ?? [] - return if links.count == 1, case let .simplexLink(_, linkType, _, smpHosts) = links[0].format { - .link(text: links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts)) + return if links.count == 1, case let .simplexLink(showText, linkType, simplexUri, smpHosts) = links[0].format { + .link(text: showText != nil ? simplexUri : links[0].text, linkType: linkType, linkText: simplexLinkText(linkType, smpHosts)) } else if links.isEmpty, - case let .simplexName(nameInfo) = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } })?.format { - .name(nameInfo) + let nameFt = parsedMd?.first(where: { if case .simplexName = $0.format { true } else { false } }), + case let .simplexName(nameInfo) = nameFt.format { + .name(text: nameFt.text, nameInfo: nameInfo) } else { nil } } -func showUnsupportedNameAlert(_ nameInfo: SimplexNameInfo) { - let upgrade = " " + NSLocalizedString("Please upgrade the app.", comment: "alert message") - if nameInfo.nameType == .contact { - showAlert( - NSLocalizedString("Unsupported contact name", comment: "alert title"), - message: NSLocalizedString("Connecting via contact name requires a newer app version.", comment: "alert message") + upgrade - ) - } else { - showAlert( - NSLocalizedString("Unsupported channel name", comment: "alert title"), - message: NSLocalizedString("Connecting via channel name requires a newer app version.", comment: "alert message") + upgrade - ) - } -} - struct IncognitoToggle: View { @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @@ -1145,6 +1115,9 @@ private func showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = nil, + verifiedDomain: SimplexDomain? = nil, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1167,11 +1140,12 @@ private func showPrepareContactAlert( information: ownerVerificationMessage(ownerVerification), cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), confirmTitle: NSLocalizedString("Open new chat", comment: "new chat action"), + secondTitle: connectOtherButton, onCancel: { cleanup?() }, onConfirm: { Task { do { - let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData) + let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData, verifiedDomain: verifiedDomain) await MainActor.run { ChatModel.shared.addChat(Chat(chat)) openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup) @@ -1184,6 +1158,9 @@ private func showPrepareContactAlert( } } } + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } } ) } @@ -1193,6 +1170,9 @@ private func showPrepareGroupAlert( groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = nil, + verifiedDomain: SimplexDomain? = nil, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil, theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? @@ -1217,11 +1197,12 @@ private func showPrepareGroupAlert( confirmTitle: isChannel ? NSLocalizedString("Open new channel", comment: "new chat action") : NSLocalizedString("Open new group", comment: "new chat action"), + secondTitle: connectOtherButton, onCancel: { cleanup?() }, onConfirm: { Task { do { - let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData) + let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData, verifiedDomain: verifiedDomain) await MainActor.run { if let relays = groupShortLinkInfo?.groupRelays, !relays.isEmpty, case let .group(gInfo, _) = chat.chatInfo { @@ -1238,6 +1219,9 @@ private func showPrepareGroupAlert( } } } + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss, cleanup: cleanup) } } ) } @@ -1245,7 +1229,9 @@ private func showPrepareGroupAlert( private func showOpenKnownContactAlert( _ contact: Contact, theme: AppTheme, - dismiss: Bool + dismiss: Bool, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil ) { showOpenChatAlert( profileName: contact.profile.displayName, @@ -1263,8 +1249,12 @@ private func showOpenKnownContactAlert( contact.nextConnectPrepared ? NSLocalizedString("Open new chat", comment: "new chat action") : NSLocalizedString("Open chat", comment: "new chat action"), + secondTitle: connectOtherButton, onConfirm: { openKnownContact(contact, dismiss: dismiss, cleanup: nil) + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss) } } ) } @@ -1272,7 +1262,9 @@ private func showOpenKnownContactAlert( private func showOpenKnownGroupAlert( _ groupInfo: GroupInfo, theme: AppTheme, - dismiss: Bool + dismiss: Bool, + connectOtherButton: String? = nil, + connectOtherLink: String? = nil ) { let subscriberCount = groupInfo.groupSummary.publicMemberCount.map { "\($0) subscribers" } showOpenChatAlert( @@ -1302,8 +1294,12 @@ private func showOpenKnownGroupAlert( ? NSLocalizedString("Open new chat", comment: "new chat action") : NSLocalizedString("Open chat", comment: "new chat action") ), + secondTitle: connectOtherButton, onConfirm: { openKnownGroup(groupInfo, dismiss: dismiss, cleanup: nil) + }, + onSecond: connectOtherLink.map { link in + { planAndConnect(link, theme: theme, dismiss: dismiss) } } ) } @@ -1319,10 +1315,6 @@ func planAndConnect( filterKnownGroup: ((GroupInfo) -> Void)? = nil ) { switch strConnectTarget(shortOrFullLink) { - case let .name(nameInfo): - showUnsupportedNameAlert(nameInfo) - cleanup?() - return case let .link(_, linkType, _): if linkType == .relay { showAlert( @@ -1332,7 +1324,9 @@ func planAndConnect( cleanup?() return } - case .none: break + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + case .name, .none: break } ConnectProgressManager.shared.cancelConnectProgress() let inProgress = BoxedValue(true) @@ -1344,12 +1338,25 @@ func planAndConnect( func connectTask(_ inProgress: BoxedValue) { Task { - let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) + let result = await apiConnectPlan(connLink: shortOrFullLink, linkOwnerSig: linkOwnerSig, inProgress: inProgress) await MainActor.run { ConnectProgressManager.shared.stopConnectProgress() } if !inProgress.boxedValue { return } - if let (connectionLink, connectionPlan) = result { + if let result { + let connectionLink = result.connLink + let connectionPlan = result.connectionPlan + let planSimplexName = result.planSimplexName + // the name can also resolve to the other kind; its type picks the verb, its short form the label and target + let connectOtherLink = result.otherSimplexName?.shortStr + let connectOtherButton: String? = result.otherSimplexName.map { info in + String.localizedStringWithFormat( + info.nameType == .publicGroup + ? NSLocalizedString("Join channel %@", comment: "new chat action") + : NSLocalizedString("Connect to %@", comment: "new chat action"), + info.shortStr + ) + } switch connectionPlan { case let .invitationLink(ilp): switch ilp { @@ -1424,6 +1431,9 @@ func planAndConnect( connectionLink: connectionLink, contactShortLinkData: contactSLinkData, ownerVerification: ownerVerification, + verifiedDomain: planSimplexName?.nameDomain, + connectOtherButton: connectOtherButton, + connectOtherLink: connectOtherLink, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1472,16 +1482,19 @@ func planAndConnect( if let f = filterKnownContact { f(contact) } else { - showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .known(contact): logger.debug("planAndConnect, .contactAddress, .known") await MainActor.run { + if ChatModel.shared.getContactChat(contact.contactId) == nil { + ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact))) + } if let f = filterKnownContact { f(contact) } else { - showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss) + showOpenKnownContactAlert(contact, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .contactViaAddress(contact): @@ -1505,6 +1518,9 @@ func planAndConnect( groupShortLinkInfo: groupShortLinkInfo_, groupShortLinkData: groupSLinkData, ownerVerification: ownerVerification, + verifiedDomain: planSimplexName?.nameDomain, + connectOtherButton: connectOtherButton, + connectOtherLink: connectOtherLink, theme: theme, dismiss: dismiss, cleanup: cleanup @@ -1557,10 +1573,13 @@ func planAndConnect( case let .known(groupInfo): logger.debug("planAndConnect, .groupLink, .known") await MainActor.run { + if ChatModel.shared.getGroupChat(groupInfo.groupId) == nil { + ChatModel.shared.addChat(Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil))) + } if let f = filterKnownGroup { f(groupInfo) } else { - showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss) + showOpenKnownGroupAlert(groupInfo, theme: theme, dismiss: dismiss, connectOtherButton: connectOtherButton, connectOtherLink: connectOtherLink) } } case let .noRelays(groupSLinkData_): @@ -1628,17 +1647,8 @@ func planAndConnect( cleanup: cleanup ) } - } else { - await MainActor.run { - if let alert { - dismissAllSheets(animated: true) { - AlertManager.shared.showAlert(alert) - cleanup?() - } - } else { - cleanup?() - } - } + } else if let cleanup { + await MainActor.run { cleanup() } } } } diff --git a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift index b61b81a46b..2c21682dba 100644 --- a/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift +++ b/apps/ios/Shared/Views/Onboarding/ChooseServerOperators.swift @@ -184,7 +184,14 @@ struct OnboardingConditionsView: View { private func completeOnboarding() { let m = ChatModel.shared onboardingStageDefault.set(.onboardingComplete) - m.onboardingStage = .onboardingComplete + // defer the stage swap off the Accept handler's call stack so the deep onboarding nav stack + // isn't torn down from inside its own event handling (UIKit crash on completion); the inner + // async guarantees this even if dismissAllSheets runs its completion synchronously. + dismissAllSheets(animated: false) { + DispatchQueue.main.async { + m.onboardingStage = .onboardingComplete + } + } } private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? { diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 3c33546436..bd9530c4ee 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -372,6 +372,8 @@ struct CreateFirstProfile: View { do { AppChatState.shared.set(.active) m.currentUser = try apiCreateActiveUser(profile) + // new users don't need the local file encryption indicator (all files are encrypted); existing users keep it on + UserDefaults.standard.set(false, forKey: DEFAULT_PRIVACY_SHOW_FILE_ENCRYPTION) try startChat(onboarding: true) onboardingStageDefault.set(.step3_ChooseServerOperators) nextStepNavLinkActive = true diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index ab84bed7df..b348057b8a 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -86,12 +86,10 @@ struct CreateSimpleXAddress: View { await MainActor.run { progressIndicator = false } } catch let error { logger.error("CreateSimpleXAddress create address: \(responseError(error))") - await MainActor.run { progressIndicator = false } - let a = getErrorAlert(error, "Error creating address") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } label: { @@ -156,11 +154,7 @@ struct CreateSimpleXAddress: View { } case let .failure(error): logger.error("CreateSimpleXAddress share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 41a342d7c8..937a0f089c 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -8,6 +8,7 @@ // Spec: spec/client/navigation.md import SwiftUI +import StoreKit import SimpleXChat private struct VersionDescription { @@ -41,6 +42,8 @@ private struct FeatureView { let view: () -> any View } +let isInUS = SKStorefront().countryCode == "USA" + private let versionDescriptions: [VersionDescription] = [ VersionDescription( version: "v4.2", @@ -664,6 +667,34 @@ private let versionDescriptions: [VersionDescription] = [ )) ] ), + VersionDescription( + version: "v7.0 ", + post: nil, + features: (isInUS ? [ + .view(FeatureView( + icon: nil, + title: "You can now invest in SimpleX Chat", + view: { InvestInSimpleXChat() } + )) + ] : []) + [ + .feature(Description( + icon: "at", + title: "SimpleX public names (BETA)", + description: "Public names for your channel or business." + )), + .feature(Description( + icon: nil, + title: "Better channels 📢", + description: nil, + subfeatures: [ + ("person.badge.plus", "Add contributors."), + ("globe", "Create web preview."), + ("server.rack", "Manage your relays."), + ("text.alignleft", "Easier to read."), + ] + )) + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -740,6 +771,134 @@ fileprivate struct CreateUpdateAddressShortLink: View { } } +fileprivate struct InvestInSimpleXChat: View { + @EnvironmentObject var theme: AppTheme + @State private var showGetStakeSheet = false + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text("You can now invest in SimpleX Chat! 🚀").font(.title3).bold() + (Text("Crowdfunding on Wefunder.") + Text(verbatim: " ") + Text("Learn more").foregroundColor(theme.colors.primary)) + .multilineTextAlignment(.leading) + .onTapGesture { showGetStakeSheet = true } + #if SIMPLEX_ASSETS + Image("crowdfunding_1") + .resizable() + .scaledToFit() + .cornerRadius(12) + .padding(.vertical, 4) + .onTapGesture { showGetStakeSheet = true } + #endif + } + .frame(maxWidth: .infinity, alignment: .leading) + .sheet(isPresented: $showGetStakeSheet) { + GetStakeView(fromSettings: false) + } + } +} + +fileprivate let getStakeSlides: [(image: String, heading: String, info: String?, text: String)] = [ + ( + "crowdfunding_1", + "The first and the only messaging network without any user IDs", + nil, + "By investing, you can benefit from the company growth, and help us build the future of private and secure communications." + ), + ( + "crowdfunding_2", + "480,000+ users joined on their own", + nil, + "SimpleX users have been more than doubling every year without any paid marketing, and donated over $650,000." + ), + ( + "crowdfunding_3", + "Developers already bet on SimpleX success", + "Independent developers created moderation and AI bots, Telegram bridges, and a public server registry.", + "Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat." + ), + ( + "crowdfunding_4", + "Revenue plan: free for users, channels & businesses pay", + "SimpleX Chat plans to earn from the infrastructure and services that creators, businesses and large communities need as they grow.", + "Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms on Wefunder." + ), +] + +private let wefunderURL = URL(string: "https://wefunder.com/simplex.chat?utm_source=app")! + +private let simplexCrowdfundingURL = URL(string: "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im")! + +struct GetStakeView: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var chatModel: ChatModel + var fromSettings: Bool + + var body: some View { + ZoomablePageView { + VStack(alignment: .leading, spacing: 18) { + Text(verbatim: "Get a stake in\nSimpleX Chat") + .font(.largeTitle) + .bold() + .fixedSize(horizontal: false, vertical: true) + .if(!fromSettings) { $0.padding(.top) } + if fromSettings { + slideImage(getStakeSlides[0]) + } + (Text(verbatim: getStakeSlides[0].text) + Text(verbatim: " Learn more and invest on Wefunder.").bold().foregroundColor(.accentColor)) + .multilineTextAlignment(.leading) + .onTapGesture { + UIApplication.shared.open(wefunderURL) + } + .padding(.bottom) + ForEach(getStakeSlides[1...3], id: \.image) { slide in + VStack(alignment: .leading) { + slideImage(slide) + Text(slide.text) + } + .padding(.bottom) + } + + Button { + UIApplication.shared.open(wefunderURL) + } label: { + Text(verbatim: "Learn more on Wefunder") + } + .buttonStyle(OnboardingButtonStyle()) + + Button { + dismiss() + DispatchQueue.main.async { + ChatModel.shared.appOpenUrl = simplexCrowdfundingURL + } + } label: { + Text(verbatim: "or ask SimpleX team") + .font(.callout) + } + .disabled(chatModel.chatRunning != true) + .frame(maxWidth: .infinity) + } + .padding() + } + .ignoresSafeArea(edges: .bottom) + .modifier(ThemedBackground(grouped: true)) + } + + @ViewBuilder + func slideImage(_ slide: (image: String, heading: String, info: String?, text: String?)) -> some View { + #if SIMPLEX_ASSETS + Image(slide.image) + .resizable() + .scaledToFit() + .cornerRadius(12) + #else + Text(slide.heading).font(.title3).bold() + if let info = slide.info { + Text(info) + } + #endif + } +} + private enum WhatsNewViewSheet: Identifiable { case showConditions diff --git a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift index d3727e196e..015a2be491 100644 --- a/apps/ios/Shared/Views/Onboarding/YourNetwork.swift +++ b/apps/ios/Shared/Views/Onboarding/YourNetwork.swift @@ -180,11 +180,9 @@ struct YourNetworkView: View { m.notificationMode = notificationMode } } catch let error { - let a = getErrorAlert(error, "Error enabling notifications") - AlertManager.shared.showAlertMsg( - title: a.title, - message: a.message - ) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error enabling notifications", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 01b25baed8..24ae5cffca 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -535,8 +535,7 @@ struct ConnectDesktopView: View { } private func errorAlert(_ error: Error) { - let a = getErrorAlert(error, "Error") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error", comment: "")) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index f10b945dc0..8095c0297e 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -111,23 +111,16 @@ struct NetworkAndServers: View { Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) }) .disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors)) } footer: { - if let errStr = globalServersError(ss.servers.serverErrors) { - ServersErrorView(errStr: errStr) + let errs = globalServersErrors(ss.servers.serverErrors) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } } else if !ss.servers.serverErrors.isEmpty { ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error")) } - if let warnStr = globalServersWarning(ss.servers.serverWarnings) { - ServersWarningView(warnStr: warnStr) - } - } - - Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) { - NavigationLink { - RTCServers() - .navigationTitle("Your ICE servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - Text("WebRTC ICE servers") + ForEach(globalServersWarnings(ss.servers.serverWarnings), id: \.self) { warn in + ServersWarningView(warnStr: warn) } } @@ -407,17 +400,12 @@ struct ServersWarningView: View { } } -func globalServersError(_ serverErrors: [UserServersError]) -> String? { - for err in serverErrors { - if let errStr = err.globalError { - return errStr - } - } - return nil +func globalServersErrors(_ serverErrors: [UserServersError]) -> [String] { + serverErrors.compactMap { $0.globalError } } -func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? { - for warn in serverWarnings { +func globalServersWarnings(_ serverWarnings: [UserServersWarning]) -> [String] { + serverWarnings.map { warn in switch warn { case let .noChatRelays(user): let text = NSLocalizedString("No chat relays enabled.", comment: "servers warning") @@ -427,9 +415,16 @@ func globalServersWarning(_ serverWarnings: [UserServersWarning]) -> String? { user.localDisplayName ) + " " + text } else { return text } + case let .noNamesServers(user): + let text = NSLocalizedString("No servers to resolve names.", comment: "servers warning") + if let user = user { + return String.localizedStringWithFormat( + NSLocalizedString("For chat profile %@:", comment: "servers warning"), + user.localDisplayName + ) + " " + text + } else { return text } } } - return nil } func bindingForChatRelays(_ userServers: Binding<[UserOperatorServers]>, _ opIndex: Int) -> Binding<[UserChatRelay]> { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift index 26f24f2f0f..4e2f1992d6 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/OperatorView.swift @@ -52,10 +52,16 @@ struct OperatorView: View { Text("Operator") .foregroundColor(theme.colors.secondary) } footer: { - if let errStr = globalServersError(serverErrors) { - ServersErrorView(errStr: errStr) - } else if let warnStr = globalServersWarning(serverWarnings) { - ServersWarningView(warnStr: warnStr) + let errs = globalServersErrors(serverErrors) + let warns = globalServersWarnings(serverWarnings) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } + } else if !warns.isEmpty { + ForEach(warns, id: \.self) { warn in + ServersWarningView(warnStr: warn) + } } else { switch (userServers[operatorIndex].operator_.conditionsAcceptance) { case let .accepted(acceptedAt, _): @@ -105,6 +111,10 @@ struct OperatorView: View { .onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in validateServers_($userServers, $serverErrors, $serverWarnings) } + Toggle("To resolve names", isOn: $userServers[operatorIndex].operator_.smpRoles.names) + .onChange(of: userServers[operatorIndex].operator_.smpRoles.names) { _ in + validateServers_($userServers, $serverErrors, $serverWarnings) + } } header: { Text("Use for messages") .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift index 5299b7d415..b953ade0a6 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift @@ -81,6 +81,9 @@ struct ProtocolServerView: View { .textSelection(.enabled) } useServerSection(true) + if let inherited = serverRolesInherited { + serverRolesSection(inherited: inherited) + } } } } @@ -110,6 +113,9 @@ struct ProtocolServerView: View { } } useServerSection(valid) + if let inherited = serverRolesInherited { + serverRolesSection(inherited: inherited) + } if valid { Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { MutableQRCode(uri: $serverToEdit.server, small: true) @@ -120,6 +126,33 @@ struct ProtocolServerView: View { } } + // inherited SMP roles for the per-server roles section, nil when the section should not be shown + private var serverRolesInherited: ServerRoles? { + guard let (serverProtocol, serverOperator) = serverProtocolAndOperator(serverToEdit, userServers), + serverProtocol == .smp && serverToEdit.enabled, !serverToEdit.preset || serverToEdit.roles != ServerRolesOverride() + else { return nil } + return serverOperator?.smpRoles ?? ServerRoles.noOperatorDefault + } + + private func serverRolesSection(inherited: ServerRoles) -> some View { + Section { + rolePicker("To receive", $serverToEdit.roles.storage, defaultOn: inherited.storage) + rolePicker("For private routing", $serverToEdit.roles.proxy, defaultOn: inherited.proxy) + rolePicker("To resolve names", $serverToEdit.roles.names, defaultOn: inherited.names) + } header: { + Text("Use for messages").foregroundColor(theme.colors.secondary) + } + } + + private func rolePicker(_ title: LocalizedStringKey, _ selection: Binding, defaultOn: Bool) -> some View { + Picker(title, selection: selection) { + Text(String.localizedStringWithFormat(NSLocalizedString("default (%@)", comment: "pref value"), NSLocalizedString(defaultOn ? "yes" : "no", comment: "pref value"))).tag(Bool?.none) + Text("yes").tag(Bool?.some(true)) + Text("no").tag(Bool?.some(false)) + } + .frame(height: 36) + } + private func useServerSection(_ valid: Bool) -> some View { Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) { HStack { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift index b059be7cb0..a92491edef 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift @@ -169,10 +169,16 @@ struct YourServersView: View { .hidden() } } footer: { - if let errStr = globalServersError(serverErrors) { - ServersErrorView(errStr: errStr) - } else if let warnStr = globalServersWarning(serverWarnings) { - ServersWarningView(warnStr: warnStr) + let errs = globalServersErrors(serverErrors) + let warns = globalServersWarnings(serverWarnings) + if !errs.isEmpty { + ForEach(errs, id: \.self) { err in + ServersErrorView(errStr: err) + } + } else if !warns.isEmpty { + ForEach(warns, id: \.self) { warn in + ServersWarningView(warnStr: warn) + } } } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index c4d0588987..131eeecef7 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -63,36 +63,6 @@ struct NotificationsView: View { } } - NavigationLink { - List { - Section { - SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in - ntfPreviewModeGroupDefault.set(previewMode) - m.notificationPreview = previewMode - } - } footer: { - VStack(alignment: .leading, spacing: 1) { - Text("You can set lock screen notification preview via settings.") - .foregroundColor(theme.colors.secondary) - Button("Open Settings") { - DispatchQueue.main.async { - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - } - } - } - } - } - .navigationTitle("Show preview") - .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.inline) - } label: { - HStack { - Text("Show preview") - Spacer() - Text(m.notificationPreview.label) - } - } - if let server = m.notificationServer { smpServers("Push server", [server], theme.colors.secondary) testTokenButton(server) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 3ae9f0eacd..d891efcd90 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -16,7 +16,10 @@ struct PrivacySettings: View { @AppStorage(GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS, store: groupDefaults) private var useLinkPreviews = true @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + @AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES) private var verifySimplexNames = false @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + @AppStorage(DEFAULT_PRIVACY_SHOW_SIGNATURE) private var showSignature = true + @AppStorage(DEFAULT_PRIVACY_SHOW_FILE_ENCRYPTION) private var showFileEncryption = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -81,30 +84,12 @@ struct PrivacySettings: View { settingsRow("link", color: theme.colors.secondary) { Toggle("Remove link tracking", isOn: $privacySanitizeLinks) } - settingsRow("message", color: theme.colors.secondary) { - Toggle("Show last messages", isOn: $showChatPreviews) - } - settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { - Toggle("Message draft", isOn: $saveLastDraft) - } - .onChange(of: saveLastDraft) { saveDraft in - if !saveDraft { - m.draft = nil - m.draftChatId = nil - } - } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) } Section { - settingsRow("lock.doc", color: theme.colors.secondary) { - Toggle("Encrypt local files", isOn: $encryptLocalFiles) - .onChange(of: encryptLocalFiles) { - setEncryptLocalFiles($0) - } - } settingsRow("photo", color: theme.colors.secondary) { Toggle("Auto-accept images", isOn: $autoAcceptImages) .onChange(of: autoAcceptImages) { @@ -126,20 +111,9 @@ struct PrivacySettings: View { } } } - settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { - Toggle("Protect IP address", isOn: $askToApproveRelays) - } } header: { Text("Files") .foregroundColor(theme.colors.secondary) - } footer: { - if askToApproveRelays { - Text("The app will ask to confirm downloads from unknown file servers (except .onion).") - .foregroundColor(theme.colors.secondary) - } else { - Text("Without Tor or VPN, your IP address will be visible to file servers.") - .foregroundColor(theme.colors.secondary) - } } Section { @@ -155,46 +129,164 @@ struct PrivacySettings: View { } Section { - settingsRow("person", color: theme.colors.secondary) { - Toggle("Contacts", isOn: $contactReceipts) + NavigationLink(destination: morePrivacyView) { + settingsRow("ellipsis", color: theme.colors.secondary) { Text("More privacy") } } - settingsRow("person.2", color: theme.colors.secondary) { - Toggle("Small groups (max 20)", isOn: $groupReceipts) - } - } header: { - Text("Send delivery receipts to") - .foregroundColor(theme.colors.secondary) - } footer: { - VStack(alignment: .leading) { - Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") - Text("They can be overridden in contact and group settings.") + } + } + } + .onChange(of: autoAcceptMemberContacts) { _ in + if autoAcceptMemberContactsReset { + autoAcceptMemberContactsReset = false + } else { + setAutoAcceptGrpDirectInvs(autoAcceptMemberContacts) + } + } + .onAppear { + if let u = m.currentUser { + if autoAcceptMemberContacts != u.autoAcceptMemberContacts { + autoAcceptMemberContactsReset = true + autoAcceptMemberContacts = u.autoAcceptMemberContacts + } + } + } + .alert(item: $alert) { alert in + switch alert { + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + } + + @ViewBuilder + private func morePrivacyView() -> some View { + List { + Section { + settingsRow("message", color: theme.colors.secondary) { + Toggle("Show last messages", isOn: $showChatPreviews) + } + settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { + Toggle("Message draft", isOn: $saveLastDraft) + } + .onChange(of: saveLastDraft) { saveDraft in + if !saveDraft { + m.draft = nil + m.draftChatId = nil } + } + settingsRow("number", color: theme.colors.secondary) { + Toggle("Verify SimpleX names", isOn: $verifySimplexNames) + } + // hidden until message signing is user-facing (recipient-only stage) +// settingsRow("checkmark.seal", color: theme.colors.secondary) { +// Toggle("Show signature", isOn: $showSignature) +// } + } header: { + Text("Chats") .foregroundColor(theme.colors.secondary) - .frame(maxWidth: .infinity, alignment: .leading) + } + + Section { + settingsRow("lock.doc", color: theme.colors.secondary) { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + .onChange(of: encryptLocalFiles) { + setEncryptLocalFiles($0) + } } - .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { - Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { - setSendReceiptsContacts(contactReceipts, clearOverrides: false) + settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { + Toggle("Protect IP address", isOn: $askToApproveRelays) + } + settingsRow("lock", color: theme.colors.secondary) { + Toggle("Show encryption", isOn: $showFileEncryption) + } + } header: { + Text("Files") + .foregroundColor(theme.colors.secondary) + } footer: { + if askToApproveRelays { + Text("The app will ask to confirm downloads from unknown file servers (except .onion).") + .foregroundColor(theme.colors.secondary) + } else { + Text("Without Tor or VPN, your IP address will be visible to file servers.") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + NavigationLink { + List { + Section { + SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in + ntfPreviewModeGroupDefault.set(previewMode) + m.notificationPreview = previewMode + } + } footer: { + VStack(alignment: .leading, spacing: 1) { + Text("You can set lock screen notification preview via settings.") + .foregroundColor(theme.colors.secondary) + Button("Open Settings") { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + } + } + } } - Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { - setSendReceiptsContacts(contactReceipts, clearOverrides: true) - } - Button("Cancel", role: .cancel) { - contactReceiptsReset = true - contactReceipts.toggle() + .navigationTitle("Show preview") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + } label: { + HStack { + Text("Show preview") + Spacer() + Text(m.notificationPreview.label) } } - .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { - Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { - setSendReceiptsGroups(groupReceipts, clearOverrides: false) - } - Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) { - setSendReceiptsGroups(groupReceipts, clearOverrides: true) - } - Button("Cancel", role: .cancel) { - groupReceiptsReset = true - groupReceipts.toggle() - } + } header: { + Text("Notifications") + .foregroundColor(theme.colors.secondary) + } + + Section { + settingsRow("person", color: theme.colors.secondary) { + Toggle("Contacts", isOn: $contactReceipts) + } + settingsRow("person.2", color: theme.colors.secondary) { + Toggle("Small groups (max 20)", isOn: $groupReceipts) + } + } header: { + Text("Send delivery receipts to") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading) { + Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") + Text("They can be overridden in contact and group settings.") + } + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { + Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsContacts(contactReceipts, clearOverrides: false) + } + Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsContacts(contactReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + contactReceiptsReset = true + contactReceipts.toggle() + } + } + .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { + Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsGroups(groupReceipts, clearOverrides: false) + } + Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsGroups(groupReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + groupReceiptsReset = true + groupReceipts.toggle() } } } @@ -212,13 +304,6 @@ 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 { @@ -229,18 +314,10 @@ struct PrivacySettings: View { groupReceiptsReset = true groupReceipts = u.sendRcptsSmallGroups } - if autoAcceptMemberContacts != u.autoAcceptMemberContacts { - autoAcceptMemberContactsReset = true - autoAcceptMemberContacts = u.autoAcceptMemberContacts - } - } - } - .alert(item: $alert) { alert in - switch alert { - case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) } } + .navigationTitle("More privacy") + .modifier(ThemedBackground(grouped: true)) } private func setEncryptLocalFiles(_ enable: Bool) { diff --git a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift index e03dace43d..e46edbc5af 100644 --- a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift @@ -69,7 +69,7 @@ struct SetDeliveryReceiptsView: View { Button { AlertManager.shared.showAlert(Alert( title: Text("Delivery receipts are disabled!"), - message: Text("You can enable them later via app Privacy & Security settings."), + message: Text("You can enable them later via app Your privacy settings."), primaryButton: .default(Text("Don't show again")) { m.setDeliveryReceipts = false privacyDeliveryReceiptsSet.set(true) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 483ca6aea8..ae317cd864 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -32,6 +32,9 @@ let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" // unused. Use GROUP_D let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved to app group let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" +let DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES = "privacyVerifySimplexNames" +let DEFAULT_PRIVACY_SHOW_SIGNATURE = "privacyShowSignature" +let DEFAULT_PRIVACY_SHOW_FILE_ENCRYPTION = "privacyShowEncryption" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" @@ -56,6 +59,7 @@ let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown" let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial" let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab" let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown" +let DEFAULT_SIGN_MESSAGE_ALERT_SHOWN = "signMessageAlertShown" let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice" let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert" let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert" @@ -99,6 +103,7 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_LINK_PREVIEWS: true, DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, + DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES: false, DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, @@ -115,6 +120,7 @@ let appDefaults: [String: Any] = [ DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial, DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false, + DEFAULT_SIGN_MESSAGE_ALERT_SHOWN: false, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true, DEFAULT_SHOW_MUTE_PROFILE_ALERT: true, DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true, @@ -143,6 +149,7 @@ let hintDefaults = [ DEFAULT_ONE_HAND_UI_CARD_SHOWN, DEFAULT_ADDRESS_CREATION_CARD_SHOWN, DEFAULT_LIVE_MESSAGE_ALERT_SHOWN, + DEFAULT_SIGN_MESSAGE_ALERT_SHOWN, DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE, DEFAULT_SHOW_MUTE_PROFILE_ALERT, DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, @@ -290,47 +297,7 @@ struct SettingsView: View { func settingsView() -> some View { List { - let user = chatModel.currentUser - Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { - NavigationLink { - NotificationsView() - .navigationTitle("Notifications") - .modifier(ThemedBackground(grouped: true)) - } label: { - HStack { - notificationsIcon() - Text("Notifications") - } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - NetworkAndServers() - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - CallSettings() - .navigationTitle("Your calls") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - PrivacySettings() - .navigationTitle("Your privacy") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } - } - .disabled(chatModel.chatRunning != true) - + Section(header: Text(verbatim: "").foregroundColor(theme.colors.secondary)) { if UIApplication.shared.supportsAlternateIcons { NavigationLink { AppearanceSettings() @@ -341,10 +308,24 @@ struct SettingsView: View { } .disabled(chatModel.chatRunning != true) } - } - Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) { + NavigationLink { + PrivacySettings() + .navigationTitle("Your privacy") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("lock", color: theme.colors.secondary) { Text("Your privacy") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + helpAndSupportView + } label: { + settingsRow("questionmark", color: theme.colors.secondary) { Text("Help & support") } + } + chatDatabaseRow() + NavigationLink { MigrateFromDevice(showProgressOnSettings: $showProgress) .toolbar { @@ -360,6 +341,69 @@ struct SettingsView: View { } } + Section(header: Text("Advanced settings").foregroundColor(theme.colors.secondary)) { + NavigationLink { + NetworkAndServers() + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + NotificationsView() + .navigationTitle("Notifications") + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + notificationsIcon() + Text("Notifications") + } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + CallSettings() + .navigationTitle("Your calls") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + VersionView() + .navigationBarTitle("App version") + .modifier(ThemedBackground()) + } label: { + Text(verbatim: "v\(appVersion ?? "?")") + } + } + + if isInUS { + Section(header: Text("You can now invest in SimpleX Chat").foregroundColor(theme.colors.secondary)) { + NavigationLink { + GetStakeView(fromSettings: true) + .navigationBarTitle("", displayMode: .inline) + } label: { + settingsRow("dollarsign.circle", color: theme.colors.secondary) { Text("Crowdfunding on Wefunder") } + } + } + } + } + .navigationTitle("Your settings") + .modifier(ThemedBackground(grouped: true)) + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } + } + + @ViewBuilder + private var helpAndSupportView: some View { + List { + let user = chatModel.currentUser Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { if let user = user { NavigationLink { @@ -378,6 +422,7 @@ struct SettingsView: View { } label: { settingsRow("plus", color: theme.colors.secondary) { Text("What's new") } } + NavigationLink { SimpleXInfo(onboarding: false) .navigationBarTitle("", displayMode: .inline) @@ -386,6 +431,9 @@ struct SettingsView: View { } label: { settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") } } + } + + Section(header: Text("Contact").foregroundColor(theme.colors.secondary)) { settingsRow("number", color: theme.colors.secondary) { Button("Send questions and ideas") { dismiss() @@ -400,7 +448,7 @@ struct SettingsView: View { settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") } } - Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) { + Section(header: Text("Support the project").foregroundColor(theme.colors.secondary)) { settingsRow("keyboard", color: theme.colors.secondary) { ExternalLink("Contribute", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#contribute")!) } @@ -423,42 +471,21 @@ struct SettingsView: View { } } } - - Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) { - NavigationLink { - DeveloperView() - .navigationTitle("Developer tools") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") } - } - NavigationLink { - VersionView() - .navigationBarTitle("App version") - .modifier(ThemedBackground()) - } label: { - Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") - } - } } - .navigationTitle("Your settings") + .navigationTitle("Help & support") .modifier(ThemedBackground(grouped: true)) - .onDisappear { - chatModel.showingTerminal = false - chatModel.terminalItems = [] - } } - + private func chatDatabaseRow() -> some View { NavigationLink { DatabaseView(dismissSettingsSheet: dismiss, chatItemTTL: chatModel.chatItemTTL) - .navigationTitle("Your chat database") + .navigationTitle("Chat data") .modifier(ThemedBackground(grouped: true)) } label: { let color: Color = chatModel.chatDbEncrypted == false ? .orange : theme.colors.secondary settingsRow("internaldrive", color: color) { HStack { - Text("Database passphrase & export") + Text("Chat data") Spacer() if chatModel.chatRunning == false { Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index e22042fa24..b9048becda 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -25,6 +25,7 @@ struct UserAddressView: View { @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @State private var progressIndicator = false + @State private var showShareViaChat = false private enum UserAddressAlert: Identifiable { case deleteAddress @@ -156,6 +157,7 @@ struct UserAddressView: View { upgradeAddressButton() } shareAddressButton(userAddress) + shareViaChatButton(userAddress) // if MFMailComposeViewController.canSendMail() { // shareViaEmailButton(userAddress) // } @@ -191,6 +193,38 @@ struct UserAddressView: View { } } + Section { + NavigationLink { + let simplexName = if let d = chatModel.currentUser?.profile.contactDomain?.domain { "@\(d)" } else { "" } + SetSimplexDomainView( + title: "Your SimpleX name", + footer: "Let people connect to you via name registered with your SimpleX address.", + prompt: "@yourname.testing", + simplexName: simplexName, + broadcastWarning: NSLocalizedString("Profile update will be sent to your SimpleX contacts.", comment: "alert title"), + save: { simplexDomain in + do { + let u = try await apiSetUserDomain(simplexDomain) + await MainActor.run { chatModel.updateUser(u) } + return true + } catch { + return false + } + } + ) + } label: { + if let d = chatModel.currentUser?.profile.contactDomain?.domain { + Label("\(d)", systemImage: "at") + } else { + Label("Get SimpleX name (BETA)", systemImage: "at") + } + } + } header: { + if chatModel.currentUser?.profile.contactDomain?.domain != nil { + Text("Your SimpleX name").foregroundColor(theme.colors.secondary) + } + } + Section { createOneTimeLinkButton() } header: { @@ -293,9 +327,10 @@ struct UserAddressView: View { } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") - let a = getErrorAlert(error, "Error creating address") - alert = .error(title: a.title, error: a.message) - await MainActor.run { progressIndicator = false } + await MainActor.run { + progressIndicator = false + showErrorAlert(error, NSLocalizedString("Error creating address", comment: "")) + } } } } @@ -345,6 +380,32 @@ struct UserAddressView: View { } } + private func shareViaChatButton(_ userAddress: UserContactLink) -> some View { + Button { + if userAddress.shouldBeUpgraded { + showAlert( + NSLocalizedString("Upgrade address?", comment: "alert title"), + message: NSLocalizedString("The address will be short, and your profile will be shared via the address.", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Upgrade", comment: "alert button"), style: .default) { _ in + addShortLink(progressIndicator: $progressIndicator, onComplete: { showShareViaChat = true }) + }, + cancelAlertAction + ]} + ) + } else { + showShareViaChat = true + } + } label: { + settingsRow("arrowshape.turn.up.forward", color: theme.colors.primary) { + Text("Share via chat").foregroundColor(theme.colors.primary) + } + } + .sheet(isPresented: $showShareViaChat) { + shareAddressPicker() + } + } + private func shareViaEmailButton(_ userAddress: UserContactLink) -> some View { Button { showMailView = true @@ -367,8 +428,7 @@ struct UserAddressView: View { case .success: () case let .failure(error): logger.error("UserAddressView share via email: \(responseError(error))") - let a = getErrorAlert(error, "Error sending email") - alert = .error(title: a.title, error: a.message) + showErrorAlert(error, NSLocalizedString("Error sending email", comment: "")) } mailViewResult = nil } @@ -419,7 +479,60 @@ func upgradeAndShareAddressAlert(progressIndicator: Binding, shareAddress: ) } -private func addShortLink(progressIndicator: Binding, shareOnCompletion: Bool = false) { +@ViewBuilder +func shareAddressPicker(composeState: Binding? = nil) -> some View { + let v = ChatItemForwardingView( + title: "Share address", + isProhibited: { $0.prohibitedByPref(hasSimplexLink: true, isMediaOrFileAttachment: false, isVoice: false) }, + onSelectChat: { chat in shareMyAddress(chat, composeState: composeState) }, + includeLocal: false + ) + if #available(iOS 16.0, *) { + v.presentationDetents([.fraction(0.8)]) + } else { + v + } +} + +func shareMyAddress(_ destChat: Chat, composeState: Binding? = nil) { + let sendAsGroup = if let gInfo = destChat.chatInfo.groupInfo { gInfo.useRelays && gInfo.membership.memberRole >= .owner } else { false } + Task { + do { + let mc = try await apiShareMyAddress( + toChatType: destChat.chatInfo.chatType, toChatId: destChat.chatInfo.apiId, + toScope: destChat.chatInfo.groupChatScope(), sendAsGroup: sendAsGroup + ) + if case let .chat(_, chatLink, ownerSig) = mc { + await MainActor.run { + dismissAllSheets { + let cs = ComposeState(preview: .chatLinkPreview(chatLink: chatLink, ownerSig: ownerSig)) + if let composeState { + composeState.wrappedValue = cs + } else { + ChatModel.shared.draft = cs + ChatModel.shared.draftChatId = destChat.id + } + if destChat.id != ChatModel.shared.chatId { + ItemsModel.shared.loadOpenChat(destChat.id) + } + } + } + } else { + logger.error("shareMyAddress: unexpected MsgContent: \(String(describing: mc))") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing address", comment: "alert title"), message: String(describing: mc)) + } + } + } catch { + logger.error("shareMyAddress error: \(error.localizedDescription)") + await MainActor.run { + showAlert(NSLocalizedString("Error sharing address", comment: "alert title"), message: error.localizedDescription) + } + } + } +} + +private func addShortLink(progressIndicator: Binding, shareOnCompletion: Bool = false, onComplete: (() -> Void)? = nil) { progressIndicator.wrappedValue = true Task { do { @@ -430,6 +543,7 @@ private func addShortLink(progressIndicator: Binding, shareOnCompletion: B if shareOnCompletion, let userAddress { userAddress.shareAddress(short: true) } + onComplete?() } } catch let error { logger.error("apiAddMyAddressShortLink: \(responseError(error))") @@ -688,6 +802,172 @@ private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettin } } +struct SetSimplexDomainView: View { + let title: LocalizedStringKey + let footer: LocalizedStringKey + let prompt: String + @State var simplexName: String + let broadcastWarning: String? + let save: (String?) async -> Bool + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State private var saving = false + @State private var original = "" + @State private var didSave = false + @State private var editing = false + @FocusState private var nameFocused: Bool + + init(title: LocalizedStringKey, footer: LocalizedStringKey, prompt: String, simplexName: String, broadcastWarning: String? = nil, save: @escaping (String?) async -> Bool) { + self.title = title + self.footer = footer + self.prompt = prompt + self._simplexName = State(initialValue: simplexName) + self.broadcastWarning = broadcastWarning + self.save = save + self._original = State(initialValue: simplexName) + self._editing = State(initialValue: simplexName.isEmpty) + } + + private var changed: Bool { + normalized(simplexName) != normalized(original) + } + + private var isValid: Bool { + guard let d = normalized(simplexName) else { return true } + return isValidSimplexDomain(d) + } + + var body: some View { + List { + Section { + if editing { + ZStack(alignment: .trailing) { + TextField(prompt, text: $simplexName) + .focused($nameFocused) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .padding(.trailing, isValid ? 0 : 20) + if !isValid { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.red) + } + } + } else { + Button { + UIPasteboard.general.string = simplexName + } label: { + HStack { + Text(simplexName) + .foregroundColor(theme.colors.onBackground) + Spacer() + Image(systemName: "doc.on.doc") + .foregroundColor(theme.colors.secondary) + } + } + } + } header: { + Text(verbatim: "") + } footer: { + Text(footer).foregroundColor(theme.colors.secondary) + } + Section { + if editing { + Button { + openBrowserAlert(uri: "https://github.com/simplex-chat/simplex-chat/blob/master/docs/guide/register-simplex-name.md") + } label: { + HStack { + Text("How to register a test name") + Image(systemName: "arrow.up.right.circle") + } + } + Button { + if let w = broadcastWarning, changed { + showAlert(w, actions: {[ + UIAlertAction(title: NSLocalizedString("Save", comment: "alert action"), style: .default) { _ in saveAndDismiss() }, + UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert action"), style: .cancel) + ]}) + } else { + saveAndDismiss() + } + } label: { + Text("Save") + } + .disabled(saving || !isValid || !changed) + } else { + Button("Remove name") { + simplexName = "" + editing = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { nameFocused = true } + } + } + } + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.large) + .onAppear { + if editing { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { nameFocused = true } + } + } + .onDisappear { + if !didSave, !saving, changed, isValid { + let domain = normalized(simplexName) + let saveName = save + showAlert( + NSLocalizedString("Save SimpleX name?", comment: "alert title"), + message: broadcastWarning, + actions: {[ + UIAlertAction(title: NSLocalizedString("Save", comment: "alert action"), style: .default) { _ in + Task { _ = await saveName(domain) } + }, + UIAlertAction(title: NSLocalizedString("Don't save", comment: "alert action"), style: .cancel) + ]} + ) + } + } + } + + private func saveAndDismiss() { + saving = true + Task { + let ok = await save(normalized(simplexName)) + await MainActor.run { + saving = false + if ok { + didSave = true + dismiss() + } + } + } + } + + private func normalized(_ s: String) -> String? { + let t = s.trimmingCharacters(in: .whitespacesAndNewlines) + return t.isEmpty + ? nil + : addSimplexTLD((t.hasPrefix("@") || t.hasPrefix("#") ? String(t.dropFirst()) : t).lowercased()) + } + + private func addSimplexTLD(_ d: String) -> String { + if d.contains(".") { d } else { "\(d).simplex" } + } + + private func isValidSimplexDomain(_ s: String) -> Bool { + if s.utf8.count > 253 { return false } + let labels = s.split(separator: ".", omittingEmptySubsequences: false) + if labels.count < 2 { return false } + for label in labels { + if !isValidNameLabel(label) { return false } + } + return true + } + + private func isValidNameLabel(_ label: Substring) -> Bool { + if label.isEmpty || label.utf8.count > 63 { return false } + return label.range(of: "^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$", options: .regularExpression) != nil + } +} + struct UserAddressView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 2e609c3f7d..a2a62b557c 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -12,10 +12,13 @@ import SimpleXChat struct UserProfile: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme + @EnvironmentObject var ss: SaveableSettings @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner @State private var profile = Profile(displayName: "", fullName: "") @State private var currentProfileHash: Int? + @State private var loaded = false @State private var shortDescr = "" + @State private var description = "" // Modals @State private var showChooseSource = false @State private var showImagePicker = false @@ -54,6 +57,13 @@ struct UserProfile: View { } } } + NavigationLink { + ProfileDescriptionEditor(description: $description) + .navigationTitle("Description") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text(description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Add description" : "Edit description") + } } footer: { Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.") } @@ -64,7 +74,8 @@ struct UserProfile: View { } .disabled( currentProfileHash == profile.hashValue && - (profile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) + (profile.shortDescr ?? "") == shortDescr.trimmingCharacters(in: .whitespaces) && + (profile.description ?? "") == description.trimmingCharacters(in: .whitespacesAndNewlines) ) Button(action: saveProfile) { Text("Save (and notify contacts)") @@ -74,19 +85,13 @@ struct UserProfile: View { } // Lifecycle .onAppear { - getCurrentProfile() - } - .onDisappear { - if canSaveProfile { - showAlert( - title: NSLocalizedString("Save your profile?", comment: "alert title"), - message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"), - buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"), - buttonAction: saveProfile, - cancelButton: true - ) + // load once — returning from the description editor re-fires onAppear and would discard edits + if !loaded { + getCurrentProfile() + loaded = true } } + .onChange(of: editSnapshot) { _ in updateProfileSaver() } .onChange(of: chosenImage) { image in Task { let resized: String? = if let image { @@ -138,7 +143,8 @@ struct UserProfile: View { private var canSaveProfile: Bool { ( currentProfileHash != profile.hashValue || - (chatModel.currentUser?.profile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) + (chatModel.currentUser?.profile.shortDescr ?? "") != shortDescr.trimmingCharacters(in: .whitespaces) || + (chatModel.currentUser?.profile.description ?? "") != description.trimmingCharacters(in: .whitespacesAndNewlines) ) && profile.displayName.trimmingCharacters(in: .whitespaces) != "" && validDisplayName(profile.displayName) && @@ -151,10 +157,14 @@ struct UserProfile: View { do { profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) profile.shortDescr = shortDescr.trimmingCharacters(in: .whitespaces) + let d = description.trimmingCharacters(in: .whitespacesAndNewlines) + profile.description = d.isEmpty ? nil : d if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { await MainActor.run { chatModel.updateCurrentUser(newProfile) getCurrentProfile() + // onChange(editSnapshot) won't fire when saved values equal typed, so clear the pending dismiss-save here + ss.profileSave = nil } } else { alert = .duplicateUserError @@ -170,6 +180,34 @@ struct UserProfile: View { profile = fromLocalProfile(user.profile) currentProfileHash = profile.hashValue shortDescr = profile.shortDescr ?? "" + description = profile.description ?? "" + } + } + + private var editSnapshot: [String] { + [profile.displayName, profile.fullName, profile.image ?? "", shortDescr, description] + } + + private func updateProfileSaver() { + guard loaded, canSaveProfile else { + ss.profileSave = nil + return + } + var edited = profile + edited.displayName = profile.displayName.trimmingCharacters(in: .whitespaces) + edited.shortDescr = shortDescr.trimmingCharacters(in: .whitespaces) + let d = description.trimmingCharacters(in: .whitespacesAndNewlines) + edited.description = d.isEmpty ? nil : d + ss.profileSave = { + Task { + do { + if let (newProfile, _) = try await apiUpdateProfile(profile: edited) { + await MainActor.run { ChatModel.shared.updateCurrentUser(newProfile) } + } + } catch { + logger.error("UserProfile save on dismiss error: \(responseError(error))") + } + } } } } @@ -235,3 +273,43 @@ func editImageButton(action: @escaping () -> Void) -> some View { .frame(width: 48) } } + +struct ProfileDescriptionEditor: View { + @EnvironmentObject var theme: AppTheme + @Binding var description: String + @FocusState private var keyboardVisible: Bool + + var body: some View { + List { + Section { + if #available(iOS 16.0, *) { + TextField("Enter description (optional)", text: $description, axis: .vertical) + .lineLimit(6...12) + .focused($keyboardVisible) + } else { + // iOS 15 has no vertically-growing TextField (axis:) — fixed-height editor instead + ZStack { + Group { + if description.isEmpty { + TextEditor(text: Binding.constant(NSLocalizedString("Enter description (optional)", comment: "placeholder"))) + .foregroundColor(theme.colors.secondary) + .disabled(true) + } + TextEditor(text: $description) + .focused($keyboardVisible) + } + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 130, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + keyboardVisible = true + } + } + } +} diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index ad3b5cdf95..3f3adbcb2d 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -319,8 +319,9 @@ struct UserProfilesView: View { } } catch let error { logger.error("Error deleting user profile: \(error)") - let a = getErrorAlert(error, "Error deleting user profile") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error deleting user profile", comment: "")) + } } func deleteUser() async throws { @@ -436,8 +437,9 @@ struct UserProfilesView: View { } } } catch let error { - let a = getErrorAlert(error, "Error updating user privacy") - alert = .error(title: a.title, error: a.message) + await MainActor.run { + showErrorAlert(error, NSLocalizedString("Error updating user privacy", comment: "")) + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/VersionView.swift b/apps/ios/Shared/Views/UserSettings/VersionView.swift index 0fc2b4cb3e..e30c11699e 100644 --- a/apps/ios/Shared/Views/UserSettings/VersionView.swift +++ b/apps/ios/Shared/Views/UserSettings/VersionView.swift @@ -10,21 +10,33 @@ import SwiftUI import SimpleXChat struct VersionView: View { + @EnvironmentObject var theme: AppTheme @State var versionInfo: CoreVersionInfo? var body: some View { - VStack(alignment: .leading) { - Text("App version: v\(appVersion ?? "?")") - Text("App build: \(appBuild ?? "?")") - if let info = versionInfo { - Text("Core version: v\(info.version)") - if let v = try? AttributedString(markdown: "simplexmq: v\(info.simplexmqVersion) ([\(info.simplexmqCommit.prefix(7))](https://github.com/simplex-chat/simplexmq/commit/\(info.simplexmqCommit)))") { - Text(v) + List { + Section { + Text("App version: v\(appVersion ?? "?")") + Text("App build: \(appBuild ?? "?")") + if let info = versionInfo { + Text("Core version: v\(info.version)") + if let v = try? AttributedString(markdown: "simplexmq: v\(info.simplexmqVersion) ([\(info.simplexmqCommit.prefix(7))](https://github.com/simplex-chat/simplexmq/commit/\(info.simplexmqCommit)))") { + Text(v) + } + } + } + + Section { + NavigationLink { + DeveloperView() + .navigationTitle("Developer") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Developer") } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding() .onAppear { do { versionInfo = try apiGetVersion() diff --git a/apps/ios/Shared/Views/ZoomableScrollView.swift b/apps/ios/Shared/Views/ZoomableScrollView.swift index 83528b593a..87eb645822 100644 --- a/apps/ios/Shared/Views/ZoomableScrollView.swift +++ b/apps/ios/Shared/Views/ZoomableScrollView.swift @@ -58,3 +58,54 @@ struct ZoomableScrollView: UIViewRepresentable { } } } + +struct ZoomablePageView: UIViewRepresentable { + private var content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + func makeUIView(context: Context) -> UIScrollView { + let scrollView = UIScrollView() + scrollView.delegate = context.coordinator + scrollView.maximumZoomScale = 5 + scrollView.minimumZoomScale = 1 + scrollView.bouncesZoom = true + scrollView.backgroundColor = .clear + + let hostedView = context.coordinator.hostingController.view! + hostedView.backgroundColor = .clear + hostedView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(hostedView) + NSLayoutConstraint.activate([ + hostedView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), + hostedView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), + hostedView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + hostedView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + hostedView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) + ]) + + return scrollView + } + + func makeCoordinator() -> Coordinator { + Coordinator(hostingController: UIHostingController(rootView: self.content)) + } + + func updateUIView(_ uiView: UIScrollView, context: Context) { + context.coordinator.hostingController.rootView = self.content + } + + class Coordinator: NSObject, UIScrollViewDelegate { + var hostingController: UIHostingController + + init(hostingController: UIHostingController) { + self.hostingController = hostingController + } + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + hostingController.view + } + } +} diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 427430b833..cddb17337c 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -1157,8 +1157,8 @@ يطور No comment provided by engineer. - - Developer tools + + Developer أدوات المطور No comment provided by engineer. @@ -1930,12 +1930,12 @@ We will be adding server redundancy to prevent lost messages. Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2543,8 +2543,8 @@ We will be adding server redundancy to prevent lost messages. Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. @@ -5560,12 +5560,12 @@ This is your own one-time link! يتم تسليمها حتى عندما تسقطها شركة Apple. - Destination server address of %@ is incompatible with forwarding server %@ settings. - عنوان خادم الوجهة %@ غير متوافق مع إعدادات خادم التوجيه %@. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + عنوان خادم الوجهة %@ غير متوافق مع إعدادات خادم التوجيه %@. - Destination server version of %@ is incompatible with forwarding server %@. - إصدار خادم الوجهة لـ %@ غير متوافق مع خادم التوجيه %@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. + إصدار خادم الوجهة لـ %@ غير متوافق مع خادم التوجيه %@. Don't create address 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 364cee97e5..0b7e24f040 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ #тайно# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +89,10 @@ %@ изтеглено No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ е свързан! @@ -110,6 +118,10 @@ %@ сървъри No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ качено @@ -185,6 +197,18 @@ %d месеца time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -385,10 +409,6 @@ channel relay bar (ново) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (това устройство v%@) @@ -723,6 +743,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends Добави приятели @@ -743,6 +771,10 @@ swipe action Добави профил No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -766,6 +798,10 @@ swipe action Добави членове на екипа No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Добави към друго устройство @@ -846,6 +882,10 @@ swipe action Разширени мрежови настройки No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings Разширени настройки @@ -953,6 +993,10 @@ swipe action Позволи No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Позволи обаждания само ако вашият контакт ги разрешава. @@ -1125,6 +1169,10 @@ swipe action Отговор на повикване No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ Компилация на приложението: %@ @@ -1333,6 +1381,10 @@ swipe action Лош хеш на съобщението No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1351,6 +1403,10 @@ in your network По-добри обаждания No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups По-добри групи @@ -1538,11 +1594,6 @@ in your network Разговорът вече приключи! No comment provided by engineer. - - Calls - Обаждания - No comment provided by engineer. - Calls prohibited! Обажданията са забранени! @@ -1653,11 +1704,6 @@ new chat action Промяна на режима на заключване authentication reason - - Change member role? - Промяна на ролята на члена? - No comment provided by engineer. - Change passcode Промени kодa за достъп @@ -1678,6 +1724,10 @@ new chat action Промени ролята No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Промени режима на самоунищожение @@ -1693,6 +1743,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1734,6 +1788,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1775,6 +1833,10 @@ alert subtitle Конзола No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database База данни @@ -2142,6 +2204,10 @@ server test step Свържете се по-бързо! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Свързване с настолно устройство @@ -2235,14 +2301,6 @@ This is your own one-time link! Свързване с настолно устройство No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Връзка @@ -2258,16 +2316,15 @@ This is your own one-time link! Връзката е блокирана No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Грешка при свързване alert title - - Connection error (AUTH) - Грешка при свързване (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2279,6 +2336,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Грешка при свързване + conn error description + Connection not ready. Връзката не е готова. @@ -2321,6 +2383,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2404,6 +2470,10 @@ This is your own one-time link! Копирай No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error No comment provided by engineer. @@ -2437,6 +2507,10 @@ This is your own one-time link! Създаване група с автоматично създаден профил. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Създаване на файл @@ -2475,15 +2549,15 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Създай опашка server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -2985,16 +3059,16 @@ alert button Настолни устройства No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3005,20 +3079,15 @@ alert button Details No comment provided by engineer. - - Develop - Разработване + + Developer + Инструменти за разработчици No comment provided by engineer. Developer options No comment provided by engineer. - - Developer tools - Инструменти за разработчици - No comment provided by engineer. - Device Устройство @@ -3156,6 +3225,10 @@ alert button Отложи No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Не изпращай история на нови членове. @@ -3187,6 +3260,10 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + alert action + Don't show again Не показвай отново @@ -3261,6 +3338,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Редактирай @@ -3270,6 +3351,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Редактирай групов профил @@ -3461,6 +3546,10 @@ chat item action Въведи правилна парола. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Въведи име на групата… @@ -3499,6 +3588,10 @@ chat item action Въведи името на това устройство… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Въведи съобщение при посрещане… @@ -3517,7 +3610,7 @@ chat item action Error Грешка при свързване със сървъра - conn error description + No comment provided by engineer. Error aborting address change @@ -3791,6 +3884,10 @@ chat item action Грешка при запазване на профила на групата No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Грешка при запазване на кода за достъп @@ -3844,6 +3941,10 @@ chat item action Грешка при настройването на потвърждениeто за доставка!! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3919,6 +4020,7 @@ chat item action Error: %@ Грешка: %@ alert message +conn error description file error text snd error text @@ -4047,6 +4149,14 @@ server test error File server error: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status No comment provided by engineer. @@ -4321,6 +4431,10 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4425,6 +4539,10 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Съобщение при посрещане в групата @@ -4449,6 +4567,10 @@ Error: %2$@ Помощ No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. No comment provided by engineer. @@ -4524,6 +4646,10 @@ Error: %2$@ Информация No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Как се използва @@ -4909,6 +5035,10 @@ More improvements are coming soon! Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Италиански интерфейс @@ -4933,6 +5063,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Влез в групата @@ -5011,7 +5145,7 @@ This is your link for group %@! Learn more Научете повече - No comment provided by engineer. + badge alert button Leave @@ -5048,6 +5182,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5151,6 +5293,10 @@ This is your link for group %@! Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Маркирай като изтрито за всички @@ -5213,20 +5359,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5357,6 +5489,14 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. Източникът на съобщението остава скрит. @@ -5516,6 +5656,10 @@ This is your link for group %@! Очаквайте скоро още подобрения! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. По-надеждна мрежова връзка. @@ -5554,6 +5698,10 @@ This is your link for group %@! Име swipe action + + Name not found + No comment provided by engineer. + Network & servers Мрежа и сървъри @@ -5848,6 +5996,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5860,6 +6012,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5868,6 +6024,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6076,6 +6236,10 @@ Requires compatible VPN. Само вашият контакт може да изпраща гласови съобщения. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open Отвори @@ -6237,8 +6401,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6421,10 +6585,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6477,11 +6637,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - Поверителност и сигурност - No comment provided by engineer. - Privacy for your customers. No comment provided by engineer. @@ -6558,7 +6713,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6666,6 +6822,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Push известия @@ -6703,7 +6863,7 @@ Enable in *Network & servers* settings. Read more Прочетете още - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -6936,11 +7096,19 @@ swipe action Острани член? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Премахване на паролата от keychain? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7032,6 +7200,10 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Задължително @@ -7072,6 +7244,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Рестартирайте приложението, за да създадете нов чат профил @@ -7148,6 +7324,24 @@ swipe action Роля No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана. + No comment provided by engineer. + Run chat Стартиране на чат @@ -7177,7 +7371,8 @@ swipe action Save Запази - alert button + alert action +alert button chat item action @@ -7193,6 +7388,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? alert title @@ -7207,6 +7406,10 @@ chat item action Запази и уведоми членовете на групата No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7271,6 +7474,10 @@ chat item action Запази сървърите? alert title + + Save webpage settings? + alert title + Save welcome message? Запази съобщението при посрещане? @@ -7545,11 +7752,6 @@ chat item action Подателят отмени прехвърлянето на файла. alert message - - Sender may have deleted the connection request. - Подателят може да е изтрил заявката за връзка. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7638,6 +7840,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7911,6 +8117,10 @@ chat item action Покажи опциите за разработчици No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Показване на последните съобщения в листа с чатовете @@ -7938,6 +8148,31 @@ chat item action Покажи: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX No comment provided by engineer. @@ -8028,6 +8263,18 @@ chat item action SimpleX линковете не са разрешени No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Еднократна покана за SimpleX @@ -8037,6 +8284,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8275,9 +8526,8 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - Подкрепете SimpleX Chat + + Support the project No comment provided by engineer. @@ -8457,6 +8707,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8483,6 +8749,14 @@ It can happen because of some bug or when the connection is compromised.Опитът за промяна на паролата на базата данни не беше завършен. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. QR кодът, който сканирахте, не е SimpleX линк за връзка. @@ -8570,6 +8844,11 @@ your contacts and groups. Втората отметка, която пропуснахме! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Подателят може да е изтрил заявката за връзка. + No comment provided by engineer. + The sender will NOT be notified Подателят НЯМА да бъде уведомен @@ -8619,6 +8898,10 @@ your contacts and groups. Те могат да бъдат променени в настройките за всеки контакт и група. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени. @@ -8638,6 +8921,10 @@ your contacts and groups. Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Този чат е защитен чрез криптиране от край до край. @@ -8780,6 +9067,10 @@ You will be prompted to complete authentication before this feature is enabled.< За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**. @@ -8811,6 +9102,10 @@ You will be prompted to complete authentication before this feature is enabled.< За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Избор на инкогнито при свързване. @@ -8894,6 +9189,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8952,13 +9251,6 @@ You will be prompted to complete authentication before this feature is enabled.< Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. -За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка. - No comment provided by engineer. - Unlink Забрави @@ -8989,17 +9281,13 @@ To connect, please ask your contact to create another connection link and check Непрочетено swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9048,7 +9336,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -9204,6 +9493,10 @@ To connect, please ask your contact to create another connection link and check Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection No comment provided by engineer. @@ -9221,6 +9514,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Потвърди кода с настолното устройство @@ -9246,6 +9543,10 @@ To connect, please ask your contact to create another connection link and check Проверете паролата на базата данни No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Провери паролата @@ -9397,6 +9698,14 @@ To connect, please ask your contact to create another connection link and check WebRTC ICE сървъри No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Добре дошли %@! @@ -9605,9 +9914,8 @@ Repeat join request? Можете да активирате по-късно през Настройки No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9666,6 +9974,10 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Можете да включите SimpleX заключване през Настройки. @@ -9844,6 +10156,10 @@ Repeat connection request? Вашият адрес в SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9857,11 +10173,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - Вашата база данни - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Вашата база данни не е криптирана - задайте парола, за да я криптирате. @@ -9888,6 +10199,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. +За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@). @@ -10045,6 +10363,10 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10298,6 +10620,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator създател @@ -10496,6 +10822,10 @@ pref value часове time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain се използва за сигурно съхраняване на парола - позволява получаване на push известия. @@ -10908,6 +11238,10 @@ last received msg: %2$@ зачеркнат No comment provided by engineer. + + subscriber + member role + this contact този контакт @@ -10955,11 +11289,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -11108,7 +11437,7 @@ last received msg: %2$@
- +
@@ -11143,9 +11472,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11167,7 +11511,7 @@ last received msg: %2$@
- +
@@ -11194,7 +11538,7 @@ last received msg: %2$@
- +
@@ -11213,7 +11557,7 @@ last received msg: %2$@
- +
@@ -11360,8 +11704,8 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json index 66d64e6539..21627f8e60 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "bg", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index fbda1abd29..b7d12316d6 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -1223,8 +1223,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -2175,12 +2175,12 @@ Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -3041,8 +3041,8 @@ Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 5ba29ec846..0549fdc20b 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ #tajný# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +89,10 @@ %@ staženo No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ je připojen! @@ -110,6 +118,10 @@ %@ servery No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ nahrán @@ -185,6 +197,18 @@ %d měsíce time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -385,10 +409,6 @@ channel relay bar (nový) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (toto zařízení v%@) @@ -718,6 +738,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends Přidat přátele @@ -736,6 +764,10 @@ swipe action Přidat profil No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -759,6 +791,10 @@ swipe action Přidat členy týmu No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Přidat do jiného zařízení @@ -837,6 +873,10 @@ swipe action Pokročilá nastavení sítě No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings Pokročilá nastavení @@ -940,6 +980,10 @@ swipe action Povolit No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Povolte hovory, pouze pokud je váš kontakt povolí. @@ -1110,6 +1154,10 @@ swipe action Přijmout hovor No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ Sestavení aplikace: %@ @@ -1308,6 +1356,10 @@ swipe action Špatný hash zprávy No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1328,6 +1380,10 @@ in your network Lepší volání No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Lepší skupiny @@ -1502,11 +1558,6 @@ in your network Hovor již skončil! No comment provided by engineer. - - Calls - Hovory - No comment provided by engineer. - Calls prohibited! Volání zakázáno! @@ -1617,11 +1668,6 @@ new chat action Změnit zamykání authentication reason - - Change member role? - Změnit roli člena? - No comment provided by engineer. - Change passcode Změnit heslo @@ -1642,6 +1688,10 @@ new chat action Změnit roli No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Změnit režim sebedestrukce @@ -1657,6 +1707,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1698,6 +1752,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1736,6 +1794,10 @@ alert subtitle Konzola pro chat No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database Chat databáze @@ -2067,6 +2129,10 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop No comment provided by engineer. @@ -2147,14 +2213,6 @@ Toto je váš vlastní jednorázový odkaz! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Připojení @@ -2168,16 +2226,15 @@ Toto je váš vlastní jednorázový odkaz! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Chyba připojení alert title - - Connection error (AUTH) - Chyba spojení (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2187,6 +2244,11 @@ Toto je váš vlastní jednorázový odkaz! %@ No comment provided by engineer. + + Connection link removed + Chyba spojení + conn error description + Connection not ready. No comment provided by engineer. @@ -2225,6 +2287,10 @@ Toto je váš vlastní jednorázový odkaz! Connections No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2308,6 +2374,10 @@ Toto je váš vlastní jednorázový odkaz! Kopírovat No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error No comment provided by engineer. @@ -2338,6 +2408,10 @@ Toto je váš vlastní jednorázový odkaz! Create a group using a random profile. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Vytvořit soubor @@ -2375,15 +2449,15 @@ Toto je váš vlastní jednorázový odkaz! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Vytvořit frontu server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -2875,16 +2949,16 @@ alert button Desktop devices No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -2895,20 +2969,15 @@ alert button Details No comment provided by engineer. - - Develop - Vyvinout + + Developer + Nástroje pro vývojáře No comment provided by engineer. Developer options No comment provided by engineer. - - Developer tools - Nástroje pro vývojáře - No comment provided by engineer. - Device Zařízení @@ -3044,6 +3113,10 @@ alert button Udělat později No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. No comment provided by engineer. @@ -3074,6 +3147,10 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + alert action + Don't show again Znovu neukazuj @@ -3144,6 +3221,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Upravit @@ -3153,6 +3234,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Upravit profil skupiny @@ -3338,6 +3423,10 @@ chat item action Zadejte správnou přístupovou frázi. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… No comment provided by engineer. @@ -3373,6 +3462,10 @@ chat item action Enter this device name… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Zadat uvítací zprávu… @@ -3390,7 +3483,7 @@ chat item action Error Chyba - conn error description + No comment provided by engineer. Error aborting address change @@ -3661,6 +3754,10 @@ chat item action Chyba při ukládání profilu skupiny No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Chyba uložení hesla @@ -3712,6 +3809,10 @@ chat item action Chyba nastavování potvrzení o doručení! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3785,6 +3886,7 @@ chat item action Error: %@ Chyba: %@ alert message +conn error description file error text snd error text @@ -3910,6 +4012,14 @@ server test error File server error: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status No comment provided by engineer. @@ -4175,6 +4285,10 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4277,6 +4391,10 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Uvítací zpráva skupin @@ -4301,6 +4419,10 @@ Error: %2$@ Pomoc No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. No comment provided by engineer. @@ -4375,6 +4497,10 @@ Error: %2$@ Jak No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Jak ji používat @@ -4746,6 +4872,10 @@ More improvements are coming soon! Zdá se, že jste již připojeni prostřednictvím tohoto odkazu. Pokud tomu tak není, došlo k chybě (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Italské rozhraní @@ -4770,6 +4900,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Připojit ke skupině @@ -4842,7 +4976,7 @@ This is your link for group %@! Learn more Zjistit více - No comment provided by engineer. + badge alert button Leave @@ -4879,6 +5013,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4979,6 +5121,10 @@ This is your link for group %@! Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Označit jako smazané pro všechny @@ -5041,20 +5187,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Role člena se změní na "%@". Člen obdrží novou pozvánku. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5184,6 +5316,14 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -5332,6 +5472,10 @@ This is your link for group %@! Další vylepšení se chystají již brzy! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. No comment provided by engineer. @@ -5369,6 +5513,10 @@ This is your link for group %@! Jméno swipe action + + Name not found + No comment provided by engineer. + Network & servers Síť a servery @@ -5659,6 +5807,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5671,6 +5823,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nikdo nesledoval vaše konverzace. Nikdo nevytvořil mapu, kde jste byli. Soukromí nikdy nebylo funkcí - byl to způsob života. @@ -5680,6 +5836,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nejde o to mít lepší zámek na dveřích někoho jiného. Ani o to mít nájemce, který respektuje vaše soukromí, ale vede evidenci všech vašich návštěvníků. Nejste host. Jste doma. Ani král k vám nemůže vstoupit - jste suverén. @@ -5887,6 +6047,10 @@ Vyžaduje povolení sítě VPN. Hlasové zprávy může odesílat pouze váš kontakt. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open Otevřít @@ -6040,8 +6204,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6217,10 +6381,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6273,11 +6433,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - Ochrana osobních údajů a zabezpečení - No comment provided by engineer. - Privacy for your customers. No comment provided by engineer. @@ -6352,7 +6507,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6459,6 +6615,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Nabízená oznámení @@ -6494,7 +6654,7 @@ Enable in *Network & servers* settings. Read more Přečíst více - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -6725,11 +6885,19 @@ swipe action Odebrat člena? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Odstranit přístupovou frázi z klíčenek? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -6818,6 +6986,10 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Povinné @@ -6858,6 +7030,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Restartujte aplikaci pro vytvoření nového profilu chatu @@ -6933,6 +7109,24 @@ swipe action Role No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Role člena se změní na "%@". Člen obdrží novou pozvánku. + No comment provided by engineer. + Run chat Spustit chat @@ -6961,7 +7155,8 @@ swipe action Save Uložit - alert button + alert action +alert button chat item action @@ -6977,6 +7172,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? alert title @@ -6991,6 +7190,10 @@ chat item action Uložit a upozornit členy skupiny No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7055,6 +7258,10 @@ chat item action Uložit servery? alert title + + Save webpage settings? + alert title + Save welcome message? Uložit uvítací zprávu? @@ -7322,11 +7529,6 @@ chat item action Odesílatel zrušil přenos souboru. alert message - - Sender may have deleted the connection request. - Odesílatel možná smazal požadavek připojení. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7415,6 +7617,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7683,6 +7889,10 @@ chat item action Zobrazit možnosti vývojáře No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Zobrazit poslední zprávy @@ -7710,6 +7920,31 @@ chat item action Zobrazit: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX No comment provided by engineer. @@ -7797,6 +8032,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Jednorázová pozvánka SimpleX @@ -7806,6 +8053,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8040,9 +8291,8 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - Podpořte SimpleX Chat + + Support the project No comment provided by engineer. @@ -8219,6 +8469,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8245,6 +8511,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Pokus o změnu přístupové fráze databáze nebyl dokončen. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. No comment provided by engineer. @@ -8332,6 +8606,11 @@ your contacts and groups. Druhé zaškrtnutí jsme přehlédli! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Odesílatel možná smazal požadavek připojení. + No comment provided by engineer. + The sender will NOT be notified Odesílatel NEBUDE informován @@ -8382,6 +8661,10 @@ your contacts and groups. Mohou být přepsány v nastavení kontaktů. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány. @@ -8401,6 +8684,10 @@ your contacts and groups. Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. E2EE info chat item @@ -8538,6 +8825,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li nahrávat hlasové zprávy, udělte povolení k použití mikrofonu. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce **Profily chatu**. @@ -8569,6 +8860,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Změnit inkognito režim při připojení. @@ -8646,6 +8941,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8704,13 +9003,6 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji. -Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti. - No comment provided by engineer. - Unlink No comment provided by engineer. @@ -8739,17 +9031,13 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Nepřečtený swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -8797,7 +9085,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -8948,6 +9237,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection No comment provided by engineer. @@ -8965,6 +9258,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8986,6 +9283,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9131,6 +9432,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu WebRTC servery ICE No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Vítejte %@! @@ -9324,9 +9633,8 @@ Repeat join request? Můžete povolit později v Nastavení No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9383,6 +9691,10 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Zámek SimpleX můžete zapnout v Nastavení. @@ -9558,6 +9870,10 @@ Repeat connection request? Vaše SimpleX adresa No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9571,11 +9887,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - Vaše chatovací databáze - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování. @@ -9602,6 +9913,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji. +Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@). @@ -9759,6 +10077,10 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10004,6 +10326,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator tvůrce @@ -10200,6 +10526,10 @@ pref value hodin time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS klíčenka slouží k bezpečnému ukládání přístupové fráze – umožňuje přijímat push notifikace. @@ -10602,6 +10932,10 @@ last received msg: %2$@ stávka No comment provided by engineer. + + subscriber + member role + this contact tento kontakt @@ -10645,11 +10979,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -10795,7 +11124,7 @@ last received msg: %2$@
- +
@@ -10829,9 +11158,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -10853,7 +11197,7 @@ last received msg: %2$@
- +
@@ -10880,7 +11224,7 @@ last received msg: %2$@
- +
@@ -10899,7 +11243,7 @@ last received msg: %2$@
- +
@@ -11046,8 +11390,8 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json index 9cd5922c24..804a5e0951 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "cs", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 797a489c92..6fd8232e93 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #geheim# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ hat SimpleX Chat unterstützt. Das Abzeichen ist am %2$@ abgelaufen. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ heruntergeladen No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ hat sich am SimpleX Chat-Crowdfunding beteiligt. + badge alert + %@ is connected! %@ ist mit Ihnen verbunden! @@ -110,6 +120,11 @@ %@ Server No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ unterstützt SimpleX Chat. + badge alert + %@ uploaded %@ hochgeladen @@ -185,6 +200,21 @@ %d Monate time interval + + %d owner + %d Eigentümer + channel owners count + + + %d owners + %d Eigentümer + channel owners count + + + %d owners & contributors + %d Eigentümer und Mitwirkende + channel members count + %d relays failed %d Relais fehlgeschlagen @@ -400,11 +430,6 @@ channel relay bar (Neu) No comment provided by engineer. - - (signed) - (signiert) - chat link info line - (this device v%@) (Dieses Gerät hat v%@) @@ -738,6 +763,7 @@ swipe action Add + Hinzufügen No comment provided by engineer. @@ -745,6 +771,16 @@ swipe action Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet. No comment provided by engineer. + + Add contributors. + Mitwirkende hinzufügen. + No comment provided by engineer. + + + Add description + Beschreibung hinzufügen + No comment provided by engineer. + Add friends Freunde aufnehmen @@ -765,12 +801,19 @@ swipe action Profil hinzufügen No comment provided by engineer. + + Add relay + Relais hinzufügen + No comment provided by engineer. + Add relays + Relais hinzufügen No comment provided by engineer. Add relays to restore message delivery. + Relais hinzufügen, um die Nachrichtenübermittlung wiederherzustellen. No comment provided by engineer. @@ -788,6 +831,11 @@ swipe action Team-Mitglieder aufnehmen No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Fügen Sie diesen Code in Ihre Webseite ein. Er zeigt die Vorschau Ihres Kanals / Ihrer Gruppe an. + No comment provided by engineer. + Add to another device Einem anderen Gerät hinzufügen @@ -868,6 +916,11 @@ swipe action Erweiterte Netzwerkeinstellungen No comment provided by engineer. + + Advanced options + Erweiterte Optionen + No comment provided by engineer. + Advanced settings Erweiterte Einstellungen @@ -978,6 +1031,11 @@ swipe action Erlauben No comment provided by engineer. + + Allow anyone to embed + Einbetten für alle erlauben + No comment provided by engineer. + Allow calls only if your contact allows them. Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. @@ -1153,6 +1211,11 @@ swipe action Anruf annehmen No comment provided by engineer. + + Any webpage can show the preview. + Eine Vorschau ist auf jeder Webseite möglich. + No comment provided by engineer. + App build: %@ App Build: %@ @@ -1195,6 +1258,7 @@ swipe action App update required + Aktualisierung der App erforderlich alert title @@ -1279,7 +1343,7 @@ swipe action Audio & video calls - Audio- & Videoanrufe + Audio- und Videoanrufe No comment provided by engineer. @@ -1362,6 +1426,11 @@ swipe action Ungültiger Nachrichten-Hash No comment provided by engineer. + + Badge cannot be verified + Abzeichen ist nicht verifizierbar + badge alert title + Be free in your network @@ -1384,6 +1453,11 @@ in Ihrem Netzwerk Verbesserte Anrufe No comment provided by engineer. + + Better channels 📢 + Verbesserte Kanäle 📢 + No comment provided by engineer. + Better groups Bessere Gruppen @@ -1574,11 +1648,6 @@ in Ihrem Netzwerk Anruf ist bereits beendet! No comment provided by engineer. - - Calls - Anrufe - No comment provided by engineer. - Calls prohibited! Anrufe nicht zugelassen! @@ -1628,10 +1697,12 @@ new chat action Cancel and delete channel + Kanal abbrechen und löschen No comment provided by engineer. Cancel creating channel? + Kanalerstellung abbrechen? alert title @@ -1689,11 +1760,6 @@ new chat action Sperr-Modus ändern authentication reason - - Change member role? - Die Mitgliederrolle ändern? - No comment provided by engineer. - Change passcode Zugangscode ändern @@ -1714,6 +1780,11 @@ new chat action Rolle ändern No comment provided by engineer. + + Change role? + Rolle ändern? + No comment provided by engineer. + Change self-destruct mode Selbstzerstörungs-Modus ändern @@ -1730,6 +1801,11 @@ set passcode view Kanal No comment provided by engineer. + + Channel SimpleX name + Im Kanal genutzter SimpleX-Name + No comment provided by engineer. + Channel display name Anzeigename des Kanals @@ -1781,6 +1857,11 @@ alert subtitle Der Kanal ist vorübergehend nicht erreichbar alert title + + Channel webpage + Kanal-Webseite + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden! @@ -1793,6 +1874,7 @@ alert subtitle Channel will start working with %1$d of %2$d relays. Continue? + Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren? alert message @@ -1825,6 +1907,11 @@ alert subtitle Chat-Konsole No comment provided by engineer. + + Chat data + Chat-Daten + No comment provided by engineer. + Chat database Chat-Datenbank @@ -2202,6 +2289,11 @@ server test step Schneller miteinander verbinden! 🚀 No comment provided by engineer. + + Connect to %@ + Mit %@ verbinden + new chat action + Connect to desktop Mit dem Desktop verbinden @@ -2296,14 +2388,6 @@ Das ist Ihr eigener Einmal-Link! Mit dem Desktop verbinden No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Verbindung @@ -2319,16 +2403,16 @@ Das ist Ihr eigener Einmal-Link! Verbindung blockiert No comment provided by engineer. + + Connection blocked: %@ + Verbindung blockiert: %@ + conn error description + Connection error Verbindungsfehler alert title - - Connection error (AUTH) - Verbindungsfehler (AUTH) - conn error description - Connection failed Verbindung fehlgeschlagen @@ -2341,6 +2425,11 @@ Das ist Ihr eigener Einmal-Link! %@ No comment provided by engineer. + + Connection link removed + Verbindungsfehler + conn error description + Connection not ready. Verbindung noch nicht bereit. @@ -2386,6 +2475,11 @@ Das ist Ihr eigener Einmal-Link! Verbindungen No comment provided by engineer. + + Contact + Kontakt + No comment provided by engineer. + Contact address Kontaktadresse @@ -2476,6 +2570,11 @@ Das ist Ihr eigener Einmal-Link! Kopieren No comment provided by engineer. + + Copy code + Code kopieren + No comment provided by engineer. + Copy error Fehlermeldung kopieren @@ -2511,6 +2610,11 @@ Das ist Ihr eigener Einmal-Link! Gruppe mit einem zufälligen Profil erstellen. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Erstellen Sie eine Webseite, die Besuchern Ihren Kanal als Vorschau zeigt, bevor sie ihn abonnieren. Hosten Sie die Seite selbst oder nutzen Sie beliebiges statisches Hosting. + No comment provided by engineer. + Create file Datei erstellen @@ -2551,16 +2655,16 @@ Das ist Ihr eigener Einmal-Link! Öffentlichen Kanal erstellen No comment provided by engineer. - - Create public channel (BETA) - Öffentlichen Kanal erstellen (BETA) - No comment provided by engineer. - Create queue Warteschlange erstellen server test step + + Create web preview. + Eine Web-Vorschau erstellen. + No comment provided by engineer. + Create your address Ihre Adresse erstellen @@ -2722,7 +2826,7 @@ Das ist Ihr eigener Einmal-Link! Database passphrase & export - Datenbank-Passwort & -Export + Datenbank-Passwort und -Export No comment provided by engineer. @@ -2907,6 +3011,7 @@ swipe action Delete from history + Aus dem Nachrichtenverlauf löschen No comment provided by engineer. @@ -3095,9 +3200,9 @@ alert button Desktop-Geräte No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Die Adresse des Zielservers von %1$@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %2$@. No comment provided by engineer. @@ -3105,9 +3210,9 @@ alert button Zielserver-Fehler: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Die Version des Zielservers %1$@ ist nicht kompatibel mit dem Weiterleitungsserver %2$@. No comment provided by engineer. @@ -3120,9 +3225,9 @@ alert button Details No comment provided by engineer. - - Develop - Entwicklung + + Developer + Entwicklertools No comment provided by engineer. @@ -3130,11 +3235,6 @@ alert button Optionen für Entwickler No comment provided by engineer. - - Developer tools - Entwicklertools - No comment provided by engineer. - Device Gerät @@ -3280,6 +3380,11 @@ alert button Später wiederholen No comment provided by engineer. + + Do not require signing messages. + Signatur für Nachrichten nicht erforderlich. + No comment provided by engineer. + Do not send history to new members. Den Nachrichtenverlauf nicht an neue Mitglieder senden. @@ -3315,6 +3420,11 @@ alert button Verpassen Sie keine wichtigen Nachrichten. No comment provided by engineer. + + Don't save + Nicht speichern + alert action + Don't show again Nicht nochmals anzeigen @@ -3396,6 +3506,11 @@ chat item action Freunde einladen – jetzt noch einfacher 👋 No comment provided by engineer. + + Easier to read. + Einfacher zu lesen. + No comment provided by engineer. + Edit Bearbeiten @@ -3406,6 +3521,11 @@ chat item action Kanalprofil bearbeiten No comment provided by engineer. + + Edit description + Beschreibung bearbeiten + No comment provided by engineer. + Edit group profile Gruppenprofil bearbeiten @@ -3443,7 +3563,7 @@ chat item action Enable at least one chat relay in Network & Servers. - Aktivieren Sie mindestens ein Chat‑Relais unter 'Netzwerk & Server'. + Aktivieren Sie mindestens ein Chat‑Relais unter "Netzwerk und Server". channel creation warning @@ -3533,7 +3653,7 @@ chat item action Encrypt stored files & media - Gespeicherte Dateien & Medien verschlüsseln + Gespeicherte Dateien und Medien verschlüsseln No comment provided by engineer. @@ -3606,6 +3726,11 @@ chat item action Geben Sie das korrekte Passwort ein. No comment provided by engineer. + + Enter description (optional) + Beschreibung eingeben (optional) + placeholder + Enter group name… Geben Sie den Gruppennamen ein… @@ -3646,6 +3771,11 @@ chat item action Geben Sie diesen Gerätenamen ein… No comment provided by engineer. + + Enter webpage URL + URL der Webseite eingeben + No comment provided by engineer. + Enter welcome message… Geben Sie eine Begrüßungsmeldung ein … @@ -3664,7 +3794,7 @@ chat item action Error Fehler - conn error description + No comment provided by engineer. Error aborting address change @@ -3698,6 +3828,7 @@ chat item action Error adding relays + Fehler beim Hinzufügen von Relais alert title @@ -3832,6 +3963,7 @@ chat item action Error deleting message + Fehler beim Löschen der Nachricht alert title @@ -3964,6 +4096,11 @@ chat item action Fehler beim Speichern des Gruppenprofils No comment provided by engineer. + + Error saving name + Fehler beim Speichern des Namens + alert title + Error saving passcode Fehler beim Speichern des Zugangscodes @@ -3996,7 +4133,7 @@ chat item action Error sending email - Fehler beim Senden der eMail + Fehler beim Senden der E-Mail No comment provided by engineer. @@ -4019,6 +4156,11 @@ chat item action Fehler beim Setzen von Empfangsbestätigungen! No comment provided by engineer. + + Error sharing address + Fehler beim Teilen der Adresse + alert title + Error sharing channel Fehler beim Teilen des Kanals @@ -4098,6 +4240,7 @@ chat item action Error: %@ Fehler: %@ alert message +conn error description file error text snd error text @@ -4241,6 +4384,16 @@ server test error Datei-Server Fehler: %@ file error text + + File servers + Datei-Server + No comment provided by engineer. + + + File servers: %@ + Datei-Server: %@ + copied message info + File status Datei-Status @@ -4278,7 +4431,7 @@ server test error Files & media - Dateien & Medien + Dateien und Medien No comment provided by engineer. @@ -4542,6 +4695,11 @@ Fehler: %2$@ GIFs und Sticker No comment provided by engineer. + + Get SimpleX name (BETA) + SimpleX-Name erhalten (BETA) + No comment provided by engineer. + Get link Link erhalten @@ -4652,6 +4810,11 @@ Fehler: %2$@ Das Gruppenprofil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an die Gruppenmitglieder gesendet. alert message + + Group webpage + Webseite der Gruppe + No comment provided by engineer. + Group welcome message Gruppen-Begrüßungsmeldung @@ -4677,6 +4840,11 @@ Fehler: %2$@ Hilfe No comment provided by engineer. + + Help & support + Hilfe & Unterstützung + No comment provided by engineer. + Help admins moderating their groups. Helfen Sie Administratoren bei der Moderation ihrer Gruppen. @@ -4757,6 +4925,11 @@ Fehler: %2$@ Anleitung No comment provided by engineer. + + How to register a test name + Wie man einen Test-Namen registriert + No comment provided by engineer. + How to use it Wie man SimpleX nutzt @@ -5162,6 +5335,11 @@ Weitere Verbesserungen sind bald verfügbar! Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Dies wird Abonnenten angezeigt und zum Laden der Vorschau genutzt. + No comment provided by engineer. + Italian interface Italienische Bedienoberfläche @@ -5187,6 +5365,11 @@ Weitere Verbesserungen sind bald verfügbar! Kanal beitreten No comment provided by engineer. + + Join channel %@ + Kanal %@ beitreten + new chat action + Join group Treten Sie der Gruppe bei @@ -5267,7 +5450,7 @@ Das ist Ihr Link für die Gruppe %@! Learn more Mehr erfahren - No comment provided by engineer. + badge alert button Leave @@ -5309,6 +5492,16 @@ Das ist Ihr Link für die Gruppe %@! Weniger Datenverkehr in mobilen Netzen. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Lassen Sie sich über den mit Ihrer SimpleX‑Adresse registrierten Namen verbinden. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Ermöglichen Sie Beitritte über den mit diesem Kanal‑Link registrierten Namen. + No comment provided by engineer. + Let someone connect to you Jemand mit Ihnen verbinden lassen @@ -5419,6 +5612,11 @@ Das ist Ihr Link für die Gruppe %@! Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind. No comment provided by engineer. + + Manage your relays. + Ihre Relais verwalten. + No comment provided by engineer. + Mark deleted for everyone Für Alle als gelöscht markieren @@ -5489,21 +5687,6 @@ Das ist Ihr Link für die Gruppe %@! Mitglieder-Meldungen chat feature - - Member role will be changed to "%@". All chat members will be notified. - Die Rolle des Mitglieds wird auf "%@" geändert. Alle Chat-Mitglieder werden darüber informiert. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Die Mitgliederrolle wird auf "%@" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden! @@ -5649,6 +5832,16 @@ Das ist Ihr Link für die Gruppe %@! Nachrichten-Form No comment provided by engineer. + + Message signing is not required. + Nachrichten müssen nicht signiert werden. + No comment provided by engineer. + + + Message signing is required. + Nachrichten müssen signiert werden. + No comment provided by engineer. + Message source remains private. Die Nachrichtenquelle bleibt privat. @@ -5819,6 +6012,11 @@ Das ist Ihr Link für die Gruppe %@! Weitere Verbesserungen sind bald verfügbar! No comment provided by engineer. + + More privacy + Weitere Privatsphäre + No comment provided by engineer. + More reliable network connection. Zuverlässigere Netzwerkverbindung. @@ -5859,9 +6057,14 @@ Das ist Ihr Link für die Gruppe %@! Name swipe action + + Name not found + Name wurde nicht gefunden + No comment provided by engineer. + Network & servers - Netzwerk & Server + Netzwerk und Server No comment provided by engineer. @@ -6029,7 +6232,7 @@ wer mit wem kommuniziert No account. No phone. No email. No ID. The most secure encryption. - Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID. + Kein Benutzerkonto. Keine Telefonnummer. Keine E‑Mail. Keine ID. Die sicherste Verschlüsselung. No comment provided by engineer. @@ -6045,6 +6248,7 @@ Die sicherste Verschlüsselung. No available relays + Keine verfügbaren Relais No comment provided by engineer. @@ -6174,6 +6378,7 @@ Die sicherste Verschlüsselung. No relays + Keine Relais No comment provided by engineer. @@ -6191,6 +6396,11 @@ Die sicherste Verschlüsselung. Keine Server für den Empfang von Nachrichten. servers error + + No servers to resolve names. + Keine Server für die Namensauflösung konfiguriert. + servers warning + No servers to send files. Keine Server für das Versenden von Dateien. @@ -6206,6 +6416,11 @@ Die sicherste Verschlüsselung. Keine ungelesenen Chats No comment provided by engineer. + + No valid link + Kein gültiger Link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich. @@ -6216,6 +6431,11 @@ Die sicherste Verschlüsselung. Non‑Profit‑Governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + Keiner Ihrer Server ist zum Auflösen von SimpleX‑Namen konfiguriert. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän. @@ -6441,6 +6661,11 @@ Dies erfordert die Aktivierung eines VPNs. Nur Ihr Kontakt kann Sprachnachrichten versenden. No comment provided by engineer. + + Only your page above can show the preview. + Nur Ihre oben genannte Seite kann die Vorschau anzeigen. + No comment provided by engineer. + Open Öffnen @@ -6630,9 +6855,9 @@ alert button Eigentümer No comment provided by engineer. - - Owners - Eigentümer + + Owners & contributors + Eigentümer und Mitwirkende No comment provided by engineer. @@ -6824,10 +7049,6 @@ Fehler: %@ Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können. @@ -6888,11 +7109,6 @@ Fehler: %@ Bisher verbundene Server No comment provided by engineer. - - Privacy & security - Datenschutz & Sicherheit - No comment provided by engineer. - Privacy for your customers. Schutz der Privatsphäre Ihrer Kunden. @@ -6981,7 +7197,8 @@ Fehler: %@ Profile update will be sent to your SimpleX contacts. Profil-Aktualisierung wird an Ihre SimpleX-Kontakte gesendet. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7057,7 +7274,7 @@ Fehler: %@ Protect your IP address from the messaging relays chosen by your contacts. Enable in *Network & servers* settings. Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben. -Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. +Aktivieren Sie es in den *Netzwerk und Server* Einstellungen. No comment provided by engineer. @@ -7100,6 +7317,11 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Öffentliche Kanäle – frei sprechen 🚀 No comment provided by engineer. + + Public names for your channel or business. + Öffentliche Namen für Ihren Kanal oder Ihr Unternehmen. + No comment provided by engineer. + Push notifications Push-Benachrichtigungen @@ -7138,7 +7360,7 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Read more Mehr erfahren - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7349,10 +7571,12 @@ swipe action Relay will be removed from channel - this cannot be undone! + Relais wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden! alert message Relays added: %@. + Relais hinzugefügt: %@. alert message @@ -7395,13 +7619,24 @@ swipe action Das Mitglied entfernen? alert title + + Remove name + Name entfernen + No comment provided by engineer. + Remove passphrase from keychain? Passwort aus dem Schlüsselbund entfernen? No comment provided by engineer. + + Remove relay + Relais entfernen + No comment provided by engineer. + Remove relay? + Relais entfernen? alert title @@ -7504,6 +7739,11 @@ swipe action Meldungen No comment provided by engineer. + + Require signing messages. + Signatur für Nachrichten erforderlich. + No comment provided by engineer. + Required Erforderlich @@ -7549,6 +7789,11 @@ swipe action Auf das Benutzer-spezifische Design zurücksetzen No comment provided by engineer. + + Resolver error: %@ + Namensauflösungs-Fehler: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Um ein neues Chat-Profil zu erstellen, starten Sie die App neu @@ -7629,6 +7874,26 @@ swipe action Rolle No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Die Rolle des Mitglieds wird auf "%@" geändert. Alle Chat-Mitglieder werden darüber informiert. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Die Mitgliederrolle wird auf "%@" geändert. Alle Gruppenmitglieder werden benachrichtigt. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + Die Rolle wird auf "%@" geändert. Alle Abonnenten werden benachrichtigt. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Die Mitgliederrolle wird auf "%@" geändert. Das Mitglied wird eine neue Einladung erhalten. + No comment provided by engineer. + Run chat Chat starten @@ -7662,7 +7927,8 @@ swipe action Save Speichern - alert button + alert action +alert button chat item action @@ -7680,6 +7946,11 @@ chat item action Speichern (Abonnenten benachrichtigen) alert button + + Save SimpleX name? + SimpleX-Name speichern? + alert title + Save admission settings? Speichern der Aufnahme-Einstellungen? @@ -7695,6 +7966,11 @@ chat item action Speichern und Gruppenmitglieder benachrichtigen No comment provided by engineer. + + Save and notify members + Speichern und Mitglieder benachrichtigen + No comment provided by engineer. + Save and notify subscribers Speichern und Abonnenten benachrichtigen @@ -7765,6 +8041,11 @@ chat item action Alle Server speichern? alert title + + Save webpage settings? + Webseiten-Einstellungen sichern? + alert title + Save welcome message? Begrüßungsmeldung speichern? @@ -8065,11 +8346,6 @@ chat item action Der Absender hat die Dateiübertragung abgebrochen. alert message - - Sender may have deleted the connection request. - Der Absender hat möglicherweise die Verbindungsanfrage gelöscht. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. Das Senden einer Link-Vorschau kann Ihre IP‑Adresse an die Website übermitteln. Sie können dies später in den Datenschutzeinstellungen ändern. @@ -8165,6 +8441,11 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + Der Server %@ unterstützt keine Namensauflösung. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink. + No comment provided by engineer. + Server added to operator %@. Der Server wurde dem Betreiber %@ hinzugefügt. @@ -8476,6 +8757,11 @@ chat item action Entwickleroptionen anzeigen No comment provided by engineer. + + Show encryption + Verschlüsselung anzeigen + No comment provided by engineer. + Show last messages Letzte Nachrichten anzeigen @@ -8506,6 +8792,37 @@ chat item action Anzeigen: No comment provided by engineer. + + Sign message + Nachricht signieren + No comment provided by engineer. + + + Sign messages + Nachrichten signieren + chat feature + + + Signature missing + Signatur fehlt + alert title +copied message info + + + Signed + Signiert + copied message info + + + Signed & verified + Signiert und verifiziert + copied message info + + + Signing proves you authored this message and can't be denied later. + Die Signatur bestätigt, dass Sie diese Nachricht verfasst haben und sie später nicht abstreiten können. + No comment provided by engineer. + SimpleX SimpleX @@ -8601,6 +8918,21 @@ chat item action SimpleX-Links sind nicht erlaubt No comment provided by engineer. + + SimpleX name + SimpleX-Name + No comment provided by engineer. + + + SimpleX name error + Fehler beim SimpleX-Namen + No comment provided by engineer. + + + SimpleX name not verified + SimpleX-Name ist nicht verifiziert + alert title + SimpleX one-time invitation SimpleX-Einmal-Einladung @@ -8611,6 +8943,11 @@ chat item action Die SimpleX-Protokolle wurden von Trail of Bits überprüft. No comment provided by engineer. + + SimpleX public names (BETA) + Öffentliche SimpleX-Namen (BETA) + No comment provided by engineer. + SimpleX relay address SimpleX Relais-Adresse @@ -8721,6 +9058,7 @@ report reason Status + Status No comment provided by engineer. @@ -8880,9 +9218,9 @@ Die Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwend Nicht beachtete Abonnements No comment provided by engineer. - - Support SimpleX Chat - Unterstützung von SimpleX Chat + + Support the project + Unterstützen Sie das Projekt No comment provided by engineer. @@ -9063,12 +9401,12 @@ server test failure Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! - Dank der Nutzer - [Tragen Sie per Weblate bei](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! + Dank der Nutzer - [Wirken Sie per Weblate mit](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! No comment provided by engineer. Thanks to the users – contribute via Weblate! - Dank der Nutzer - Tragen Sie per Weblate bei! + Dank der Nutzer - Wirken Sie per Weblate mit! No comment provided by engineer. @@ -9078,6 +9416,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + Der SimpleX‑Name #%@ wurde ohne Kanal‑Link registriert. Fügen Sie den Kanal‑Link über die Registrierungsseite hinzu. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + Der SimpleX-Name %@ wurde registriert, hat aber keinen gültigen Link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + Der SimpleX‑Name %@ wurde registriert, jedoch nicht in Ihrem Profil hinterlegt. Bitte zu Ihrer Adresse oder zum Kanalprofil hinzufügen, sofern Sie der Besitzer sind. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + Der SimpleX‑Name @%@ wurde ohne SimpleX-Adresse registriert. Fügen Sie die SimpleX-Adresse über die Registrierungsseite hinzu. + alert message + The address will be short, and your profile will be shared via the address. Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt. @@ -9108,6 +9466,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + Das Abzeichen ist mit einem Schlüssel signiert, den diese App‑Version nicht erkennt. Aktualisieren Sie die App, um dieses Abzeichen zu verifizieren. + badge alert + + + The channel required this message to be signed, but the signature is missing. + Der Kanal verlangt für diese Nachricht eine Signatur, welche aber fehlt. + alert message + The code you scanned is not a SimpleX link QR code. Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code. @@ -9205,6 +9573,11 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. Wir haben das zweite Häkchen vermisst! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Der Absender hat möglicherweise die Verbindungsanfrage gelöscht. + No comment provided by engineer. + The sender will NOT be notified Der Absender wird NICHT benachrichtigt @@ -9242,7 +9615,7 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. - Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist. + Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzerkonten, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist. No comment provided by engineer. @@ -9260,6 +9633,11 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. Sie können in den Kontakteinstellungen überschrieben werden. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + Dieser SimpleX-Name wurde nicht registriert. Bitte überprüfen Sie den Namen. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Dieser Vorgang kann nicht rückgängig gemacht werden - alle empfangenen und gesendeten Dateien sowie Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. @@ -9277,9 +9655,14 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden! + Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + Dieses Abzeichen konnte nicht verifiziert werden und ist möglicherweise nicht echt. + badge alert + This chat is protected by end-to-end encryption. Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt. @@ -9312,6 +9695,7 @@ in dem Sie Ihre Kontakte und Gruppen besitzen. This group requires a newer version of the app. Please update the app to join. + Diese Gruppe erfordert eine neuere App‑Version. Bitte aktualisieren Sie die App, um beizutreten. alert message alert subtitle @@ -9322,6 +9706,7 @@ alert subtitle This is the last active relay. Removing it will prevent message delivery to subscribers. + Dies ist das letzte aktive Relais. Wenn Sie es entfernen, können keine Nachrichten mehr an Abonnenten zugestellt werden. alert message @@ -9436,6 +9821,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können. No comment provided by engineer. + + To resolve names + Für Namensauflösung + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen. @@ -9471,6 +9861,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + Für die Überprüfung der Schlüssel mit diesem Abonnenten vergleichen oder scannen Sie den Code auf Ihren Geräten. + No comment provided by engineer. + Toggle incognito when connecting. Inkognito beim Verbinden einschalten. @@ -9561,6 +9956,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Abonnent für alle freigeben? No comment provided by engineer. + + Unconfirmed name + Unbestätigter Name + No comment provided by engineer. + Undelivered messages Nicht ausgelieferte Nachrichten @@ -9621,13 +10021,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Aktivieren Sie den Modus "Bitte nicht stören", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns. -Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben. - No comment provided by engineer. - Unlink Entkoppeln @@ -9658,18 +10051,15 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Ungelesen swipe action - - Unsupported channel name - alert title - Unsupported connection link Verbindungs-Link wird nicht unterstützt conn error description - - Unsupported contact name - alert title + + Unverified badge + Abzeichen nicht verifiziert + badge alert title Up to 100 last messages are sent to new members. @@ -9724,7 +10114,8 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Upgrade address? Adresse aktualisieren? - alert message + alert message +alert title Upgrade and open chat @@ -9901,6 +10292,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Web-Port nutzen No comment provided by engineer. + + Used chat relays do not support webpages. + Die verwendeten Chat‑Relais unterstützen keine Webseiten. + No comment provided by engineer. + User selection Benutzer-Auswahl @@ -9921,6 +10317,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Überprüfen relay test step + + Verify SimpleX names + SimpleX-Namen überprüfen + No comment provided by engineer. + Verify code with desktop Code mit dem Desktop überprüfen @@ -9946,6 +10347,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Überprüfen Sie das Datenbank-Passwort No comment provided by engineer. + + Verify name + Name überprüfen + No comment provided by engineer. + Verify passphrase Überprüfen Sie das Passwort @@ -10106,6 +10512,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s WebRTC ICE-Server No comment provided by engineer. + + Webpage code + Webseiten-Code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + Die Webseiten-Einstellungen wurden geändert. Wenn Sie sie abspeichern, werden die aktualisierten Einstellungen an die Abonnenten gesendet. + alert message + Welcome %@! Willkommen %@! @@ -10328,9 +10744,9 @@ Verbindungsanfrage wiederholen? Sie können diese später in den Einstellungen aktivieren No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren. + + You can enable them later via app Your privacy settings. + Sie können diese später in Ihren Privatsphäre-Einstellungen der App aktivieren. No comment provided by engineer. @@ -10393,6 +10809,11 @@ Verbindungsanfrage wiederholen? Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + Sie können SimpleX ab der App-Version v7 unterstützen. + badge alert + You can turn on SimpleX Lock via Settings. Sie können die SimpleX-Sperre über die Einstellungen aktivieren. @@ -10506,7 +10927,7 @@ Verbindungsanfrage wiederholen? You were born without an account - Sie wurden ohne eine Benutzerkennung geboren. + Sie wurden ohne ein Benutzerkonto geboren. No comment provided by engineer. @@ -10584,6 +11005,11 @@ Verbindungsanfrage wiederholen? Ihre SimpleX-Adresse No comment provided by engineer. + + Your SimpleX name + Ihr SimpleX-Name + No comment provided by engineer. + Your business contact Ihr geschäftlicher Kontakt @@ -10599,11 +11025,6 @@ Verbindungsanfrage wiederholen? Ihr Kanal No comment provided by engineer. - - Your chat database - Chat-Datenbank - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen. @@ -10634,6 +11055,13 @@ Verbindungsanfrage wiederholen? Ihr Kontakt No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns. +Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@). @@ -10682,6 +11110,8 @@ Verbindungsanfrage wiederholen? Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + Ihr neuer Kanal %1$@ ist mit %2$d von %3$d Relais verbunden. +Wenn Sie abbrechen, wird der Kanal gelöscht. Sie können ihn später erneut erstellen. alert message @@ -10806,6 +11236,11 @@ Relais können auf Kanalnachrichten zugreifen. hat Sie angenommen rcv group event chat item + + acknowledged roster + Bestätigter Relaisbestand + No comment provided by engineer. + active Aktiv @@ -11072,6 +11507,11 @@ marked deleted chat item preview text Kontakt sollte annehmen… No comment provided by engineer. + + contributor + Mitwirkender + member role + creator Ersteller @@ -11278,6 +11718,11 @@ pref value Stunden time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. Für die sichere Speicherung des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen. @@ -11720,6 +12165,11 @@ Zuletzt empfangene Nachricht: %2$@ durchstreichen No comment provided by engineer. + + subscriber + Abonnent + member role + this contact Dieser Kontakt @@ -11770,11 +12220,6 @@ Zuletzt empfangene Nachricht: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ via %@ @@ -11929,7 +12374,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -11964,9 +12409,24 @@ Zuletzt empfangene Nachricht: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11988,7 +12448,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -12020,7 +12480,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -12042,7 +12502,7 @@ Zuletzt empfangene Nachricht: %2$@
- +
@@ -12225,9 +12685,9 @@ Zuletzt empfangene Nachricht: %2$@ Falsches Datenbank-Passwort No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben. + + You can allow sharing in Your privacy / SimpleX Lock settings. + Sie können das Teilen in Ihren Privatsphäre‑ / SimpleX-Sperre‑Einstellungen erlauben. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/contents.json b/apps/ios/SimpleX Localizations/de.xcloc/contents.json index e8d71cf38c..8a5b0f6b96 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/de.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "de", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 7a560bb41b..2db5b67ec8 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -1100,8 +1100,8 @@ Available in v5.1 Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -1964,12 +1964,12 @@ Available in v5.1 Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2706,8 +2706,8 @@ Available in v5.1 Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. @@ -4225,6 +4225,14 @@ SimpleX servers cannot see your profile. %@, %@ και %lld άλλα μέλη συνδέθηκαν No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ υποστήριζε το SimpleX Chat. Η ισχύς του σήματος έληξε στις %2$@. + + + %@ downloaded + %@ κατεβασμένο +
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 c108dcc904..97a8d4879c 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #secret# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ downloaded No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ is connected! @@ -110,6 +120,11 @@ %@ servers No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ uploaded @@ -185,6 +200,21 @@ %d months time interval + + %d owner + %d owner + channel owners count + + + %d owners + %d owners + channel owners count + + + %d owners & contributors + %d owners & contributors + channel members count + %d relays failed %d relays failed @@ -400,11 +430,6 @@ channel relay bar (new) No comment provided by engineer. - - (signed) - (signed) - chat link info line - (this device v%@) (this device v%@) @@ -746,6 +771,16 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + Add contributors. + No comment provided by engineer. + + + Add description + Add description + No comment provided by engineer. + Add friends Add friends @@ -766,6 +801,11 @@ swipe action Add profile No comment provided by engineer. + + Add relay + Add relay + No comment provided by engineer. + Add relays Add relays @@ -791,6 +831,11 @@ swipe action Add team members No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Add to another device @@ -871,6 +916,11 @@ swipe action Advanced network settings No comment provided by engineer. + + Advanced options + Advanced options + No comment provided by engineer. + Advanced settings Advanced settings @@ -981,6 +1031,11 @@ swipe action Allow No comment provided by engineer. + + Allow anyone to embed + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Allow calls only if your contact allows them. @@ -1156,6 +1211,11 @@ swipe action Answer call No comment provided by engineer. + + Any webpage can show the preview. + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ App build: %@ @@ -1366,6 +1426,11 @@ swipe action Bad message hash No comment provided by engineer. + + Badge cannot be verified + Badge cannot be verified + badge alert title + Be free in your network @@ -1388,6 +1453,11 @@ in your network Better calls No comment provided by engineer. + + Better channels 📢 + Better channels 📢 + No comment provided by engineer. + Better groups Better groups @@ -1578,11 +1648,6 @@ in your network Call already ended! No comment provided by engineer. - - Calls - Calls - No comment provided by engineer. - Calls prohibited! Calls prohibited! @@ -1695,11 +1760,6 @@ new chat action Change lock mode authentication reason - - Change member role? - Change member role? - No comment provided by engineer. - Change passcode Change passcode @@ -1720,6 +1780,11 @@ new chat action Change role No comment provided by engineer. + + Change role? + Change role? + No comment provided by engineer. + Change self-destruct mode Change self-destruct mode @@ -1736,6 +1801,11 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + Channel SimpleX name + No comment provided by engineer. + Channel display name Channel display name @@ -1787,6 +1857,11 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! Channel will be deleted for all subscribers - this cannot be undone! @@ -1832,6 +1907,11 @@ alert subtitle Chat console No comment provided by engineer. + + Chat data + Chat data + No comment provided by engineer. + Chat database Chat database @@ -2209,6 +2289,11 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + Connect to %@ + new chat action + Connect to desktop Connect to desktop @@ -2303,16 +2388,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - Connecting via contact name requires a newer app version. - alert message - Connection Connection @@ -2328,16 +2403,16 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + Connection blocked: %@ + conn error description + Connection error Connection error alert title - - Connection error (AUTH) - Connection error (AUTH) - conn error description - Connection failed Connection failed @@ -2350,6 +2425,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Connection link removed + conn error description + Connection not ready. Connection not ready. @@ -2395,6 +2475,11 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact + Contact + No comment provided by engineer. + Contact address Contact address @@ -2485,6 +2570,11 @@ This is your own one-time link! Copy No comment provided by engineer. + + Copy code + Copy code + No comment provided by engineer. + Copy error Copy error @@ -2520,6 +2610,11 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Create file @@ -2560,16 +2655,16 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - Create public channel (BETA) - No comment provided by engineer. - Create queue Create queue server test step + + Create web preview. + Create web preview. + No comment provided by engineer. + Create your address Create your address @@ -3105,9 +3200,9 @@ alert button Desktop devices No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. @@ -3115,9 +3210,9 @@ alert button Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -3130,9 +3225,9 @@ alert button Details No comment provided by engineer. - - Develop - Develop + + Developer + Developer No comment provided by engineer. @@ -3140,11 +3235,6 @@ alert button Developer options No comment provided by engineer. - - Developer tools - Developer tools - No comment provided by engineer. - Device Device @@ -3290,6 +3380,11 @@ alert button Do it later No comment provided by engineer. + + Do not require signing messages. + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Do not send history to new members. @@ -3325,6 +3420,11 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + Don't save + alert action + Don't show again Don't show again @@ -3406,6 +3506,11 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + Easier to read. + No comment provided by engineer. + Edit Edit @@ -3416,6 +3521,11 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + Edit description + No comment provided by engineer. + Edit group profile Edit group profile @@ -3616,6 +3726,11 @@ chat item action Enter correct passphrase. No comment provided by engineer. + + Enter description (optional) + Enter description (optional) + placeholder + Enter group name… Enter group name… @@ -3656,6 +3771,11 @@ chat item action Enter this device name… No comment provided by engineer. + + Enter webpage URL + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Enter welcome message… @@ -3674,7 +3794,7 @@ chat item action Error Error - conn error description + No comment provided by engineer. Error aborting address change @@ -3976,6 +4096,11 @@ chat item action Error saving group profile No comment provided by engineer. + + Error saving name + Error saving name + alert title + Error saving passcode Error saving passcode @@ -4031,6 +4156,11 @@ chat item action Error setting delivery receipts! No comment provided by engineer. + + Error sharing address + Error sharing address + alert title + Error sharing channel Error sharing channel @@ -4110,6 +4240,7 @@ chat item action Error: %@ Error: %@ alert message +conn error description file error text snd error text @@ -4253,6 +4384,16 @@ server test error File server error: %@ file error text + + File servers + File servers + No comment provided by engineer. + + + File servers: %@ + File servers: %@ + copied message info + File status File status @@ -4554,6 +4695,11 @@ Error: %2$@ GIFs and stickers No comment provided by engineer. + + Get SimpleX name (BETA) + Get SimpleX name (BETA) + No comment provided by engineer. + Get link Get link @@ -4664,6 +4810,11 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + Group webpage + No comment provided by engineer. + Group welcome message Group welcome message @@ -4689,6 +4840,11 @@ Error: %2$@ Help No comment provided by engineer. + + Help & support + Help & support + No comment provided by engineer. + Help admins moderating their groups. Help admins moderating their groups. @@ -4769,6 +4925,11 @@ Error: %2$@ How to No comment provided by engineer. + + How to register a test name + How to register a test name + No comment provided by engineer. + How to use it How to use it @@ -5174,6 +5335,11 @@ More improvements are coming soon! It seems like you are already connected via this link. If it is not the case, there was an error (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Italian interface @@ -5199,6 +5365,11 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + Join channel %@ + new chat action + Join group Join group @@ -5279,7 +5450,7 @@ This is your link for group %@! Learn more Learn more - No comment provided by engineer. + badge alert button Leave @@ -5321,6 +5492,16 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you Let someone connect to you @@ -5431,6 +5612,11 @@ This is your link for group %@! Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. No comment provided by engineer. + + Manage your relays. + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Mark deleted for everyone @@ -5501,21 +5687,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Member will be removed from chat - this cannot be undone! @@ -5661,6 +5832,16 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + Message signing is required. + No comment provided by engineer. + Message source remains private. Message source remains private. @@ -5831,6 +6012,11 @@ This is your link for group %@! More improvements are coming soon! No comment provided by engineer. + + More privacy + More privacy + No comment provided by engineer. + More reliable network connection. More reliable network connection. @@ -5871,6 +6057,11 @@ This is your link for group %@! Name swipe action + + Name not found + Name not found + No comment provided by engineer. + Network & servers Network & servers @@ -6205,6 +6396,11 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + No servers to resolve names. + servers warning + No servers to send files. No servers to send files. @@ -6220,6 +6416,11 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. @@ -6230,6 +6431,11 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. @@ -6455,6 +6661,11 @@ Requires compatible VPN. Only your contact can send voice messages. No comment provided by engineer. + + Only your page above can show the preview. + Only your page above can show the preview. + No comment provided by engineer. + Open Open @@ -6644,9 +6855,9 @@ alert button Owner No comment provided by engineer. - - Owners - Owners + + Owners & contributors + Owners & contributors No comment provided by engineer. @@ -6838,11 +7049,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Please wait for group moderators to review your request to join the group. @@ -6903,11 +7109,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - Privacy & security - No comment provided by engineer. - Privacy for your customers. Privacy for your customers. @@ -6996,7 +7197,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7115,6 +7317,11 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + Public names for your channel or business. + No comment provided by engineer. + Push notifications Push notifications @@ -7153,7 +7360,7 @@ Enable in *Network & servers* settings. Read more Read more - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7412,11 +7619,21 @@ swipe action Remove member? alert title + + Remove name + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Remove passphrase from keychain? No comment provided by engineer. + + Remove relay + Remove relay + No comment provided by engineer. + Remove relay? Remove relay? @@ -7522,6 +7739,11 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + Require signing messages. + No comment provided by engineer. + Required Required @@ -7567,6 +7789,11 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Restart the app to create a new chat profile @@ -7647,6 +7874,26 @@ swipe action Role No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat Run chat @@ -7680,7 +7927,8 @@ swipe action Save Save - alert button + alert action +alert button chat item action @@ -7698,6 +7946,11 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + Save SimpleX name? + alert title + Save admission settings? Save admission settings? @@ -7713,6 +7966,11 @@ chat item action Save and notify group members No comment provided by engineer. + + Save and notify members + Save and notify members + No comment provided by engineer. + Save and notify subscribers Save and notify subscribers @@ -7783,6 +8041,11 @@ chat item action Save servers? alert title + + Save webpage settings? + Save webpage settings? + alert title + Save welcome message? Save welcome message? @@ -8083,11 +8346,6 @@ chat item action Sender cancelled file transfer. alert message - - Sender may have deleted the connection request. - Sender may have deleted the connection request. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. @@ -8183,6 +8441,11 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Server added to operator %@. @@ -8494,6 +8757,11 @@ chat item action Show developer options No comment provided by engineer. + + Show encryption + Show encryption + No comment provided by engineer. + Show last messages Show last messages @@ -8524,6 +8792,37 @@ chat item action Show: No comment provided by engineer. + + Sign message + Sign message + No comment provided by engineer. + + + Sign messages + Sign messages + chat feature + + + Signature missing + Signature missing + alert title +copied message info + + + Signed + Signed + copied message info + + + Signed & verified + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8619,6 +8918,21 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + SimpleX name + No comment provided by engineer. + + + SimpleX name error + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX one-time invitation @@ -8629,6 +8943,11 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address SimpleX relay address @@ -8899,9 +9218,9 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - Support SimpleX Chat + + Support the project + Support the project No comment provided by engineer. @@ -9097,6 +9416,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. The address will be short, and your profile will be shared via the address. @@ -9127,6 +9466,16 @@ It can happen because of some bug or when the connection is compromised.The attempt to change database passphrase was not completed. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. The code you scanned is not a SimpleX link QR code. @@ -9224,6 +9573,11 @@ your contacts and groups. The second tick we missed! ✅ No comment provided by engineer. + + The sender deleted the connection request. + The sender deleted the connection request. + No comment provided by engineer. + The sender will NOT be notified The sender will NOT be notified @@ -9279,6 +9633,11 @@ your contacts and groups. They can be overridden in contact and group settings. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. @@ -9299,6 +9658,11 @@ your contacts and groups. This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. This chat is protected by end-to-end encryption. @@ -9457,6 +9821,11 @@ You will be prompted to complete authentication before this feature is enabled.< To record voice message please grant permission to use Microphone. No comment provided by engineer. + + To resolve names + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. @@ -9492,6 +9861,11 @@ You will be prompted to complete authentication before this feature is enabled.< To verify end-to-end encryption with your contact compare (or scan) the code on your devices. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Toggle incognito when connecting. @@ -9582,6 +9956,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + Unconfirmed name + No comment provided by engineer. + Undelivered messages Undelivered messages @@ -9642,13 +10021,6 @@ You will be prompted to complete authentication before this feature is enabled.< Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - No comment provided by engineer. - Unlink Unlink @@ -9679,20 +10051,15 @@ To connect, please ask your contact to create another connection link and check Unread swipe action - - Unsupported channel name - Unsupported channel name - alert title - Unsupported connection link Unsupported connection link conn error description - - Unsupported contact name - Unsupported contact name - alert title + + Unverified badge + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9747,7 +10114,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -9924,6 +10292,11 @@ To connect, please ask your contact to create another connection link and check Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + Used chat relays do not support webpages. + No comment provided by engineer. + User selection User selection @@ -9944,6 +10317,11 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Verify code with desktop @@ -9969,6 +10347,11 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + Verify name + No comment provided by engineer. + Verify passphrase Verify passphrase @@ -10129,6 +10512,16 @@ To connect, please ask your contact to create another connection link and check WebRTC ICE servers No comment provided by engineer. + + Webpage code + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Welcome %@! @@ -10351,9 +10744,9 @@ Repeat join request? You can enable later via Settings No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - You can enable them later via app Privacy & Security settings. + + You can enable them later via app Your privacy settings. + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -10416,6 +10809,11 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. You can turn on SimpleX Lock via Settings. @@ -10607,6 +11005,11 @@ Repeat connection request? Your SimpleX address No comment provided by engineer. + + Your SimpleX name + Your SimpleX name + No comment provided by engineer. + Your business contact Your business contact @@ -10622,11 +11025,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - Your chat database - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Your chat database is not encrypted - set passphrase to encrypt it. @@ -10657,6 +11055,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Your contact sent a file that is larger than currently supported maximum size (%@). @@ -10831,6 +11236,11 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + acknowledged roster + No comment provided by engineer. + active active @@ -11097,6 +11507,11 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + contributor + member role + creator creator @@ -11303,6 +11718,11 @@ pref value hours time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain is used to securely store passphrase - it allows receiving push notifications. @@ -11745,6 +12165,11 @@ last received msg: %2$@ strike No comment provided by engineer. + + subscriber + subscriber + member role + this contact this contact @@ -11795,11 +12220,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ via %@ @@ -11954,7 +12374,7 @@ last received msg: %2$@
- +
@@ -11989,9 +12409,26 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -12013,7 +12450,7 @@ last received msg: %2$@
- +
@@ -12045,7 +12482,7 @@ last received msg: %2$@
- +
@@ -12067,7 +12504,7 @@ last received msg: %2$@
- +
@@ -12250,9 +12687,9 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index ec2accf27e..7b50cab8e7 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 d93e692a63..6d140c7f78 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #secreto# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ ha apoyado a SimpleX Chat. La insignia caducó el %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ descargado No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ ha participado en la financiación colectiva de SimpleX Chat. + badge alert + %@ is connected! %@ ¡está conectado! @@ -110,6 +120,11 @@ %@ servidores No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ apoya a SimpleX Chat. + badge alert + %@ uploaded %@ subido @@ -185,6 +200,21 @@ %d mes(es) time interval + + %d owner + %d propietario + channel owners count + + + %d owners + %d propietarios + channel owners count + + + %d owners & contributors + %d propietarios & colaboradores + channel members count + %d relays failed %d servidores han fallado @@ -400,11 +430,6 @@ channel relay bar (nuevo) No comment provided by engineer. - - (signed) - (firmado) - chat link info line - (this device v%@) (este dispositivo v%@) @@ -477,7 +502,7 @@ channel relay bar \*bold* - \*bold* + \*negrita* No comment provided by engineer. @@ -738,6 +763,7 @@ swipe action Add + Añadir No comment provided by engineer. @@ -745,6 +771,16 @@ swipe action Añade la dirección a tu perfil para que tus contactos SimpleX puedan compartirla con otros. La actualización del perfil se enviará a tus contactos SimpleX. No comment provided by engineer. + + Add contributors. + Añade colaboradores. + No comment provided by engineer. + + + Add description + Añadir descripción + No comment provided by engineer. + Add friends Añadir amigos @@ -765,12 +801,19 @@ swipe action Añadir perfil No comment provided by engineer. + + Add relay + Añadir servidor + No comment provided by engineer. + Add relays + Añadir servidores No comment provided by engineer. Add relays to restore message delivery. + Añade servidores para restaurar la entrega de mensajes. No comment provided by engineer. @@ -788,6 +831,11 @@ swipe action Añadir miembros del equipo No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Añade este código a tu web. Mostrará una vista previa de tu canal o grupo. + No comment provided by engineer. + Add to another device Añadir a otro dispositivo @@ -868,6 +916,11 @@ swipe action Configuración avanzada de red No comment provided by engineer. + + Advanced options + Opciones avanzadas + No comment provided by engineer. + Advanced settings Configuración avanzada @@ -978,6 +1031,11 @@ swipe action Se permite No comment provided by engineer. + + Allow anyone to embed + Permitir que cualquiera pueda añadirlo a su web + No comment provided by engineer. + Allow calls only if your contact allows them. Se permiten las llamadas pero sólo si tu contacto también las permite. @@ -1010,7 +1068,7 @@ swipe action Allow members to chat with admins. - Permitir que los miembros chateen con administradores. + Permite que los miembros chateen con los administradores. No comment provided by engineer. @@ -1045,7 +1103,7 @@ swipe action Allow subscribers to chat with admins. - Permitir que los suscriptores chateen con administradores. + Permite que los suscriptores chateen con los administradores. No comment provided by engineer. @@ -1153,6 +1211,11 @@ swipe action Responder llamada No comment provided by engineer. + + Any webpage can show the preview. + Cualquier página web puede mostrar la vista previa. + No comment provided by engineer. + App build: %@ Compilación app: %@ @@ -1195,6 +1258,7 @@ swipe action App update required + Es necesario actualizar la aplicación alert title @@ -1362,6 +1426,11 @@ swipe action Hash de mensaje incorrecto No comment provided by engineer. + + Badge cannot be verified + No se pudo verificar la insignia + badge alert title + Be free in your network @@ -1384,6 +1453,11 @@ en tu red Llamadas mejoradas No comment provided by engineer. + + Better channels 📢 + Canales mejorados 📢 + No comment provided by engineer. + Better groups Grupos mejorados @@ -1574,11 +1648,6 @@ en tu red ¡La llamada ha terminado! No comment provided by engineer. - - Calls - Llamadas - No comment provided by engineer. - Calls prohibited! ¡Llamadas no permitidas! @@ -1628,10 +1697,12 @@ new chat action Cancel and delete channel + Cancelar y eliminar el canal No comment provided by engineer. Cancel creating channel? + ¿Cancelar la creación del canal? alert title @@ -1689,11 +1760,6 @@ new chat action Cambiar el modo de bloqueo authentication reason - - Change member role? - ¿Cambiar rol? - No comment provided by engineer. - Change passcode Cambiar código de acceso @@ -1714,6 +1780,11 @@ new chat action Cambiar rol No comment provided by engineer. + + Change role? + ¿Cambiar el rol? + No comment provided by engineer. + Change self-destruct mode Cambiar el modo de autodestrucción @@ -1730,6 +1801,11 @@ set passcode view Canal No comment provided by engineer. + + Channel SimpleX name + Nombre SimpleX del canal + No comment provided by engineer. + Channel display name Título mostrado del canal @@ -1781,6 +1857,11 @@ alert subtitle Canales no disponibles temporalmente alert title + + Channel webpage + Web del canal + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! El canal será eliminado para todos los suscriptores. ¡No puede deshacerse! @@ -1793,6 +1874,7 @@ alert subtitle Channel will start working with %1$d of %2$d relays. Continue? + El canal comenzará a funcionar con %1$d de %2$d servidores. ¿Deseas continuar? alert message @@ -1825,6 +1907,11 @@ alert subtitle Consola de Chat No comment provided by engineer. + + Chat data + Datos del chat + No comment provided by engineer. + Chat database Base de datos de SimpleX @@ -2068,7 +2155,7 @@ chat toolbar Community guidelines violation - Violación de las normas de la comunidad + Violación de las normas report reason @@ -2202,6 +2289,11 @@ server test step ¡Conéctate más rápido! 🚀 No comment provided by engineer. + + Connect to %@ + Conectar con %@ + new chat action + Connect to desktop Conectar con ordenador @@ -2288,7 +2380,7 @@ This is your own one-time link! Connecting to contact, please wait or check later! - Conectando con el contacto, por favor espera o revisa más tarde. + Se está estableciendo la conexión con el contacto. ¡Por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. @@ -2296,14 +2388,6 @@ This is your own one-time link! Conectando con ordenador No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Conexión @@ -2319,16 +2403,16 @@ This is your own one-time link! Conexión bloqueada No comment provided by engineer. + + Connection blocked: %@ + Conexión bloqueada: %@ + conn error description + Connection error Error conexión alert title - - Connection error (AUTH) - Error de conexión (Autenticación) - conn error description - Connection failed Conexión fallida @@ -2341,6 +2425,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Error de conexión + conn error description + Connection not ready. Conexión no establecida. @@ -2386,6 +2475,11 @@ This is your own one-time link! Conexiones No comment provided by engineer. + + Contact + Contacto + No comment provided by engineer. + Contact address Dirección de contacto @@ -2423,7 +2517,7 @@ This is your own one-time link! Contact name - Contacto + Nombre de contacto No comment provided by engineer. @@ -2476,6 +2570,11 @@ This is your own one-time link! Copiar No comment provided by engineer. + + Copy code + Copiar código + No comment provided by engineer. + Copy error Copiar error @@ -2511,6 +2610,11 @@ This is your own one-time link! Crear grupo usando perfil aleatorio. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Crea una página web para mostrar la vista previa de tu canal a las visitas antes de suscribirse. Alójala tú mismo o usa cualquier servicio de alojamiento estático. + No comment provided by engineer. + Create file Crear archivo @@ -2538,7 +2642,7 @@ This is your own one-time link! Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 - Crea perfil nuevo en la [aplicación para PC](https://simplex.Descargas/de chat/). 💻 + Crea un nuevo perfil en la [aplicación de escritorio](https://simplex.chat/downloads/). 💻 No comment provided by engineer. @@ -2551,16 +2655,16 @@ This is your own one-time link! Crear canal público No comment provided by engineer. - - Create public channel (BETA) - Crear canal público (BETA) - No comment provided by engineer. - Create queue Crear cola server test step + + Create web preview. + Crea previsualizaciones web. + No comment provided by engineer. + Create your address Crea tu dirección @@ -2907,6 +3011,7 @@ swipe action Delete from history + Eliminar del historial No comment provided by engineer. @@ -3095,9 +3200,9 @@ alert button Ordenadores No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + La dirección del servidor de destino de %1$@ es incompatible con la configuración del servidor de reenvío %2$@. No comment provided by engineer. @@ -3105,9 +3210,9 @@ alert button Error del servidor de destino: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + La versión del servidor de destino de %1$@ es incompatible con el servidor de reenvío %2$@. No comment provided by engineer. @@ -3120,9 +3225,9 @@ alert button Detalles No comment provided by engineer. - - Develop - Desarrollo + + Developer + Herramientas desarrollo No comment provided by engineer. @@ -3130,11 +3235,6 @@ alert button Opciones desarrollador No comment provided by engineer. - - Developer tools - Herramientas desarrollo - No comment provided by engineer. - Device Dispositivo @@ -3172,7 +3272,7 @@ alert button Direct messages between subscribers are prohibited. - Los mensajes directos entre suscriptores del canal no están permitidos. + Los mensajes directos entre suscriptores no están permitidos. No comment provided by engineer. @@ -3280,6 +3380,11 @@ alert button Hacer más tarde No comment provided by engineer. + + Do not require signing messages. + No requerir la firma de mensajes. + No comment provided by engineer. + Do not send history to new members. No se envía el historial a los miembros nuevos. @@ -3315,6 +3420,11 @@ alert button No pierdas los mensajes importantes. No comment provided by engineer. + + Don't save + No guardar + alert action + Don't show again No volver a mostrar @@ -3396,6 +3506,11 @@ chat item action Invitar a tus amigos es más fácil 👋 No comment provided by engineer. + + Easier to read. + Fácil de leer. + No comment provided by engineer. + Edit Editar @@ -3406,6 +3521,11 @@ chat item action Editar perfil del canal No comment provided by engineer. + + Edit description + Editar descripción + No comment provided by engineer. + Edit group profile Editar perfil de grupo @@ -3606,6 +3726,11 @@ chat item action Introduce la contraseña correcta. No comment provided by engineer. + + Enter description (optional) + Introduce descripción (opcional) + placeholder + Enter group name… Nombre del grupo… @@ -3646,6 +3771,11 @@ chat item action Nombre de este dispositivo… No comment provided by engineer. + + Enter webpage URL + Introduce la URL de la web + No comment provided by engineer. + Enter welcome message… Deja un mensaje de bienvenida… @@ -3664,7 +3794,7 @@ chat item action Error Error - conn error description + No comment provided by engineer. Error aborting address change @@ -3698,6 +3828,7 @@ chat item action Error adding relays + Error al añadir servidores alert title @@ -3832,6 +3963,7 @@ chat item action Error deleting message + Error al eliminar el mensaje alert title @@ -3964,6 +4096,11 @@ chat item action Error al guardar perfil de grupo No comment provided by engineer. + + Error saving name + Error al guardar el nombre + alert title + Error saving passcode Error al guardar código de acceso @@ -4019,6 +4156,11 @@ chat item action ¡Error al configurar confirmaciones de entrega! No comment provided by engineer. + + Error sharing address + Error compartiendo dirección + alert title + Error sharing channel Error al compartir el canal @@ -4098,6 +4240,7 @@ chat item action Error: %@ Error: %@ alert message +conn error description file error text snd error text @@ -4241,6 +4384,16 @@ server test error Error del servidor de archivos: %@ file error text + + File servers + Servidores de archivos + No comment provided by engineer. + + + File servers: %@ + Servidores de archivos: %@ + copied message info + File status Estado del archivo @@ -4542,6 +4695,11 @@ Error: %2$@ GIFs y stickers No comment provided by engineer. + + Get SimpleX name (BETA) + Obtener nombre SimpleX (BETA) + No comment provided by engineer. + Get link Recibir el enlace @@ -4652,6 +4810,11 @@ Error: %2$@ El perfil del grupo ha cambiado. Si lo guardas, el perfil actualizado se enviará a los miembros del grupo. alert message + + Group webpage + Web del grupo + No comment provided by engineer. + Group welcome message Mensaje de bienvenida en grupos @@ -4677,6 +4840,11 @@ Error: %2$@ Ayuda No comment provided by engineer. + + Help & support + Ayuda y asistencia + No comment provided by engineer. + Help admins moderating their groups. Ayuda a los admins a moderar sus grupos. @@ -4757,6 +4925,11 @@ Error: %2$@ Cómo No comment provided by engineer. + + How to register a test name + Cómo registrar un nombre de prueba + No comment provided by engineer. + How to use it Guía de uso @@ -5162,6 +5335,11 @@ More improvements are coming soon! Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Se mostrará a los suscriptores y se usará para permitir la carga de la vista previa. + No comment provided by engineer. + Italian interface Interfaz en italiano @@ -5187,6 +5365,11 @@ More improvements are coming soon! Unirme al canal No comment provided by engineer. + + Join channel %@ + Unirme al canal %@ + new chat action + Join group Unirme al grupo @@ -5267,7 +5450,7 @@ This is your link for group %@! Learn more Más información - No comment provided by engineer. + badge alert button Leave @@ -5309,6 +5492,16 @@ This is your link for group %@! Menos tráfico en redes móviles. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Permitir el contacto mediante tu nombre registrado contra tu dirección SimpleX. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Permitir unirse al canal mediante el nombre registrado con este enlace. + No comment provided by engineer. + Let someone connect to you Conecta con alguien @@ -5419,6 +5612,11 @@ This is your link for group %@! Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas. No comment provided by engineer. + + Manage your relays. + Gestiona tus servidores. + No comment provided by engineer. + Mark deleted for everyone Marcar como eliminado para todos @@ -5489,21 +5687,6 @@ This is your link for group %@! Informes de miembros chat feature - - Member role will be changed to "%@". All chat members will be notified. - El rol del miembro cambiará a "%@". Se notificará en el chat. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - El rol del miembro cambiará a "%@" y se notificará al grupo. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - El rol del miembro cambiará a "%@" y recibirá una invitación nueva. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! El miembro será eliminado del chat. ¡No puede deshacerse! @@ -5571,7 +5754,7 @@ This is your link for group %@! Menus - Menus + Menús No comment provided by engineer. @@ -5649,6 +5832,16 @@ This is your link for group %@! Forma del mensaje No comment provided by engineer. + + Message signing is not required. + Los mensajes firmados no son obligatorios. + No comment provided by engineer. + + + Message signing is required. + Los mensajes firmados son obligatorios. + No comment provided by engineer. + Message source remains private. El autor del mensaje se mantiene privado. @@ -5819,6 +6012,11 @@ This is your link for group %@! ¡Pronto habrá más mejoras! No comment provided by engineer. + + More privacy + Más privacidad + No comment provided by engineer. + More reliable network connection. Conexión de red más fiable. @@ -5859,6 +6057,11 @@ This is your link for group %@! Nombre swipe action + + Name not found + Nombre no encontrado + No comment provided by engineer. + Network & servers Servidores y Redes @@ -5968,7 +6171,7 @@ quién se comunica con quién New desktop app! - Nueva aplicación para PC! + ¡Nueva aplicación de escritorio! No comment provided by engineer. @@ -6045,6 +6248,7 @@ El cifrado más seguro. No available relays + Sin servidores disponibles No comment provided by engineer. @@ -6174,6 +6378,7 @@ El cifrado más seguro. No relays + Sin servidores No comment provided by engineer. @@ -6191,6 +6396,11 @@ El cifrado más seguro. Sin servidores para recibir mensajes. servers error + + No servers to resolve names. + Sin servidores para resolver nombres. + servers warning + No servers to send files. Sin servidores para enviar archivos. @@ -6206,6 +6416,11 @@ El cifrado más seguro. Ningún chat sin leer No comment provided by engineer. + + No valid link + Ningún enlace válido + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir. @@ -6216,6 +6431,11 @@ El cifrado más seguro. Gobernanza no lucrativa No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No tienes servidores configurados para resolver nombres SimpleX. Hazlo, o usa un enlace para conectarte. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano. @@ -6338,7 +6558,7 @@ Requiere activación de la VPN. Only channel owners can change channel preferences. - Sólo los propietarios pueden modificar las preferencias de los canales. + Sólo los propietarios pueden modificar las preferencias del canal. No comment provided by engineer. @@ -6441,6 +6661,11 @@ Requiere activación de la VPN. Sólo tu contacto puede enviar mensajes de voz. No comment provided by engineer. + + Only your page above can show the preview. + Solo la página superior puede mostrar la vista previa. + No comment provided by engineer. + Open Abrir @@ -6630,9 +6855,9 @@ alert button Propietario No comment provided by engineer. - - Owners - Propietarios + + Owners & contributors + Propietarios y colaboradores No comment provided by engineer. @@ -6824,10 +7049,6 @@ Error: %@ Por favor, intenta desactivar y reactivar las notificaciones. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Por favor, espera a que tu solicitud sea revisada por los moderadores del grupo. @@ -6888,11 +7109,6 @@ Error: %@ Servidores conectados previamente No comment provided by engineer. - - Privacy & security - Seguridad y Privacidad - No comment provided by engineer. - Privacy for your customers. Privacidad para tus clientes. @@ -6981,7 +7197,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. La actualización del perfil se enviará a tus contactos SimpleX. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7100,6 +7317,11 @@ Actívalo en ajustes de *Servidores y Redes*. Canales públicos - habla con libertad 🚀 No comment provided by engineer. + + Public names for your channel or business. + Nombres públicos para tu canal o negocio. + No comment provided by engineer. + Push notifications Notificaciones push @@ -7138,7 +7360,7 @@ Actívalo en ajustes de *Servidores y Redes*. Read more Saber más - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7257,7 +7479,7 @@ Actívalo en ajustes de *Servidores y Redes*. Record updated at - Registro actualiz. + Registro actualizado a las No comment provided by engineer. @@ -7349,10 +7571,12 @@ swipe action Relay will be removed from channel - this cannot be undone! + El servidor será eliminado del canal. ¡No puede deshacerse! alert message Relays added: %@. + Servidores añadidos: %@. alert message @@ -7395,18 +7619,29 @@ swipe action ¿Expulsar miembro? alert title + + Remove name + Eliminar nombre + No comment provided by engineer. + Remove passphrase from keychain? ¿Eliminar contraseña de Keychain? No comment provided by engineer. + + Remove relay + Quitar servidor + No comment provided by engineer. + Remove relay? + ¿Eliminar el servidor? alert title Remove subscriber? - ¿Eliminar suscriptor? + ¿Eliminar el suscriptor? alert title @@ -7504,6 +7739,11 @@ swipe action Informes No comment provided by engineer. + + Require signing messages. + Requerir la firma de mensajes. + No comment provided by engineer. + Required Obligatorio @@ -7549,6 +7789,11 @@ swipe action Restablecer al tema del usuario No comment provided by engineer. + + Resolver error: %@ + Error de resolución: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Reinicia la aplicación para crear un perfil nuevo @@ -7629,6 +7874,26 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + El rol del miembro cambiará a "%@" y se notificará en el chat. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + El rol del miembro cambiará a "%@" y se notificará en el grupo. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + El rol cambiará a "%@" y se notificará a los suscriptores. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + El rol del miembro cambiará a "%@" y recibirá una invitación nueva. + No comment provided by engineer. + Run chat Ejecutar SimpleX @@ -7662,7 +7927,8 @@ swipe action Save Guardar - alert button + alert action +alert button chat item action @@ -7680,6 +7946,11 @@ chat item action Guardar (y notificar suscriptores) alert button + + Save SimpleX name? + ¿Guardar el nombre SimpleX? + alert title + Save admission settings? ¿Guardar configuración? @@ -7695,6 +7966,11 @@ chat item action Guardar y notificar grupo No comment provided by engineer. + + Save and notify members + Guardar e informar miembros + No comment provided by engineer. + Save and notify subscribers Guardar y notificar suscriptores @@ -7765,6 +8041,11 @@ chat item action ¿Guardar servidores? alert title + + Save webpage settings? + ¿Guardar la configuración web? + alert title + Save welcome message? ¿Guardar mensaje de bienvenida? @@ -8047,12 +8328,12 @@ chat item action Send up to 100 last messages to new members. - Se envían hasta 100 mensajes más recientes a los miembros nuevos. + Se envían los 100 últimos mensajes a los miembros nuevos. No comment provided by engineer. Send up to 100 last messages to new subscribers. - Se envían hasta 100 mensajes más recientes a los suscriptores nuevos. + Se envían los 100 últimos mensajes a los suscriptores nuevos. No comment provided by engineer. @@ -8065,11 +8346,6 @@ chat item action El remitente ha cancelado la transferencia de archivos. alert message - - Sender may have deleted the connection request. - El remitente puede haber eliminado la solicitud de conexión. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. Enviar una previsualización del enlace puede revelar tu dirección IP al sitio web. Puedes cambiarlo más tarde en los ajustes de privacidad. @@ -8165,6 +8441,11 @@ chat item action Servidor No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + El servidor %@ no admite la resolución de nombres. Configura un servidor, o usa un enlace para conectarte. + No comment provided by engineer. + Server added to operator %@. Servidor añadido al operador %@. @@ -8476,6 +8757,11 @@ chat item action Mostrar opciones de desarrollador No comment provided by engineer. + + Show encryption + Mostrar cifrado + No comment provided by engineer. + Show last messages Mostrar último mensaje @@ -8506,6 +8792,37 @@ chat item action Muestra: No comment provided by engineer. + + Sign message + Firmar mensaje + No comment provided by engineer. + + + Sign messages + Firmar mensajes + chat feature + + + Signature missing + Falta la firma + alert title +copied message info + + + Signed + Firmado + copied message info + + + Signed & verified + Firmado y verificado + copied message info + + + Signing proves you authored this message and can't be denied later. + La firma prueba que eres el autor del mensaje sin posibilidad de repudio. + No comment provided by engineer. + SimpleX SimpleX @@ -8601,6 +8918,21 @@ chat item action Enlaces SimpleX no permitidos No comment provided by engineer. + + SimpleX name + Nombre SimpleX + No comment provided by engineer. + + + SimpleX name error + Error del nombre SimpleX + No comment provided by engineer. + + + SimpleX name not verified + Nombre SimpleX no verificado + alert title + SimpleX one-time invitation Invitación SimpleX de un uso @@ -8611,6 +8943,11 @@ chat item action Protocolos de SimpleX auditados por Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + Nombres públicos SimpleX (BETA) + No comment provided by engineer. + SimpleX relay address Dirección de servidor SimpleX @@ -8721,6 +9058,7 @@ report reason Status + Estado No comment provided by engineer. @@ -8830,7 +9168,7 @@ report reason Subscribers can irreversibly delete sent messages. (24 hours) - Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas) + Los suscriptores pueden eliminar mensajes de forma irreversible. (24 horas) No comment provided by engineer. @@ -8840,27 +9178,27 @@ report reason Subscribers can send SimpleX links. - Los suscriptores del canal pueden enviar enlaces SimpleX. + Los suscriptores pueden enviar enlaces SimpleX. No comment provided by engineer. Subscribers can send direct messages. - Los suscriptores del canal pueden enviar mensajes directos. + Los suscriptores pueden enviar mensajes directos. No comment provided by engineer. Subscribers can send disappearing messages. - Los suscriptores del canal pueden enviar mensajes temporales. + Los suscriptores pueden enviar mensajes temporales. No comment provided by engineer. Subscribers can send files and media. - Los suscriptores del canal pueden enviar archivos y multimedia. + Los suscriptores pueden enviar archivos y multimedia. No comment provided by engineer. Subscribers can send voice messages. - Los suscriptores del canal pueden enviar mensajes de voz. + Los suscriptores pueden enviar mensajes de voz. No comment provided by engineer. @@ -8880,9 +9218,9 @@ La dirección del servidor se usó para establecer el servidor para el canal.Suscripciones ignoradas No comment provided by engineer. - - Support SimpleX Chat - Soporte SimpleX Chat + + Support the project + Apoya el proyecto No comment provided by engineer. @@ -9078,6 +9416,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + El nombre SimpleX #%@ está registrado sin un enlace de canal. Añádelo en la página de registro. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + El nombre SimpleX %@ está registrado, pero no tiene un enlace válido. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + El nombre SimpleX %@ está registrado, pero no se ha añadido a un perfil. Por favor, si eres el propietario, añádelo a tu dirección o al perfil del canal. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + El nombre SimpleX @%@ está registrado sin una dirección SimpleX. Añádela en la página de registro. + alert message + 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. @@ -9108,6 +9466,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El intento de cambiar la contraseña de la base de datos no se ha completado. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + La insignia está firmada con una clave que esta versión de la app no reconoce. Actualiza la app para verificar la insignia. + badge alert + + + The channel required this message to be signed, but the signature is missing. + El canal requiere que el mensaje esté firmado, pero falta la firma. + alert message + The code you scanned is not a SimpleX link QR code. El código QR escaneado no es un enlace de SimpleX. @@ -9205,6 +9573,11 @@ y los contactos son tuyos. ¡El doble check que nos faltaba! ✅ No comment provided by engineer. + + The sender deleted the connection request. + El remitente puede haber eliminado la solicitud de conexión. + No comment provided by engineer. + The sender will NOT be notified El remitente NO será notificado @@ -9260,6 +9633,11 @@ y los contactos son tuyos. Se puede modificar desde la configuración particular de cada grupo y contacto. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + El nombre SimpleX no está registrado. Por favor, comprueba el nombre. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán. @@ -9280,6 +9658,11 @@ y los contactos son tuyos. Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + No se ha podido verificar la insignia, podría no ser auténtica. + badge alert + This chat is protected by end-to-end encryption. Este chat está protegido por cifrado de extremo a extremo. @@ -9312,6 +9695,7 @@ y los contactos son tuyos. This group requires a newer version of the app. Please update the app to join. + Este grupo requiere una versión más reciente de la app. Por favor, actualizala para unirte. alert message alert subtitle @@ -9322,6 +9706,7 @@ alert subtitle This is the last active relay. Removing it will prevent message delivery to subscribers. + Este es el último servidor activo. Si lo eliminas, se impedirá la entrega de mensajes a los suscriptores. alert message @@ -9436,6 +9821,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para grabar el mensaje de voz concede permiso para usar el micrófono. No comment provided by engineer. + + To resolve names + Para resolver nombres + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**. @@ -9471,6 +9861,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + Para verificar las claves con este suscriptor, compara (o escanea) el código en ambos dispositivos. + No comment provided by engineer. + Toggle incognito when connecting. Activa incógnito al conectar. @@ -9561,6 +9956,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.¿Desbloquear al suscriptor para todos? No comment provided by engineer. + + Unconfirmed name + Nombre sin confirmar + No comment provided by engineer. + Undelivered messages Mensajes no entregados @@ -9621,13 +10021,6 @@ Se te pedirá que completes la autenticación antes de activar esta función.A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo. -Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red. - No comment provided by engineer. - Unlink Desenlazar @@ -9658,18 +10051,15 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión No leído swipe action - - Unsupported channel name - alert title - Unsupported connection link Enlace de conexión no compatible conn error description - - Unsupported contact name - alert title + + Unverified badge + Insignia sin verificar + badge alert title Up to 100 last messages are sent to new members. @@ -9724,7 +10114,8 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Upgrade address? ¿Actualizar la dirección? - alert message + alert message +alert title Upgrade and open chat @@ -9901,6 +10292,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Usar puerto web No comment provided by engineer. + + Used chat relays do not support webpages. + Los servidores usados no admiten páginas web. + No comment provided by engineer. + User selection Selección de usuarios @@ -9921,6 +10317,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Verificar relay test step + + Verify SimpleX names + Verificar nombres SimpleX + No comment provided by engineer. + Verify code with desktop Verificar código con ordenador @@ -9946,6 +10347,11 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Verificar la contraseña de la base de datos No comment provided by engineer. + + Verify name + Verificar nombre + No comment provided by engineer. + Verify passphrase Verificar frase de contraseña @@ -9978,7 +10384,7 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Video will be received when your contact is online, please wait or check later! - El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. + El vídeo se recibirá cuando tu contacto esté conectado. ¡Por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. @@ -10106,6 +10512,16 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión Servidores WebRTC ICE No comment provided by engineer. + + Webpage code + Código web + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + Se han modificado los ajustes de la página web. Si guardas los cambios, los nuevos ajustes se enviarán a los suscriptores. + alert message + Welcome %@! ¡Bienvenido %@! @@ -10328,9 +10744,9 @@ Repeat join request? Puedes activar más tarde en Configuración No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Puedes activarlos más tarde en la configuración de Privacidad y Seguridad. + + You can enable them later via app Your privacy settings. + Puedes habilitarlos más tarde en el menú privacidad. No comment provided by engineer. @@ -10393,6 +10809,11 @@ Repeat join request? Aún puedes ver la conversación con %@ en la lista de chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + Puedes apoyar SimpleX desde la versión 7. + badge alert + You can turn on SimpleX Lock via Settings. Puedes activar el Bloqueo SimpleX a través de Configuración. @@ -10486,7 +10907,7 @@ Repeat connection request? You need to allow your contact to send voice messages to be able to send them. - Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos. + Para poder enviar mensajes de voz, antes debes permitir que tu contacto pueda enviarlos. No comment provided by engineer. @@ -10516,22 +10937,22 @@ Repeat connection request? You will be connected to group when the group host's device is online, please wait or check later! - Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde. + Te conectarás al grupo cuando el dispositivo del administrador del grupo esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. You will be connected when group link host's device is online, please wait or check later! - Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde. + Te conectarás cuando el dispositivo del anfitrión del enlace de grupo esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. You will be connected when your connection request is accepted, please wait or check later! - Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde. + Te conectarás cuando se acepte tu solicitud de conexión. ¡Por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. You will be connected when your contact's device is online, please wait or check later! - Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde. + Te conectarás cuando el dispositivo de tu contacto esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde! No comment provided by engineer. @@ -10584,6 +11005,11 @@ Repeat connection request? Mi dirección SimpleX No comment provided by engineer. + + Your SimpleX name + Mi nombre SimpleX + No comment provided by engineer. + Your business contact Mi contacto empresarial @@ -10599,11 +11025,6 @@ Repeat connection request? Tu canal No comment provided by engineer. - - Your chat database - Base de datos - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. La base de datos no está cifrada - establece una contraseña para cifrarla. @@ -10634,6 +11055,13 @@ Repeat connection request? Mi contacto No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo. +Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). El contacto ha enviado un archivo mayor al máximo admitido (%@). @@ -10682,6 +11110,8 @@ Repeat connection request? Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + Tu canal %1$@ está conectado a %2$d de %3$d servidores. +Si cancelas, el canal se eliminará. Puedes volver a crearlo. alert message @@ -10806,6 +11236,11 @@ Los servidores tienen acceso a los mensajes del canal. te ha admitido rcv group event chat item + + acknowledged roster + lista confirmada + No comment provided by engineer. + active activo @@ -10994,7 +11429,7 @@ marked deleted chat item preview text connecting - conectando... + conectando No comment provided by engineer. @@ -11072,6 +11507,11 @@ marked deleted chat item preview text el contacto debe aceptarte… No comment provided by engineer. + + contributor + colaborador + member role + creator creador @@ -11278,6 +11718,11 @@ pref value horas time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain se usa para almacenar la contraseña de forma segura. Esto permite recibir notificaciones automáticas. @@ -11720,6 +12165,11 @@ last received msg: %2$@ tachado No comment provided by engineer. + + subscriber + suscriptor + member role + this contact este contacto @@ -11770,11 +12220,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ mediante %@ @@ -11929,7 +12374,7 @@ last received msg: %2$@
- +
@@ -11964,9 +12409,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11988,7 +12448,7 @@ last received msg: %2$@
- +
@@ -12020,7 +12480,7 @@ last received msg: %2$@
- +
@@ -12042,7 +12502,7 @@ last received msg: %2$@
- +
@@ -12212,7 +12672,7 @@ last received msg: %2$@ Unsupported format - Formato sin soporte + Formato no compatible No comment provided by engineer. @@ -12225,9 +12685,9 @@ last received msg: %2$@ Contraseña incorrecta de la base de datos No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX. + + You can allow sharing in Your privacy / SimpleX Lock settings. + Puedes habilitar el uso compartido en el menú Privacidad / Bloqueo Simplex. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/es.xcloc/contents.json b/apps/ios/SimpleX Localizations/es.xcloc/contents.json index 80cffac8d2..5a4c833adf 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/es.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "es", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 5656516b7d..cd6030b2be 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ #salaisuus# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ % @ @@ -82,6 +86,10 @@ %@ downloaded No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ on yhdistetty! @@ -105,6 +113,10 @@ %@ servers No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded No comment provided by engineer. @@ -172,6 +184,18 @@ %d kuukautta time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -365,10 +389,6 @@ channel relay bar (new) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) No comment provided by engineer. @@ -675,6 +695,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends No comment provided by engineer. @@ -692,6 +720,10 @@ swipe action Lisää profiili No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -714,6 +746,10 @@ swipe action Add team members No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Lisää toiseen laitteeseen @@ -784,6 +820,10 @@ swipe action Verkon lisäasetukset No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings No comment provided by engineer. @@ -880,6 +920,10 @@ swipe action Salli No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Salli puhelut vain, jos kontaktisi sallii ne. @@ -1041,6 +1085,10 @@ swipe action Vastaa puheluun No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ Sovellusversio: %@ @@ -1231,6 +1279,10 @@ swipe action Virheellinen viestin tarkiste No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1248,6 +1300,10 @@ in your network Better calls No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups No comment provided by engineer. @@ -1408,11 +1464,6 @@ in your network Puhelu on jo päättynyt! No comment provided by engineer. - - Calls - Puhelut - No comment provided by engineer. - Calls prohibited! No comment provided by engineer. @@ -1511,11 +1562,6 @@ new chat action Vaihda lukitustilaa authentication reason - - Change member role? - Vaihda jäsenroolia? - No comment provided by engineer. - Change passcode Vaihda pääsykoodi @@ -1536,6 +1582,10 @@ new chat action Vaihda rooli No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Vaihda itsetuhotilaa @@ -1551,6 +1601,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1592,6 +1646,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1629,6 +1687,10 @@ alert subtitle Chat-konsoli No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database Chat-tietokanta @@ -1956,6 +2018,10 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop No comment provided by engineer. @@ -2034,14 +2100,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Yhteys @@ -2055,16 +2113,15 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Yhteysvirhe alert title - - Connection error (AUTH) - Yhteysvirhe (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2074,6 +2131,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Yhteysvirhe + conn error description + Connection not ready. No comment provided by engineer. @@ -2112,6 +2174,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2195,6 +2261,10 @@ This is your own one-time link! Kopioi No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error No comment provided by engineer. @@ -2225,6 +2295,10 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Luo tiedosto @@ -2262,15 +2336,15 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Luo jono server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -2762,16 +2836,16 @@ alert button Desktop devices No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -2782,20 +2856,15 @@ alert button Details No comment provided by engineer. - - Develop - Kehitä + + Developer + Kehittäjätyökalut No comment provided by engineer. Developer options No comment provided by engineer. - - Developer tools - Kehittäjätyökalut - No comment provided by engineer. - Device Laite @@ -2931,6 +3000,10 @@ alert button Tee myöhemmin No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. No comment provided by engineer. @@ -2961,6 +3034,10 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + alert action + Don't show again Älä näytä uudelleen @@ -3031,6 +3108,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Muokkaa @@ -3040,6 +3121,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Muokkaa ryhmäprofiilia @@ -3224,6 +3309,10 @@ chat item action Anna oikea tunnuslause. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… No comment provided by engineer. @@ -3259,6 +3348,10 @@ chat item action Enter this device name… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Kirjoita tervetuloviesti… @@ -3276,7 +3369,7 @@ chat item action Error Virhe - conn error description + No comment provided by engineer. Error aborting address change @@ -3546,6 +3639,10 @@ chat item action Virhe ryhmäprofiilin tallentamisessa No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Virhe pääsykoodin tallentamisessa @@ -3596,6 +3693,10 @@ chat item action Virhe toimituskuittauksien asettamisessa! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3669,6 +3770,7 @@ chat item action Error: %@ Virhe: %@ alert message +conn error description file error text snd error text @@ -3794,6 +3896,14 @@ server test error File server error: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status No comment provided by engineer. @@ -4059,6 +4169,10 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4161,6 +4275,10 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Ryhmän tervetuloviesti @@ -4185,6 +4303,10 @@ Error: %2$@ Apua No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. No comment provided by engineer. @@ -4259,6 +4381,10 @@ Error: %2$@ Miten No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Kuinka sitä käytetään @@ -4630,6 +4756,10 @@ More improvements are coming soon! Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Italialainen käyttöliittymä @@ -4654,6 +4784,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Liity ryhmään @@ -4726,7 +4860,7 @@ This is your link for group %@! Learn more Lue lisää - No comment provided by engineer. + badge alert button Leave @@ -4763,6 +4897,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4863,6 +5005,10 @@ This is your link for group %@! Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Merkitse poistetuksi kaikilta @@ -4925,20 +5071,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5068,6 +5200,14 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -5216,6 +5356,10 @@ This is your link for group %@! Lisää parannuksia on tulossa pian! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. No comment provided by engineer. @@ -5253,6 +5397,10 @@ This is your link for group %@! Nimi swipe action + + Name not found + No comment provided by engineer. + Network & servers Verkko ja palvelimet @@ -5542,6 +5690,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5554,6 +5706,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5562,6 +5718,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -5768,6 +5928,10 @@ Edellyttää VPN:n sallimista. Vain kontaktisi voi lähettää ääniviestejä. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open alert action @@ -5920,8 +6084,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6097,10 +6261,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6153,11 +6313,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - Yksityisyys ja turvallisuus - No comment provided by engineer. - Privacy for your customers. No comment provided by engineer. @@ -6232,7 +6387,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6339,6 +6495,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Push-ilmoitukset @@ -6374,7 +6534,7 @@ Enable in *Network & servers* settings. Read more Lue lisää - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -6605,11 +6765,19 @@ swipe action Poista jäsen? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Poista tunnuslause avainnipusta? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -6698,6 +6866,10 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Pakollinen @@ -6738,6 +6910,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi @@ -6813,6 +6989,24 @@ swipe action Rooli No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun. + No comment provided by engineer. + Run chat Käynnistä chat @@ -6841,7 +7035,8 @@ swipe action Save Tallenna - alert button + alert action +alert button chat item action @@ -6857,6 +7052,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? alert title @@ -6871,6 +7070,10 @@ chat item action Tallenna ja ilmoita ryhmän jäsenille No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -6935,6 +7138,10 @@ chat item action Tallenna palvelimet? alert title + + Save webpage settings? + alert title + Save welcome message? Tallenna tervetuloviesti? @@ -7201,11 +7408,6 @@ chat item action Lähettäjä peruutti tiedoston siirron. alert message - - Sender may have deleted the connection request. - Lähettäjä on saattanut poistaa yhteyspyynnön. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7294,6 +7496,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7562,6 +7768,10 @@ chat item action Näytä kehittäjävaihtoehdot No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Näytä viimeiset viestit @@ -7589,6 +7799,31 @@ chat item action Näytä: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX No comment provided by engineer. @@ -7676,6 +7911,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX-kertakutsu @@ -7685,6 +7932,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -7918,9 +8169,8 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - SimpleX Chat tuki + + Support the project No comment provided by engineer. @@ -8097,6 +8347,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8123,6 +8389,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. No comment provided by engineer. @@ -8209,6 +8483,11 @@ your contacts and groups. Toinen kuittaus, joka uupui! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Lähettäjä on saattanut poistaa yhteyspyynnön. + No comment provided by engineer. + The sender will NOT be notified Lähettäjälle EI ilmoiteta @@ -8257,6 +8536,10 @@ your contacts and groups. Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät. @@ -8276,6 +8559,10 @@ your contacts and groups. Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. E2EE info chat item @@ -8413,6 +8700,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla. @@ -8444,6 +8735,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. @@ -8520,6 +8815,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8578,13 +8877,6 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä. -Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa. - No comment provided by engineer. - Unlink No comment provided by engineer. @@ -8613,17 +8905,13 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Lukematon swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -8671,7 +8959,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -8822,6 +9111,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection No comment provided by engineer. @@ -8839,6 +9132,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8860,6 +9157,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9005,6 +9306,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja WebRTC ICE -palvelimet No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Tervetuloa %@! @@ -9198,9 +9507,8 @@ Repeat join request? Voit ottaa käyttöön myöhemmin asetusten kautta No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9257,6 +9565,10 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Voit ottaa SimpleX Lockin käyttöön Asetusten kautta. @@ -9431,6 +9743,10 @@ Repeat connection request? SimpleX-osoitteesi No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9444,11 +9760,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - Keskustelut-tietokantasi - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi. @@ -9475,6 +9786,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä. +Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@). @@ -9631,6 +9949,10 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -9876,6 +10198,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator luoja @@ -10072,6 +10398,10 @@ pref value tuntia time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen - se mahdollistaa push-ilmoitusten vastaanottamisen. @@ -10474,6 +10804,10 @@ last received msg: %2$@ soita No comment provided by engineer. + + subscriber + member role + this contact tämä kontakti @@ -10517,11 +10851,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -10667,7 +10996,7 @@ last received msg: %2$@
- +
@@ -10701,9 +11030,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -10725,7 +11069,7 @@ last received msg: %2$@
- +
@@ -10752,7 +11096,7 @@ last received msg: %2$@
- +
@@ -10771,7 +11115,7 @@ last received msg: %2$@
- +
@@ -10918,8 +11262,8 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json index 11f7a4861c..f61becaece 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fi", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 3ea0859d76..cf8eafaac6 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #secret# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ a soutenu SimpleX Chat. Le badge a expiré le %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ téléchargé No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ a investi dans le financement participatif de SimpleX Chat. + badge alert + %@ is connected! %@ est connecté·e ! @@ -110,14 +120,19 @@ Serveurs %@ No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ soutient SimpleX Chat. + badge alert + %@ uploaded - %@ envoyé + %@ téléversé No comment provided by engineer. %@ wants to connect! - %@ veut se connecter ! + %@ veut se connecter ! notification title @@ -185,6 +200,18 @@ %d mois time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -192,17 +219,19 @@ channel subscriber relay bar %d relays not active + %d relais inactifs channel relay bar channel subscriber relay bar %d relays removed + %d relais supprimé(s) channel relay bar channel subscriber relay bar %d sec - %d sec + %d s time interval @@ -217,10 +246,12 @@ channel subscriber relay bar %d subscriber + %d abonné·e channel subscriber count %d subscribers + %d abonné·es channel subscriber count @@ -230,11 +261,13 @@ channel subscriber relay bar %1$d/%2$d relays active + %1$d/%2$d relais actifs channel creation progress channel relay bar progress %1$d/%2$d relays active, %3$d errors + %1$d/%2$d relais actifs, %3$d erreurs channel relay bar @@ -248,10 +281,12 @@ channel relay bar %1$d/%2$d relays connected + %1$d/%2$d relais connectés channel subscriber relay bar progress %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d relais connectés, %3$d erreurs channel subscriber relay bar @@ -274,6 +309,7 @@ channel relay bar %lld channel events + %lld évènements du canal No comment provided by engineer. @@ -378,6 +414,7 @@ channel relay bar (from owner) + (du propriétaire) chat link info line @@ -385,10 +422,6 @@ channel relay bar (nouveau) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (cet appareil v%@) @@ -411,7 +444,7 @@ channel relay bar **Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app. - **Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app). + **Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages périodiquement en arrière plan (dépend de l'utilisation de l'appli). No comment provided by engineer. @@ -436,6 +469,7 @@ channel relay bar **Test relay** to retrieve its name. + **Tester le relais** pour récupérer son nom. No comment provided by engineer. @@ -485,6 +519,9 @@ channel relay bar - opt-in to send link previews. - prevent hyperlink phishing. - remove link tracking. + - choisir d'envoyer des aperçus de lien. +- empêcher l'hameçonnage par hyperlien. +- retirer le traçage par liens. No comment provided by engineer. @@ -576,7 +613,7 @@ time interval <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> - <p>Bonjour !</p> + <p>Bonjour !</p> <p><a href="%@">Contactez-moi via SimpleX Chat</a></p> email text @@ -587,6 +624,7 @@ time interval A link for one person to connect + Un lien pour qu'une personne se connecte No comment provided by engineer. @@ -601,7 +639,7 @@ time interval A separate TCP connection will be used **for each chat profile you have in the app**. - Une connexion TCP distincte sera utilisée **pour chaque profil de chat que vous avez dans l'application**. + Une connexion TCP distincte sera utilisée **pour chaque profil de discussion que vous avez dans l'application**. No comment provided by engineer. @@ -717,10 +755,20 @@ swipe action Add + Ajouter No comment provided by engineer. Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Ajoutez une adresse à votre profil afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts. + No comment provided by engineer. + + + Add contributors. + No comment provided by engineer. + + + Add description No comment provided by engineer. @@ -743,12 +791,19 @@ swipe action Ajouter un profil No comment provided by engineer. + + Add relay + Ajouter un relais + No comment provided by engineer. + Add relays + Ajouter des relais No comment provided by engineer. Add relays to restore message delivery. + Ajouter un relais pour restaurer la livraison des messages. No comment provided by engineer. @@ -766,6 +821,11 @@ swipe action Ajouter des membres à l'équipe No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Ajoutez ce code sur votre page Web. Il affichera l'aperçu de votre canal / groupe. + No comment provided by engineer. + Add to another device Ajouter à un autre appareil @@ -846,6 +906,11 @@ swipe action Paramètres réseau avancés No comment provided by engineer. + + Advanced options + Options avancées + No comment provided by engineer. + Advanced settings Paramètres avancés @@ -868,7 +933,7 @@ swipe action All chats will be removed from the list %@, and the list deleted. - Tous les chats seront supprimés de la liste %@, et la liste sera supprimée. + Toutes les discussions seront supprimées de la liste %@ et la liste sera supprimée. alert message @@ -888,6 +953,7 @@ swipe action All messages + Tous les messages No comment provided by engineer. @@ -917,10 +983,12 @@ swipe action All relays failed + Tous les relais échoués No comment provided by engineer. All relays removed + Tous les relais retirés No comment provided by engineer. @@ -953,6 +1021,11 @@ swipe action Autoriser No comment provided by engineer. + + Allow anyone to embed + Autoriser n'importe qui à incorporer + No comment provided by engineer. + Allow calls only if your contact allows them. Autoriser les appels que si votre contact les autorise. @@ -985,6 +1058,7 @@ swipe action Allow members to chat with admins. + Autoriser les membres à discuter avec les administrateurs. No comment provided by engineer. @@ -1004,6 +1078,7 @@ swipe action Allow sending direct messages to subscribers. + Autoriser l'envoi de messages directs aux abonné·es. No comment provided by engineer. @@ -1018,6 +1093,7 @@ swipe action Allow subscribers to chat with admins. + Autoriser tous les abonné·es à discuter avec les admins. No comment provided by engineer. @@ -1112,7 +1188,7 @@ swipe action An empty chat profile with the provided name is created, and the app opens as usual. - Un profil de chat vierge portant le nom fourni est créé et l'application s'ouvre normalement. + Un profil de discussion vierge portant le nom fourni est créé et l'application s'ouvre normalement. No comment provided by engineer. @@ -1125,6 +1201,11 @@ swipe action Répondre à l'appel No comment provided by engineer. + + Any webpage can show the preview. + N'importe quelle page Web peut afficher l'aperçu. + No comment provided by engineer. + App build: %@ Build de l'app : %@ @@ -1142,6 +1223,7 @@ swipe action App group: + Groupe de l'appli : No comment provided by engineer. @@ -1166,6 +1248,7 @@ swipe action App update required + Mise à jour de l'appli nécessaire alert title @@ -1260,6 +1343,7 @@ swipe action Audio call + Appel audio No comment provided by engineer. @@ -1332,6 +1416,11 @@ swipe action Mauvais hash de message No comment provided by engineer. + + Badge cannot be verified + Le badge ne peut pas être vérifié + badge alert title + Be free in your network @@ -1339,6 +1428,7 @@ in your network Be free in your network. + Soyez libre dans votre réseau. No comment provided by engineer. @@ -1350,6 +1440,10 @@ in your network Appels améliorés No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Des groupes plus performants @@ -1397,10 +1491,12 @@ in your network Bio + Biographie No comment provided by engineer. Bio too large + Biographie trop longue alert title @@ -1492,6 +1588,7 @@ in your network Bottom bar + Barre inférieure No comment provided by engineer. @@ -1500,7 +1597,7 @@ in your network Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! - Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) ! + Bulgare, finnois, thaï et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) ! No comment provided by engineer. @@ -1515,6 +1612,7 @@ in your network Business connection + Connexion pro No comment provided by engineer. @@ -1532,11 +1630,6 @@ in your network Appel déjà terminé ! No comment provided by engineer. - - Calls - Appels - No comment provided by engineer. - Calls prohibited! Les appels ne sont pas autorisés ! @@ -1559,6 +1652,7 @@ in your network Can't change profile + Impossible de changer le profil alert title @@ -1585,10 +1679,12 @@ new chat action Cancel and delete channel + Annuler et supprimer le canal No comment provided by engineer. Cancel creating channel? + Annuler la création du canal ? alert title @@ -1646,11 +1742,6 @@ new chat action Modifier le mode de verrouillage authentication reason - - Change member role? - Changer le rôle du membre ? - No comment provided by engineer. - Change passcode Modifier le code d'accès @@ -1671,6 +1762,10 @@ new chat action Changer le rôle No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Modifier le mode d'autodestruction @@ -1684,63 +1779,87 @@ set passcode view Channel + Canal + No comment provided by engineer. + + + Channel SimpleX name No comment provided by engineer. Channel display name + Nom d'affichage du canal No comment provided by engineer. Channel full name (optional) + Nom complet du canal (optionnel) No comment provided by engineer. Channel has no active relays. Please try to join later. + Le canal n'a aucun relais actif. Veuillez réessayer de vous connecter plus tard. alert message alert subtitle Channel image + Image du canal No comment provided by engineer. Channel link + Lien du canal chat link info line Channel preferences + Préférences du canal No comment provided by engineer. Channel profile + Profil du canal No comment provided by engineer. Channel profile is stored on subscribers' devices and on the chat relays. + Le profil du canal est stocké sur les périphériques des abonné·es et sur les relais de discussion. No comment provided by engineer. Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + Le profil a été changé. Si vous l'enregistrez, le profil mis à jour sera envoyé aux abonné·es du canal. alert message Channel temporarily unavailable + Canal temporairement indisponible alert title + + Channel webpage + Page Web du canal + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! + Le canal sera supprimé pour tous les abonné·es ; ceci ne peut pas être annulé ! No comment provided by engineer. Channel will be deleted for you - this cannot be undone! + Le canal sera supprimé pour vous ; ceci ne peut pas être annulé ! No comment provided by engineer. Channel will start working with %1$d of %2$d relays. Continue? + Le canal commencera à fonctionner avec %1$d relais sur %2$d. Continuer ? alert message Channels + Canaux No comment provided by engineer. @@ -1768,6 +1887,11 @@ alert subtitle Console du chat No comment provided by engineer. + + Chat data + Données de la discussion + No comment provided by engineer. + Chat database Base de données du chat @@ -1800,7 +1924,7 @@ alert subtitle Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat. + La discussion est arrêtée. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer la discussion. No comment provided by engineer. @@ -1830,18 +1954,22 @@ alert subtitle Chat relay + Relais de la discussion No comment provided by engineer. Chat relays + Relais de la discussion No comment provided by engineer. Chat relays forward messages in channels you create. + Les relais de discussion transmettent les messages dans les canaux que vous créez. No comment provided by engineer. Chat relays forward messages to channel subscribers. + Les relais de discussion transmettent les messages aux abonné·es du canal. No comment provided by engineer. @@ -1861,15 +1989,18 @@ alert subtitle Chat with admins + Discuter avec les admins chat feature chat toolbar Chat with member + Discuter avec un membre No comment provided by engineer. Chat with members before they join. + Discuter avec les membres avant qu'ils rejoignent le canal. No comment provided by engineer. @@ -1879,18 +2010,22 @@ chat toolbar Chats with admins are prohibited. + Les discussions avec les admins sont interdites. No comment provided by engineer. Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + Les discussions avec les admins dans les canaux publics n'ont pas de chiffrement E2E ; à utiliser uniquement avec des relais de discussion fiables. alert message Chats with members + Discussions avec les membres No comment provided by engineer. Chats with members are disabled + Les discussions avec les membres sont désactivées No comment provided by engineer. @@ -1905,10 +2040,12 @@ chat toolbar Check relay address and try again. + Vérifiez l'adresse du relais et réessayez. alert message Check relay name and try again. + Vérifiez le nom du relais et réessayez. alert message @@ -2058,6 +2195,7 @@ chat toolbar Configure relays + Configurer les relais No comment provided by engineer. @@ -2128,8 +2266,13 @@ server test step Connect faster! 🚀 + Connectez-vous plus vite ! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Connexion au bureau @@ -2166,6 +2309,7 @@ Il s'agit de votre propre lien unique ! Connect via link or QR code + Se connecter via un lien ou un code QR No comment provided by engineer. @@ -2223,14 +2367,6 @@ Il s'agit de votre propre lien unique ! Connexion au bureau No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Connexion @@ -2246,18 +2382,18 @@ Il s'agit de votre propre lien unique ! Connexion bloquée No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Erreur de connexion alert title - - Connection error (AUTH) - Erreur de connexion (AUTH) - conn error description - Connection failed + La connexion a échoué No comment provided by engineer. @@ -2267,6 +2403,11 @@ Il s'agit de votre propre lien unique ! %@ No comment provided by engineer. + + Connection link removed + Erreur de connexion + conn error description + Connection not ready. La connexion n'est pas prête. @@ -2312,8 +2453,14 @@ Il s'agit de votre propre lien unique ! Connexions No comment provided by engineer. + + Contact + Contact + No comment provided by engineer. + Contact address + Adresse du contact chat link info line @@ -2358,6 +2505,7 @@ Il s'agit de votre propre lien unique ! Contact requests from groups + Demandes de contact des groupes No comment provided by engineer. @@ -2400,6 +2548,11 @@ Il s'agit de votre propre lien unique ! Copier No comment provided by engineer. + + Copy code + Copier le code + No comment provided by engineer. + Copy error Erreur de copie @@ -2435,6 +2588,10 @@ Il s'agit de votre propre lien unique ! Création de groupes via un profil aléatoire. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Créer un fichier @@ -2472,10 +2629,7 @@ Il s'agit de votre propre lien unique ! Create public channel - No comment provided by engineer. - - - Create public channel (BETA) + Créer un canal public No comment provided by engineer. @@ -2483,12 +2637,18 @@ Il s'agit de votre propre lien unique ! Créer une file d'attente server test step + + Create web preview. + No comment provided by engineer. + Create your address + Créer votre adresse No comment provided by engineer. Create your link + Créer votre lien No comment provided by engineer. @@ -2498,6 +2658,7 @@ Il s'agit de votre propre lien unique ! Create your public address + Créer votre adresse publique No comment provided by engineer. @@ -2522,6 +2683,7 @@ Il s'agit de votre propre lien unique ! Creating channel + Création du canal No comment provided by engineer. @@ -2684,6 +2846,7 @@ Il s'agit de votre propre lien unique ! Decode link + Décoder le lien relay test step @@ -2734,10 +2897,12 @@ swipe action Delete channel + Supprimer le canal No comment provided by engineer. Delete channel? + Supprimer le canal ? No comment provided by engineer. @@ -2762,6 +2927,7 @@ swipe action Delete chat with member? + Supprimer la discussion avec le membre ? alert title @@ -2821,6 +2987,7 @@ swipe action Delete from history + Supprimer de l'historique No comment provided by engineer. @@ -2860,10 +3027,12 @@ swipe action Delete member messages + Supprimer les messages du membre No comment provided by engineer. Delete member messages? + Supprimer les messages des membres ? alert title @@ -2914,6 +3083,7 @@ alert button Delete relay + Supprimer le relais No comment provided by engineer. @@ -2978,6 +3148,7 @@ alert button Deprecated options + Options obsolètes No comment provided by engineer. @@ -2987,6 +3158,7 @@ alert button Description too large + Description trop longue alert title @@ -3004,9 +3176,9 @@ alert button Appareils de bureau No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + L'adresse du serveur de destination %1$@ est incompatible avec les paramètres du serveur de redirection %2$@. No comment provided by engineer. @@ -3014,9 +3186,9 @@ alert button Erreur du serveur de destination : %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - La version du serveur de destination %@ est incompatible avec le serveur de redirection %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + La version du serveur de destination %1$@ est incompatible avec le serveur de redirection %2$@. No comment provided by engineer. @@ -3029,9 +3201,9 @@ alert button Détails No comment provided by engineer. - - Develop - Développer + + Developer + Outils du développeur No comment provided by engineer. @@ -3039,11 +3211,6 @@ alert button Options pour les développeurs No comment provided by engineer. - - Developer tools - Outils du développeur - No comment provided by engineer. - Device Appareil @@ -3081,10 +3248,12 @@ alert button Direct messages between subscribers are prohibited. + Les messages directs entre les abonné·es sont interdits. No comment provided by engineer. Disable + Désactiver alert button @@ -3187,6 +3356,10 @@ alert button Faites-le plus tard No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Ne pas envoyer d'historique aux nouveaux membres. @@ -3194,6 +3367,7 @@ alert button Do not send history to new subscribers. + Ne pas envoyer l'historique à de nouveaux abonné·es. No comment provided by engineer. @@ -3221,6 +3395,10 @@ alert button Ne manquez pas les messages importants. No comment provided by engineer. + + Don't save + alert action + Don't show again Ne plus afficher @@ -3299,6 +3477,11 @@ chat item action Easier to invite your friends 👋 + Plus facile d'inviter vos ami·es 👋 + No comment provided by engineer. + + + Easier to read. No comment provided by engineer. @@ -3308,6 +3491,11 @@ chat item action Edit channel profile + Modifier le profil du canal + No comment provided by engineer. + + + Edit description No comment provided by engineer. @@ -3317,6 +3505,7 @@ chat item action Empty message! + Message vide ! No comment provided by engineer. @@ -3346,6 +3535,7 @@ chat item action Enable at least one chat relay in Network & Servers. + Activez au moins un relais de discussion dans Réseaux et serveurs. channel creation warning @@ -3360,10 +3550,12 @@ chat item action Enable chats with admins? + Activer les discussions avec les admins ? alert title Enable disappearing messages by default. + Activer les messages éphémères par défaut. No comment provided by engineer. @@ -3383,6 +3575,7 @@ chat item action Enable link previews? + Activer les aperçus de lien ? alert title @@ -3497,6 +3690,7 @@ chat item action Enter channel name… + Entrez le nom du canal… No comment provided by engineer. @@ -3504,6 +3698,10 @@ chat item action Entrez la phrase secrète correcte. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Entrer un nom de groupe… @@ -3526,10 +3724,12 @@ chat item action Enter profile name... + Entrez le nom du profil… No comment provided by engineer. Enter relay name… + Entrez le nom du relais… No comment provided by engineer. @@ -3542,6 +3742,11 @@ chat item action Entrez le nom de l'appareil… No comment provided by engineer. + + Enter webpage URL + Entrez l'URL de la page Web + No comment provided by engineer. + Enter welcome message… Entrez un message de bienvenue… @@ -3560,7 +3765,7 @@ chat item action Error Erreur - conn error description + No comment provided by engineer. Error aborting address change @@ -3579,6 +3784,7 @@ chat item action Error accepting member + Erreur lors de l'acceptation du membre alert title @@ -3588,10 +3794,12 @@ chat item action Error adding relay + Erreur lors de l'ajout du relais alert title Error adding relays + Erreur lors de l'ajout des relais alert title @@ -3601,6 +3809,7 @@ chat item action Error adding short link + Erreur lors de l'ajout d'un lien court No comment provided by engineer. @@ -3610,6 +3819,7 @@ chat item action Error changing chat profile + Erreur lors du changement du profil de discussion alert title @@ -3644,6 +3854,7 @@ chat item action Error connecting to the server used to receive messages from this connection: %@ + Erreur de connexion au serveur utilisé pour recevoir des messages de cette connexion : %@ subscription status explanation @@ -3653,6 +3864,7 @@ chat item action Error creating channel + Erreur lors de la création du canal alert title @@ -3697,6 +3909,7 @@ chat item action Error deleting chat + Erreur lors de la suppression de la discussion alert title @@ -3721,6 +3934,7 @@ chat item action Error deleting message + Erreur lors de la suppression du message alert title @@ -3815,6 +4029,7 @@ chat item action Error rejecting contact request + Erreur lors du rejet de la demande de contact alert title @@ -3839,6 +4054,7 @@ chat item action Error saving channel profile + Erreur lors de l'enregistrement du profil du canal No comment provided by engineer. @@ -3851,6 +4067,10 @@ chat item action Erreur lors de la sauvegarde du profil de groupe No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Erreur lors de la sauvegarde du code d'accès @@ -3883,7 +4103,7 @@ chat item action Error sending email - Erreur lors de l'envoi de l'e-mail + Erreur lors de l'envoi du courriel No comment provided by engineer. @@ -3898,6 +4118,7 @@ chat item action Error setting auto-accept + Erreur lors de la définition de l'acceptation automatique No comment provided by engineer. @@ -3905,8 +4126,13 @@ chat item action Erreur lors de la configuration des accusés de réception ! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel + Erreur lors du partage du canal alert title @@ -3966,7 +4192,7 @@ chat item action Error uploading the archive - Erreur lors de l'envoi de l'archive + Erreur lors du téléversement de l'archive No comment provided by engineer. @@ -3983,11 +4209,13 @@ chat item action Error: %@ Erreur : %@ alert message +conn error description file error text snd error text Error: %@. + Erreur : %@. relay test error server test error @@ -4125,6 +4353,14 @@ server test error Erreur de serveur de fichiers : %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status Statut du fichier @@ -4172,6 +4408,7 @@ server test error Files and media are prohibited in this chat. + Les fichiers et médias sont interdits dans cette discussion. No comment provided by engineer. @@ -4191,6 +4428,7 @@ server test error Filter + Filtre No comment provided by engineer. @@ -4220,6 +4458,7 @@ server test error Fingerprint in destination server address does not match certificate: %@. + L'empreinte dans l'adresse du serveur de destination ne correspond pas au certificat : %@. No comment provided by engineer. @@ -4272,6 +4511,7 @@ server test error For anyone to reach you + Pour que n'importe qui puisse vous contacter No comment provided by engineer. @@ -4292,6 +4532,7 @@ servers warning For me + Pour moi No comment provided by engineer. @@ -4418,16 +4659,23 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link + Obtenir un lien relay test step Get notified when mentioned. + Soyez averti·e quand vous êtes mentionné·e. No comment provided by engineer. Get started + Commençons No comment provided by engineer. @@ -4522,8 +4770,14 @@ Erreur : %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. + Le profil du groupe a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé aux membres du groupe. alert message + + Group webpage + Page Web du groupe + No comment provided by engineer. + Group welcome message Message d'accueil du groupe @@ -4541,6 +4795,7 @@ Erreur : %2$@ Groups + Groupes No comment provided by engineer. @@ -4548,8 +4803,14 @@ Erreur : %2$@ Aide No comment provided by engineer. + + Help & support + Aide et assistance + No comment provided by engineer. + Help admins moderating their groups. + Aidez les admins à modérer leurs groupes. No comment provided by engineer. @@ -4599,6 +4860,7 @@ Erreur : %2$@ History is not sent to new subscribers. + L'historique n'est pas envoyé aux nouveaux abonné·es. No comment provided by engineer. @@ -4618,6 +4880,7 @@ Erreur : %2$@ How it works + Comment ça marche alert button @@ -4625,6 +4888,10 @@ Erreur : %2$@ Comment faire No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Comment l'utiliser @@ -4667,11 +4934,12 @@ Erreur : %2$@ If you joined or created channels, they will stop working permanently. + Si vous avez rejoint ou créé des canaux, ils arrêteront de fonctionner définitivement. down migration warning If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - Si vous avez besoin d'utiliser le chat maintenant appuyez sur **le faire plus tard** (vous pourrez migrer la base de données quand vous relancerez l'app). + Si vous avez besoin d'utiliser le chat maintenant appuyez sur **le faire plus tard** (vous pourrez migrer la base de données quand vous relancerez l'appli). No comment provided by engineer. @@ -4691,6 +4959,7 @@ Erreur : %2$@ Images + Images No comment provided by engineer. @@ -4767,10 +5036,12 @@ D'autres améliorations sont à venir ! Inappropriate content + Contenu inapproprié report reason Inappropriate profile + Profil inapproprié report reason @@ -4867,22 +5138,27 @@ D'autres améliorations sont à venir ! Invalid + Invalide token status text Invalid (bad token) + Invalide (mauvais jeton) token status text Invalid (expired) + Invalide (expiré) token status text Invalid (unregistered) + Invalide (non enregistré) token status text Invalid (wrong topic) + Invalide (mauvais sujet) token status text @@ -4917,10 +5193,12 @@ D'autres améliorations sont à venir ! Invalid relay address! + Adresse de relais invalide ! alert title Invalid relay name! + Nom du relais invalide ! alert title @@ -4950,6 +5228,7 @@ D'autres améliorations sont à venir ! Invite member + Inviter un membre No comment provided by engineer. @@ -4959,6 +5238,7 @@ D'autres améliorations sont à venir ! Invite someone privately + Inviter quelqu'un en privé No comment provided by engineer. @@ -5017,6 +5297,11 @@ D'autres améliorations sont à venir ! Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Ceci sera montré aux abonné·es et utilisé pour permettre le chargement de l'aperçu. + No comment provided by engineer. + Italian interface Interface en italien @@ -5039,8 +5324,13 @@ D'autres améliorations sont à venir ! Join channel + Rejoindre le canal No comment provided by engineer. + + Join channel %@ + new chat action + Join group Rejoindre le groupe @@ -5090,6 +5380,7 @@ Voici votre lien pour le groupe %@ ! Keep your chats clean + Gardez vos discussions propres No comment provided by engineer. @@ -5120,7 +5411,7 @@ Voici votre lien pour le groupe %@ ! Learn more En savoir plus - No comment provided by engineer. + badge alert button Leave @@ -5129,10 +5420,12 @@ Voici votre lien pour le groupe %@ ! Leave channel + Quitter le canal No comment provided by engineer. Leave channel? + Quitter le canal ? No comment provided by engineer. @@ -5157,10 +5450,20 @@ Voici votre lien pour le groupe %@ ! Less traffic on mobile networks. + Moins de transferts de données sur les réseaux mobiles. + No comment provided by engineer. + + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. No comment provided by engineer. Let someone connect to you + Laisser quelqu'un se connecter à vous No comment provided by engineer. @@ -5185,6 +5488,7 @@ Voici votre lien pour le groupe %@ ! Link signature verified. + Signature du lien vérifiée. owner verification @@ -5199,18 +5503,22 @@ Voici votre lien pour le groupe %@ ! Links + Liens No comment provided by engineer. List + Liste swipe action List name and emoji should be different for all lists. + Le nom de la liste et les émojis devraient être différents pour toutes les listes. No comment provided by engineer. List name... + Nom de la liste… No comment provided by engineer. @@ -5225,6 +5533,7 @@ Voici votre lien pour le groupe %@ ! Loading profile… + Chargement du profil… in progress text @@ -5262,6 +5571,10 @@ Voici votre lien pour le groupe %@ ! Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Marquer comme supprimé pour tout le monde @@ -5304,10 +5617,12 @@ Voici votre lien pour le groupe %@ ! Member %@ + Membre %@ past/unknown group member Member admission + Admission du membre No comment provided by engineer. @@ -5317,31 +5632,19 @@ Voici votre lien pour le groupe %@ ! Member is deleted - can't accept request + Le membre est supprimé ; impossible d'accepter la demande No comment provided by engineer. Member messages will be deleted - this cannot be undone! + Les messages des membres seront supprimés ; ceci ne peut pas être annulé ! alert message Member reports + Signalements des membres chat feature - - Member role will be changed to "%@". All chat members will be notified. - Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Le rôle du membre sera changé pour "%@". Ce membre recevra une nouvelle invitation. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Le membre sera retiré de la discussion - cela ne peut pas être annulé ! @@ -5354,6 +5657,7 @@ Voici votre lien pour le groupe %@ ! Member will join the group, accept member? + Le membre rejoindra le groupe ; accepter le membre ? alert message @@ -5363,6 +5667,7 @@ Voici votre lien pour le groupe %@ ! Members can chat with admins. + Les membres peuvent discuter avec les admins. No comment provided by engineer. @@ -5372,6 +5677,7 @@ Voici votre lien pour le groupe %@ ! Members can report messsages to moderators. + Les membres peuvent signaler les messages aux modérateur·ices. No comment provided by engineer. @@ -5401,6 +5707,7 @@ Voici votre lien pour le groupe %@ ! Mention members 👋 + Mentionnez les membres 👋 No comment provided by engineer. @@ -5430,6 +5737,7 @@ Voici votre lien pour le groupe %@ ! Message error + Erreur du message No comment provided by engineer. @@ -5439,6 +5747,7 @@ Voici votre lien pour le groupe %@ ! Message instantly once you tap Connect. + Parlez instantanément dès que vous appuyez sur Connecter. No comment provided by engineer. @@ -5481,6 +5790,14 @@ Voici votre lien pour le groupe %@ ! Forme du message No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. La source du message reste privée. @@ -5518,6 +5835,7 @@ Voici votre lien pour le groupe %@ ! Messages are protected by **end-to-end encryption**. + Les messages sont protégés par le **chiffrement de bout-en-bout**. No comment provided by engineer. @@ -5527,14 +5845,17 @@ Voici votre lien pour le groupe %@ ! Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + Les messages dans ce canal **ne sont pas chiffrés de bout-en-bout**. Les relais de discussion peuvent voir ces messages. No comment provided by engineer. Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + Les messages dans ce canal ne sont pas chiffrés de bout-en-bout. Les relais de discussion peuvent voir ces messages. E2EE info chat item Messages in this chat will never be deleted. + Les messages dans cette discussion ne seront jamais supprimés. alert message @@ -5564,6 +5885,7 @@ Voici votre lien pour le groupe %@ ! Migrate + Migrer No comment provided by engineer. @@ -5608,7 +5930,7 @@ Voici votre lien pour le groupe %@ ! Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - Echec de la migration. Appuyez sur **Passer** ci-dessous pour continuer à utiliser la base de données actuelle. Veuillez signaler le problème aux développeurs de l'app par chat ou par e-mail [chat@simplex.chat](mailto:chat@simplex.chat). + Échec de la migration. Appuyez sur **Passer** ci-dessous pour continuer à utiliser la base de données actuelle. Veuillez signaler le problème aux développeurs de l'appli par discussion ou par courriel [chat@simplex.chat](mailto:chat@simplex.chat). No comment provided by engineer. @@ -5638,6 +5960,7 @@ Voici votre lien pour le groupe %@ ! More + Plus swipe action @@ -5645,6 +5968,11 @@ Voici votre lien pour le groupe %@ ! Plus d'améliorations à venir ! No comment provided by engineer. + + More privacy + Plus de vie privée + No comment provided by engineer. + More reliable network connection. Connexion réseau plus fiable. @@ -5672,6 +6000,7 @@ Voici votre lien pour le groupe %@ ! Mute all + Tout en sourdine notification label action @@ -5684,6 +6013,10 @@ Voici votre lien pour le groupe %@ ! Nom swipe action + + Name not found + No comment provided by engineer. + Network & servers Réseau et serveurs @@ -5691,6 +6024,7 @@ Voici votre lien pour le groupe %@ ! Network commitments + Engagements réseau No comment provided by engineer. @@ -5705,6 +6039,7 @@ Voici votre lien pour le groupe %@ ! Network error + Erreur réseau conn error description @@ -5725,6 +6060,8 @@ Voici votre lien pour le groupe %@ ! Network routers cannot know who talks to whom + Les routeurs réseau ne peuvent pas savoir +qui parle à qui No comment provided by engineer. @@ -5739,10 +6076,12 @@ who talks to whom New + Nouveau token status text New 1-time link + Nouveau lien unique No comment provided by engineer. @@ -5762,7 +6101,7 @@ who talks to whom New chat - Nouveau chat + Nouvelle discussion No comment provided by engineer. @@ -5772,6 +6111,7 @@ who talks to whom New chat relay + Nouveau relais de discussion No comment provided by engineer. @@ -5820,6 +6160,7 @@ who talks to whom New member wants to join the group. + Un nouveau membre veut rejoindre le groupe. rcv group event chat item @@ -5845,10 +6186,13 @@ who talks to whom No account. No phone. No email. No ID. The most secure encryption. + Pas de compte. Pas de téléphone. Pas de courriel. Pas d'identifiant. +Le chiffrement le plus sûr. No comment provided by engineer. No active relays + Aucun relais actif No comment provided by engineer. @@ -5858,30 +6202,37 @@ The most secure encryption. No available relays + Aucun relais disponible No comment provided by engineer. No chat relays + Aucun relais de discussion No comment provided by engineer. No chat relays enabled. + Aucun relais de discussion disponible. servers warning No chats + Aucune discussion No comment provided by engineer. No chats found + Aucune discussion trouvée No comment provided by engineer. No chats in list %@ + Aucune discussion dans la liste %@ No comment provided by engineer. No chats with members + Aucune discussion avec les membres No comment provided by engineer. @@ -5936,6 +6287,7 @@ The most secure encryption. No message + Aucun message No comment provided by engineer. @@ -5965,6 +6317,7 @@ The most secure encryption. No private routing session + Aucune session de routage privée alert title @@ -5979,6 +6332,7 @@ The most secure encryption. No relays + Aucun relais No comment provided by engineer. @@ -5996,6 +6350,10 @@ The most secure encryption. Pas de serveurs pour recevoir des messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. Pas de serveurs pour envoyer des fichiers. @@ -6003,26 +6361,40 @@ The most secure encryption. No token! + Aucun jeton ! alert title No unread chats + Aucune discussion non lue + No comment provided by engineer. + + + No valid link No comment provided by engineer. Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + Personne ne suivait vos conversations. Personne ne dessinait de carte d'où vous étiez. La vie privée n'était jamais une caractéristique – c'était le mode de vie. No comment provided by engineer. Non-profit governance + Gouvernance à but non lucratif + No comment provided by engineer. + + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. No comment provided by engineer. Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + Ce n’est pas une meilleure serrure sur la porte de quelqu’un d’autre. Ce n’est pas un propriétaire plus aimable qui respecte votre vie privée, mais qui tient tout de même un registre de tous les visiteurs. Vous n’êtes pas un·e invité·e, vous êtes chez vous. Aucun roi ne peut y entrer : c’est vous le souverain. No comment provided by engineer. Not all relays connected + Les relais ne sont pas tous connectés alert title @@ -6032,6 +6404,7 @@ The most secure encryption. Notes + Notes No comment provided by engineer. @@ -6056,6 +6429,7 @@ The most secure encryption. Notifications error + Erreur de notification alert title @@ -6065,6 +6439,7 @@ The most secure encryption. Notifications status + État des notifications alert title @@ -6083,12 +6458,12 @@ The most secure encryption. Off - Off + Désactivé blur media Ok - Ok + D'accord alert action alert button new chat action @@ -6100,6 +6475,7 @@ new chat action On your phone, not on servers. + Sur votre téléphone, pas sur les serveurs. No comment provided by engineer. @@ -6109,6 +6485,7 @@ new chat action One-time link + Lien unique chat link info line @@ -6132,6 +6509,7 @@ Nécessite l'activation d'un VPN. Only channel owners can change channel preferences. + Seuls les propriétaires du canal peuvent changer les préférences du canal. No comment provided by engineer. @@ -6166,10 +6544,12 @@ Nécessite l'activation d'un VPN. Only sender and moderators see it + Seul l'expéditeur et les modérateurs le voient No comment provided by engineer. Only you and moderators see it + Seuls vous et les modérateurs le voyez No comment provided by engineer. @@ -6194,6 +6574,7 @@ Nécessite l'activation d'un VPN. Only you can send files and media. + Seul·e vous pouvez envoyer des fichiers et des médias. No comment provided by engineer. @@ -6223,6 +6604,7 @@ Nécessite l'activation d'un VPN. Only your contact can send files and media. + Seul votre contact peut envoyer des fichiers et des médias. No comment provided by engineer. @@ -6230,6 +6612,11 @@ Nécessite l'activation d'un VPN. Seul votre contact peut envoyer des messages vocaux. No comment provided by engineer. + + Only your page above can show the preview. + Seule votre page ci-dessus peut afficher l'aperçu. + No comment provided by engineer. + Open Ouvrir @@ -6248,6 +6635,7 @@ alert button Open channel + Ouvrir le canal new chat action @@ -6262,6 +6650,7 @@ alert button Open clean link + Ouvrir le lien nettoyé alert action @@ -6271,10 +6660,12 @@ alert button Open external link? + Ouvrir le lien externe ? alert title Open full link + Ouvrir le lien complet alert action @@ -6284,6 +6675,7 @@ alert button Open link? + Ouvrir le lien ? alert title @@ -6293,30 +6685,37 @@ alert button Open new channel + Ouvrir un nouveau canal new chat action Open new chat + Ouvrir une nouvelle discussion new chat action Open new group + Ouvrir le nouveau groupe new chat action Open to accept + Ouvrir pour accepter No comment provided by engineer. Open to connect + Ouvrir pour connecter No comment provided by engineer. Open to join + Ouvrir pour rejoindre No comment provided by engineer. Open to use bot + Ouvrir pour utiliser le bot No comment provided by engineer. @@ -6339,6 +6738,10 @@ alert button - Be independent - Minimize metadata usage - Run verified open-source code + Les opérateurs s'engagent à : +- Être indépendants +- Minimiser l'utilisation des métadonnées +- Exécuter un code ouvert vérifié No comment provided by engineer. @@ -6363,6 +6766,7 @@ alert button Or show QR in person or via video call. + Ou montrez le code QR en personne ou via un appel vidéo. No comment provided by engineer. @@ -6377,10 +6781,12 @@ alert button Or use this QR - print or show online. + Ou utilisez ce code QR : imprimez-le ou affichez-le en ligne. No comment provided by engineer. Organize chats into lists + Organisez des discussions en listes No comment provided by engineer. @@ -6397,14 +6803,16 @@ alert button Owner + Propriétaire No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. Ownership: you can run your own relays. + Propriétaire : vous pouvez exécuter vos propres relais. No comment provided by engineer. @@ -6464,6 +6872,7 @@ alert button Paste link / Scan + Coller le lien / Scanner No comment provided by engineer. @@ -6587,22 +6996,22 @@ Erreur : %@ Please try to disable and re-enable notfications. + Veuillez essayer de désactiver et réactiver les notifications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. + Veuillez attendre que les modérateur·ices de groupe examinent votre demande pour rejoindre le groupe. snd group event chat item Please wait for token activation to complete. + Veuillez attendre la fin de l'activation du jeton. token info Please wait for token to be registered. + Veuillez attendre que le jeton soit enregistré. token info @@ -6622,10 +7031,12 @@ Erreur : %@ Preset relay address + Adresse de relais prédéfinie No comment provided by engineer. Preset relay name + Nom de relais prédéfini No comment provided by engineer. @@ -6648,11 +7059,6 @@ Erreur : %@ Serveurs précédemment connectés No comment provided by engineer. - - Privacy & security - Vie privée et sécurité - No comment provided by engineer. - Privacy for your customers. Respect de la vie privée de vos clients. @@ -6660,14 +7066,17 @@ Erreur : %@ Privacy policy and conditions of use. + Politique de confidentialité et conditions d'utilisation. No comment provided by engineer. Privacy: for owners and subscribers. + Confidentialité : pour les propriétaires et les abonné·es. No comment provided by engineer. Private and secure messaging. + Messagerie privée et sécurisée. No comment provided by engineer. @@ -6677,6 +7086,7 @@ Erreur : %@ Private media file names. + Noms de fichiers multimédias privés. No comment provided by engineer. @@ -6706,6 +7116,7 @@ Erreur : %@ Private routing timeout + Temps de routage privé alert title @@ -6735,7 +7146,9 @@ Erreur : %@ Profile update will be sent to your SimpleX contacts. - alert message + La mise à jour du profil sera envoyée à vos contacts SimpleX. + alert message +alert title Prohibit audio/video calls. @@ -6744,6 +7157,7 @@ Erreur : %@ Prohibit chats with admins. + Interdire les conversations avec les admins. No comment provided by engineer. @@ -6763,6 +7177,7 @@ Erreur : %@ Prohibit reporting messages to moderators. + Interdire de signaler des messages aux modérateur·ices. No comment provided by engineer. @@ -6777,6 +7192,7 @@ Erreur : %@ Prohibit sending direct messages to subscribers. + Interdire l'envoi de messages directs aux abonné·es. No comment provided by engineer. @@ -6801,7 +7217,7 @@ Erreur : %@ Protect app screen - Protéger l'écran de l'app + Protéger l'écran de l'appli No comment provided by engineer. @@ -6813,11 +7229,12 @@ Activez-le dans les paramètres *Réseau et serveurs*. Protect your chat profiles with a password! - Protégez vos profils de chat par un mot de passe ! + Protégez vos profils de discussion par un mot de passe ! No comment provided by engineer. Protocol background timeout + Expiration du protocole en arrière-plan No comment provided by engineer. @@ -6847,6 +7264,11 @@ Activez-le dans les paramètres *Réseau et serveurs*. Public channels - speak freely 🚀 + Les canaux publics – parlez librement 🚀 + No comment provided by engineer. + + + Public names for your channel or business. No comment provided by engineer. @@ -6866,7 +7288,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Rate the app - Évaluer l'app + Évaluer l'appli No comment provided by engineer. @@ -6887,7 +7309,7 @@ Activez-le dans les paramètres *Réseau et serveurs*. Read more En savoir plus - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7021,14 +7443,17 @@ Activez-le dans les paramètres *Réseau et serveurs*. Register + Inscrire No comment provided by engineer. Register notification token? + Inscrire le jeton de notification ? token info Registered + Inscrit token status text @@ -7050,31 +7475,37 @@ swipe action Reject member? + Rejeter le membre ? alert title Relay + Relais No comment provided by engineer. Relay address + Adresse de relais alert title Relay connection failed + Échec de la connexion au relais alert title Relay link + Lien de relais No comment provided by engineer. Relay results: + Résultats du relais : alert message Relay server is only used if necessary. Another party can observe your IP address. - Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP. + Le serveur du relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP. No comment provided by engineer. @@ -7119,6 +7550,7 @@ swipe action Remove link tracking + Retirer le traçage par lien No comment provided by engineer. @@ -7131,11 +7563,19 @@ swipe action Retirer ce membre ? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Supprimer la phrase secrète de la keychain ? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7185,46 +7625,61 @@ swipe action Report + Signaler chat item action Report content: only group moderators will see it. + Contenu du signalement : seuls les modérateurs de groupe le verront. report reason Report member profile: only group moderators will see it. + Signaler le profil d'un membre : seuls les modérateurs de groupe le verront. report reason Report other: only group moderators will see it. + Signaler autre chose : seuls les modérateurs de groupe le verront. report reason Report reason? + Motif du signalement ? No comment provided by engineer. Report sent to moderators + Signalement envoyé aux modérateurs alert title Report spam: only group moderators will see it. + Signaler du spam : seuls les modérateurs de groupe le verront. report reason Report violation: only group moderators will see it. + Signaler une violation : seuls les modérateurs de groupe le verront. report reason Report: %@ + Signalement : %@ report in notification Reporting messages to moderators is prohibited. + Signaler des messages aux modérateur·ices est interdit. No comment provided by engineer. Reports + Signalements + No comment provided by engineer. + + + Require signing messages. No comment provided by engineer. @@ -7272,9 +7727,13 @@ swipe action Réinitialisation au thème de l'utilisateur No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile - Redémarrez l'application pour créer un nouveau profil de chat + Redémarrez l'appli pour créer un nouveau profil de discussion No comment provided by engineer. @@ -7319,14 +7778,17 @@ swipe action Review group members + Contrôler les membres du groupe No comment provided by engineer. Review members + Contrôler les membres admission stage Review members before admitting ("knocking"). + Contrôler les membres avant de les admettre (« toquer »). admission stage description @@ -7349,6 +7811,25 @@ swipe action Rôle No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Le rôle du membre sera changé pour "%@". Ce membre recevra une nouvelle invitation. + No comment provided by engineer. + Run chat Exécuter le chat @@ -7366,6 +7847,7 @@ swipe action Safe web links + Liens Web sûrs No comment provided by engineer. @@ -7381,7 +7863,8 @@ swipe action Save Enregistrer - alert button + alert action +alert button chat item action @@ -7391,14 +7874,21 @@ chat item action Save (and notify members) + Enregistrer (et notifier les membres) alert button Save (and notify subscribers) + Enregistrer (et notifier les abonné·es) alert button + + Save SimpleX name? + alert title + Save admission settings? + Enregistrer les réglages d'admission ? alert title @@ -7411,8 +7901,14 @@ chat item action Enregistrer et en informer les membres du groupe No comment provided by engineer. + + Save and notify members + Enregistrer (et notifier les membres) + No comment provided by engineer. + Save and notify subscribers + Enregistrer et notifier les abonné·es No comment provided by engineer. @@ -7427,10 +7923,12 @@ chat item action Save channel profile + Enregistrer le profil du canal No comment provided by engineer. Save channel profile? + Enregistrer le profil du canal ? alert title @@ -7440,10 +7938,12 @@ chat item action Save group profile? + Enregistrer le profil du groupe ? alert title Save list + Enregistrer la liste No comment provided by engineer. @@ -7476,6 +7976,11 @@ chat item action Enregistrer les serveurs ? alert title + + Save webpage settings? + Enregistrer les paramètres de la page Web ? + alert title + Save welcome message? Enregistrer le message d'accueil ? @@ -7558,14 +8063,17 @@ chat item action Search files + Rechercher des fichiers No comment provided by engineer. Search images + Recherche des images No comment provided by engineer. Search links + Rechercher des liens No comment provided by engineer. @@ -7575,10 +8083,12 @@ chat item action Search videos + Rechercher des vidéos No comment provided by engineer. Search voice messages + Rechercher des messages vocaux No comment provided by engineer. @@ -7608,6 +8118,7 @@ chat item action Security: owners hold channel keys. + Sécurité : les propriétaires détiennent des clés de canal. No comment provided by engineer. @@ -7662,6 +8173,7 @@ chat item action Send contact request? + Envoyer une demande de contact ? No comment provided by engineer. @@ -7716,6 +8228,7 @@ chat item action Send private reports + Envoyer des signalements privés No comment provided by engineer. @@ -7730,14 +8243,17 @@ chat item action Send request + Envoyer la demande No comment provided by engineer. Send request without message + Envoyer la demande sans message No comment provided by engineer. Send the link via any messenger - it's secure. Ask to paste into SimpleX. + Envoyez le lien par n'importe quelle messagerie – il est sécurisé. Demandez de coller dans SimpleX. No comment provided by engineer. @@ -7752,10 +8268,12 @@ chat item action Send up to 100 last messages to new subscribers. + Envoyer jusqu'aux 100 derniers messages aux nouveaux abonné·es. No comment provided by engineer. Send your private feedback to groups. + Envoyer vos remarques privées aux groupes. No comment provided by engineer. @@ -7763,13 +8281,9 @@ chat item action L'expéditeur a annulé le transfert de fichiers. alert message - - Sender may have deleted the connection request. - L'expéditeur a peut-être supprimé la demande de connexion. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + L'envoi d'un aperçu de lien peut révéler votre adresse IP au site Web. Vous pouvez modifier ceci dans les paramètres de confidentialité plus tard. alert message @@ -7862,6 +8376,10 @@ chat item action Serveur No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Serveur ajouté à l'opérateur %@. @@ -7899,6 +8417,7 @@ chat item action Server requires authorization to connect to relay, check password. + Le serveur demande l'autorisation de se connecter au relais, vérifier le mot de passe. relay test error @@ -7958,6 +8477,7 @@ chat item action Set chat name… + Paramétrer le nom de la discussion… No comment provided by engineer. @@ -7982,10 +8502,12 @@ chat item action Set member admission + Paramétrer l'admission des membres No comment provided by engineer. Set message expiration in chats. + Paramétrer l'expiration des messages dans les discussions. No comment provided by engineer. @@ -8005,6 +8527,7 @@ chat item action Set profile bio and welcome message. + Définir la biographie du profil et le message d'accueil. No comment provided by engineer. @@ -8029,10 +8552,12 @@ chat item action Setup notifications + Configurer les notifications No comment provided by engineer. Setup routers + Configurer les routeurs No comment provided by engineer. @@ -8073,10 +8598,12 @@ chat item action Share address with SimpleX contacts? + Partager l'adresse avec les contacts SimpleX ? alert title Share channel + Partager le canal No comment provided by engineer. @@ -8091,10 +8618,12 @@ chat item action Share old address + Partager l'ancienne adresse alert button Share old link + Partager l'ancien lien alert button @@ -8104,6 +8633,7 @@ chat item action Share relay address + Partager l'adresse du relais No comment provided by engineer. @@ -8118,26 +8648,32 @@ chat item action Share via chat + Partager via la discussion No comment provided by engineer. Share with SimpleX contacts + Partager avec les contacts SimpleX No comment provided by engineer. Share your address + Partager votre adresse No comment provided by engineer. Short SimpleX address + Adresse SimpleX courte No comment provided by engineer. Short description + Brève description No comment provided by engineer. Short link + Lien court No comment provided by engineer. @@ -8155,6 +8691,10 @@ chat item action Afficher les options pour les développeurs No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Aperçu des derniers messages @@ -8185,6 +8725,31 @@ chat item action Afficher : No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8247,6 +8812,7 @@ chat item action SimpleX channel link + Lien de canal SimpleX simplex link type @@ -8279,6 +8845,18 @@ chat item action Les liens SimpleX ne sont pas autorisés No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Invitation unique SimpleX @@ -8289,8 +8867,13 @@ chat item action Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address + Adresse relais SimpleX simplex link type @@ -8357,6 +8940,7 @@ chat item action Spam + Spam blocking reason report reason @@ -8367,17 +8951,17 @@ report reason Star on GitHub - Star sur GitHub + Donnez une étoile sur GitHub No comment provided by engineer. Start chat - Démarrer le chat + Démarrer la discussion No comment provided by engineer. Start chat? - Lancer le chat ? + Démarrer la discussion ? No comment provided by engineer. @@ -8397,6 +8981,7 @@ report reason Status + État No comment provided by engineer. @@ -8411,17 +8996,17 @@ report reason Stop chat - Arrêter le chat + Arrêter la discussion No comment provided by engineer. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté. + Arrêtez la discussion pour exporter, importer ou supprimer la base de données de la discussion. Vous ne pourrez pas recevoir et envoyer de messages pendant que la discussion est arrêtée. No comment provided by engineer. Stop chat? - Arrêter le chat ? + Arrêter la discussion ? No comment provided by engineer. @@ -8456,6 +9041,7 @@ report reason Storage + Stockage No comment provided by engineer. @@ -8475,6 +9061,7 @@ report reason Subscriber + Abonné·e No comment provided by engineer. @@ -8540,9 +9127,8 @@ Relay address was used to set up this relay for the channel. Inscriptions ignorées No comment provided by engineer. - - Support SimpleX Chat - Supporter SimpleX Chat + + Support the project No comment provided by engineer. @@ -8610,6 +9196,7 @@ Relay address was used to set up this relay for the channel. Talk to someone + Parlez à quelqu'un No comment provided by engineer. @@ -8727,6 +9314,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8755,6 +9358,14 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. La tentative de modification de la phrase secrète de la base de données n'a pas abouti. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. Le code scanné n'est pas un code QR de lien SimpleX. @@ -8847,6 +9458,11 @@ your contacts and groups. Le deuxième coche que nous avons manqué ! ✅ No comment provided by engineer. + + The sender deleted the connection request. + L'expéditeur a peut-être supprimé la demande de connexion. + No comment provided by engineer. + The sender will NOT be notified L'expéditeur N'en sera PAS informé @@ -8859,7 +9475,7 @@ your contacts and groups. The servers for new files of your current chat profile **%@**. - Les serveurs pour les nouveaux fichiers de votre profil de chat actuel **%@**. + Les serveurs pour les nouveaux fichiers de votre profil de discussion actuel **%@**. No comment provided by engineer. @@ -8879,6 +9495,7 @@ your contacts and groups. Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + Puis nous avons déménagé en ligne, et chaque plateforme a demandé un morceau de vous - votre nom, votre numéro, vos amis. Nous avons accepté que le prix à payer pour parler aux autres est de faire savoir à quelqu'un à qui nous parlons. À chaque génération, les gens et la technique le faisaient de cette manière : téléphone, courriels, messagers, médias sociaux. C'était le seul moyen possible. No comment provided by engineer. @@ -8900,6 +9517,10 @@ your contacts and groups. Ils peuvent être modifiés dans les paramètres des contacts et des groupes. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées. @@ -8919,6 +9540,10 @@ your contacts and groups. Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Cette discussion est protégée par un chiffrement de bout en bout. @@ -8990,6 +9615,7 @@ alert subtitle Time to disappear is set only for new contacts. + Le délai de disparition est défini seulement pour les nouveaux contacts. No comment provided by engineer. @@ -9068,9 +9694,13 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**. + Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de discussion**. No comment provided by engineer. @@ -9101,6 +9731,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Basculer en mode incognito lors de la connexion. @@ -9117,6 +9751,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Top bar + Barre supérieure No comment provided by engineer. @@ -9187,6 +9822,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Messages non distribués @@ -9247,13 +9886,6 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s À moins que vous utilisiez l'interface d'appel d'iOS, activez le mode "Ne pas déranger" pour éviter les interruptions. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s'agir d'un bug - veuillez le signaler. -Pour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d'une connexion réseau stable. - No comment provided by engineer. - Unlink Délier @@ -9284,17 +9916,13 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Non lu swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9344,7 +9972,8 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -9505,12 +10134,17 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Use this address in your social media profile, website, or email signature. + Utilisez cette adresse dans votre profil, votre site Web ou votre signature de courriel. No comment provided by engineer. Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection Sélection de l'utilisateur @@ -9528,8 +10162,13 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Verify + Vérifier relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Vérifier le code avec le bureau @@ -9555,6 +10194,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Vérifier la phrase secrète de la base de données No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Vérifier la phrase secrète @@ -9592,6 +10235,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Videos + Vidéos No comment provided by engineer. @@ -9651,14 +10295,17 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Wait + Attendez alert action Wait response + Attendre la réponse relay test step Waiting for channel owner to add relays. + En attente que le propriétaire du canal ajoute des relais. No comment provided by engineer. @@ -9693,7 +10340,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages + Attention : démarrer une session de discussion sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages No comment provided by engineer. @@ -9710,6 +10357,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Serveurs WebRTC ICE No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Bienvenue %@ ! @@ -9727,6 +10382,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Welcome your contacts 👋 + Accueillez vos contacts 👋 No comment provided by engineer. @@ -9928,9 +10584,8 @@ Répéter la demande d'adhésion ? Vous pouvez l'activer ultérieurement via Paramètres No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9992,6 +10647,10 @@ Répéter la demande d'adhésion ? Vous pouvez toujours voir la conversation avec %@ dans la liste des discussions. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Vous pouvez activer SimpleX Lock dans les Paramètres. @@ -10040,7 +10699,7 @@ Répéter la demande de connexion ? You have to enter passphrase every time the app starts - it is not stored on the device. - Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil. + Vous devez saisir la phrase secrète à chaque fois que l'application démarre ; elle n'est pas stockée sur l'appareil. No comment provided by engineer. @@ -10174,8 +10833,13 @@ Répéter la demande de connexion ? Votre adresse SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact + Votre contact professionnel No comment provided by engineer. @@ -10187,11 +10851,6 @@ Répéter la demande de connexion ? Your channel No comment provided by engineer. - - Your chat database - Votre base de données de chat - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète. @@ -10204,7 +10863,7 @@ Répéter la demande de connexion ? Your chat profiles - Vos profils de chat + Vos profils de discussion No comment provided by engineer. @@ -10218,6 +10877,14 @@ Répéter la demande de connexion ? Your contact + Votre contact + No comment provided by engineer. + + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s'agir d'un bug - veuillez le signaler. +Pour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d'une connexion réseau stable. No comment provided by engineer. @@ -10256,10 +10923,12 @@ Répéter la demande de connexion ? Your group + Votre groupe No comment provided by engineer. Your network + Votre réseau No comment provided by engineer. @@ -10309,6 +10978,7 @@ Relays can access channel messages. Your public address + Votre adresse publique No comment provided by engineer. @@ -10318,10 +10988,12 @@ Relays can access channel messages. Your relay address + L'adresse de votre relais No comment provided by engineer. Your relay name + Le nom de votre relais No comment provided by engineer. @@ -10341,7 +11013,7 @@ Relays can access channel messages. [Send us email](mailto:chat@simplex.chat) - [Contact par mail](mailto:chat@simplex.chat) + [Envoyez-nous un courriel](mailto:chat@simplex.chat) No comment provided by engineer. @@ -10361,10 +11033,12 @@ Relays can access channel messages. accepted + accepté No comment provided by engineer. accepted %@ + %@ accepté rcv group event chat item @@ -10379,10 +11053,16 @@ Relays can access channel messages. accepted you + vous a accepté·e rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active + actif No comment provided by engineer. @@ -10534,10 +11214,12 @@ marked deleted chat item preview text channel + canal shown as sender role for channel messages channel profile updated + profil du canal mis à jour snd group event chat item @@ -10612,10 +11294,12 @@ marked deleted chat item preview text contact deleted + contact supprimé No comment provided by engineer. contact disabled + contact désactivé No comment provided by engineer. @@ -10630,12 +11314,17 @@ marked deleted chat item preview text contact not ready + contact non prêt No comment provided by engineer. contact should accept… No comment provided by engineer. + + contributor + member role + creator créateur @@ -10816,6 +11505,7 @@ pref value group + groupe shown on group welcome message @@ -10837,6 +11527,10 @@ pref value heures time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. La keychain d'iOS est utilisée pour stocker en toute sécurité la phrase secrète - elle permet de recevoir les notifications push. @@ -10929,6 +11623,7 @@ pref value link + lien No comment provided by engineer. @@ -10953,6 +11648,7 @@ pref value member has old version + le membre a une ancienne version No comment provided by engineer. @@ -10987,6 +11683,7 @@ pref value moderator + modérateur·ice member role @@ -11001,6 +11698,7 @@ pref value new + nouveau No comment provided by engineer. @@ -11020,6 +11718,7 @@ pref value no subscription + aucun abonnement No comment provided by engineer. @@ -11029,6 +11728,7 @@ pref value not synchronized + non synchronisé No comment provided by engineer. @@ -11038,7 +11738,7 @@ pref value off - off + désactivé enabled status group pref value member criteria value @@ -11056,7 +11756,7 @@ time to disappear on - on + activé group pref value @@ -11086,14 +11786,17 @@ time to disappear pending + en attente No comment provided by engineer. pending approval + en attente d'approbation No comment provided by engineer. pending review + en attente de révision No comment provided by engineer. @@ -11113,6 +11816,7 @@ time to disappear rejected + rejeté No comment provided by engineer. @@ -11122,11 +11826,12 @@ time to disappear relay + relais member role removed - supprimé + retiré No comment provided by engineer. @@ -11136,10 +11841,12 @@ time to disappear removed (%d attempts) + retiré (%d tentatives) receive error chat item removed by operator + retiré par un opérateur No comment provided by engineer. @@ -11149,6 +11856,7 @@ time to disappear removed from group + retiré du groupe No comment provided by engineer. @@ -11163,18 +11871,22 @@ time to disappear request is sent + la demande est envoyée No comment provided by engineer. request to join rejected + demande de connexion rejetée No comment provided by engineer. requested connection + a demandé une connexion rcv group event chat item requested connection from group %@ + connexion demandée du groupe %@ rcv direct event chat item @@ -11184,10 +11896,12 @@ time to disappear review + révision No comment provided by engineer. reviewed by admins + révisé par les admins No comment provided by engineer. @@ -11259,6 +11973,10 @@ dernier message reçu : %2$@ barré No comment provided by engineer. + + subscriber + member role + this contact ce contact @@ -11291,6 +12009,7 @@ dernier message reçu : %2$@ updated channel profile + profil du canal mis à jour rcv group event chat item @@ -11308,13 +12027,9 @@ dernier message reçu : %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ + via %@ relay hostname @@ -11384,6 +12099,7 @@ dernier message reçu : %2$@ you accepted this member + vous avez accepté ce membre snd group event chat item @@ -11393,6 +12109,7 @@ dernier message reçu : %2$@ you are subscriber + vous êtes abonné·e No comment provided by engineer. @@ -11457,13 +12174,14 @@ dernier message reçu : %2$@ ⚠️ Signature verification failed: %@. + ⚠️ Échec de la vérification de la signature : %@. owner verification
- +
@@ -11498,9 +12216,24 @@ dernier message reçu : %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11522,7 +12255,7 @@ dernier message reçu : %2$@
- +
@@ -11532,6 +12265,7 @@ dernier message reçu : %2$@ From %d chat(s) + De %d discussion(s) notification body @@ -11553,7 +12287,7 @@ dernier message reçu : %2$@
- +
@@ -11575,7 +12309,7 @@ dernier message reçu : %2$@
- +
@@ -11758,9 +12492,9 @@ dernier message reçu : %2$@ Mauvaise phrase secrète pour la base de données No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock. + + You can allow sharing in Your privacy / SimpleX Lock settings. + Vous pouvez autoriser le partage dans Votre vie privée / Réglages de verrouillage SimpleX. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json index d026c874ec..f41d7b4888 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fr", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index f94d6cefd8..f3286bdfd3 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -1356,8 +1356,8 @@ Available in v5.1 לְפַתֵחַ No comment provided by engineer. - - Developer tools + + Developer כלי מפתחים No comment provided by engineer. @@ -2438,13 +2438,13 @@ Available in v5.1 חבר קבוצה No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. תפקיד חבר הקבוצה ישתנה ל-"%@". כל חברי הקבוצה יקבלו הודעה. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. תפקיד חבר הקבוצה ישתנה ל-"%@". חבר הקבוצה יקבל הזמנה חדשה. No comment provided by engineer. @@ -3232,8 +3232,8 @@ Available in v5.1 Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index 2aa945f603..3a6fed4720 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -1012,8 +1012,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -1747,12 +1747,12 @@ We will be adding server redundancy to prevent lost messages. Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2355,8 +2355,8 @@ We will be adding server redundancy to prevent lost messages. Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 129436ecb0..b3facbe03e 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #titok# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ támogatta a SimpleX Chatet. A kitűző lejárt ekkor: %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ letöltve No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ befektetett a SimpleX Chat közösségi finanszírozásába. + badge alert + %@ is connected! %@ kapcsolódott! @@ -110,6 +120,11 @@ %@ kiszolgáló No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ támogatja a SimpleX Chatet. + badge alert + %@ uploaded %@ feltöltve @@ -185,6 +200,21 @@ %d hónap time interval + + %d owner + %d tulajdonos + channel owners count + + + %d owners + %d tulajdonos + channel owners count + + + %d owners & contributors + %d tulajdonos és közreműködő + channel members count + %d relays failed %d átjátszóhoz nem sikerült kapcsolódni @@ -400,11 +430,6 @@ channel relay bar (új) No comment provided by engineer. - - (signed) - (aláírva) - chat link info line - (this device v%@) (ez az eszköz: v%@) @@ -484,7 +509,7 @@ channel relay bar - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable. - - kapcsolódás a [könyvtárszolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! + - kapcsolódás a [könyvtárszolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (béta)! - kézbesítési jelentések (legfeljebb 20 tagig). - gyorsabb és stabilabb. No comment provided by engineer. @@ -738,6 +763,7 @@ swipe action Add + Hozzáadás No comment provided by engineer. @@ -745,6 +771,16 @@ swipe action Cím hozzáadása a profilhoz, hogy a SimpleX partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve a SimpleX partnerei számára. No comment provided by engineer. + + Add contributors. + Közreműködők hozzáadása. + No comment provided by engineer. + + + Add description + Leírás hozzáadása + No comment provided by engineer. + Add friends Barátok hozzáadása @@ -765,12 +801,19 @@ swipe action Profil hozzáadása No comment provided by engineer. + + Add relay + Átjátszó hozzáadása + No comment provided by engineer. + Add relays + Átjátszók hozzáadása No comment provided by engineer. Add relays to restore message delivery. + Átjátszók hozzáadása az üzenetküldés helyreállításához. No comment provided by engineer. @@ -788,6 +831,11 @@ swipe action Munkatársak hozzáadása No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Adja hozzá ezt a kódot a weboldalához. Meg fogja jeleníteni a csatornája / csoportja előnézetét. + No comment provided by engineer. + Add to another device Hozzáadás egy másik eszközhöz @@ -868,6 +916,11 @@ swipe action Speciális hálózati beállítások No comment provided by engineer. + + Advanced options + Speciális beállítások + No comment provided by engineer. + Advanced settings Speciális beállítások @@ -978,6 +1031,11 @@ swipe action Engedélyezés No comment provided by engineer. + + Allow anyone to embed + Beágyazás engedélyezése bárki számára + No comment provided by engineer. + Allow calls only if your contact allows them. A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. @@ -1153,6 +1211,11 @@ swipe action Hívás fogadása No comment provided by engineer. + + Any webpage can show the preview. + Bármelyik weboldal megjelenítheti az előnézetet. + No comment provided by engineer. + App build: %@ Alkalmazás összeállítási száma: %@ @@ -1195,6 +1258,7 @@ swipe action App update required + Alkalmazásfrissítés szükséges alert title @@ -1362,6 +1426,11 @@ swipe action Hibás az üzenet kivonata No comment provided by engineer. + + Badge cannot be verified + Nem lehetett ellenőrizni a kitűzőt + badge alert title + Be free in your network @@ -1384,6 +1453,11 @@ a saját hálózatában Továbbfejlesztett hívásélmény No comment provided by engineer. + + Better channels 📢 + Továbbfejlesztett csatornák 📢 + No comment provided by engineer. + Better groups Továbbfejlesztett csoportok @@ -1566,7 +1640,7 @@ a saját hálózatában By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA). + A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (béta). No comment provided by engineer. @@ -1574,11 +1648,6 @@ a saját hálózatában A hívás már véget ért! No comment provided by engineer. - - Calls - Hívások - No comment provided by engineer. - Calls prohibited! A hívások le vannak tiltva! @@ -1628,10 +1697,12 @@ new chat action Cancel and delete channel + Visszavonás és a csatorna törlése No comment provided by engineer. Cancel creating channel? + Visszavonja a csatorna létrehozását? alert title @@ -1689,11 +1760,6 @@ new chat action Zárolási mód módosítása authentication reason - - Change member role? - Módosítja a tag szerepkörét? - No comment provided by engineer. - Change passcode Jelkód módosítása @@ -1714,6 +1780,11 @@ new chat action Szerepkör módosítása No comment provided by engineer. + + Change role? + Módosítja a tag szerepkörét? + No comment provided by engineer. + Change self-destruct mode Önmegsemmisítő-mód módosítása @@ -1730,6 +1801,11 @@ set passcode view Csatorna No comment provided by engineer. + + Channel SimpleX name + Csatorna SimpleX-neve + No comment provided by engineer. + Channel display name Csatorna megjelenítendő neve @@ -1781,6 +1857,11 @@ alert subtitle A csatorna ideiglenesen nem érhető el alert title + + Channel webpage + Csatorna weboldala + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza! @@ -1793,6 +1874,7 @@ alert subtitle Channel will start working with %1$d of %2$d relays. Continue? + A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja? alert message @@ -1825,6 +1907,11 @@ alert subtitle Csevegési konzol No comment provided by engineer. + + Chat data + Csevegési adatok + No comment provided by engineer. + Chat database Csevegési adatbázis @@ -2202,6 +2289,11 @@ server test step Gyorsabb kapcsolódás! 🚀 No comment provided by engineer. + + Connect to %@ + Kapcsolódás hozzá: %@ + new chat action + Connect to desktop Társítás számítógéppel @@ -2296,14 +2388,6 @@ Ez a saját egyszer használható meghívója! Társítás számítógéppel No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Kapcsolat @@ -2319,16 +2403,16 @@ Ez a saját egyszer használható meghívója! A kapcsolat le van tiltva No comment provided by engineer. + + Connection blocked: %@ + A kapcsolat le van tiltva: %@ + conn error description + Connection error Kapcsolódási hiba alert title - - Connection error (AUTH) - Kapcsolódási hiba (AUTH) - conn error description - Connection failed Nem sikerült létrehozni a kapcsolatot @@ -2341,6 +2425,11 @@ Ez a saját egyszer használható meghívója! %@ No comment provided by engineer. + + Connection link removed + Kapcsolódási hiba + conn error description + Connection not ready. A kapcsolat nem áll készen. @@ -2386,6 +2475,11 @@ Ez a saját egyszer használható meghívója! Kapcsolatok No comment provided by engineer. + + Contact + Kapcsolat + No comment provided by engineer. + Contact address Kapcsolattartási cím @@ -2476,6 +2570,11 @@ Ez a saját egyszer használható meghívója! Másolás No comment provided by engineer. + + Copy code + Kód másolása + No comment provided by engineer. + Copy error Hiba másolása @@ -2511,6 +2610,11 @@ Ez a saját egyszer használható meghívója! Csoport létrehozása véletlenszerű profillal. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Hozzon létre egy weboldalt a csatorna előnézetének megjelenítéséhez a látogatók számára, mielőtt feliratkoznának. Üzemeltesse saját maga, vagy használjon tetszőleges statikus tárhelyet. + No comment provided by engineer. + Create file Fájl létrehozása @@ -2551,16 +2655,16 @@ Ez a saját egyszer használható meghívója! Nyilvános csatorna létrehozása No comment provided by engineer. - - Create public channel (BETA) - Nyilvános csatorna létrehozása (BÉTA) - No comment provided by engineer. - Create queue Várólista létrehozása server test step + + Create web preview. + Előnézet készítése a weboldalakhoz. + No comment provided by engineer. + Create your address Saját cím létrehozása @@ -2907,6 +3011,7 @@ swipe action Delete from history + Törlés az előzményekből No comment provided by engineer. @@ -3095,9 +3200,9 @@ alert button Számítógépek No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + A(z) %1$@ célkiszolgáló címe nem kompatibilis a(z) %2$@ továbbító kiszolgáló beállításaival. No comment provided by engineer. @@ -3105,9 +3210,9 @@ alert button Célkiszolgáló-hiba: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + A(z) %1$@ célkiszolgáló verziója nem kompatibilis a(z) %2$@ továbbító kiszolgálóval. No comment provided by engineer. @@ -3120,9 +3225,9 @@ alert button További részletek No comment provided by engineer. - - Develop - Fejlesztés + + Developer + Fejlesztői eszközök No comment provided by engineer. @@ -3130,11 +3235,6 @@ alert button Fejlesztői beállítások No comment provided by engineer. - - Developer tools - Fejlesztői eszközök - No comment provided by engineer. - Device Eszköz @@ -3280,6 +3380,11 @@ alert button Befejezés később No comment provided by engineer. + + Do not require signing messages. + Üzenetek aláírásának mellőzése. + No comment provided by engineer. + Do not send history to new members. Az előzmények ne legyenek elküldve az új tagok számára. @@ -3315,6 +3420,11 @@ alert button Ne maradjon le a fontos üzenetekről. No comment provided by engineer. + + Don't save + Folytatás mentés nélkül + alert action + Don't show again Ne jelenjen meg újra @@ -3396,6 +3506,11 @@ chat item action Könnyebben hívhatja meg a barátait 👋 No comment provided by engineer. + + Easier to read. + Könnyebb olvashatóság. + No comment provided by engineer. + Edit Szerkesztés @@ -3406,6 +3521,11 @@ chat item action Csatornaprofil szerkesztése No comment provided by engineer. + + Edit description + Leírás szerkesztése + No comment provided by engineer. + Edit group profile Csoportprofil szerkesztése @@ -3473,7 +3593,7 @@ chat item action Enable in direct chats (BETA)! - Engedélyezés a közvetlen csevegésekben (BÉTA)! + Engedélyezés a közvetlen csevegésekben (béta)! No comment provided by engineer. @@ -3606,6 +3726,11 @@ chat item action Adja meg a helyes jelmondatot. No comment provided by engineer. + + Enter description (optional) + Adja meg a leírást (nem kötelező) + placeholder + Enter group name… Adja meg a csoport nevét… @@ -3646,6 +3771,11 @@ chat item action Adja meg ennek az eszköznek a nevét… No comment provided by engineer. + + Enter webpage URL + Adja meg az oldal webcímét + No comment provided by engineer. + Enter welcome message… Adja meg az üdvözlőüzenetet… @@ -3664,7 +3794,7 @@ chat item action Error Hiba - conn error description + No comment provided by engineer. Error aborting address change @@ -3698,6 +3828,7 @@ chat item action Error adding relays + Hiba történt az átjátszók hozzáadásakor alert title @@ -3832,6 +3963,7 @@ chat item action Error deleting message + Hiba történt az üzenet törlésekor alert title @@ -3964,6 +4096,11 @@ chat item action Hiba történt a csoportprofil mentésekor No comment provided by engineer. + + Error saving name + Hiba történt a név mentésekor + alert title + Error saving passcode Hiba történt a jelkód mentésekor @@ -4019,6 +4156,11 @@ chat item action Hiba történt a kézbesítési jelentések beállításakor! No comment provided by engineer. + + Error sharing address + Hiba történt a cím megosztásakor + alert title + Error sharing channel Hiba történt a csatorna megosztásakor @@ -4098,6 +4240,7 @@ chat item action Error: %@ Hiba: %@ alert message +conn error description file error text snd error text @@ -4139,7 +4282,7 @@ server test error Expand - Kibontás + Felfedés chat item action @@ -4241,6 +4384,16 @@ server test error Fájlkiszolgáló-hiba: %@ file error text + + File servers + Fájlkiszolgálók + No comment provided by engineer. + + + File servers: %@ + Fájlkiszolgálók: %@ + copied message info + File status Fájl állapota @@ -4420,7 +4573,7 @@ servers warning For private routing - A privát útválasztáshoz + Privát útválasztáshoz No comment provided by engineer. @@ -4542,6 +4695,11 @@ Hiba: %2$@ GIF-ek és matricák No comment provided by engineer. + + Get SimpleX name (BETA) + SimpleX-név beszerzése (béta) + No comment provided by engineer. + Get link Hivatkozás megtekintése @@ -4652,6 +4810,11 @@ Hiba: %2$@ Csoportprofil módosítva. Ha menti, akkor a frissített profil el lesz küldve a csoport tagjainak. alert message + + Group webpage + Csoport weboldala + No comment provided by engineer. + Group welcome message A csoport üdvözlőüzenete @@ -4677,6 +4840,11 @@ Hiba: %2$@ Súgó No comment provided by engineer. + + Help & support + Súgó és támogatás + No comment provided by engineer. + Help admins moderating their groups. Segítsen az adminisztrátoroknak a csoportjaik moderálásában. @@ -4699,7 +4867,7 @@ Hiba: %2$@ Hide - Összecsukás + Elrejtés chat item action @@ -4757,6 +4925,11 @@ Hiba: %2$@ Útmutató No comment provided by engineer. + + How to register a test name + Egy név regisztrálása tesztelési céllal + No comment provided by engineer. + How to use it Használati útmutató @@ -5162,6 +5335,11 @@ További fejlesztések hamarosan! Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Meg fog jelenni a feliratkozóknak, és az előnézet betöltésének engedélyezésére szolgál. + No comment provided by engineer. + Italian interface Olasz kezelőfelület @@ -5187,6 +5365,11 @@ További fejlesztések hamarosan! Csatlakozás a csatornához No comment provided by engineer. + + Join channel %@ + Csatlakozás a(z) %@ nevű csatornához + new chat action + Join group Csatlakozás a csoporthoz @@ -5267,7 +5450,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Learn more Tudjon meg többet - No comment provided by engineer. + badge alert button Leave @@ -5309,6 +5492,16 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Kevesebb adatforgalom a mobilhálózatokon. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Tegye lehetővé mások számára a kapcsolódást a saját SimpleX-címével regisztrált néven keresztül. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Tegye lehetővé mások számára a csatlakozást az ezzel a csatornahivatkozással regisztrált néven keresztül. + No comment provided by engineer. + Let someone connect to you Legyen elérhető mások számára @@ -5419,6 +5612,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva. No comment provided by engineer. + + Manage your relays. + Saját átjátszók kezelése. + No comment provided by engineer. + Mark deleted for everyone Jelölje meg az összes tag számára töröltként @@ -5489,21 +5687,6 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Tagok jelentései chat feature - - Member role will be changed to "%@". All chat members will be notified. - A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza! @@ -5616,7 +5799,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Message queue info - Üzenet várólista információi + Üzenet várólista-információi No comment provided by engineer. @@ -5649,6 +5832,16 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Üzenetbuborék alakja No comment provided by engineer. + + Message signing is not required. + Nem kötelező aláírni az üzeneteket. + No comment provided by engineer. + + + Message signing is required. + Kötelező aláírni az üzeneteket. + No comment provided by engineer. + Message source remains private. Az üzenet forrása titokban marad. @@ -5819,6 +6012,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Hamarosan további fejlesztések érkeznek! No comment provided by engineer. + + More privacy + További adatvédelem + No comment provided by engineer. + More reliable network connection. Megbízhatóbb hálózati kapcsolat. @@ -5859,6 +6057,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Név swipe action + + Name not found + Nem található a név + No comment provided by engineer. + Network & servers Hálózat és kiszolgálók @@ -6045,6 +6248,7 @@ A legbiztonságosabb titkosítás. No available relays + Nincsenek elérhető átjátszók No comment provided by engineer. @@ -6174,6 +6378,7 @@ A legbiztonságosabb titkosítás. No relays + Nincsenek átjátszók No comment provided by engineer. @@ -6191,6 +6396,11 @@ A legbiztonságosabb titkosítás. Nincsenek üzenetfogadási kiszolgálók. servers error + + No servers to resolve names. + Nincsenek kiszolgálók a nevek feloldásához. + servers warning + No servers to send files. Nincsenek fájlküldési kiszolgálók. @@ -6206,6 +6416,11 @@ A legbiztonságosabb titkosítás. Nincsenek olvasatlan csevegések No comment provided by engineer. + + No valid link + Nincs érvényes hivatkozás + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk. @@ -6216,6 +6431,11 @@ A legbiztonságosabb titkosítás. Nonprofit irányítás No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + Egyik saját kiszolgálója sincs beállítva a SimpleX-nevek feloldásához. Állítsa be a kiszolgálókat, vagy használjon egy kapcsolattartási hivatkozást. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén. @@ -6441,6 +6661,11 @@ VPN engedélyezése szükséges. Csak a partnere küldhet hangüzeneteket. No comment provided by engineer. + + Only your page above can show the preview. + Csak az Ön fenti oldala jelenítheti meg az előnézetet. + No comment provided by engineer. + Open Megnyitás @@ -6630,9 +6855,9 @@ alert button Tulajdonos No comment provided by engineer. - - Owners - Tulajdonosok + + Owners & contributors + Tulajdonosok és közreműködők No comment provided by engineer. @@ -6824,10 +7049,6 @@ Hiba: %@ Próbálja meg letiltani és újra engedélyezni az értesítéseket. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérését. @@ -6888,11 +7109,6 @@ Hiba: %@ Korábban kapcsolódott kiszolgálók No comment provided by engineer. - - Privacy & security - Adatvédelem és biztonság - No comment provided by engineer. - Privacy for your customers. Saját ügyfeleinek adatvédelme. @@ -6981,7 +7197,8 @@ Hiba: %@ Profile update will be sent to your SimpleX contacts. A profilfrissítés el lesz küldve a SimpleX partnerei számára. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7100,6 +7317,11 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Nyilvános csatornák – mondja el szabadon a véleményét 🚀 No comment provided by engineer. + + Public names for your channel or business. + Nyilvános nevek a csatornákhoz vagy az üzleti profilokhoz. + No comment provided by engineer. + Push notifications Leküldéses értesítések @@ -7138,16 +7360,16 @@ Engedélyezze a *Hálózat és kiszolgálók* menüben. Read more Tudjon meg többet - No comment provided by engineer. + profile description teaser Read more in User Guide. - További információ a Használati útmutatóban. + További információkat a használati útmutatóban talál. No comment provided by engineer. Read more in our GitHub repository. - További információ a GitHub-tárolónkban. + További információkat a GitHub-tárolónkban talál. No comment provided by engineer. @@ -7349,10 +7571,12 @@ swipe action Relay will be removed from channel - this cannot be undone! + Az átjátszó el lesz távolítva a csatornából – ez a művelet nem vonható vissza! alert message Relays added: %@. + Átjátszók hozzáadva: %@. alert message @@ -7395,13 +7619,24 @@ swipe action Eltávolítja a tagot? alert title + + Remove name + Név eltávolítása + No comment provided by engineer. + Remove passphrase from keychain? Eltávolítja a jelmondatot a kulcstartóból? No comment provided by engineer. + + Remove relay + Átjátszó eltávolítása + No comment provided by engineer. + Remove relay? + Eltávolítja az átjátszót? alert title @@ -7504,6 +7739,11 @@ swipe action Jelentések No comment provided by engineer. + + Require signing messages. + Üzenetek aláírásának kötelezővé tétele. + No comment provided by engineer. + Required Szükséges @@ -7549,6 +7789,11 @@ swipe action Felhasználó által létrehozott téma visszaállítása No comment provided by engineer. + + Resolver error: %@ + Feloldási hiba: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Új csevegési profil létrehozásához indítsa újra az alkalmazást @@ -7629,6 +7874,26 @@ swipe action Szerepkör No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + A szerepkör a következőre fog módosulni: „%@”. A csatorna összes feliratkozója értesítést fog kapni. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni. + No comment provided by engineer. + Run chat Csevegési szolgáltatás indítása @@ -7662,7 +7927,8 @@ swipe action Save Mentés - alert button + alert action +alert button chat item action @@ -7680,6 +7946,11 @@ chat item action Mentés (és a feliratkozók értesítése) alert button + + Save SimpleX name? + Menti a SimpleX-nevet? + alert title + Save admission settings? Menti a befogadási beállításokat? @@ -7695,6 +7966,11 @@ chat item action Mentés és a csoporttagok értesítése No comment provided by engineer. + + Save and notify members + Mentés és a tagok értesítése + No comment provided by engineer. + Save and notify subscribers Mentés és a feliratkozók értesítése @@ -7765,6 +8041,11 @@ chat item action Menti a kiszolgálókat? alert title + + Save webpage settings? + Menti a weboldal beállításait? + alert title + Save welcome message? Menti az üdvözlőüzenetet? @@ -8065,11 +8346,6 @@ chat item action A fájl küldője visszavonta az átvitelt. alert message - - Sender may have deleted the connection request. - A kérés küldője törölhette a kapcsolódási kérést. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. A hivatkozáselőnézet küldése felfedheti az Ön IP-címét a weboldal számára. Ezt később módosíthatja az adatvédelmi beállításokban. @@ -8165,6 +8441,11 @@ chat item action Kiszolgáló No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + A(z) %@ kiszolgáló nem támogatja a névfeloldást. Állítsa be a kiszolgálókat, vagy használjon kapcsolattartási hivatkozást. + No comment provided by engineer. + Server added to operator %@. Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@. @@ -8476,6 +8757,11 @@ chat item action Fejlesztői beállítások megjelenítése No comment provided by engineer. + + Show encryption + Titkosítás megjelenítése + No comment provided by engineer. + Show last messages Legutóbbi üzenetek előnézetének megjelenítése @@ -8506,6 +8792,37 @@ chat item action Megjelenítve: No comment provided by engineer. + + Sign message + Üzenet aláírása + No comment provided by engineer. + + + Sign messages + Üzenetek aláírása + chat feature + + + Signature missing + Hiányzik az aláírás + alert title +copied message info + + + Signed + Aláírva + copied message info + + + Signed & verified + Aláírva és ellenőrizve + copied message info + + + Signing proves you authored this message and can't be denied later. + Az aláírás igazolja, hogy Ön írta ezt az üzenetet, és azt később már nem lehet letagadni. + No comment provided by engineer. + SimpleX SimpleX @@ -8601,6 +8918,21 @@ chat item action A SimpleX-hivatkozások küldése le van tiltva No comment provided by engineer. + + SimpleX name + SimpleX-név + No comment provided by engineer. + + + SimpleX name error + Hibás SimpleX-név + No comment provided by engineer. + + + SimpleX name not verified + Nincs ellenőrizve a SimpleX-név + alert title + SimpleX one-time invitation Egyszer használható SimpleX meghívó @@ -8611,6 +8943,11 @@ chat item action A SimpleX protokollokat a Trail of Bits auditálta. No comment provided by engineer. + + SimpleX public names (BETA) + Nyilvános SimpleX-nevek (béta) + No comment provided by engineer. + SimpleX relay address SimpleX-átjátszó címe @@ -8721,6 +9058,7 @@ report reason Status + Állapot No comment provided by engineer. @@ -8880,9 +9218,9 @@ Az átjátszó címe ennek az átjátszónak a beállítására szolgált a csat Mellőzött feliratkozások No comment provided by engineer. - - Support SimpleX Chat - SimpleX Chat támogatása + + Support the project + A projekt támogatása No comment provided by engineer. @@ -9078,6 +9416,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + A(z) #%@ SimpleX-név csatornahivatkozás nélkül lett regisztrálva. A regisztrációs oldalon adjon hozzá a névhez egy csatornahivatkozást. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + A(z) %@ SimpleX-név regisztrálva van, de nem rendelkezik érvényes hivatkozással. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + A(z) %@ SimpleX-név regisztrálva van, de nincs hozzáadva a profilhoz. Adja hozzá a címéhez vagy a csatornaprofiljához, amennyiben Ön a tulajdonosa. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + A(z) @%@ SimpleX-név SimpleX-cím nélkül lett regisztrálva. A regisztrációs oldalon adja hozzá a névhez a saját SimpleX-címét. + alert message + The address will be short, and your profile will be shared via the address. A cím rövid lesz és a profil meg lesz osztva a címen keresztül. @@ -9108,6 +9466,16 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + A kitűző egy olyan kulccsal van aláírva, amelyet az alkalmazás ezen verziója nem ismer fel. Frissítse az alkalmazást a kitűző ellenőrzéséhez. + badge alert + + + The channel required this message to be signed, but the signature is missing. + A csatorna megköveteli az üzenet aláírását, de az hiányzik. + alert message + The code you scanned is not a SimpleX link QR code. A beolvasott QR-kód nem egy SimpleX-hivatkozás. @@ -9205,6 +9573,11 @@ a saját kapcsolatait és csoportjait. A második pipa, ami már nagyon hiányzott! ✅ No comment provided by engineer. + + The sender deleted the connection request. + A kérés küldője törölhette a kapcsolódási kérést. + No comment provided by engineer. + The sender will NOT be notified A kérés küldője NEM lesz értesítve @@ -9260,6 +9633,11 @@ a saját kapcsolatait és csoportjait. Ezek felülbírálhatók a partner- és csoportbeállításokban. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + Ez a SimpleX-név nincs regisztrálva. Ellenőrizze a nevet. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Ez a művelet nem vonható vissza – az összes fogadott és küldött fájl a médiatartalmakkal együtt törölve lesznek. Az alacsony felbontású képek viszont megmaradnak. @@ -9280,6 +9658,11 @@ a saját kapcsolatait és csoportjait. Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + Nem sikerült ellenőrizni ezt a kitűzőt, és lehet, hogy nem eredeti. + badge alert + This chat is protected by end-to-end encryption. Ez a csevegés végpontok közötti titkosítással védett. @@ -9312,6 +9695,7 @@ a saját kapcsolatait és csoportjait. This group requires a newer version of the app. Please update the app to join. + Ehhez a csoporthoz az alkalmazás újabb verziója szükséges. A csatlakozáshoz frissítse az alkalmazást. alert message alert subtitle @@ -9322,6 +9706,7 @@ alert subtitle This is the last active relay. Removing it will prevent message delivery to subscribers. + Ez az utolsó aktív átjátszó. Ha eltávolítja, akkor azzal megakadályozza az üzenetek eljuttatását a feliratkozóknak. alert message @@ -9418,7 +9803,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To receive - A fogadáshoz + Üzenetek fogadásához No comment provided by engineer. @@ -9436,6 +9821,11 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz. No comment provided by engineer. + + To resolve names + Nevek feloldásához + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben. @@ -9443,7 +9833,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll To send - A küldéshez + Üzenetek küldéséhez No comment provided by engineer. @@ -9471,6 +9861,11 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + A kulcsok ellenőrzéséhez ezzel a feliratkozóval hasonlítsa össze (vagy olvassa be) az eszközökön található kódot. + No comment provided by engineer. + Toggle incognito when connecting. Inkognitó profil használata kapcsolódáskor ki/be. @@ -9561,6 +9956,11 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Az összes feliratkozó számára feloldja a feliratkozó letiltását? No comment provided by engineer. + + Unconfirmed name + Megerősítetlen név + No comment provided by engineer. + Undelivered messages Kézbesítetlen üzenetek @@ -9621,13 +10021,6 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakadások elkerülése érdekében. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát. -A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. - No comment provided by engineer. - Unlink Leválasztás @@ -9658,18 +10051,15 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Olvasatlan swipe action - - Unsupported channel name - alert title - Unsupported connection link Nem támogatott kapcsolattartási hivatkozás conn error description - - Unsupported contact name - alert title + + Unverified badge + Ellenőrizetlen kitűző + badge alert title Up to 100 last messages are sent to new members. @@ -9724,7 +10114,8 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Upgrade address? Frissíti a címet? - alert message + alert message +alert title Upgrade and open chat @@ -9901,6 +10292,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Webport használata No comment provided by engineer. + + Used chat relays do not support webpages. + Az Ön által használt csevegési átjátszók nem támogatják a weboldalakat. + No comment provided by engineer. + User selection Felhasználó kiválasztása @@ -9921,6 +10317,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Ellenőrzés relay test step + + Verify SimpleX names + SimpleX-nevek ellenőrzése + No comment provided by engineer. + Verify code with desktop Kód ellenőrzése a számítógépen @@ -9946,6 +10347,11 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso Adatbázis jelmondatának ellenőrzése No comment provided by engineer. + + Verify name + Név ellenőrzése + No comment provided by engineer. + Verify passphrase Jelmondat ellenőrzése @@ -10106,6 +10512,16 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso WebRTC ICE-kiszolgálók No comment provided by engineer. + + Webpage code + Weboldalba ágyazható kód + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + A weboldal beállításai módosultak. Ha menti a módosításokat, a frissített beállítások el lesznek küldve a feliratkozóknak. + alert message + Welcome %@! Üdvözöljük %@! @@ -10328,9 +10744,9 @@ Megismétli a csatlakozási kérést? Később engedélyezheti a beállításokban No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. + + You can enable them later via app Your privacy settings. + Később is engedélyezheti őket az „Adatvédelem” menüben. No comment provided by engineer. @@ -10393,6 +10809,11 @@ Megismétli a csatlakozási kérést? A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + A SimpleXet az alkalmazás v7-es verziójától kezdve támogathatja. + badge alert + You can turn on SimpleX Lock via Settings. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be. @@ -10584,6 +11005,11 @@ Megismétli a kapcsolódási kérést? Profil SimpleX-címe No comment provided by engineer. + + Your SimpleX name + Saját SimpleX-név + No comment provided by engineer. + Your business contact Üzleti partner @@ -10599,11 +11025,6 @@ Megismétli a kapcsolódási kérést? Saját csatorna No comment provided by engineer. - - Your chat database - Csevegési adatbázis - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz. @@ -10634,6 +11055,13 @@ Megismétli a kapcsolódási kérést? Partner No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát. +A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). A partnere a jelenleg támogatott legnagyobb (%@) fájlméretnél nagyobbat küldött. @@ -10682,6 +11110,8 @@ Megismétli a kapcsolódási kérést? Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + Az új %1$@ nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódott. +Ha visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja. alert message @@ -10806,6 +11236,11 @@ Az átjátszók hozzáférhetnek a csatornaüzenetekhez. befogadta Önt rcv group event chat item + + acknowledged roster + visszaigazolt névsor + No comment provided by engineer. + active aktív @@ -11072,6 +11507,11 @@ marked deleted chat item preview text a partnernek el kell fogadnia… No comment provided by engineer. + + contributor + közreműködő + member role + creator készítő @@ -11278,6 +11718,11 @@ pref value óra time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál – lehetővé teszi a leküldéses értesítések fogadását. @@ -11690,7 +12135,7 @@ time to disappear server queue info: %1$@ last received msg: %2$@ - a kiszolgáló várólista információi: %1$@ + kiszolgáló várólista-információi: %1$@ utoljára fogadott üzenet: %2$@ queue info @@ -11720,6 +12165,11 @@ utoljára fogadott üzenet: %2$@ áthúzott No comment provided by engineer. + + subscriber + feliratkozó + member role + this contact ez a partner @@ -11770,11 +12220,6 @@ utoljára fogadott üzenet: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ a következőn keresztül: %@ @@ -11929,7 +12374,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -11964,9 +12409,24 @@ utoljára fogadott üzenet: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11988,7 +12448,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -12020,7 +12480,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -12042,7 +12502,7 @@ utoljára fogadott üzenet: %2$@
- +
@@ -12225,9 +12685,9 @@ utoljára fogadott üzenet: %2$@ Érvénytelen adatbázis-jelmondat No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti. + + You can allow sharing in Your privacy / SimpleX Lock settings. + A megosztást az Adatvédelem / SimpleX-zár menüben engedélyezheti. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json index c07ec0f900..31997434c3 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "hu", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 469da88ce2..5381b8cbb5 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #segreto# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ ha sostenuto SimpleX Chat. La targhetta è scaduta il %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ scaricati No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ ha investito nella raccolta fondi di SimpleX Chat. + badge alert + %@ is connected! %@ è connesso/a! @@ -110,6 +120,11 @@ %@ server No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ sostiene SimpleX Chat. + badge alert + %@ uploaded %@ caricati @@ -185,6 +200,21 @@ %d mesi time interval + + %d owner + %d proprietario + channel owners count + + + %d owners + %d proprietari + channel owners count + + + %d owners & contributors + %d proprietari e collaboratori + channel members count + %d relays failed %d relay falliti @@ -205,7 +235,7 @@ channel subscriber relay bar %d sec - %d sec + %d s time interval @@ -400,11 +430,6 @@ channel relay bar (nuovo) No comment provided by engineer. - - (signed) - (firmato) - chat link info line - (this device v%@) (questo dispositivo v%@) @@ -738,6 +763,7 @@ swipe action Add + Aggiungi No comment provided by engineer. @@ -745,6 +771,16 @@ swipe action Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. No comment provided by engineer. + + Add contributors. + Aggiungi collaboratori. + No comment provided by engineer. + + + Add description + Aggiungi descrizione + No comment provided by engineer. + Add friends Aggiungi amici @@ -765,12 +801,19 @@ swipe action Aggiungi profilo No comment provided by engineer. + + Add relay + Aggiungi relay + No comment provided by engineer. + Add relays + Aggiungi relay No comment provided by engineer. Add relays to restore message delivery. + Aggiungi relay per ripristinare la consegna dei messaggi. No comment provided by engineer. @@ -788,6 +831,11 @@ swipe action Aggiungi membri del team No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Aggiungi questo codice alla tua pagina web. Mostrerà l'anteprima del tuo canale / gruppo. + No comment provided by engineer. + Add to another device Aggiungi ad un altro dispositivo @@ -868,6 +916,11 @@ swipe action Impostazioni di rete avanzate No comment provided by engineer. + + Advanced options + Opzioni avanzate + No comment provided by engineer. + Advanced settings Impostazioni avanzate @@ -978,6 +1031,11 @@ swipe action Consenti No comment provided by engineer. + + Allow anyone to embed + Consenti a chiunque di incorporare + No comment provided by engineer. + Allow calls only if your contact allows them. Consenti le chiamate solo se il tuo contatto le consente. @@ -1153,6 +1211,11 @@ swipe action Rispondi alla chiamata No comment provided by engineer. + + Any webpage can show the preview. + Qualsiasi pagina web può mostrare l'anteprima. + No comment provided by engineer. + App build: %@ Build dell'app: %@ @@ -1195,6 +1258,7 @@ swipe action App update required + Aggiornamento dell'app necessario alert title @@ -1362,6 +1426,11 @@ swipe action Hash del messaggio errato No comment provided by engineer. + + Badge cannot be verified + La targhetta non può essere verificata + badge alert title + Be free in your network @@ -1384,6 +1453,11 @@ nella tua rete Chiamate migliorate No comment provided by engineer. + + Better channels 📢 + Canali migliorati 📢 + No comment provided by engineer. + Better groups Gruppi migliorati @@ -1574,11 +1648,6 @@ nella tua rete Chiamata già terminata! No comment provided by engineer. - - Calls - Chiamate - No comment provided by engineer. - Calls prohibited! Chiamate proibite! @@ -1628,10 +1697,12 @@ new chat action Cancel and delete channel + Annulla ed elimina il canale No comment provided by engineer. Cancel creating channel? + Annullare la creazione del canale? alert title @@ -1689,11 +1760,6 @@ new chat action Cambia modalità di blocco authentication reason - - Change member role? - Cambiare ruolo del membro? - No comment provided by engineer. - Change passcode Cambia codice di accesso @@ -1714,6 +1780,11 @@ new chat action Cambia ruolo No comment provided by engineer. + + Change role? + Cambiare il ruolo? + No comment provided by engineer. + Change self-destruct mode Cambia modalità di autodistruzione @@ -1730,6 +1801,11 @@ set passcode view Canale No comment provided by engineer. + + Channel SimpleX name + Nome SimpleX per il canale + No comment provided by engineer. + Channel display name Nome da mostrare del canale @@ -1781,6 +1857,11 @@ alert subtitle Canale non disponibile temporaneamente alert title + + Channel webpage + Pagina web del canale + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! Il canale verrà eliminato per tutti gli iscritti, non è reversibile! @@ -1793,6 +1874,7 @@ alert subtitle Channel will start working with %1$d of %2$d relays. Continue? + Il canale sarà operativo con %1$d di %2$d relay. Continuare? alert message @@ -1825,6 +1907,11 @@ alert subtitle Console della chat No comment provided by engineer. + + Chat data + Dati della chat + No comment provided by engineer. + Chat database Database della chat @@ -2202,6 +2289,11 @@ server test step Connettiti più velocemente! 🚀 No comment provided by engineer. + + Connect to %@ + Connetti a %@ + new chat action + Connect to desktop Connetti al desktop @@ -2296,14 +2388,6 @@ Questo è il tuo link una tantum! Connessione al desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Connessione @@ -2319,16 +2403,16 @@ Questo è il tuo link una tantum! Connessione bloccata No comment provided by engineer. + + Connection blocked: %@ + Connessione bloccata: %@ + conn error description + Connection error Errore di connessione alert title - - Connection error (AUTH) - Errore di connessione (AUTH) - conn error description - Connection failed Connessione fallita @@ -2341,6 +2425,11 @@ Questo è il tuo link una tantum! %@ No comment provided by engineer. + + Connection link removed + Errore di connessione + conn error description + Connection not ready. Connessione non pronta. @@ -2386,6 +2475,11 @@ Questo è il tuo link una tantum! Connessioni No comment provided by engineer. + + Contact + Contatto + No comment provided by engineer. + Contact address Indirizzo di contatto @@ -2476,6 +2570,11 @@ Questo è il tuo link una tantum! Copia No comment provided by engineer. + + Copy code + Copia codice + No comment provided by engineer. + Copy error Copia errore @@ -2511,6 +2610,11 @@ Questo è il tuo link una tantum! Crea un gruppo usando un profilo casuale. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Crea una pagina web per mostrare l'anteprima del tuo canale ai visitatori prima che si iscrivano. Ospitala da solo o usa un qualsiasi hosting statico. + No comment provided by engineer. + Create file Crea file @@ -2551,16 +2655,16 @@ Questo è il tuo link una tantum! Crea canale pubblico No comment provided by engineer. - - Create public channel (BETA) - Crea canale pubblico (BETA) - No comment provided by engineer. - Create queue Crea coda server test step + + Create web preview. + Crea un'anteprima web. + No comment provided by engineer. + Create your address Crea il tuo indirizzo @@ -2907,6 +3011,7 @@ swipe action Delete from history + Elimina dalla cronologia No comment provided by engineer. @@ -3095,9 +3200,9 @@ alert button Dispositivi desktop No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + L'indirizzo del server di destinazione di %1$@ è incompatibile con le impostazioni del server di inoltro %2$@. No comment provided by engineer. @@ -3105,9 +3210,9 @@ alert button Errore del server di destinazione: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + La versione del server di destinazione di %1$@ è incompatibile con il server di inoltro %2$@. No comment provided by engineer. @@ -3120,9 +3225,9 @@ alert button Dettagli No comment provided by engineer. - - Develop - Sviluppa + + Developer + Strumenti di sviluppo No comment provided by engineer. @@ -3130,11 +3235,6 @@ alert button Opzioni sviluppatore No comment provided by engineer. - - Developer tools - Strumenti di sviluppo - No comment provided by engineer. - Device Dispositivo @@ -3280,6 +3380,11 @@ alert button Fallo dopo No comment provided by engineer. + + Do not require signing messages. + Non richiedere la firma dei messaggi. + No comment provided by engineer. + Do not send history to new members. Non inviare la cronologia ai nuovi membri. @@ -3315,6 +3420,11 @@ alert button Non perdere messaggi importanti. No comment provided by engineer. + + Don't save + Non salvare + alert action + Don't show again Non mostrare più @@ -3396,6 +3506,11 @@ chat item action È più facile invitare i tuoi amici 👋 No comment provided by engineer. + + Easier to read. + Lettura più facile. + No comment provided by engineer. + Edit Modifica @@ -3406,6 +3521,11 @@ chat item action Modifica profilo canale No comment provided by engineer. + + Edit description + Modifica descrizione + No comment provided by engineer. + Edit group profile Modifica il profilo del gruppo @@ -3606,6 +3726,11 @@ chat item action Inserisci la password giusta. No comment provided by engineer. + + Enter description (optional) + Inserisci la descrizione (facoltativa) + placeholder + Enter group name… Inserisci il nome del gruppo… @@ -3646,6 +3771,11 @@ chat item action Inserisci il nome di questo dispositivo… No comment provided by engineer. + + Enter webpage URL + Inserisci URL della pagina + No comment provided by engineer. + Enter welcome message… Inserisci il messaggio di benvenuto… @@ -3664,7 +3794,7 @@ chat item action Error Errore - conn error description + No comment provided by engineer. Error aborting address change @@ -3698,6 +3828,7 @@ chat item action Error adding relays + Errore di aggiunta dei relay alert title @@ -3832,6 +3963,7 @@ chat item action Error deleting message + Errore di eliminazione del messaggio alert title @@ -3964,6 +4096,11 @@ chat item action Errore nel salvataggio del profilo del gruppo No comment provided by engineer. + + Error saving name + Errore di salvataggio del nome + alert title + Error saving passcode Errore nel salvataggio del codice di accesso @@ -3996,7 +4133,7 @@ chat item action Error sending email - Errore nell'invio dell'email + Errore nell'invio dell'e-mail No comment provided by engineer. @@ -4019,6 +4156,11 @@ chat item action Errore nell'impostazione delle ricevute di consegna! No comment provided by engineer. + + Error sharing address + Errore di condivisione dell'indirizzo + alert title + Error sharing channel Errore nella condivisione del canale @@ -4098,6 +4240,7 @@ chat item action Error: %@ Errore: %@ alert message +conn error description file error text snd error text @@ -4241,6 +4384,16 @@ server test error Errore del server dei file: %@ file error text + + File servers + Server di file + No comment provided by engineer. + + + File servers: %@ + Server di file: %@ + copied message info + File status Stato del file @@ -4542,6 +4695,11 @@ Errore: %2$@ GIF e adesivi No comment provided by engineer. + + Get SimpleX name (BETA) + Ottieni nome SimpleX (BETA) + No comment provided by engineer. + Get link Ottieni link @@ -4652,6 +4810,11 @@ Errore: %2$@ Il profilo del gruppo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato ai membri del gruppo. alert message + + Group webpage + Pagina web del gruppo + No comment provided by engineer. + Group welcome message Messaggio di benvenuto del gruppo @@ -4677,6 +4840,11 @@ Errore: %2$@ Aiuto No comment provided by engineer. + + Help & support + Aiuto e supporto + No comment provided by engineer. + Help admins moderating their groups. Aiuta gli amministratori a moderare i loro gruppi. @@ -4757,6 +4925,11 @@ Errore: %2$@ Come si fa No comment provided by engineer. + + How to register a test name + Registra un nome di prova + No comment provided by engineer. + How to use it Come si usa @@ -5162,6 +5335,11 @@ Altri miglioramenti sono in arrivo! Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Verrà mostrato agli iscritti e usato per permettere il caricamento dell'anteprima. + No comment provided by engineer. + Italian interface Interfaccia italiana @@ -5187,6 +5365,11 @@ Altri miglioramenti sono in arrivo! Iscriviti al canale No comment provided by engineer. + + Join channel %@ + Entra nel canale %@ + new chat action + Join group Entra nel gruppo @@ -5267,7 +5450,7 @@ Questo è il tuo link per il gruppo %@! Learn more Maggiori informazioni - No comment provided by engineer. + badge alert button Leave @@ -5309,6 +5492,16 @@ Questo è il tuo link per il gruppo %@! Meno traffico sulle reti mobili. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Consenti alle persone di collegarsi tramite il nome registrato con il tuo indirizzo SimpleX. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Consenti alle persone di entrare attraverso il nome registrato con questo link del canale. + No comment provided by engineer. + Let someone connect to you Lascia che qualcuno si connetta a te @@ -5419,6 +5612,11 @@ Questo è il tuo link per il gruppo %@! Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi. No comment provided by engineer. + + Manage your relays. + Gestisci i tuoi relay. + No comment provided by engineer. + Mark deleted for everyone Contrassegna eliminato per tutti @@ -5489,21 +5687,6 @@ Questo è il tuo link per il gruppo %@! Segnalazioni dei membri chat feature - - Member role will be changed to "%@". All chat members will be notified. - Il ruolo del membro verrà cambiato in "%@". Verranno notificati tutti i membri della chat. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Il ruolo del membro verrà cambiato in "%@". Tutti i membri del gruppo verranno avvisati. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Il ruolo del membro verrà cambiato in "%@". Il membro riceverà un invito nuovo. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Il membro verrà rimosso dalla chat, non è reversibile! @@ -5649,6 +5832,16 @@ Questo è il tuo link per il gruppo %@! Forma del messaggio No comment provided by engineer. + + Message signing is not required. + La firma dei messaggi non è richiesta. + No comment provided by engineer. + + + Message signing is required. + La firma dei messaggi è richiesta. + No comment provided by engineer. + Message source remains private. La fonte del messaggio resta privata. @@ -5819,6 +6012,11 @@ Questo è il tuo link per il gruppo %@! Altri miglioramenti sono in arrivo! No comment provided by engineer. + + More privacy + Più privacy + No comment provided by engineer. + More reliable network connection. Connessione di rete più affidabile. @@ -5859,6 +6057,11 @@ Questo è il tuo link per il gruppo %@! Nome swipe action + + Name not found + Nome non trovato + No comment provided by engineer. + Network & servers Rete e server @@ -6045,6 +6248,7 @@ La crittografia più sicura. No available relays + Nessun relay disponibile No comment provided by engineer. @@ -6174,6 +6378,7 @@ La crittografia più sicura. No relays + Nessun relay No comment provided by engineer. @@ -6191,6 +6396,11 @@ La crittografia più sicura. Nessun server per ricevere messaggi. servers error + + No servers to resolve names. + Nessun server per risolvere i nomi. + servers warning + No servers to send files. Nessun server per inviare file. @@ -6206,6 +6416,11 @@ La crittografia più sicura. Nessuna chat non letta No comment provided by engineer. + + No valid link + Nessun link valido + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita. @@ -6216,6 +6431,11 @@ La crittografia più sicura. Organizzazione non a scopo di lucro No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + Nessuno dei tuoi server è impostato per risolvere i nomi SimpleX. Configura i server o usa un link di connessione. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano. @@ -6287,7 +6507,7 @@ La crittografia più sicura. Off - Off + Disattivato blur media @@ -6299,7 +6519,7 @@ new chat action Old database - Database vecchio + Base di dati vecchia No comment provided by engineer. @@ -6441,6 +6661,11 @@ Richiede l'attivazione della VPN. Solo il tuo contatto può inviare messaggi vocali. No comment provided by engineer. + + Only your page above can show the preview. + Solo la tua pagina soprastante può mostrare l'anteprima. + No comment provided by engineer. + Open Apri @@ -6630,9 +6855,9 @@ alert button Proprietario No comment provided by engineer. - - Owners - Proprietari + + Owners & contributors + Proprietari e collaboratori No comment provided by engineer. @@ -6824,10 +7049,6 @@ Errore: %@ Prova a disattivare e riattivare le notifiche. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo. @@ -6888,11 +7109,6 @@ Errore: %@ Server precedentemente connessi No comment provided by engineer. - - Privacy & security - Privacy e sicurezza - No comment provided by engineer. - Privacy for your customers. Privacy per i tuoi clienti. @@ -6981,7 +7197,8 @@ Errore: %@ Profile update will be sent to your SimpleX contacts. L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7100,6 +7317,11 @@ Attivalo nelle impostazioni *Rete e server*. Canali pubblici - parla liberamente 🚀 No comment provided by engineer. + + Public names for your channel or business. + Nomi pubblici per il tuo canale o per il lavoro. + No comment provided by engineer. + Push notifications Notifiche push @@ -7138,7 +7360,7 @@ Attivalo nelle impostazioni *Rete e server*. Read more Leggi tutto - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7349,10 +7571,12 @@ swipe action Relay will be removed from channel - this cannot be undone! + Il relay verrà rimosso dal canale, non è reversibile! alert message Relays added: %@. + Relay aggiunti: %@. alert message @@ -7395,13 +7619,24 @@ swipe action Rimuovere il membro? alert title + + Remove name + Rimuovi nome + No comment provided by engineer. + Remove passphrase from keychain? Rimuovere la password dal portachiavi? No comment provided by engineer. + + Remove relay + Rimuovi relay + No comment provided by engineer. + Remove relay? + Rimuovere il relay? alert title @@ -7504,6 +7739,11 @@ swipe action Segnalazioni No comment provided by engineer. + + Require signing messages. + Richiedi la firma dei messaggi. + No comment provided by engineer. + Required Obbligatorio @@ -7549,6 +7789,11 @@ swipe action Ripristina al tema dell'utente No comment provided by engineer. + + Resolver error: %@ + Errore del risolutore: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Riavvia l'app per creare un nuovo profilo di chat @@ -7629,6 +7874,26 @@ swipe action Ruolo No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Il ruolo del membro verrà cambiato in "%@". Verranno avvisati tutti i membri della chat. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Il ruolo del membro verrà cambiato in "%@". Verranno avvisati tutti i membri del gruppo. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + Il ruolo verrà cambiato in "%@". Verranno avvisati tutti gli iscritti. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Il ruolo del membro verrà cambiato in "%@". Il membro riceverà un invito nuovo. + No comment provided by engineer. + Run chat Avvia chat @@ -7662,7 +7927,8 @@ swipe action Save Salva - alert button + alert action +alert button chat item action @@ -7680,6 +7946,11 @@ chat item action Salva (e avvisa gli iscritti) alert button + + Save SimpleX name? + Salvare il nome SimpleX? + alert title + Save admission settings? Salvare le impostazioni di ammissione? @@ -7695,6 +7966,11 @@ chat item action Salva e avvisa i membri del gruppo No comment provided by engineer. + + Save and notify members + Salva e avvisa i membri + No comment provided by engineer. + Save and notify subscribers Salva e avvisa gli iscritti @@ -7765,6 +8041,11 @@ chat item action Salvare i server? alert title + + Save webpage settings? + Salvare le impostazioni della pagina web? + alert title + Save welcome message? Salvare il messaggio di benvenuto? @@ -8065,11 +8346,6 @@ chat item action Il mittente ha annullato il trasferimento del file. alert message - - Sender may have deleted the connection request. - Il mittente potrebbe aver eliminato la richiesta di connessione. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. L'invio di un'anteprima del link può rivelare il tuo indirizzo IP al sito. Puoi modificarlo nelle impostazioni di Privacy più tardi. @@ -8165,6 +8441,11 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + Il server %@ non supporta la risoluzione dei nomi. Configura i server o usa un link di connessione. + No comment provided by engineer. + Server added to operator %@. Server aggiunto all'operatore %@. @@ -8476,6 +8757,11 @@ chat item action Mostra opzioni sviluppatore No comment provided by engineer. + + Show encryption + Mostra la crittografia + No comment provided by engineer. + Show last messages Mostra ultimi messaggi @@ -8506,6 +8792,37 @@ chat item action Mostra: No comment provided by engineer. + + Sign message + Firma il messaggio + No comment provided by engineer. + + + Sign messages + Firma i messaggi + chat feature + + + Signature missing + Firma mancante + alert title +copied message info + + + Signed + Firmato + copied message info + + + Signed & verified + Firmato e verificato + copied message info + + + Signing proves you authored this message and can't be denied later. + La firma dimostra che hai scritto questo messaggio e non può essere negato più tardi. + No comment provided by engineer. + SimpleX SimpleX @@ -8601,6 +8918,21 @@ chat item action Link di SimpleX non consentiti No comment provided by engineer. + + SimpleX name + Nome SimpleX + No comment provided by engineer. + + + SimpleX name error + Errore del nome SimpleX + No comment provided by engineer. + + + SimpleX name not verified + Nome SimpleX non verificato + alert title + SimpleX one-time invitation Invito SimpleX una tantum @@ -8611,6 +8943,11 @@ chat item action Protocolli di SimpleX esaminati da Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + Nomi pubblici SimpleX (BETA) + No comment provided by engineer. + SimpleX relay address Indirizzo del relay SimpleX @@ -8721,6 +9058,7 @@ report reason Status + Stato No comment provided by engineer. @@ -8880,9 +9218,9 @@ L'indirizzo del relay è stato usato per impostare questo relay per il canale.Iscrizioni ignorate No comment provided by engineer. - - Support SimpleX Chat - Supporta SimpleX Chat + + Support the project + Sostieni il progetto No comment provided by engineer. @@ -9078,6 +9416,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + Il nome SimpleX #%@ è registrato senza link del canale. Aggiungi il link del canale al nome tramite la pagina di registrazione. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + Il nome SimpleX %@ è registrato, ma non ha alcun link valido. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + Il nome SimpleX %@ è registrato, ma non aggiunto al profilo. Aggiungilo al profilo del tuo indirizzo o canale, se sei il proprietario. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + Il nome SimpleX @%@ è registrato senza indirizzo SimpleX. Aggiungi il tuo indirizzo SimpleX al nome tramite la pagina di registrazione. + alert message + The address will be short, and your profile will be shared via the address. L'indirizzo sarà breve e il tuo profilo verrà condiviso attraverso l'indirizzo. @@ -9108,6 +9466,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il tentativo di cambiare la password del database non è stato completato. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + La targhetta è firmata con una chiave che questa versione dell'app non riconosce. Aggiorna l'app per verificare questa targhetta. + badge alert + + + The channel required this message to be signed, but the signature is missing. + Il canale ha richiesto di firmare questo messaggio, ma la firma non è presente. + alert message + The code you scanned is not a SimpleX link QR code. Il codice che hai scansionato non è un codice QR di link SimpleX. @@ -9205,6 +9573,11 @@ i tuoi contatti e i tuoi gruppi. Il secondo segno di spunta che ci mancava! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Il mittente potrebbe aver eliminato la richiesta di connessione. + No comment provided by engineer. + The sender will NOT be notified Il mittente NON verrà avvisato @@ -9237,7 +9610,7 @@ i tuoi contatti e i tuoi gruppi. Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. - Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile. + Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, e-mail, messenger, social media. Sembrava l'unico modo possibile. No comment provided by engineer. @@ -9260,6 +9633,11 @@ i tuoi contatti e i tuoi gruppi. Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + Questo nome SimpleX non è registrato. Controlla il nome. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione. @@ -9280,6 +9658,11 @@ i tuoi contatti e i tuoi gruppi. Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + Non è stato possibile verificare questa targhetta e potrebbe non essere autentica. + badge alert + This chat is protected by end-to-end encryption. Questa chat è protetta da crittografia end-to-end. @@ -9312,6 +9695,7 @@ i tuoi contatti e i tuoi gruppi. This group requires a newer version of the app. Please update the app to join. + Questo gruppo richiede una versione dell'app più recente. Aggiorna l'app per entrare. alert message alert subtitle @@ -9322,6 +9706,7 @@ alert subtitle This is the last active relay. Removing it will prevent message delivery to subscribers. + Questo è l'ultimo relay attivo. La sua rimozione impedirà la consegna dei messaggi agli iscritti. alert message @@ -9436,6 +9821,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono. No comment provided by engineer. + + To resolve names + Per risolvere nomi + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**. @@ -9471,6 +9861,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + Per verificare le chiavi con questo iscritto, confrontate (o scansionate) il codice sui vostri dispositivi. + No comment provided by engineer. + Toggle incognito when connecting. Attiva/disattiva l'incognito quando ti colleghi. @@ -9561,6 +9956,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare l'iscritto per tutti? No comment provided by engineer. + + Unconfirmed name + Nome non confermato + No comment provided by engineer. + Undelivered messages Messaggi non consegnati @@ -9621,13 +10021,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio A meno che non utilizzi l'interfaccia di chiamata iOS, attiva la modalità Non disturbare per evitare interruzioni. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo. -Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile. - No comment provided by engineer. - Unlink Scollega @@ -9658,18 +10051,15 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Non letto swipe action - - Unsupported channel name - alert title - Unsupported connection link Link di connessione non supportato conn error description - - Unsupported contact name - alert title + + Unverified badge + Targhetta non verificata + badge alert title Up to 100 last messages are sent to new members. @@ -9724,7 +10114,8 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Upgrade address? Aggiornare l'indirizzo? - alert message + alert message +alert title Upgrade and open chat @@ -9893,7 +10284,7 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Use this address in your social media profile, website, or email signature. - Usa questo indirizzo nel tuo profilo di social media, sito web o firma email. + Usa questo indirizzo nel tuo profilo di social media, sito web o firma e-mail. No comment provided by engineer. @@ -9901,6 +10292,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa porta web No comment provided by engineer. + + Used chat relays do not support webpages. + I relay di chat usati non supportano le pagine web. + No comment provided by engineer. + User selection Selezione utente @@ -9921,6 +10317,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Verifica relay test step + + Verify SimpleX names + Verifica nomi SimpleX + No comment provided by engineer. + Verify code with desktop Verifica il codice con il desktop @@ -9946,6 +10347,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Verifica password del database No comment provided by engineer. + + Verify name + Verifica nome + No comment provided by engineer. + Verify passphrase Verifica password @@ -10106,6 +10512,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Server WebRTC ICE No comment provided by engineer. + + Webpage code + Codice pagina web + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + Le impostazioni della pagina web sono state cambiate. Se salvi, le impostazioni aggiornate verranno inviate agli iscritti. + alert message + Welcome %@! Benvenuto/a %@! @@ -10328,9 +10744,9 @@ Ripetere la richiesta di ingresso? Puoi attivarle più tardi nelle impostazioni No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app. + + You can enable them later via app Your privacy settings. + Puoi attivarle più tardi nelle impostazioni dell'app "La tua privacy". No comment provided by engineer. @@ -10393,6 +10809,11 @@ Ripetere la richiesta di ingresso? Puoi ancora vedere la conversazione con %@ nell'elenco delle chat. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + Puoi sostenere SimpleX dalla versione 7 dell'app. + badge alert + You can turn on SimpleX Lock via Settings. Puoi attivare SimpleX Lock tramite le impostazioni. @@ -10584,6 +11005,11 @@ Ripetere la richiesta di connessione? Il tuo indirizzo SimpleX No comment provided by engineer. + + Your SimpleX name + Il tuo nome SimpleX + No comment provided by engineer. + Your business contact Il tuo contatto lavorativo @@ -10599,11 +11025,6 @@ Ripetere la richiesta di connessione? Il tuo canale No comment provided by engineer. - - Your chat database - Il tuo database della chat - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Il tuo database della chat non è crittografato: imposta la password per crittografarlo. @@ -10634,6 +11055,13 @@ Ripetere la richiesta di connessione? Il tuo contatto No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo. +Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@). @@ -10682,6 +11110,8 @@ Ripetere la richiesta di connessione? Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + Il tuo nuovo canale %1$@ è connesso a %2$d di %3$d relay. +Se annulli, il canale verrà eliminato. Potrai crearlo di nuovo. alert message @@ -10763,7 +11193,7 @@ I relay hanno accesso ai messaggi del canale. [Send us email](mailto:chat@simplex.chat) - [Inviaci un'email](mailto:chat@simplex.chat) + [Inviaci un'e-mail](mailto:chat@simplex.chat) No comment provided by engineer. @@ -10806,6 +11236,11 @@ I relay hanno accesso ai messaggi del canale. ti ha accettato/a rcv group event chat item + + acknowledged roster + lista riconosciuta + No comment provided by engineer. + active attivo @@ -11069,9 +11504,14 @@ marked deleted chat item preview text contact should accept… - il contatto dovrebbe accettare… + il contatto deve accettare… No comment provided by engineer. + + contributor + collaboratore + member role + creator creatore @@ -11278,6 +11718,11 @@ pref value ore time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. Il portachiavi di iOS viene usato per archiviare in modo sicuro la password; consente di ricevere notifiche push. @@ -11485,7 +11930,7 @@ pref value off - off + disattivato enabled status group pref value member criteria value @@ -11503,7 +11948,7 @@ time to disappear on - on + attivato group pref value @@ -11720,6 +12165,11 @@ ultimo msg ricevuto: %2$@ barrato No comment provided by engineer. + + subscriber + iscritto + member role + this contact questo contatto @@ -11770,11 +12220,6 @@ ultimo msg ricevuto: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ via %@ @@ -11929,7 +12374,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -11964,9 +12409,24 @@ ultimo msg ricevuto: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11988,7 +12448,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -12020,7 +12480,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -12042,7 +12502,7 @@ ultimo msg ricevuto: %2$@
- +
@@ -12225,9 +12685,9 @@ ultimo msg ricevuto: %2$@ Password del database sbagliata No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock. + + You can allow sharing in Your privacy / SimpleX Lock settings. + Puoi consentire la condivisione nelle impostazioni "La tua privacy" / "SimpleX Lock". No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/it.xcloc/contents.json b/apps/ios/SimpleX Localizations/it.xcloc/contents.json index a42f254bd9..36fd18d76d 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/it.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "it", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 13396b13a4..c8b2285649 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ シークレット No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +89,10 @@ %@ ダウンロード済 No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ 接続中! @@ -110,6 +118,10 @@ %@ サーバー No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ アップロード済 @@ -185,8 +197,21 @@ %d 月 time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed + %d リレーが失敗 channel relay bar channel subscriber relay bar @@ -230,40 +255,49 @@ channel subscriber relay bar
%1$d/%2$d relays active + %2$d 個中 %1$d 個のリレーがアクティブ channel creation progress channel relay bar progress %1$d/%2$d relays active, %3$d errors + %2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個がエラー channel relay bar %1$d/%2$d relays active, %3$d failed + %2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が失敗 channel creation progress with errors channel relay bar %1$d/%2$d relays active, %3$d removed + %2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が削除済み channel relay bar %1$d/%2$d relays connected + %2$d 個中 %1$d 個のリレーが接続済み channel subscriber relay bar progress %1$d/%2$d relays connected, %3$d errors + %2$d 個中 %1$d 個のリレーが接続済み、%3$d 個がエラー channel subscriber relay bar %1$d/%2$d relays connected, %3$d failed + %2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が失敗 channel subscriber relay bar %1$d/%2$d relays connected, %3$d removed + %2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が削除済み channel subscriber relay bar %lld + %lld No comment provided by engineer. @@ -384,10 +418,6 @@ channel relay bar (新規) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (このデバイス v%@) @@ -719,6 +749,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends 友達を追加 @@ -738,6 +776,10 @@ swipe action プロフィールを追加 No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -761,6 +803,10 @@ swipe action チームメンバーを追加 No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device 別の端末に追加 @@ -835,6 +881,10 @@ swipe action ネットワーク詳細設定 No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings 詳細設定 @@ -883,7 +933,7 @@ swipe action All messages will be deleted - this cannot be undone! - すべてのメッセージが削除されます。この操作は元に戻せません! + 全てのメッセージが削除されます - これは元に戻せません! No comment provided by engineer. @@ -936,6 +986,10 @@ swipe action 許可 No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. 連絡先が通話を許可している場合のみ通話を許可する。 @@ -1105,6 +1159,10 @@ swipe action 通話に応答 No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ アプリのビルド: %@ @@ -1302,6 +1360,10 @@ swipe action メッセージのハッシュ値問題 No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1319,6 +1381,10 @@ in your network Better calls No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups No comment provided by engineer. @@ -1480,11 +1546,6 @@ in your network 通話は既に終了してます! No comment provided by engineer. - - Calls - 通話 - No comment provided by engineer. - Calls prohibited! No comment provided by engineer. @@ -1586,11 +1647,6 @@ new chat action ロックモードを変更 authentication reason - - Change member role? - メンバーの役割を変更しますか? - No comment provided by engineer. - Change passcode パスコードを変更 @@ -1611,6 +1667,10 @@ new chat action 役割変更 No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode 自己破壊モードの変更 @@ -1626,6 +1686,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1667,6 +1731,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1705,6 +1773,10 @@ alert subtitle チャットのコンソール No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database チャットのデータベース @@ -2040,6 +2112,10 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop デスクトップに接続 @@ -2127,14 +2203,6 @@ This is your own one-time link! デスクトップに接続中 No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection 接続 @@ -2149,16 +2217,15 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error 接続エラー alert title - - Connection error (AUTH) - 接続エラー (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2168,6 +2235,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + 接続エラー + conn error description + Connection not ready. No comment provided by engineer. @@ -2207,6 +2279,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2290,6 +2366,10 @@ This is your own one-time link! コピー No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error No comment provided by engineer. @@ -2320,6 +2400,10 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file ファイルを作成 @@ -2357,15 +2441,15 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue キューの作成 server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -2861,16 +2945,16 @@ alert button デスクトップ機器 No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -2881,9 +2965,9 @@ alert button Details No comment provided by engineer. - - Develop - 開発 + + Developer + 開発ツール No comment provided by engineer. @@ -2891,11 +2975,6 @@ alert button 開発者向けの設定 No comment provided by engineer. - - Developer tools - 開発ツール - No comment provided by engineer. - Device 端末 @@ -3031,6 +3110,10 @@ alert button 後で行う No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. No comment provided by engineer. @@ -3061,6 +3144,10 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + alert action + Don't show again 次から表示しない @@ -3082,10 +3169,12 @@ chat item action Download errors + ダウンロードエラー No comment provided by engineer. Download failed + ダウンロード失敗 No comment provided by engineer. @@ -3099,14 +3188,17 @@ chat item action Downloaded + ダウンロード済 No comment provided by engineer. Downloaded files + ダウンロード済ファイル No comment provided by engineer. Downloading archive + アーカイブをダウンロード中 No comment provided by engineer. @@ -3131,6 +3223,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit 編集する @@ -3140,6 +3236,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile グループのプロフィールを編集 @@ -3147,6 +3247,7 @@ chat item action Empty message! + メッセージが空です! No comment provided by engineer. @@ -3325,6 +3426,10 @@ chat item action 正しいパスフレーズを入力してください。 No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… No comment provided by engineer. @@ -3360,6 +3465,10 @@ chat item action Enter this device name… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… ウェルカムメッセージを入力してください… @@ -3377,7 +3486,7 @@ chat item action Error エラー - conn error description + No comment provided by engineer. Error aborting address change @@ -3647,6 +3756,10 @@ chat item action グループのプロフィール保存にエラー発生 No comment provided by engineer. + + Error saving name + alert title + Error saving passcode パスコードの保存にエラー発生 @@ -3697,6 +3810,10 @@ chat item action Error setting delivery receipts! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3770,6 +3887,7 @@ chat item action Error: %@ エラー : %@ alert message +conn error description file error text snd error text @@ -3895,6 +4013,14 @@ server test error File server error: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status No comment provided by engineer. @@ -4160,6 +4286,10 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4262,6 +4392,10 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message グループのウェルカムメッセージ @@ -4286,6 +4420,10 @@ Error: %2$@ ヘルプ No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. No comment provided by engineer. @@ -4360,6 +4498,10 @@ Error: %2$@ 使い方 No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it 使い方 @@ -4731,6 +4873,10 @@ More improvements are coming soon! このリンクからすでに接続されているようです。そうでない場合は、エラー(%@)が発生しました。 No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface イタリア語UI @@ -4755,6 +4901,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group グループに参加 @@ -4827,7 +4977,7 @@ This is your link for group %@! Learn more さらに詳しく - No comment provided by engineer. + badge alert button Leave @@ -4864,6 +5014,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4964,6 +5122,10 @@ This is your link for group %@! WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。 No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone 全員に対して削除済みマークを付ける @@ -5026,20 +5188,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。 - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5168,6 +5316,14 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -5318,6 +5474,10 @@ This is your link for group %@! まだまだ改善してまいります! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. No comment provided by engineer. @@ -5355,6 +5515,10 @@ This is your link for group %@! 名前 swipe action + + Name not found + No comment provided by engineer. + Network & servers ネットワークとサーバ @@ -5645,6 +5809,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5657,6 +5825,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5665,6 +5837,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -5871,6 +6047,10 @@ VPN を有効にする必要があります。 音声メッセージを送れるのはあなたの連絡相手だけです。 No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open 開く @@ -6024,8 +6204,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6201,10 +6381,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6257,11 +6433,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - プライバシーとセキュリティ - No comment provided by engineer. - Privacy for your customers. No comment provided by engineer. @@ -6337,7 +6508,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6444,6 +6616,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications プッシュ通知 @@ -6479,7 +6655,7 @@ Enable in *Network & servers* settings. Read more 続きを読む - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -6709,11 +6885,19 @@ swipe action メンバーを除名しますか? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? キーチェーンからパスフレーズを削除しますか? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -6802,6 +6986,10 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required 必須 @@ -6842,6 +7030,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile 新しいチャットプロファイルを作成するためにアプリを再起動する @@ -6917,6 +7109,24 @@ swipe action 役割 No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + メンバーの役割が "%@" に変更されます。 メンバーは新たな招待を受け取ります。 + No comment provided by engineer. + Run chat チャット起動 @@ -6945,7 +7155,8 @@ swipe action Save 保存 - alert button + alert action +alert button chat item action @@ -6961,6 +7172,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? alert title @@ -6975,6 +7190,10 @@ chat item action 保存して、グループのメンバーにに知らせる No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7039,6 +7258,10 @@ chat item action サーバを保存しますか? alert title + + Save webpage settings? + alert title + Save welcome message? ウェルカムメッセージを保存しますか? @@ -7304,11 +7527,6 @@ chat item action 送信者がファイル転送をキャンセルしました。 alert message - - Sender may have deleted the connection request. - 送信元が繋がりリクエストを削除したかもしれません。 - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7391,6 +7609,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7425,7 +7647,7 @@ chat item action Server requires authorization to create queues, check password. - キューを作成するにはサーバーの認証が必要です。パスワードを確認してください + キューを作成するにはサーバーの認証が必要です。パスワードを確認してください。 server test error @@ -7659,6 +7881,10 @@ chat item action 開発者向けオプションを表示 No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages 最新のメッセージを表示 @@ -7686,6 +7912,31 @@ chat item action 表示する: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX No comment provided by engineer. @@ -7773,6 +8024,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX使い捨て招待リンク @@ -7782,6 +8045,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8016,9 +8283,8 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - Simplex Chatを支援 + + Support the project No comment provided by engineer. @@ -8195,6 +8461,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8221,6 +8503,14 @@ It can happen because of some bug or when the connection is compromised.データベースのパスフレーズ変更が完了してません。 No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. No comment provided by engineer. @@ -8307,6 +8597,11 @@ your contacts and groups. 長らくお待たせしました! ✅ No comment provided by engineer. + + The sender deleted the connection request. + 送信元が繋がりリクエストを削除したかもしれません。 + No comment provided by engineer. + The sender will NOT be notified 送信者には通知されません @@ -8355,6 +8650,10 @@ your contacts and groups. これらは連絡先の設定が優先します。 No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。 @@ -8374,6 +8673,10 @@ your contacts and groups. あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。 No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. E2EE info chat item @@ -8510,6 +8813,10 @@ You will be prompted to complete authentication before this feature is enabled.< 音声メッセージを録音する場合は、マイクの使用を許可してください。 No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. 非表示のプロフィールを表示するには、**チャット プロフィール** ページの検索フィールドに完全なパスワードを入力します。 @@ -8541,6 +8848,10 @@ You will be prompted to complete authentication before this feature is enabled.< エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. @@ -8617,6 +8928,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8675,13 +8990,6 @@ You will be prompted to complete authentication before this feature is enabled.< iOS 通話インターフェイスを使用しない場合は、中断を避けるために「おやすみモード」を有効にしてください。 No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - 連絡先が接続を削除したか、このリンクがすでに使用されている場合を除き、バグである可能性がありますので、報告してください。 -接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。 - No comment provided by engineer. - Unlink No comment provided by engineer. @@ -8710,17 +9018,13 @@ To connect, please ask your contact to create another connection link and check 未読 swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -8768,7 +9072,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -8919,6 +9224,10 @@ To connect, please ask your contact to create another connection link and check Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection No comment provided by engineer. @@ -8936,6 +9245,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8957,6 +9270,10 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -9102,6 +9419,14 @@ To connect, please ask your contact to create another connection link and check WebRTC ICEサーバ No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! ようこそ %@! @@ -9295,9 +9620,8 @@ Repeat join request? あとで設定から有効にできます No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。 + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9355,6 +9679,10 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. 設定からSimpleXのロックをオンにすることができます。 @@ -9529,6 +9857,10 @@ Repeat connection request? あなたのSimpleXアドレス No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9542,11 +9874,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - あなたのチャットデータベース - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。 @@ -9573,6 +9900,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + 連絡先が接続を削除したか、このリンクがすでに使用されている場合を除き、バグである可能性がありますので、報告してください。 +接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。 + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). 連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。 @@ -9729,6 +10063,10 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -9974,6 +10312,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator 作成者 @@ -10054,6 +10396,7 @@ pref value duplicates + 重複 No comment provided by engineer. @@ -10170,6 +10513,10 @@ pref value 時間 time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS キーチェーンはパスフレーズを安全に保存するために使用され、プッシュ通知を受信できるようになります。 @@ -10572,6 +10919,10 @@ last received msg: %2$@ 取り消し線 No comment provided by engineer. + + subscriber + member role + this contact この連絡先 @@ -10615,11 +10966,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -10765,7 +11111,7 @@ last received msg: %2$@
- +
@@ -10799,9 +11145,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -10823,7 +11184,7 @@ last received msg: %2$@
- +
@@ -10850,7 +11211,7 @@ last received msg: %2$@
- +
@@ -10869,7 +11230,7 @@ last received msg: %2$@
- +
@@ -11016,8 +11377,8 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json index ce6052fc44..f52b6f4654 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ja", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index ca51a875c7..df94f8a814 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -1141,8 +1141,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -1927,12 +1927,12 @@ We will be adding server redundancy to prevent lost messages. Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2587,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages. 상대방이 파일 전송을 취소했습니다. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index 4b51d66a34..ff20ee100d 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -1005,8 +1005,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -1739,12 +1739,12 @@ We will be adding server redundancy to prevent lost messages. Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2359,8 +2359,8 @@ We will be adding server redundancy to prevent lost messages. Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 9f1818fba9..11f81cf90b 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ #geheim# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +89,10 @@ %@ gedownload No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ is verbonden! @@ -110,6 +118,10 @@ %@ servers No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ geüpload @@ -185,6 +197,18 @@ %d maanden time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -385,10 +409,6 @@ channel relay bar (nieuw) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (dit apparaat v%@) @@ -722,6 +742,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends Vrienden toevoegen @@ -741,6 +769,10 @@ swipe action Profiel toevoegen No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -764,6 +796,10 @@ swipe action Teamleden toevoegen No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Toevoegen aan een ander apparaat @@ -844,6 +880,10 @@ swipe action Geavanceerde netwerk instellingen No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings Geavanceerde instellingen @@ -951,6 +991,10 @@ swipe action Toestaan No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Sta oproepen alleen toe als uw contact dit toestaat. @@ -1121,6 +1165,10 @@ swipe action Beantwoord oproep No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ App build: %@ @@ -1329,6 +1377,10 @@ swipe action Onjuiste bericht hash No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1347,6 +1399,10 @@ in your network Betere gesprekken No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Betere groepen @@ -1529,11 +1585,6 @@ in your network Oproep al beëindigd! No comment provided by engineer. - - Calls - Oproepen - No comment provided by engineer. - Calls prohibited! Bellen niet toegestaan! @@ -1643,11 +1694,6 @@ new chat action Wijzig de vergrendelings modus authentication reason - - Change member role? - Rol van lid wijzigen? - No comment provided by engineer. - Change passcode Toegangscode wijzigen @@ -1668,6 +1714,10 @@ new chat action Rol wijzigen No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Zelfvernietigings modus wijzigen @@ -1683,6 +1733,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1724,6 +1778,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1765,6 +1823,10 @@ alert subtitle Chat console No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database Chat database @@ -2130,6 +2192,10 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Verbinden met desktop @@ -2223,14 +2289,6 @@ Dit is uw eigen eenmalige link! Verbinding maken met desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Verbinding @@ -2246,16 +2304,15 @@ Dit is uw eigen eenmalige link! Verbinding geblokkeerd No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Verbindingsfout alert title - - Connection error (AUTH) - Verbindingsfout (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2267,6 +2324,11 @@ Dit is uw eigen eenmalige link! %@ No comment provided by engineer. + + Connection link removed + Verbindingsfout + conn error description + Connection not ready. Verbinding nog niet klaar. @@ -2312,6 +2374,10 @@ Dit is uw eigen eenmalige link! Verbindingen No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2400,6 +2466,10 @@ Dit is uw eigen eenmalige link! Kopiëren No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error Kopieerfout @@ -2435,6 +2505,10 @@ Dit is uw eigen eenmalige link! Maak een groep met een willekeurig profiel. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Bestand maken @@ -2474,15 +2548,15 @@ Dit is uw eigen eenmalige link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Maak een wachtrij server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -3005,9 +3079,9 @@ alert button Desktop apparaten No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Het bestemmingsserveradres van %1$@ is niet compatibel met de doorstuurserverinstellingen %2$@. No comment provided by engineer. @@ -3015,9 +3089,9 @@ alert button Bestemmingsserverfout: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + De versie van de bestemmingsserver %1$@ is niet compatibel met de doorstuurserver %2$@. No comment provided by engineer. @@ -3030,9 +3104,9 @@ alert button Details No comment provided by engineer. - - Develop - Ontwikkelen + + Developer + Ontwikkelaar No comment provided by engineer. @@ -3040,11 +3114,6 @@ alert button Ontwikkelaars opties No comment provided by engineer. - - Developer tools - Ontwikkel gereedschap - No comment provided by engineer. - Device Apparaat @@ -3188,6 +3257,10 @@ alert button Doe het later No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Stuur geen geschiedenis naar nieuwe leden. @@ -3222,6 +3295,10 @@ alert button Mis geen belangrijke berichten. No comment provided by engineer. + + Don't save + alert action + Don't show again Niet meer weergeven @@ -3302,6 +3379,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Bewerk @@ -3311,6 +3392,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Groep profiel bewerken @@ -3505,6 +3590,10 @@ chat item action Voer het juiste wachtwoord in. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Groep naam invoeren… @@ -3543,6 +3632,10 @@ chat item action Voer deze apparaatnaam in… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Welkom bericht invoeren… @@ -3561,7 +3654,7 @@ chat item action Error Fout - conn error description + No comment provided by engineer. Error aborting address change @@ -3854,6 +3947,10 @@ chat item action Fout bij opslaan van groep profiel No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Fout bij opslaan van toegangscode @@ -3908,6 +4005,10 @@ chat item action Fout bij het instellen van ontvangst bevestiging! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3986,6 +4087,7 @@ chat item action Error: %@ Fout: %@ alert message +conn error description file error text snd error text @@ -4128,6 +4230,14 @@ server test error Bestandsserverfout: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status Bestandsstatus @@ -4423,6 +4533,10 @@ Fout: %2$@ GIF's en stickers No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4530,6 +4644,10 @@ Fout: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Groep welkom bericht @@ -4555,6 +4673,10 @@ Fout: %2$@ Help No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. Help beheerders bij het modereren van hun groepen. @@ -4634,6 +4756,10 @@ Fout: %2$@ Hoe No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Hoe te gebruiken @@ -5033,6 +5159,10 @@ Binnenkort meer verbeteringen! Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Italiaanse interface @@ -5057,6 +5187,10 @@ Binnenkort meer verbeteringen! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Word lid van groep @@ -5136,7 +5270,7 @@ Dit is jouw link voor groep %@! Learn more Kom meer te weten - No comment provided by engineer. + badge alert button Leave @@ -5175,6 +5309,14 @@ Dit is jouw link voor groep %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5281,6 +5423,10 @@ Dit is jouw link voor groep %@! Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Markeer verwijderd voor iedereen @@ -5348,21 +5494,6 @@ Dit is jouw link voor groep %@! Ledenrapporten chat feature - - Member role will be changed to "%@". All chat members will be notified. - De rol van het lid wordt gewijzigd naar "%@". Alle chatleden worden op de hoogte gebracht. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - De rol van lid wordt gewijzigd in "%@". Het lid ontvangt een nieuwe uitnodiging. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt! @@ -5505,6 +5636,14 @@ Dit is jouw link voor groep %@! Berichtvorm No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. Berichtbron blijft privé. @@ -5671,6 +5810,10 @@ Dit is jouw link voor groep %@! Meer verbeteringen volgen snel! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. Betrouwbaardere netwerkverbinding. @@ -5711,6 +5854,10 @@ Dit is jouw link voor groep %@! Naam swipe action + + Name not found + No comment provided by engineer. + Network & servers Netwerk & servers @@ -6030,6 +6177,10 @@ The most secure encryption. Geen servers om berichten te ontvangen. servers error + + No servers to resolve names. + servers warning + No servers to send files. Geen servers om bestanden te verzenden. @@ -6045,6 +6196,10 @@ The most secure encryption. Geen ongelezen chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6053,6 +6208,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6271,6 +6430,10 @@ Vereist het inschakelen van VPN. Alleen uw contact kan spraak berichten verzenden. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open Open @@ -6442,8 +6605,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6633,10 +6796,6 @@ Fout: %@ Probeer meldingen uit en weer in te schakelen. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Wacht totdat de moderators van de groep uw verzoek tot lidmaatschap van de groep hebben beoordeeld. @@ -6695,11 +6854,6 @@ Fout: %@ Eerder verbonden servers No comment provided by engineer. - - Privacy & security - Privacy en beveiliging - No comment provided by engineer. - Privacy for your customers. Privacy voor uw klanten. @@ -6784,7 +6938,8 @@ Fout: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6899,6 +7054,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Push meldingen @@ -6937,7 +7096,7 @@ Schakel dit in in *Netwerk en servers*-instellingen. Read more Lees meer - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7185,11 +7344,19 @@ swipe action Lid verwijderen? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Wachtwoord van de keychain verwijderen? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7292,6 +7459,10 @@ swipe action Rapporten No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Vereist @@ -7337,6 +7508,10 @@ swipe action Terugzetten naar gebruikersthema No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Start de app opnieuw om een nieuw chatprofiel aan te maken @@ -7416,6 +7591,25 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + De rol van het lid wordt gewijzigd naar "%@". Alle chatleden worden op de hoogte gebracht. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + De rol van lid wordt gewijzigd in "%@". Het lid ontvangt een nieuwe uitnodiging. + No comment provided by engineer. + Run chat Chat uitvoeren @@ -7448,7 +7642,8 @@ swipe action Save Opslaan - alert button + alert action +alert button chat item action @@ -7464,6 +7659,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? Toegangsinstellingen opslaan? @@ -7479,6 +7678,10 @@ chat item action Opslaan en groep leden melden No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7545,6 +7748,10 @@ chat item action Servers opslaan? alert title + + Save webpage settings? + alert title + Save welcome message? Welkom bericht opslaan? @@ -7833,11 +8040,6 @@ chat item action Afzender heeft bestandsoverdracht geannuleerd. alert message - - Sender may have deleted the connection request. - De afzender heeft mogelijk het verbindingsverzoek verwijderd. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7932,6 +8134,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Server toegevoegd aan operator %@. @@ -8229,6 +8435,10 @@ chat item action Ontwikkelaars opties tonen No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Laat laatste berichten zien @@ -8259,6 +8469,31 @@ chat item action Toon: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8354,6 +8589,18 @@ chat item action SimpleX-links zijn niet toegestaan No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Eenmalige SimpleX uitnodiging @@ -8364,6 +8611,10 @@ chat item action SimpleX-protocollen beoordeeld door Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8617,9 +8868,8 @@ Relay address was used to set up this relay for the channel. Subscriptions genegeerd No comment provided by engineer. - - Support SimpleX Chat - Ondersteuning van SimpleX Chat + + Support the project No comment provided by engineer. @@ -8806,6 +9056,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8834,6 +9100,14 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De poging om het wachtwoord van de database te wijzigen is niet voltooid. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. De code die u heeft gescand is geen SimpleX link QR-code. @@ -8926,6 +9200,11 @@ your contacts and groups. De tweede vink die we gemist hebben! ✅ No comment provided by engineer. + + The sender deleted the connection request. + De afzender heeft mogelijk het verbindingsverzoek verwijderd. + No comment provided by engineer. + The sender will NOT be notified De afzender wordt NIET op de hoogte gebracht @@ -8979,6 +9258,10 @@ your contacts and groups. Ze kunnen worden overschreven in contactinstellingen No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto's met een lage resolutie blijven behouden. @@ -8999,6 +9282,10 @@ your contacts and groups. Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan definitief verloren. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Deze chat is beveiligd met end-to-end codering. @@ -9150,6 +9437,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Geef toestemming om de microfoon te gebruiken om een spraakbericht op te nemen. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoek veld in op de pagina **Uw chatprofielen**. @@ -9183,6 +9474,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Schakel incognito in tijdens het verbinden. @@ -9270,6 +9565,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Niet afgeleverde berichten @@ -9330,13 +9629,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. -Om verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft. - No comment provided by engineer. - Unlink Ontkoppelen @@ -9367,18 +9659,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Ongelezen swipe action - - Unsupported channel name - alert title - Unsupported connection link Niet-ondersteunde verbindingslink conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9429,7 +9717,8 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -9599,6 +9888,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik een webpoort No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection Gebruikersselectie @@ -9618,6 +9911,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Code verifiëren met desktop @@ -9643,6 +9940,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Controleer het wachtwoord van de database No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Controleer het wachtwoord @@ -9798,6 +10099,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak WebRTC ICE servers No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Welkom %@! @@ -10016,9 +10325,8 @@ Deelnameverzoek herhalen? U kunt later inschakelen via Instellingen No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -10080,6 +10388,10 @@ Deelnameverzoek herhalen? Je kunt het gesprek met %@ nog steeds bekijken in de lijst met chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Je kunt SimpleX Vergrendeling aanzetten via Instellingen. @@ -10264,6 +10576,10 @@ Verbindingsverzoek herhalen? Uw SimpleX adres No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -10277,11 +10593,6 @@ Verbindingsverzoek herhalen? Your channel No comment provided by engineer. - - Your chat database - Uw chat database - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen. @@ -10310,6 +10621,13 @@ Verbindingsverzoek herhalen? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. +Om verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@). @@ -10473,6 +10791,10 @@ Relays can access channel messages. heb je geaccepteerd rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10734,6 +11056,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator creator @@ -10936,6 +11262,10 @@ pref value uren time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS-keychain wordt gebruikt om het wachtwoord veilig op te slaan, het maakt het ontvangen van push meldingen mogelijk. @@ -11369,6 +11699,10 @@ laatst ontvangen bericht: %2$@ staking No comment provided by engineer. + + subscriber + member role + this contact dit contact @@ -11418,11 +11752,6 @@ laatst ontvangen bericht: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -11574,7 +11903,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -11609,9 +11938,24 @@ laatst ontvangen bericht: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11633,7 +11977,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -11665,7 +12009,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -11687,7 +12031,7 @@ laatst ontvangen bericht: %2$@
- +
@@ -11870,9 +12214,8 @@ laatst ontvangen bericht: %2$@ Verkeerde database wachtwoord No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json index 4b8d468de2..36c6d27526 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "nl", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 2644708927..19401459d5 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,10 @@ #sekret# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -85,6 +89,10 @@ %@ pobrane No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ jest połączony! @@ -110,6 +118,10 @@ %@ serwery/ów No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded %@ wgrane @@ -185,6 +197,18 @@ %d miesięcy time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -385,10 +409,6 @@ channel relay bar (nowy) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (to urządzenie v%@) @@ -723,6 +743,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends Dodaj znajomych @@ -743,6 +771,10 @@ swipe action Dodaj profil No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -766,6 +798,10 @@ swipe action Dodaj członków zespołu No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Dodaj do innego urządzenia @@ -846,6 +882,10 @@ swipe action Zaawansowane ustawienia sieci No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings Zaawansowane ustawienia @@ -954,6 +994,10 @@ swipe action Pozwól No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala. @@ -1126,6 +1170,10 @@ swipe action Odbierz połączenie No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ Kompilacja aplikacji: %@ @@ -1335,6 +1383,10 @@ swipe action Zły hash wiadomości No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1355,6 +1407,10 @@ in your network Lepsze połączenia No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Lepsze grupy @@ -1542,11 +1598,6 @@ in your network Połączenie już zakończone! No comment provided by engineer. - - Calls - Połączenia - No comment provided by engineer. - Calls prohibited! Połączenia zakazane! @@ -1657,11 +1708,6 @@ new chat action Zmień tryb blokady authentication reason - - Change member role? - Zmienić rolę członka? - No comment provided by engineer. - Change passcode Zmień pin @@ -1682,6 +1728,10 @@ new chat action Zmień rolę No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Zmień tryb samozniszczenia @@ -1697,6 +1747,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1738,6 +1792,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1779,6 +1837,10 @@ alert subtitle Konsola czatu No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database Baza danych czatu @@ -2146,6 +2208,10 @@ server test step Połącz się szybciej! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Połącz do komputera @@ -2239,14 +2305,6 @@ To jest twój jednorazowy link! Łączenie z komputerem No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Połączenie @@ -2262,16 +2320,15 @@ To jest twój jednorazowy link! Połączenie zablokowane No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Błąd połączenia alert title - - Connection error (AUTH) - Błąd połączenia (UWIERZYTELNIANIE) - conn error description - Connection failed Połączenie nie powiodło się @@ -2284,6 +2341,11 @@ To jest twój jednorazowy link! %@ No comment provided by engineer. + + Connection link removed + Błąd połączenia + conn error description + Connection not ready. Połączenie nie jest gotowe. @@ -2329,6 +2391,10 @@ To jest twój jednorazowy link! Połączenia No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2418,6 +2484,10 @@ To jest twój jednorazowy link! Kopiuj No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error Kopiuj błąd @@ -2453,6 +2523,10 @@ To jest twój jednorazowy link! Utwórz grupę używając losowego profilu. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Utwórz plik @@ -2492,15 +2566,15 @@ To jest twój jednorazowy link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Utwórz kolejkę server test step + + Create web preview. + No comment provided by engineer. + Create your address Utwórz swój adres @@ -3028,9 +3102,9 @@ alert button Urządzenia komputerowe No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Adres serwera docelowego %1$@ jest niekompatybilny z ustawieniami serwera przekazującego %2$@. No comment provided by engineer. @@ -3038,9 +3112,9 @@ alert button Błąd docelowego serwera: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Wersja serwera docelowego %1$@ jest niekompatybilna z serwerem przekierowującym %2$@. No comment provided by engineer. @@ -3053,9 +3127,9 @@ alert button Szczegóły No comment provided by engineer. - - Develop - Deweloperskie + + Developer + Narzędzia deweloperskie No comment provided by engineer. @@ -3063,11 +3137,6 @@ alert button Opcje deweloperskie No comment provided by engineer. - - Developer tools - Narzędzia deweloperskie - No comment provided by engineer. - Device Urządzenie @@ -3211,6 +3280,10 @@ alert button Zrób to później No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Nie wysyłaj historii do nowych członków. @@ -3245,6 +3318,10 @@ alert button Nie przegap ważnych wiadomości. No comment provided by engineer. + + Don't save + alert action + Don't show again Nie pokazuj ponownie @@ -3325,6 +3402,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Edytuj @@ -3334,6 +3415,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Edytuj profil grupy @@ -3530,6 +3615,10 @@ chat item action Wprowadź poprawne hasło. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Wpisz nazwę grupy… @@ -3568,6 +3657,10 @@ chat item action Podaj nazwę urządzenia… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Wpisz wiadomość powitalną… @@ -3586,7 +3679,7 @@ chat item action Error Błąd - conn error description + No comment provided by engineer. Error aborting address change @@ -3883,6 +3976,10 @@ chat item action Błąd zapisu profilu grupy No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Błąd zapisu pinu @@ -3938,6 +4035,10 @@ chat item action Błąd ustawiania potwierdzeń dostawy! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -4016,6 +4117,7 @@ chat item action Error: %@ Błąd: %@ alert message +conn error description file error text snd error text @@ -4159,6 +4261,14 @@ server test error Błąd serwera plików: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status Status pliku @@ -4459,6 +4569,10 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4567,6 +4681,10 @@ Błąd: %2$@ Profil grupy został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do członków grupy. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Wiadomość powitalna grupy @@ -4592,6 +4710,10 @@ Błąd: %2$@ Pomoc No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. Pomóż administratorom moderować ich grupy. @@ -4671,6 +4793,10 @@ Błąd: %2$@ Jak No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Jak korzystać @@ -5073,6 +5199,10 @@ Wkrótce pojawią się kolejne ulepszenia! Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Włoski interfejs @@ -5097,6 +5227,10 @@ Wkrótce pojawią się kolejne ulepszenia! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Dołącz do grupy @@ -5177,7 +5311,7 @@ To jest twój link do grupy %@! Learn more Dowiedz się więcej - No comment provided by engineer. + badge alert button Leave @@ -5217,6 +5351,14 @@ To jest twój link do grupy %@! Mniejszy ruch w sieciach komórkowych. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5325,6 +5467,10 @@ To jest twój link do grupy %@! Upewnij się, że adresy serwerów WebRTC ICE są w poprawnym formacie, rozdzielone liniami i nie są zduplikowane. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Oznacz jako usunięty dla wszystkich @@ -5395,21 +5541,6 @@ To jest twój link do grupy %@! Raporty członków chat feature - - Member role will be changed to "%@". All chat members will be notified. - Rola członka zostanie zmieniona na "%@". Wszyscy członkowie czatu zostaną o tym poinformowani. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Członek zostanie usunięty z czatu – nie można tego cofnąć! @@ -5553,6 +5684,14 @@ To jest twój link do grupy %@! Kształt wiadomości No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. Źródło wiadomości pozostaje prywatne. @@ -5720,6 +5859,10 @@ To jest twój link do grupy %@! Więcej ulepszeń już wkrótce! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. Bardziej niezawodne połączenia sieciowe. @@ -5760,6 +5903,10 @@ To jest twój link do grupy %@! Nazwa swipe action + + Name not found + No comment provided by engineer. + Network & servers Sieć i serwery @@ -6081,6 +6228,10 @@ The most secure encryption. Brak serwerów aby otrzymać wiadomości. servers error + + No servers to resolve names. + servers warning + No servers to send files. Brak serwerów do wysyłania plików. @@ -6096,6 +6247,10 @@ The most secure encryption. Brak nieprzeczytanych czatów No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Nikt nie śledził twoich rozmów. Nikt nie rysował mapy miejsc, w których byłeś. Prywatność nigdy nie była funkcją - była sposobem na życie. @@ -6105,6 +6260,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Nie chodzi o lepszy zamek w drzwiach kogoś innego. Nie chodzi o milszego właściciela, który szanuje twoją prywatność, ale nadal prowadzi rejestr wszystkich odwiedzających. Nie jesteś gościem. Jesteś w domu. Żaden król nie może do niego wejść - jesteś suwerenem. @@ -6326,6 +6485,10 @@ Wymaga włączenia VPN. Tylko Twój kontakt może wysyłać wiadomości głosowe. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open Otwórz @@ -6505,8 +6668,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6696,10 +6859,6 @@ Błąd: %@ Spróbuj wyłączyć, a następnie ponownie włączyć powiadomienia. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Poczekaj, aż moderatorzy grupy rozpatrzą Twoją prośbę o dołączenie do grupy. @@ -6758,11 +6917,6 @@ Błąd: %@ Wcześniej połączone serwery No comment provided by engineer. - - Privacy & security - Prywatność i bezpieczeństwo - No comment provided by engineer. - Privacy for your customers. Prywatność dla Twoich klientów. @@ -6848,7 +7002,8 @@ Błąd: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6964,6 +7119,10 @@ Włącz w ustawianiach *Sieć i serwery* . Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Powiadomienia push @@ -7002,7 +7161,7 @@ Włącz w ustawianiach *Sieć i serwery* . Read more Przeczytaj więcej - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7252,11 +7411,19 @@ swipe action Usunąć członka? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Usunąć hasło z pęku kluczy? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7360,6 +7527,10 @@ swipe action Zgłoszenia No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Wymagane @@ -7405,6 +7576,10 @@ swipe action Zresetuj do motywu użytkownika No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Uruchom ponownie aplikację, aby utworzyć nowy profil czatu @@ -7485,6 +7660,25 @@ swipe action Rola No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Rola członka zostanie zmieniona na "%@". Wszyscy członkowie czatu zostaną o tym poinformowani. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Rola członka zostanie zmieniona na "%@". Członek otrzyma nowe zaproszenie. + No comment provided by engineer. + Run chat Uruchom czat @@ -7517,7 +7711,8 @@ swipe action Save Zapisz - alert button + alert action +alert button chat item action @@ -7534,6 +7729,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? Zapisać ustawienia wstępu? @@ -7549,6 +7748,10 @@ chat item action Zapisz i powiadom członków grupy No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7616,6 +7819,10 @@ chat item action Zapisać serwery? alert title + + Save webpage settings? + alert title + Save welcome message? Zapisać wiadomość powitalną? @@ -7913,11 +8120,6 @@ chat item action Nadawca anulował transfer pliku. alert message - - Sender may have deleted the connection request. - Nadawca mógł usunąć prośbę o połączenie. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -8012,6 +8214,10 @@ chat item action Serwer No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Serwer został dodany do operatora %@. @@ -8315,6 +8521,10 @@ chat item action Pokaż opcje dewelopera No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Pokaż ostatnie wiadomości @@ -8345,6 +8555,31 @@ chat item action Pokaż: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8440,6 +8675,18 @@ chat item action Linki SimpleX są niedozwolone No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Zaproszenie jednorazowe SimpleX @@ -8450,6 +8697,10 @@ chat item action Protokoły SimpleX sprawdzone przez Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8703,9 +8954,8 @@ Relay address was used to set up this relay for the channel. Subskrypcje zignorowane No comment provided by engineer. - - Support SimpleX Chat - Wspieraj SimpleX Chat + + Support the project No comment provided by engineer. @@ -8897,6 +9147,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Adres będzie krótki, a Twój profil zostanie udostępniony za pośrednictwem adresu. @@ -8926,6 +9192,14 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Próba zmiany hasła bazy danych nie została zakończona. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. Kod, który zeskanowałeś nie jest kodem QR linku SimpleX. @@ -9020,6 +9294,11 @@ your contacts and groups. Drugi tik, który przegapiliśmy! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Nadawca mógł usunąć prośbę o połączenie. + No comment provided by engineer. + The sender will NOT be notified Nadawca NIE zostanie powiadomiony @@ -9075,6 +9354,10 @@ your contacts and groups. Można je nadpisać w ustawieniach kontaktu. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną. @@ -9095,6 +9378,10 @@ your contacts and groups. Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Ten czat jest chroniony przez szyfrowanie end-to-end. @@ -9248,6 +9535,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby nagrać wiadomość głosową należy udzielić zgody na użycie Mikrofonu. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Aby ujawnić Twój ukryty profil, wprowadź pełne hasło w pole wyszukiwania na stronie **Twoich profili czatu**. @@ -9283,6 +9574,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Przełącz incognito przy połączeniu. @@ -9371,6 +9666,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Niedostarczone wiadomości @@ -9431,13 +9730,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go. -Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią. - No comment provided by engineer. - Unlink Odłącz @@ -9468,18 +9760,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Nieprzeczytane swipe action - - Unsupported channel name - alert title - Unsupported connection link Nieobsługiwane łącze połączenia conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9533,7 +9821,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Upgrade address? Uaktualnić adres? - alert message + alert message +alert title Upgrade and open chat @@ -9707,6 +9996,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Użyj portu internetowego No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection Wybór użytkownika @@ -9726,6 +10019,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Zweryfikuj kod z komputera @@ -9751,6 +10048,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zweryfikuj hasło bazy danych No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Zweryfikuj hasło @@ -9907,6 +10208,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Serwery WebRTC ICE No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Witaj %@! @@ -10128,9 +10437,8 @@ Powtórzyć prośbę dołączenia? Możesz włączyć później w Ustawieniach No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -10192,6 +10500,10 @@ Powtórzyć prośbę dołączenia? Nadal możesz przeglądać rozmowę z %@ na liście czatów. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Możesz włączyć blokadę SimpleX poprzez Ustawienia. @@ -10378,6 +10690,10 @@ Powtórzyć prośbę połączenia? Twój adres SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Twój kontakt biznesowy @@ -10392,11 +10708,6 @@ Powtórzyć prośbę połączenia? Your channel No comment provided by engineer. - - Your chat database - Twoja baza danych czatu - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować. @@ -10427,6 +10738,13 @@ Powtórzyć prośbę połączenia? Twój kontakt No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go. +Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@). @@ -10592,6 +10910,10 @@ Relays can access channel messages. przyjął cię rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10854,6 +11176,10 @@ marked deleted chat item preview text kontakt powinien zaakceptować… No comment provided by engineer. + + contributor + member role + creator twórca @@ -11058,6 +11384,10 @@ pref value godziny time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain służy do bezpiecznego przechowywania hasła - umożliwia otrzymywanie powiadomień push. @@ -11495,6 +11825,10 @@ ostatnia otrzymana wiadomość: %2$@ strajk No comment provided by engineer. + + subscriber + member role + this contact ten kontakt @@ -11544,11 +11878,6 @@ ostatnia otrzymana wiadomość: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -11700,7 +12029,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -11735,9 +12064,24 @@ ostatnia otrzymana wiadomość: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11759,7 +12103,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -11791,7 +12135,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -11813,7 +12157,7 @@ ostatnia otrzymana wiadomość: %2$@
- +
@@ -11996,9 +12340,8 @@ ostatnia otrzymana wiadomość: %2$@ Nieprawidłowe hasło bazy danych No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json index c79fba1c1e..2f5237052c 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "pl", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index d9af0624bf..c4adeb62ef 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -1179,8 +1179,8 @@ Desenvolver No comment provided by engineer. - - Developer tools + + Developer Ferramentas de desenvolvimento No comment provided by engineer. @@ -2020,12 +2020,12 @@ We will be adding server redundancy to prevent lost messages. Membro No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2708,8 +2708,8 @@ We will be adding server redundancy to prevent lost messages. O remetente cancelou a transferência de arquivos. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. O remetente pode ter excluído a solicitação de conexão. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index e4fac55bcb..b6adde62ce 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -1203,8 +1203,8 @@ Available in v5.1 Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. @@ -2067,12 +2067,12 @@ Available in v5.1 Member No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. No comment provided by engineer. @@ -2809,8 +2809,8 @@ Available in v5.1 Sender cancelled file transfer. No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index a3971c0325..7254d80c9c 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #секрет# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ поддерживал(а) SimpleX Chat. Срок действия значка истёк %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ загружено No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ инвестировал(а) в краудфандинг SimpleX Chat. + badge alert + %@ is connected! Установлено соединение с %@! @@ -110,6 +120,11 @@ %@ серверы No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ поддерживает SimpleX Chat. + badge alert + %@ uploaded %@ загружено @@ -185,6 +200,21 @@ %d мес time interval + + %d owner + %d владелец + channel owners count + + + %d owners + %d владельцев + channel owners count + + + %d owners & contributors + %d владельцев и авторов + channel members count + %d relays failed %d релеев с ошибками @@ -400,11 +430,6 @@ channel relay bar (новое) No comment provided by engineer. - - (signed) - (с подписью) - chat link info line - (this device v%@) (это устройство v%@) @@ -738,6 +763,7 @@ swipe action Add + Добавить No comment provided by engineer. @@ -745,6 +771,16 @@ swipe action Добавьте адрес в свой профиль, чтобы Ваши SimpleX контакты могли поделиться им. Профиль будет отправлен Вашим SimpleX контактам. No comment provided by engineer. + + Add contributors. + Добавить соавторов. + No comment provided by engineer. + + + Add description + Добавить описание + No comment provided by engineer. + Add friends Добавить друзей @@ -765,12 +801,19 @@ swipe action Добавить профиль No comment provided by engineer. + + Add relay + Добавить релей + No comment provided by engineer. + Add relays + Добавить релеи No comment provided by engineer. Add relays to restore message delivery. + Добавить релеи для восстановления доставки сообщений. No comment provided by engineer. @@ -788,6 +831,11 @@ swipe action Добавить сотрудников No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Добавьте этот код на свой веб-сайт. Он отобразит предпросмотр вашего канала или группы. + No comment provided by engineer. + Add to another device Добавить на другое устройство @@ -868,6 +916,11 @@ swipe action Настройки сети No comment provided by engineer. + + Advanced options + Продвинутые настройки + No comment provided by engineer. + Advanced settings Дополнительные настройки @@ -978,6 +1031,11 @@ swipe action Разрешить No comment provided by engineer. + + Allow anyone to embed + Разрешить всем встраивать + No comment provided by engineer. + Allow calls only if your contact allows them. Разрешить звонки, только если их разрешает Ваш контакт. @@ -1153,6 +1211,11 @@ swipe action Принять звонок No comment provided by engineer. + + Any webpage can show the preview. + Предпросмотр можно отобразить на любой веб-странице. + No comment provided by engineer. + App build: %@ Сборка приложения: %@ @@ -1195,6 +1258,7 @@ swipe action App update required + Необходимо обновление приложения alert title @@ -1362,6 +1426,11 @@ swipe action Ошибка хэша сообщения No comment provided by engineer. + + Badge cannot be verified + Не удалось проверить подлинность значка + badge alert title + Be free in your network @@ -1384,6 +1453,11 @@ in your network Улучшенные звонки No comment provided by engineer. + + Better channels 📢 + Улучшенные каналы 📢 + No comment provided by engineer. + Better groups Улучшенные группы @@ -1574,11 +1648,6 @@ in your network Звонок уже завершён! No comment provided by engineer. - - Calls - Звонки - No comment provided by engineer. - Calls prohibited! Звонки запрещены! @@ -1628,10 +1697,12 @@ new chat action Cancel and delete channel + Отменить и удалить канал No comment provided by engineer. Cancel creating channel? + Отменить создание канала? alert title @@ -1689,11 +1760,6 @@ new chat action Изменить режим блокировки authentication reason - - Change member role? - Поменять роль члена группы? - No comment provided by engineer. - Change passcode Изменить код доступа @@ -1714,6 +1780,11 @@ new chat action Поменять роль No comment provided by engineer. + + Change role? + Изменить роль? + No comment provided by engineer. + Change self-destruct mode Изменить режим самоуничтожения @@ -1730,6 +1801,11 @@ set passcode view Канал No comment provided by engineer. + + Channel SimpleX name + SimpleX имя канала + No comment provided by engineer. + Channel display name Имя канала @@ -1781,6 +1857,11 @@ alert subtitle Канал временно недоступен alert title + + Channel webpage + Веб-страница канала + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! Канал будет удалён для всех подписчиков - это нельзя отменить! @@ -1793,6 +1874,7 @@ alert subtitle Channel will start working with %1$d of %2$d relays. Continue? + Канал начнёт работу с %1$d из %2$d релеев. Продолжить? alert message @@ -1825,6 +1907,11 @@ alert subtitle Консоль No comment provided by engineer. + + Chat data + Данные чата + No comment provided by engineer. + Chat database Архив чата @@ -2202,6 +2289,11 @@ server test step Соединяйтесь быстрее! 🚀 No comment provided by engineer. + + Connect to %@ + Соединиться с %@ + new chat action + Connect to desktop Подключиться к компьютеру @@ -2296,14 +2388,6 @@ This is your own one-time link! Подключение к компьютеру No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Соединение @@ -2319,16 +2403,16 @@ This is your own one-time link! Соединение заблокировано No comment provided by engineer. + + Connection blocked: %@ + Соединение заблокировано: %@ + conn error description + Connection error Ошибка соединения alert title - - Connection error (AUTH) - Ошибка соединения (AUTH) - conn error description - Connection failed Ошибка соединения @@ -2341,6 +2425,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Ошибка соединения + conn error description + Connection not ready. Соединение не готово. @@ -2386,6 +2475,11 @@ This is your own one-time link! Соединения No comment provided by engineer. + + Contact + Контакт + No comment provided by engineer. + Contact address Адрес контакта @@ -2476,6 +2570,11 @@ This is your own one-time link! Копировать No comment provided by engineer. + + Copy code + Скопировать код + No comment provided by engineer. + Copy error Скопировать ошибку @@ -2511,6 +2610,11 @@ This is your own one-time link! Создайте группу, используя случайный профиль. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + Создайте веб-страницу, чтобы показывать предпросмотр Вашего канала посетителям до подписки. Хостите её сами или используйте любой статический хостинг. + No comment provided by engineer. + Create file Создание файла @@ -2551,16 +2655,16 @@ This is your own one-time link! Создать публичный канал No comment provided by engineer. - - Create public channel (BETA) - Создать публичный канал (БЕТА) - No comment provided by engineer. - Create queue Создание очереди server test step + + Create web preview. + Создать веб-предпросмотр. + No comment provided by engineer. + Create your address Создайте Ваш адрес @@ -2907,6 +3011,7 @@ swipe action Delete from history + Удалить из истории No comment provided by engineer. @@ -3095,9 +3200,9 @@ alert button Компьютеры No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Адрес сервера назначения %1$@ несовместим с настройками пересылающего сервера %2$@. No comment provided by engineer. @@ -3105,9 +3210,9 @@ alert button Ошибка сервера получателя: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Версия сервера назначения %@ несовместима с пересылающим сервером %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Версия сервера назначения %1$@ несовместима с пересылающим сервером %2$@. No comment provided by engineer. @@ -3120,9 +3225,9 @@ alert button Подробности No comment provided by engineer. - - Develop - Для разработчиков + + Developer + Инструменты разработчика No comment provided by engineer. @@ -3130,11 +3235,6 @@ alert button Опции разработчика No comment provided by engineer. - - Developer tools - Инструменты разработчика - No comment provided by engineer. - Device Устройство @@ -3280,6 +3380,11 @@ alert button Отложить No comment provided by engineer. + + Do not require signing messages. + Не требовать подпись сообщений. + No comment provided by engineer. + Do not send history to new members. Не отправлять историю новым членам. @@ -3315,6 +3420,11 @@ alert button Не пропустите важные сообщения. No comment provided by engineer. + + Don't save + Не сохранять + alert action + Don't show again Не показывать @@ -3396,6 +3506,11 @@ chat item action Проще пригласить друзей 👋 No comment provided by engineer. + + Easier to read. + Легче для чтения. + No comment provided by engineer. + Edit Редактировать @@ -3406,6 +3521,11 @@ chat item action Редактировать профиль канала No comment provided by engineer. + + Edit description + Редактировать описание + No comment provided by engineer. + Edit group profile Редактировать профиль группы @@ -3606,6 +3726,11 @@ chat item action Введите правильный пароль. No comment provided by engineer. + + Enter description (optional) + Введите описание (необязательно) + placeholder + Enter group name… Введите имя группы… @@ -3646,6 +3771,11 @@ chat item action Введите имя этого устройства… No comment provided by engineer. + + Enter webpage URL + Введите адрес страницы + No comment provided by engineer. + Enter welcome message… Введите приветственное сообщение… @@ -3664,7 +3794,7 @@ chat item action Error Ошибка - conn error description + No comment provided by engineer. Error aborting address change @@ -3698,6 +3828,7 @@ chat item action Error adding relays + Ошибка добавления релеев alert title @@ -3832,6 +3963,7 @@ chat item action Error deleting message + Ошибка удаления сообщения alert title @@ -3964,6 +4096,11 @@ chat item action Ошибка при сохранении профиля группы No comment provided by engineer. + + Error saving name + Ошибка сохранения имени + alert title + Error saving passcode Ошибка сохранения кода @@ -4019,6 +4156,11 @@ chat item action Ошибка настроек отчётов о доставке! No comment provided by engineer. + + Error sharing address + Ошибка отправки адреса + alert title + Error sharing channel Ошибка при публикации канала @@ -4098,6 +4240,7 @@ chat item action Error: %@ Ошибка: %@ alert message +conn error description file error text snd error text @@ -4241,6 +4384,16 @@ server test error Ошибка сервера файлов: %@ file error text + + File servers + Серверы файлов + No comment provided by engineer. + + + File servers: %@ + Серверы файлов: %@ + copied message info + File status Статус файла @@ -4542,6 +4695,11 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Get SimpleX name (BETA) + Зарегистрировать SimpleX имя (BETA) + No comment provided by engineer. + Get link Получить ссылку @@ -4652,6 +4810,11 @@ Error: %2$@ Профиль группы изменен. Если Вы сохраните его, новый профиль будет отправлен членам группы. alert message + + Group webpage + Веб-страница группы + No comment provided by engineer. + Group welcome message Приветственное сообщение группы @@ -4677,6 +4840,11 @@ Error: %2$@ Помощь No comment provided by engineer. + + Help & support + Помощь и поддержка + No comment provided by engineer. + Help admins moderating their groups. Помогайте админам модерировать их группы. @@ -4757,6 +4925,11 @@ Error: %2$@ Инфо No comment provided by engineer. + + How to register a test name + Как зарегистрировать тестовое имя + No comment provided by engineer. + How to use it Как использовать @@ -5161,6 +5334,11 @@ More improvements are coming soon! Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + Адрес будет показан подписчикам и разрешит загрузку предпросмотра. + No comment provided by engineer. + Italian interface Итальянский интерфейс @@ -5178,7 +5356,7 @@ More improvements are coming soon! Join as %@ - Вступить как %s + Вступить как %@ No comment provided by engineer. @@ -5186,6 +5364,11 @@ More improvements are coming soon! Вступить в канал No comment provided by engineer. + + Join channel %@ + Вступить в канал %@ + new chat action + Join group Вступить в группу @@ -5266,7 +5449,7 @@ This is your link for group %@! Learn more Узнать больше - No comment provided by engineer. + badge alert button Leave @@ -5308,6 +5491,16 @@ This is your link for group %@! Меньше трафик в мобильных сетях. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + Позвольте людям соединяться с Вами через имя, зарегистрированное для Вашего SimpleX адреса. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + Позвольте людям вступать через имя, зарегистрированное для ссылки этого канала. + No comment provided by engineer. + Let someone connect to you Дайте собеседнику Вашу ссылку @@ -5418,6 +5611,11 @@ This is your link for group %@! Пожалуйста, проверьте, что адреса WebRTC ICE-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется. No comment provided by engineer. + + Manage your relays. + Управлять своими релеями. + No comment provided by engineer. + Mark deleted for everyone Пометить как удалённое для всех @@ -5488,21 +5686,6 @@ This is your link for group %@! Сообщения о нарушениях chat feature - - Member role will be changed to "%@". All chat members will be notified. - Роль участника будет изменена на "%@". Все участники разговора получат уведомление. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Роль члена будет изменена на "%@". Все члены группы получат уведомление. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Роль члена будет изменена на "%@". Будет отправлено новое приглашение. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Член будет удалён из разговора - это действие нельзя отменить! @@ -5648,6 +5831,16 @@ This is your link for group %@! Форма сообщений No comment provided by engineer. + + Message signing is not required. + Подпись сообщений не обязательна. + No comment provided by engineer. + + + Message signing is required. + Подпись сообщений обязательна. + No comment provided by engineer. + Message source remains private. Источник сообщения остаётся конфиденциальным. @@ -5818,6 +6011,11 @@ This is your link for group %@! Дополнительные улучшения скоро! No comment provided by engineer. + + More privacy + Больше конфиденциальности + No comment provided by engineer. + More reliable network connection. Более надёжное соединение с сетью. @@ -5858,6 +6056,11 @@ This is your link for group %@! Имя swipe action + + Name not found + Имя не найдено + No comment provided by engineer. + Network & servers Сеть и серверы @@ -6044,6 +6247,7 @@ The most secure encryption. No available relays + Нет доступных релеев No comment provided by engineer. @@ -6173,6 +6377,7 @@ The most secure encryption. No relays + Релеи отсутствуют No comment provided by engineer. @@ -6190,6 +6395,11 @@ The most secure encryption. Нет серверов для приёма сообщений. servers error + + No servers to resolve names. + Нет серверов для разрешения имён. + servers warning + No servers to send files. Нет серверов для отправки файлов. @@ -6205,6 +6415,11 @@ The most secure encryption. Нет непрочитанных чатов No comment provided by engineer. + + No valid link + Нет действительной ссылки + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни. @@ -6215,6 +6430,11 @@ The most secure encryption. Некоммерческое управление No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + Ни один из Ваших серверов не настроен для разрешения SimpleX имён. Настройте серверы или используйте ссылку для соединения. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны. @@ -6440,6 +6660,11 @@ Requires compatible VPN. Только Ваш контакт может отправлять голосовые сообщения. No comment provided by engineer. + + Only your page above can show the preview. + Предпросмотр можно отобразить только на Вашей странице, указанной выше. + No comment provided by engineer. + Open Открыть @@ -6629,9 +6854,9 @@ alert button Владелец No comment provided by engineer. - - Owners - Владельцы + + Owners & contributors + Владельцы и соавторы No comment provided by engineer. @@ -6823,10 +7048,6 @@ Error: %@ Попробуйте выключить и снова включить уведомления. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление. @@ -6887,11 +7108,6 @@ Error: %@ Ранее подключенные серверы No comment provided by engineer. - - Privacy & security - Конфиденциальность - No comment provided by engineer. - Privacy for your customers. Конфиденциальность для ваших покупателей. @@ -6980,7 +7196,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. Обновление профиля будет отправлено Вашим SimpleX контактам. - alert message + alert message +alert title Prohibit audio/video calls. @@ -7099,6 +7316,11 @@ Enable in *Network & servers* settings. Публичные каналы - говорите свободно 🚀 No comment provided by engineer. + + Public names for your channel or business. + Публичные имена для Вашего канала или бизнеса. + No comment provided by engineer. + Push notifications Доставка уведомлений @@ -7137,7 +7359,7 @@ Enable in *Network & servers* settings. Read more Узнать больше - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7348,10 +7570,12 @@ swipe action Relay will be removed from channel - this cannot be undone! + Релей будет удалён из канала - это нельзя отменить! alert message Relays added: %@. + Добавлены релеи: %@. alert message @@ -7394,13 +7618,24 @@ swipe action Удалить члена группы? alert title + + Remove name + Удалить имя + No comment provided by engineer. + Remove passphrase from keychain? Удалить пароль из Keychain? No comment provided by engineer. + + Remove relay + Удалить релей + No comment provided by engineer. + Remove relay? + Удалить релей? alert title @@ -7503,6 +7738,11 @@ swipe action Сообщения о нарушениях No comment provided by engineer. + + Require signing messages. + Требовать подпись сообщений. + No comment provided by engineer. + Required Обязательно @@ -7548,6 +7788,11 @@ swipe action Сбросить на тему пользователя No comment provided by engineer. + + Resolver error: %@ + Ошибка разрешения имени: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустите приложение, чтобы создать новый профиль @@ -7628,6 +7873,26 @@ swipe action Роль No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Роль участника будет изменена на "%@". Все участники разговора получат уведомление. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Роль члена будет изменена на "%@". Все члены группы получат уведомление. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + Роль будет изменена на "%@". Все подписчики получат сообщение. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Роль члена будет изменена на "%@". Будет отправлено новое приглашение. + No comment provided by engineer. + Run chat Запустить chat @@ -7661,7 +7926,8 @@ swipe action Save Сохранить - alert button + alert action +alert button chat item action @@ -7679,6 +7945,11 @@ chat item action Сохранить (и уведомить подписчиков) alert button + + Save SimpleX name? + Сохранить SimpleX имя? + alert title + Save admission settings? Сохранить настройки вступления? @@ -7694,6 +7965,11 @@ chat item action Сохранить и уведомить членов группы No comment provided by engineer. + + Save and notify members + Сохранить и уведомить членов группы + No comment provided by engineer. + Save and notify subscribers Сохранить и уведомить подписчиков @@ -7764,6 +8040,11 @@ chat item action Сохранить серверы? alert title + + Save webpage settings? + Сохранить настройки веб-страницы? + alert title + Save welcome message? Сохранить приветственное сообщение? @@ -8064,11 +8345,6 @@ chat item action Отправитель отменил передачу файла. alert message - - Sender may have deleted the connection request. - Отправитель мог удалить запрос на соединение. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. Отправка картинки ссылки может раскрыть Ваш IP-адрес веб-сайту. Вы можете изменить это в настройках безопасности позже. @@ -8164,6 +8440,11 @@ chat item action Сервер No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + Сервер %@ не поддерживает разрешение имён. Настройте серверы или используйте ссылку для соединения. + No comment provided by engineer. + Server added to operator %@. Сервер добавлен к оператору %@. @@ -8475,6 +8756,11 @@ chat item action Показать опции для разработчиков No comment provided by engineer. + + Show encryption + Показывать шифрование + No comment provided by engineer. + Show last messages Показывать последние сообщения @@ -8505,6 +8791,37 @@ chat item action Показать: No comment provided by engineer. + + Sign message + Подписать сообщение + No comment provided by engineer. + + + Sign messages + Подпись сообщений + chat feature + + + Signature missing + Подпись отсутствует + alert title +copied message info + + + Signed + Подписано + copied message info + + + Signed & verified + Подписано и проверено + copied message info + + + Signing proves you authored this message and can't be denied later. + Подпись доказывает, что Вы — автор этого сообщения, и это нельзя будет отрицать. + No comment provided by engineer. + SimpleX SimpleX @@ -8600,6 +8917,21 @@ chat item action Ссылки SimpleX не разрешены No comment provided by engineer. + + SimpleX name + SimpleX имя + No comment provided by engineer. + + + SimpleX name error + Ошибка SimpleX имени + No comment provided by engineer. + + + SimpleX name not verified + SimpleX имя не проверено + alert title + SimpleX one-time invitation SimpleX одноразовая ссылка @@ -8610,6 +8942,11 @@ chat item action Аудит SimpleX протоколов от Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + Публичные SimpleX имена (BETA) + No comment provided by engineer. + SimpleX relay address Адрес релея SimpleX @@ -8720,6 +9057,7 @@ report reason Status + Статус No comment provided by engineer. @@ -8879,9 +9217,9 @@ Relay address was used to set up this relay for the channel. Подписок игнорировано No comment provided by engineer. - - Support SimpleX Chat - Поддержать SimpleX Chat + + Support the project + Поддержать проект No comment provided by engineer. @@ -9077,6 +9415,26 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + SimpleX имя #%@ зарегистрировано без ссылки канала. Добавьте ссылку канала к имени на странице регистрации. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + SimpleX имя %@ зарегистрировано, но не имеет действительной ссылки. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + SimpleX имя %@ зарегистрировано, но не добавлено в профиль. Пожалуйста, добавьте его в профиль Вашего адреса или канала, если Вы владелец. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + SimpleX имя @%@ зарегистрировано без SimpleX адреса. Добавьте Ваш SimpleX адрес к имени на странице регистрации. + alert message + The address will be short, and your profile will be shared via the address. Адрес будет коротким, и Ваш профиль будет добавлен в адрес. @@ -9107,6 +9465,16 @@ It can happen because of some bug or when the connection is compromised.Попытка поменять пароль базы данных не была завершена. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + Этот значок подписан ключом, который неизвестен текущей версии приложения. Обновите приложение, чтобы проверить его подлинность. + badge alert + + + The channel required this message to be signed, but the signature is missing. + Канал требует подпись сообщений, но у этого сообщения подпись отсутствует. + alert message + The code you scanned is not a SimpleX link QR code. Этот QR-код не является SimpleX-ccылкой. @@ -9191,7 +9559,7 @@ your contacts and groups. The same conditions will apply to operator **%@**. - Те же условия будут действовать для оператора **%s**. + Те же условия будут действовать для оператора **%@**. No comment provided by engineer. @@ -9204,6 +9572,11 @@ your contacts and groups. Вторая галочка - знать, что доставлено! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Отправитель мог удалить запрос на соединение. + No comment provided by engineer. + The sender will NOT be notified Отправитель не будет уведомлён @@ -9259,6 +9632,11 @@ your contacts and groups. Они могут быть изменены в настройках контактов и групп. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + Это SimpleX имя не зарегистрировано. Пожалуйста, проверьте имя. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Это действие нельзя отменить - все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении. @@ -9279,6 +9657,11 @@ your contacts and groups. Это действие нельзя отменить - Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + Не удалось проверить подлинность этого значка. Возможно, он не является подлинным. + badge alert + This chat is protected by end-to-end encryption. Чат защищён сквозным шифрованием. @@ -9311,6 +9694,7 @@ your contacts and groups. This group requires a newer version of the app. Please update the app to join. + Эта группа требует более новой версии приложения. Пожалуйста, обновите приложение, чтобы вступить. alert message alert subtitle @@ -9321,6 +9705,7 @@ alert subtitle This is the last active relay. Removing it will prevent message delivery to subscribers. + Это последний активный релей. После его удаления доставка сообщений подписчикам будет невозможна. alert message @@ -9435,6 +9820,11 @@ You will be prompted to complete authentication before this feature is enabled.< Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону. No comment provided by engineer. + + To resolve names + Для разрешения имён + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**. @@ -9470,6 +9860,11 @@ You will be prompted to complete authentication before this feature is enabled.< Чтобы подтвердить безопасность сквозного шифрования с Вашим контактом сравните (или сканируйте) код на ваших устройствах. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + Чтобы подтвердить ключи с этим подписчиком, сравните (или сканируйте) код на ваших устройствах. + No comment provided by engineer. + Toggle incognito when connecting. Установите режим Инкогнито при соединении. @@ -9560,6 +9955,11 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать подписчика для всех? No comment provided by engineer. + + Unconfirmed name + Неподтверждённое имя + No comment provided by engineer. + Undelivered messages Недоставленные сообщения @@ -9620,13 +10020,6 @@ You will be prompted to complete authentication before this feature is enabled.< Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом. -Чтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью. - No comment provided by engineer. - Unlink Забыть @@ -9657,18 +10050,15 @@ To connect, please ask your contact to create another connection link and check Не прочитано swipe action - - Unsupported channel name - alert title - Unsupported connection link Ссылка не поддерживается conn error description - - Unsupported contact name - alert title + + Unverified badge + Неподтверждённый значок + badge alert title Up to 100 last messages are sent to new members. @@ -9723,7 +10113,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? Обновить адрес? - alert message + alert message +alert title Upgrade and open chat @@ -9900,6 +10291,11 @@ To connect, please ask your contact to create another connection link and check Использовать веб-порт No comment provided by engineer. + + Used chat relays do not support webpages. + Используемые чат-релеи не поддерживают веб-страницы. + No comment provided by engineer. + User selection Выбор пользователя @@ -9920,6 +10316,11 @@ To connect, please ask your contact to create another connection link and check Проверить relay test step + + Verify SimpleX names + Проверять SimpleX имена + No comment provided by engineer. + Verify code with desktop Сверьте код с компьютером @@ -9945,6 +10346,11 @@ To connect, please ask your contact to create another connection link and check Проверка пароля базы данных No comment provided by engineer. + + Verify name + Проверить имя + No comment provided by engineer. + Verify passphrase Проверить пароль @@ -10105,6 +10511,16 @@ To connect, please ask your contact to create another connection link and check WebRTC ICE-серверы No comment provided by engineer. + + Webpage code + Код веб-страницы + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + Настройки веб-страницы были изменены. Если Вы сохраните их, обновлённые настройки будут отправлены подписчикам. + alert message + Welcome %@! Здравствуйте %@! @@ -10327,8 +10743,8 @@ Repeat join request? Вы можете включить их позже в Настройках No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. + + You can enable them later via app Your privacy settings. Вы можете включить их позже в настройках Конфиденциальности. No comment provided by engineer. @@ -10392,6 +10808,11 @@ Repeat join request? Вы по-прежнему можете просмотреть разговор с %@ в списке чатов. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + Вы можете поддержать SimpleX начиная с версии приложения v7. + badge alert + You can turn on SimpleX Lock via Settings. Вы можете включить Блокировку SimpleX через Настройки. @@ -10583,6 +11004,11 @@ Repeat connection request? Ваш адрес SimpleX No comment provided by engineer. + + Your SimpleX name + Ваше SimpleX имя + No comment provided by engineer. + Your business contact Ваш бизнес-контакт @@ -10598,11 +11024,6 @@ Repeat connection request? Ваш канал No comment provided by engineer. - - Your chat database - База данных - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные. @@ -10633,6 +11054,13 @@ Repeat connection request? Ваш контакт No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом. +Чтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт отправил файл, размер которого превышает максимальный размер (%@). @@ -10681,6 +11109,8 @@ Repeat connection request? Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + Ваш новый канал %1$@ подключен к %2$d из %3$d релеев. +Если Вы отмените, канал будет удалён - Вы сможете создать его снова. alert message @@ -10805,6 +11235,11 @@ Relays can access channel messages. Вы приняты rcv group event chat item + + acknowledged roster + подтверждённый список + No comment provided by engineer. + active активный @@ -11071,6 +11506,11 @@ marked deleted chat item preview text контакт должен принять… No comment provided by engineer. + + contributor + соавтор + member role + creator создатель @@ -11277,6 +11717,11 @@ pref value часов time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления. @@ -11719,6 +12164,11 @@ last received msg: %2$@ зачеркнуть No comment provided by engineer. + + subscriber + подписчик + member role + this contact этот контакт @@ -11769,11 +12219,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ через %@ @@ -11928,7 +12373,7 @@ last received msg: %2$@
- +
@@ -11963,9 +12408,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11987,7 +12447,7 @@ last received msg: %2$@
- +
@@ -12019,7 +12479,7 @@ last received msg: %2$@
- +
@@ -12041,7 +12501,7 @@ last received msg: %2$@
- +
@@ -12224,9 +12684,9 @@ last received msg: %2$@ Неправильный пароль базы данных No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX. + + You can allow sharing in Your privacy / SimpleX Lock settings. + Вы можете разрешить отправку в настройках Конфиденциальность / Блокировка SimpleX. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index b49b25d653..9907ddc7fc 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 cd2e30977d..20dec9b68d 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,7 +2,7 @@
- +
@@ -32,6 +32,10 @@ #ความลับ# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + badge alert + %@ %@ @@ -78,6 +82,10 @@ %@ downloaded No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + badge alert + %@ is connected! %@ เชื่อมต่อสำเร็จ! @@ -101,6 +109,10 @@ %@ servers No comment provided by engineer. + + %@ supports SimpleX Chat. + badge alert + %@ uploaded No comment provided by engineer. @@ -167,6 +179,18 @@ %d เดือน time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed channel relay bar @@ -359,10 +383,6 @@ channel relay bar (new) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) No comment provided by engineer. @@ -667,6 +687,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends No comment provided by engineer. @@ -684,6 +712,10 @@ swipe action เพิ่มโปรไฟล์ No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -706,6 +738,10 @@ swipe action Add team members No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device เพิ่มเข้าไปในอุปกรณ์อื่น @@ -776,6 +812,10 @@ swipe action การตั้งค่าระบบเครือข่ายขั้นสูง No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings No comment provided by engineer. @@ -872,6 +912,10 @@ swipe action อนุญาต No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น. @@ -1033,6 +1077,10 @@ swipe action รับสาย No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ รุ่นแอป: %@ @@ -1223,6 +1271,10 @@ swipe action แฮชข้อความไม่ดี No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1240,6 +1292,10 @@ in your network Better calls No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups No comment provided by engineer. @@ -1400,11 +1456,6 @@ in your network สิ้นสุดการโทรแล้ว! No comment provided by engineer. - - Calls - โทร - No comment provided by engineer. - Calls prohibited! No comment provided by engineer. @@ -1503,11 +1554,6 @@ new chat action เปลี่ยนโหมดล็อค authentication reason - - Change member role? - เปลี่ยนบทบาทของสมาชิก? - No comment provided by engineer. - Change passcode เปลี่ยนรหัสผ่าน @@ -1528,6 +1574,10 @@ new chat action เปลี่ยนบทบาท No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode เปลี่ยนโหมดทําลายตัวเอง @@ -1543,6 +1593,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1584,6 +1638,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1621,6 +1679,10 @@ alert subtitle คอนโซลแชท No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database ฐานข้อมูลแชท @@ -1948,6 +2010,10 @@ server test step Connect faster! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop No comment provided by engineer. @@ -2025,14 +2091,6 @@ This is your own one-time link! Connecting to desktop No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection การเชื่อมต่อ @@ -2046,16 +2104,15 @@ This is your own one-time link! Connection blocked No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error การเชื่อมต่อผิดพลาด alert title - - Connection error (AUTH) - การเชื่อมต่อผิดพลาด (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2065,6 +2122,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + การเชื่อมต่อผิดพลาด + conn error description + Connection not ready. No comment provided by engineer. @@ -2103,6 +2165,10 @@ This is your own one-time link! Connections No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2186,6 +2252,10 @@ This is your own one-time link! คัดลอก No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error No comment provided by engineer. @@ -2216,6 +2286,10 @@ This is your own one-time link! Create a group using a random profile. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file สร้างไฟล์ @@ -2251,15 +2325,15 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue สร้างคิว server test step + + Create web preview. + No comment provided by engineer. + Create your address No comment provided by engineer. @@ -2750,16 +2824,16 @@ alert button Desktop devices No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. No comment provided by engineer. Destination server error: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. No comment provided by engineer. @@ -2770,20 +2844,15 @@ alert button Details No comment provided by engineer. - - Develop - พัฒนา + + Developer + เครื่องมือสำหรับนักพัฒนา No comment provided by engineer. Developer options No comment provided by engineer. - - Developer tools - เครื่องมือสำหรับนักพัฒนา - No comment provided by engineer. - Device อุปกรณ์ @@ -2918,6 +2987,10 @@ alert button ทำในภายหลัง No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. No comment provided by engineer. @@ -2948,6 +3021,10 @@ alert button Don't miss important messages. No comment provided by engineer. + + Don't save + alert action + Don't show again ไม่ต้องแสดงอีก @@ -3018,6 +3095,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit แก้ไข @@ -3027,6 +3108,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile แก้ไขโปรไฟล์กลุ่ม @@ -3210,6 +3295,10 @@ chat item action ใส่รหัสผ่านที่ถูกต้อง No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… No comment provided by engineer. @@ -3245,6 +3334,10 @@ chat item action Enter this device name… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… ใส่ข้อความต้อนรับ… @@ -3262,7 +3355,7 @@ chat item action Error ผิดพลาด - conn error description + No comment provided by engineer. Error aborting address change @@ -3531,6 +3624,10 @@ chat item action เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม No comment provided by engineer. + + Error saving name + alert title + Error saving passcode เกิดข้อผิดพลาดในการบันทึกรหัสผ่าน @@ -3581,6 +3678,10 @@ chat item action เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -3654,6 +3755,7 @@ chat item action Error: %@ ข้อผิดพลาด: % @ alert message +conn error description file error text snd error text @@ -3779,6 +3881,14 @@ server test error File server error: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status No comment provided by engineer. @@ -4044,6 +4154,10 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4146,6 +4260,10 @@ Error: %2$@ Group profile was changed. If you save it, the updated profile will be sent to group members. alert message + + Group webpage + No comment provided by engineer. + Group welcome message ข้อความต้อนรับกลุ่ม @@ -4170,6 +4288,10 @@ Error: %2$@ ความช่วยเหลือ No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. No comment provided by engineer. @@ -4244,6 +4366,10 @@ Error: %2$@ วิธี No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it วิธีการใช้งาน @@ -4613,6 +4739,10 @@ More improvements are coming soon! ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface อินเทอร์เฟซภาษาอิตาลี @@ -4637,6 +4767,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group เข้าร่วมกลุ่ม @@ -4709,7 +4843,7 @@ This is your link for group %@! Learn more ศึกษาเพิ่มเติม - No comment provided by engineer. + badge alert button Leave @@ -4746,6 +4880,14 @@ This is your link for group %@! Less traffic on mobile networks. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -4846,6 +4988,10 @@ This is your link for group %@! ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน @@ -4908,20 +5054,6 @@ This is your link for group %@! Member reports chat feature - - Member role will be changed to "%@". All chat members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่ - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! alert message @@ -5051,6 +5183,14 @@ This is your link for group %@! Message shape No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. No comment provided by engineer. @@ -5199,6 +5339,10 @@ This is your link for group %@! การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. No comment provided by engineer. @@ -5235,6 +5379,10 @@ This is your link for group %@! ชื่อ swipe action + + Name not found + No comment provided by engineer. + Network & servers เครือข่ายและเซิร์ฟเวอร์ @@ -5523,6 +5671,10 @@ The most secure encryption. No servers to receive messages. servers error + + No servers to resolve names. + servers warning + No servers to send files. servers error @@ -5535,6 +5687,10 @@ The most secure encryption. No unread chats No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -5543,6 +5699,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -5747,6 +5907,10 @@ Requires compatible VPN. ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open alert action @@ -5899,8 +6063,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6076,10 +6240,6 @@ Error: %@ Please try to disable and re-enable notfications. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. snd group event chat item @@ -6132,11 +6292,6 @@ Error: %@ Previously connected servers No comment provided by engineer. - - Privacy & security - ความเป็นส่วนตัวและความปลอดภัย - No comment provided by engineer. - Privacy for your customers. No comment provided by engineer. @@ -6211,7 +6366,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6318,6 +6474,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications การแจ้งเตือนแบบทันที @@ -6353,7 +6513,7 @@ Enable in *Network & servers* settings. Read more อ่านเพิ่มเติม - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -6582,11 +6742,19 @@ swipe action ลบสมาชิกออก? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? ลบรหัสผ่านออกจาก keychain หรือไม่? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -6675,6 +6843,10 @@ swipe action Reports No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required ที่จำเป็น @@ -6715,6 +6887,10 @@ swipe action Reset to user theme No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่ @@ -6790,6 +6966,22 @@ swipe action บทบาท No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + No comment provided by engineer. + Run chat เรียกใช้แชท @@ -6818,7 +7010,8 @@ swipe action Save บันทึก - alert button + alert action +alert button chat item action @@ -6834,6 +7027,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? alert title @@ -6848,6 +7045,10 @@ chat item action บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -6912,6 +7113,10 @@ chat item action บันทึกเซิร์ฟเวอร์? alert title + + Save webpage settings? + alert title + Save welcome message? บันทึกข้อความต้อนรับ? @@ -7178,11 +7383,6 @@ chat item action ผู้ส่งยกเลิกการโอนไฟล์ alert message - - Sender may have deleted the connection request. - ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7269,6 +7469,10 @@ chat item action Server No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. alert message @@ -7537,6 +7741,10 @@ chat item action แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์ No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages No comment provided by engineer. @@ -7563,6 +7771,31 @@ chat item action แสดง: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX No comment provided by engineer. @@ -7650,6 +7883,18 @@ chat item action SimpleX links not allowed No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation คำเชิญ SimpleX แบบครั้งเดียว @@ -7659,6 +7904,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -7891,9 +8140,8 @@ Relay address was used to set up this relay for the channel. Subscriptions ignored No comment provided by engineer. - - Support SimpleX Chat - สนับสนุน SimpleX แชท + + Support the project No comment provided by engineer. @@ -8071,6 +8319,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. alert message @@ -8097,6 +8361,14 @@ It can happen because of some bug or when the connection is compromised.ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์ No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. No comment provided by engineer. @@ -8183,6 +8455,11 @@ your contacts and groups. ขีดที่สองที่เราพลาด! ✅ No comment provided by engineer. + + The sender deleted the connection request. + ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว + No comment provided by engineer. + The sender will NOT be notified ผู้ส่งจะไม่ได้รับแจ้ง @@ -8230,6 +8507,10 @@ your contacts and groups. They can be overridden in contact and group settings. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่ @@ -8249,6 +8530,10 @@ your contacts and groups. การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. E2EE info chat item @@ -8385,6 +8670,10 @@ You will be prompted to complete authentication before this feature is enabled.< ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ** @@ -8416,6 +8705,10 @@ You will be prompted to complete authentication before this feature is enabled.< ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. @@ -8492,6 +8785,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages No comment provided by engineer. @@ -8550,13 +8847,6 @@ You will be prompted to complete authentication before this feature is enabled.< ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน -ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร - No comment provided by engineer. - Unlink No comment provided by engineer. @@ -8585,17 +8875,13 @@ To connect, please ask your contact to create another connection link and check เปลี่ยนเป็นยังไม่ได้อ่าน swipe action - - Unsupported channel name - alert title - Unsupported connection link conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -8643,7 +8929,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? - alert message + alert message +alert title Upgrade and open chat @@ -8792,6 +9079,10 @@ To connect, please ask your contact to create another connection link and check Use web port No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection No comment provided by engineer. @@ -8809,6 +9100,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop No comment provided by engineer. @@ -8830,6 +9125,10 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase No comment provided by engineer. @@ -8975,6 +9274,14 @@ To connect, please ask your contact to create another connection link and check เซิร์ฟเวอร์ WebRTC ICE No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! ยินดีต้อนรับ %@! @@ -9168,9 +9475,8 @@ Repeat join request? คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -9227,6 +9533,10 @@ Repeat join request? You can still view conversation with %@ in the list of chats. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า @@ -9400,6 +9710,10 @@ Repeat connection request? ที่อยู่ SimpleX ของคุณ No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact No comment provided by engineer. @@ -9413,11 +9727,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - ฐานข้อมูลการแชทของคุณ - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt @@ -9444,6 +9753,13 @@ Repeat connection request? Your contact No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน +ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@) @@ -9599,6 +9915,10 @@ Relays can access channel messages. accepted you rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -9844,6 +10164,10 @@ marked deleted chat item preview text contact should accept… No comment provided by engineer. + + contributor + member role + creator ผู้สร้าง @@ -10039,6 +10363,10 @@ pref value ชั่วโมง time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain ใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัย - อนุญาตให้รับการแจ้งเตือนแบบทันที @@ -10441,6 +10769,10 @@ last received msg: %2$@ ตี No comment provided by engineer. + + subscriber + member role + this contact ผู้ติดต่อนี้ @@ -10484,11 +10816,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -10634,7 +10961,7 @@ last received msg: %2$@
- +
@@ -10668,9 +10995,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -10692,7 +11034,7 @@ last received msg: %2$@
- +
@@ -10719,7 +11061,7 @@ last received msg: %2$@
- +
@@ -10738,7 +11080,7 @@ last received msg: %2$@
- +
@@ -10885,8 +11227,8 @@ last received msg: %2$@ Wrong database passphrase No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/contents.json b/apps/ios/SimpleX Localizations/th.xcloc/contents.json index ee6ee63ea9..a18ced87af 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/th.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "th", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 1189b53e3c..00385e5312 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #gizli# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@, SimpleX Chat'i destekledi. Rozetin süresi %2$@ tarihinde doldu. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ indirildi No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@, SimpleX Chat kitle fonlamasına yatırım yaptı. + badge alert + %@ is connected! %@ bağlandı! @@ -110,6 +120,11 @@ %@ sunucular No comment provided by engineer. + + %@ supports SimpleX Chat. + %@, SimpleX Chat'i destekliyor. + badge alert + %@ uploaded %@ yüklendi @@ -185,6 +200,21 @@ %d ay time interval + + %d owner + %d sahibi + channel owners count + + + %d owners + %d sahibi + channel owners count + + + %d owners & contributors + %d sahibi & katkıda bulunanlar + channel members count + %d relays failed %d aktarıcı başarısız oldu @@ -395,10 +425,6 @@ channel relay bar (yeni) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (bu cihaz v%@) @@ -733,6 +759,14 @@ swipe action Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. No comment provided by engineer. + + Add contributors. + No comment provided by engineer. + + + Add description + No comment provided by engineer. + Add friends Arkadaş ekle @@ -753,6 +787,10 @@ swipe action Profil ekle No comment provided by engineer. + + Add relay + No comment provided by engineer. + Add relays No comment provided by engineer. @@ -776,6 +814,10 @@ swipe action Takım üyesi ekle No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + No comment provided by engineer. + Add to another device Başka bir cihaza ekle @@ -856,6 +898,10 @@ swipe action Gelişmiş ağ ayarları No comment provided by engineer. + + Advanced options + No comment provided by engineer. + Advanced settings Gelişmiş ayarlar @@ -963,6 +1009,10 @@ swipe action İzin ver No comment provided by engineer. + + Allow anyone to embed + No comment provided by engineer. + Allow calls only if your contact allows them. Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin. @@ -1135,6 +1185,10 @@ swipe action Aramayı cevapla No comment provided by engineer. + + Any webpage can show the preview. + No comment provided by engineer. + App build: %@ Uygulama sürümü: %@ @@ -1343,6 +1397,10 @@ swipe action Kötü mesaj karması No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1361,6 +1419,10 @@ in your network Daha iyi aramalar No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Daha iyi gruplar @@ -1548,11 +1610,6 @@ in your network Arama çoktan bitti! No comment provided by engineer. - - Calls - Aramalar - No comment provided by engineer. - Calls prohibited! Aramalara izin verilmiyor! @@ -1663,11 +1720,6 @@ new chat action Kilit modunu değiştir authentication reason - - Change member role? - Üye rolünü değiştir? - No comment provided by engineer. - Change passcode Şifreyi değiştir @@ -1688,6 +1740,10 @@ new chat action Rolü değiştir No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Kendini yok etme modunu değiştir @@ -1703,6 +1759,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1744,6 +1804,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1785,6 +1849,10 @@ alert subtitle Sohbet konsolu No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database Sohbet veritabanı @@ -2152,6 +2220,10 @@ server test step Daha hızlı bağlanın! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Bilgisayara bağlan @@ -2245,14 +2317,6 @@ Bu senin kendi tek kullanımlık bağlantın! Bilgisayara bağlanıyor No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Bağlantı @@ -2268,16 +2332,15 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı engellendi No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Bağlantı hatası alert title - - Connection error (AUTH) - Bağlantı hatası (DOĞRULAMA) - conn error description - Connection failed No comment provided by engineer. @@ -2289,6 +2352,11 @@ Bu senin kendi tek kullanımlık bağlantın! %@ No comment provided by engineer. + + Connection link removed + Bağlantı hatası + conn error description + Connection not ready. Bağlantı hazır değil. @@ -2334,6 +2402,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantılar No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2423,6 +2495,10 @@ Bu senin kendi tek kullanımlık bağlantın! Kopyala No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error Kopyalama hatası @@ -2458,6 +2534,10 @@ Bu senin kendi tek kullanımlık bağlantın! Rasgele profil kullanarak grup oluştur. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Dosya oluştur @@ -2497,15 +2577,15 @@ Bu senin kendi tek kullanımlık bağlantın! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Sıra oluştur server test step + + Create web preview. + No comment provided by engineer. + Create your address Adresinizi oluşturun @@ -3031,9 +3111,9 @@ alert button Bilgisayar cihazları No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Hedef sunucu adresi %@, yönlendirme sunucusu %@ ayarlarıyla uyumlu değil. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Hedef sunucu adresi %1$@, yönlendirme sunucusu %2$@ ayarlarıyla uyumlu değil. No comment provided by engineer. @@ -3041,9 +3121,9 @@ alert button Hedef sunucu hatası: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Hedef sunucu %@ sürümü, yönlendirme sunucusu %@ ile uyumlu değil. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Hedef sunucu %1$@ sürümü, yönlendirme sunucusu %2$@ ile uyumlu değil. No comment provided by engineer. @@ -3056,9 +3136,9 @@ alert button Detaylar No comment provided by engineer. - - Develop - Geliştir + + Developer + Geliştirici araçları No comment provided by engineer. @@ -3066,11 +3146,6 @@ alert button Geliştirici seçenekleri No comment provided by engineer. - - Developer tools - Geliştirici araçları - No comment provided by engineer. - Device Cihaz @@ -3214,6 +3289,10 @@ alert button Sonra yap No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Yeni üyelere geçmişi gönderme. @@ -3248,6 +3327,10 @@ alert button Önemli mesajları kaçırmayın. No comment provided by engineer. + + Don't save + alert action + Don't show again Yeniden gösterme @@ -3328,6 +3411,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Düzenle @@ -3337,6 +3424,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Grup profilini düzenle @@ -3533,6 +3624,10 @@ chat item action Doğru şifreyi gir. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Grup adı gir… @@ -3571,6 +3666,10 @@ chat item action Bu cihazın adını gir… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Hoşgeldin mesajı gir… @@ -3589,7 +3688,7 @@ chat item action Error Hata - conn error description + No comment provided by engineer. Error aborting address change @@ -3885,6 +3984,10 @@ chat item action Grup profili kaydedilirken sorun oluştu No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Parola kaydedilirken sorun oluştu @@ -3940,6 +4043,10 @@ chat item action Görüldü ayarlanırken hata oluştu! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -4018,6 +4125,7 @@ chat item action Error: %@ Hata: %@ alert message +conn error description file error text snd error text @@ -4160,6 +4268,14 @@ server test error Dosya sunucusu hatası: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status Dosya durumu @@ -4456,6 +4572,10 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4564,6 +4684,10 @@ Hata: %2$@ Grup profili değiştirildi. Eğer kaydederseniz, güncellenmiş profil grup üyelerine gönderilecektir. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Grup hoşgeldin mesajı @@ -4589,6 +4713,10 @@ Hata: %2$@ Yardım No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. Yöneticilere gruplarını yönetmelerinde yardımcı olun. @@ -4668,6 +4796,10 @@ Hata: %2$@ Nasıl yapılır No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Nasıl kullanılır @@ -5067,6 +5199,10 @@ Daha fazla iyileştirme yakında geliyor! Bu bağlantı üzerinden zaten bağlanmışsınız gibi görünüyor. Eğer durum böyle değilse, bir hata oluştu (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface İtalyanca arayüz @@ -5091,6 +5227,10 @@ Daha fazla iyileştirme yakında geliyor! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Gruba katıl @@ -5171,7 +5311,7 @@ Bu senin grup için bağlantın %@! Learn more Daha fazlası - No comment provided by engineer. + badge alert button Leave @@ -5211,6 +5351,14 @@ Bu senin grup için bağlantın %@! Mobil ağlarda daha az trafik. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5318,6 +5466,10 @@ Bu senin grup için bağlantın %@! WebRTC ICE sunucu adreslerinin doğru formatta olduğundan, satırlara ayrıldığından ve yinelenmediğinden emin olun. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Herkes için silinmiş olarak işaretle @@ -5387,21 +5539,6 @@ Bu senin grup için bağlantın %@! Üye raporları chat feature - - Member role will be changed to "%@". All chat members will be notified. - Üye rolü "%@" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Üye sohbetten kaldırılacak - bu geri alınamaz! @@ -5545,6 +5682,14 @@ Bu senin grup için bağlantın %@! Mesaj şekli No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. Mesaj kaynağı gizli kalır. @@ -5712,6 +5857,10 @@ Bu senin grup için bağlantın %@! Daha fazla geliştirmeler yakında geliyor! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. Daha güvenilir ağ bağlantısı. @@ -5752,6 +5901,10 @@ Bu senin grup için bağlantın %@! İsim swipe action + + Name not found + No comment provided by engineer. + Network & servers Ağ & sunucular @@ -6073,6 +6226,10 @@ The most secure encryption. Mesaj almak için hiç sunucu yok. servers error + + No servers to resolve names. + servers warning + No servers to send files. Dosya göndermek için hiç sunucu yok. @@ -6088,6 +6245,10 @@ The most secure encryption. Okunmamış sohbet yok No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6096,6 +6257,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6316,6 +6481,10 @@ VPN'nin etkinleştirilmesi gerekir. Sadece karşıdaki kişi sesli mesajlar gönderebilir. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open @@ -6495,8 +6664,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6686,10 +6855,6 @@ Hata: %@ Lütfen bildirimleri devre dışı bırakmayı ve yeniden etkinleştirmeyi deneyin. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Lütfen grup moderatörlerinin gruba katılma isteğinizi incelemesini bekleyin. @@ -6748,11 +6913,6 @@ Hata: %@ Önceden bağlanılmış sunucular No comment provided by engineer. - - Privacy & security - Gizlilik & güvenlik - No comment provided by engineer. - Privacy for your customers. Müşterileriniz için gizlilik. @@ -6838,7 +6998,8 @@ Hata: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6954,6 +7115,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Anında bildirimler @@ -6992,7 +7157,7 @@ Enable in *Network & servers* settings. Read more Dahasını oku - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7241,11 +7406,19 @@ swipe action Kişi silinsin mi? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Anahtar Zinciri'ndeki parola silinsin mi? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7349,6 +7522,10 @@ swipe action Raporlar No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Gerekli @@ -7394,6 +7571,10 @@ swipe action Kullanıcı temasına sıfırla No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Yeni bir sohbet profili oluşturmak için uygulamayı yeniden başlatın @@ -7474,6 +7655,25 @@ swipe action Rol No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Üye rolü "%@" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Üye rolü "%@" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır. + No comment provided by engineer. + Run chat Sohbeti çalıştır @@ -7506,7 +7706,8 @@ swipe action Save Kaydet - alert button + alert action +alert button chat item action @@ -7523,6 +7724,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? Kabul ayarlarını kaydet? @@ -7538,6 +7743,10 @@ chat item action Kaydet ve grup üyelerine bildir No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7605,6 +7814,10 @@ chat item action Sunucular kaydedilsin mi? alert title + + Save webpage settings? + alert title + Save welcome message? Hoşgeldin mesajı kaydedilsin mi? @@ -7897,11 +8110,6 @@ chat item action Gönderici dosya gönderimini iptal etti. alert message - - Sender may have deleted the connection request. - Gönderici bağlantı isteğini silmiş olabilir. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7996,6 +8204,10 @@ chat item action Sunucu No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Sunucu operatör %@'ya eklendi. @@ -8299,6 +8511,10 @@ chat item action Geliştirici ayarlarını göster No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Son mesajları göster @@ -8329,6 +8545,31 @@ chat item action Göster: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8424,6 +8665,18 @@ chat item action SimpleX bağlantılarına izin verilmiyor No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation SimpleX tek kullanımlık davet @@ -8434,6 +8687,10 @@ chat item action SimpleX protokolleri Trail of Bits tarafından incelenmiştir. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8687,9 +8944,8 @@ Relay address was used to set up this relay for the channel. Abonelikler göz ardı edildi No comment provided by engineer. - - Support SimpleX Chat - SimpleX Chat'e destek ol + + Support the project No comment provided by engineer. @@ -8881,6 +9137,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Adres kısa olacak ve profiliniz bu adres üzerinden paylaşılacaktır. @@ -8910,6 +9182,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Veritabanı parolasını değiştirme girişimi tamamlanmadı. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. Taradığınız kod bir SimpleX bağlantı QR kodu değildir. @@ -9003,6 +9283,11 @@ your contacts and groups. Özlediğimiz ikinci tik! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Gönderici bağlantı isteğini silmiş olabilir. + No comment provided by engineer. + The sender will NOT be notified Gönderene BİLDİRİLMEYECEKTİR @@ -9056,6 +9341,10 @@ your contacts and groups. Bunlar kişi ve grup ayarlarında geçersiz kılınabilir. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Bu işlem geri alınamaz - alınan ve gönderilen tüm dosyalar ve medya silinecektir. Düşük çözünürlüklü resimler kalacaktır. @@ -9076,6 +9365,10 @@ your contacts and groups. Bu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri döndürülemez şekilde kaybolacaktır. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Bu sohbet uçtan uca şifreleme ile korunmaktadır. @@ -9229,6 +9522,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Sesli mesaj kaydetmek için lütfen Mikrofon kullanım izni verin. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Gizli profilinizi ortaya çıkarmak için **Sohbet profilleriniz** sayfasındaki arama alanına tam bir şifre girin. @@ -9264,6 +9561,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın). No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Bağlanırken gizli moda geçiş yap. @@ -9351,6 +9652,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Teslim edilmemiş mesajlar @@ -9411,13 +9716,6 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec iOS arama arayüzünü kullanmadığınız sürece, kesintileri önlemek için Rahatsız Etmeyin modunu etkinleştirin. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin. -Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin. - No comment provided by engineer. - Unlink Bağlantıyı Kaldır @@ -9448,18 +9746,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Okunmamış swipe action - - Unsupported channel name - alert title - Unsupported connection link Desteklenmeyen bağlantı bağlantısı conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9513,7 +9807,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Upgrade address? Adres güncellensin mi? - alert message + alert message +alert title Upgrade and open chat @@ -9687,6 +9982,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Web portunu kullan No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection Kullanıcı seçimi @@ -9706,6 +10005,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Bilgisayarla kodu doğrula @@ -9731,6 +10034,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Veritabanı parolasını doğrulayın No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Parolayı doğrula @@ -9886,6 +10193,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste WebRTC ICE sunucuları No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Hoşgeldin %@! @@ -10105,9 +10420,8 @@ Katılma isteği tekrarlansın mı? Daha sonra Ayarlardan etkinleştirebilirsin No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -10169,6 +10483,10 @@ Katılma isteği tekrarlansın mı? Sohbet listesinde %@ ile konuşmayı görüntülemeye devam edebilirsiniz. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. @@ -10354,6 +10672,10 @@ Bağlantı isteği tekrarlansın mı? SimpleX adresin No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact İş bağlantınız @@ -10368,11 +10690,6 @@ Bağlantı isteği tekrarlansın mı? Your channel No comment provided by engineer. - - Your chat database - Sohbet veritabanınız - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın. @@ -10403,6 +10720,13 @@ Bağlantı isteği tekrarlansın mı? İrtibat kişiniz No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin. +Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi. @@ -10567,6 +10891,10 @@ Relays can access channel messages. seni kabul etti rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10829,6 +11157,10 @@ marked deleted chat item preview text kişi kabul etmeli… No comment provided by engineer. + + contributor + member role + creator oluşturan @@ -11032,6 +11364,10 @@ pref value saat time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Anahtar Zinciri parolayı güvenli bir şekilde saklamak için kullanılır - anlık bildirimlerin alınmasını sağlar. @@ -11468,6 +11804,10 @@ son alınan msj: %2$@ çizik No comment provided by engineer. + + subscriber + member role + this contact Bu kişi @@ -11517,11 +11857,6 @@ son alınan msj: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -11673,7 +12008,7 @@ son alınan msj: %2$@
- +
@@ -11708,9 +12043,24 @@ son alınan msj: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11732,7 +12082,7 @@ son alınan msj: %2$@
- +
@@ -11764,7 +12114,7 @@ son alınan msj: %2$@
- +
@@ -11786,7 +12136,7 @@ son alınan msj: %2$@
- +
@@ -11969,9 +12319,8 @@ son alınan msj: %2$@ Yanlış veritabanı parolası No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json index 2e32ea2080..1aa8013358 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "tr", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 49f9e21eda..fe69dea6ab 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #секрет# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ підтримував SimpleX Chat. Термін дії значка вичерпався %2$@. + badge alert + %@ %@ @@ -85,6 +90,11 @@ %@ встановлено No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ інвестував в SimpleX Chat краудфандінг. + badge alert + %@ is connected! %@ підключено! @@ -110,6 +120,11 @@ %@ сервери No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ підтримує SimpleX Chat. + badge alert + %@ uploaded %@ завантажено @@ -185,18 +200,33 @@ %d місяців time interval + + %d owner + channel owners count + + + %d owners + channel owners count + + + %d owners & contributors + channel members count + %d relays failed + %d перемикач вийшов з ладу channel relay bar channel subscriber relay bar %d relays not active + %d перемикач не працює channel relay bar channel subscriber relay bar %d relays removed + %d перемикач видалений channel relay bar channel subscriber relay bar @@ -217,10 +247,12 @@ channel subscriber relay bar
%d subscriber + %d підписник channel subscriber count %d subscribers + %d підписники channel subscriber count @@ -230,36 +262,44 @@ channel subscriber relay bar %1$d/%2$d relays active + %1$d/%2$d перемикач активний channel creation progress channel relay bar progress %1$d/%2$d relays active, %3$d errors + %1$d/%2$d перемикач активний, %3$d помилки channel relay bar %1$d/%2$d relays active, %3$d failed + %1$d/%2$d перемикач активний, %3$d невдачно channel creation progress with errors channel relay bar %1$d/%2$d relays active, %3$d removed + %1$d/%2$d перемикач активний, %3$d видалено channel relay bar %1$d/%2$d relays connected + %1$d/%2$d перемикачі зʼєднані channel subscriber relay bar progress %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d перемикачі зʼєднані, %3$d помилки channel subscriber relay bar %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d перемикачі зʼєднані, %3$d невдачно channel subscriber relay bar %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d перемикачі зʼєднані, %3$d видалено channel subscriber relay bar @@ -274,6 +314,7 @@ channel relay bar %lld channel events + %lld події каналу No comment provided by engineer. @@ -378,6 +419,7 @@ channel relay bar (from owner) + (від власника) chat link info line @@ -385,10 +427,6 @@ channel relay bar (новий) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (цей пристрій v%@) @@ -436,6 +474,7 @@ channel relay bar **Test relay** to retrieve its name. + **Тестування перемикача** щоб дізнатися його назву. No comment provided by engineer. @@ -485,6 +524,9 @@ channel relay bar - opt-in to send link previews. - prevent hyperlink phishing. - remove link tracking. + - увімкнути надсилання попереднього перегляду посилань. +- запобігти фішингу за допомогою гіперпосилань. +- вимкнути відстеження посилань. No comment provided by engineer. @@ -577,7 +619,7 @@ time interval <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> <p>Привіт!</p> -<p><a href="%@"> Зв'яжіться зі мною через SimpleX Chat</a></p> +<p><a href="%@">Зв'яжіться зі мною через SimpleX Chat</a></p> email text @@ -587,6 +629,7 @@ time interval A link for one person to connect + Посилання для підключення однієї особи No comment provided by engineer. @@ -717,10 +760,20 @@ swipe action Add + Додати No comment provided by engineer. Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + Додайте адресу до свого профілю, щоб ваші контакти в SimpleX могли поділитися нею з іншими людьми. Інформація про оновлення профілю буде надіслана вашим контактам у SimpleX. + No comment provided by engineer. + + + Add contributors. + No comment provided by engineer. + + + Add description No comment provided by engineer. @@ -743,12 +796,19 @@ swipe action Додати профіль No comment provided by engineer. + + Add relay + Додати перемикач + No comment provided by engineer. + Add relays + Додати перемикачі No comment provided by engineer. Add relays to restore message delivery. + Додати перемикачі для відновлення доставки повідомлень. No comment provided by engineer. @@ -766,6 +826,11 @@ swipe action Додайте учасників команди No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + Додай цей код на твою сторінку. Це буде відображено для передперегляду твого каналу / групи. + No comment provided by engineer. + Add to another device Додати до іншого пристрою @@ -846,6 +911,11 @@ swipe action Розширені налаштування мережі No comment provided by engineer. + + Advanced options + Розширені параметри + No comment provided by engineer. + Advanced settings Додаткові налаштування @@ -888,6 +958,7 @@ swipe action All messages + Усі повідомлення No comment provided by engineer. @@ -917,10 +988,12 @@ swipe action All relays failed + Усі перемикачі провалилися No comment provided by engineer. All relays removed + Усі перемикачі видалені No comment provided by engineer. @@ -953,9 +1026,14 @@ swipe action Дозволити No comment provided by engineer. + + Allow anyone to embed + Дозволити будь-кому вбудовувати + No comment provided by engineer. + Allow calls only if your contact allows them. - Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх. + Дозволити дзвінки, тільки якщо ваш контакт дозволяє їх. No comment provided by engineer. @@ -975,6 +1053,7 @@ swipe action Allow files and media only if your contact allows them. + Дозволяйте доступ до файлів та мультимедіа лише в тому випадку, якщо ваш контакт на це дав згоду. No comment provided by engineer. @@ -984,6 +1063,7 @@ swipe action Allow members to chat with admins. + Дозволити учасникам спілкуватися в чаті з адміністраторами. No comment provided by engineer. @@ -1003,6 +1083,7 @@ swipe action Allow sending direct messages to subscribers. + Дозволити надсилання прямих повідомлень підписникам. No comment provided by engineer. @@ -1017,6 +1098,7 @@ swipe action Allow subscribers to chat with admins. + Дозволяє абонентам спілкуватися з адміністраторами. No comment provided by engineer. @@ -1076,6 +1158,7 @@ swipe action Allow your contacts to send files and media. + Дозволяє вашим контактам надсилати файли та медіа. No comment provided by engineer. @@ -1123,6 +1206,11 @@ swipe action Відповісти на дзвінок No comment provided by engineer. + + Any webpage can show the preview. + Будь-яка веб-сторінка може відображати попередній огляд. + No comment provided by engineer. + App build: %@ Збірка програми: %@ @@ -1165,6 +1253,7 @@ swipe action App update required + Потрібно оновити додаток alert title @@ -1259,6 +1348,7 @@ swipe action Audio call + Аудіодзвінок No comment provided by engineer. @@ -1331,6 +1421,10 @@ swipe action Поганий хеш повідомлення No comment provided by engineer. + + Badge cannot be verified + badge alert title + Be free in your network @@ -1349,6 +1443,10 @@ in your network Кращі дзвінки No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups Кращі групи @@ -1534,11 +1632,6 @@ in your network Дзвінок вже закінчився! No comment provided by engineer. - - Calls - Дзвінки - No comment provided by engineer. - Calls prohibited! Дзвінки заборонені! @@ -1649,11 +1742,6 @@ new chat action Зміна режиму блокування authentication reason - - Change member role? - Змінити роль учасника? - No comment provided by engineer. - Change passcode Змінити код доступу @@ -1674,6 +1762,10 @@ new chat action Змінити роль No comment provided by engineer. + + Change role? + No comment provided by engineer. + Change self-destruct mode Змінити режим самознищення @@ -1689,6 +1781,10 @@ set passcode view Channel No comment provided by engineer. + + Channel SimpleX name + No comment provided by engineer. + Channel display name No comment provided by engineer. @@ -1730,6 +1826,10 @@ alert subtitle Channel temporarily unavailable alert title + + Channel webpage + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! No comment provided by engineer. @@ -1771,6 +1871,10 @@ alert subtitle Консоль чату No comment provided by engineer. + + Chat data + No comment provided by engineer. + Chat database База даних чату @@ -2138,6 +2242,10 @@ server test step Підключайтеся швидше! 🚀 No comment provided by engineer. + + Connect to %@ + new chat action + Connect to desktop Підключення до комп'ютера @@ -2228,17 +2336,9 @@ This is your own one-time link! Connecting to desktop - Підключення до ПК + Підключення до компʼютера No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection Підключення @@ -2254,16 +2354,15 @@ This is your own one-time link! Підключення заблоковано No comment provided by engineer. + + Connection blocked: %@ + conn error description + Connection error Помилка підключення alert title - - Connection error (AUTH) - Помилка підключення (AUTH) - conn error description - Connection failed No comment provided by engineer. @@ -2275,6 +2374,11 @@ This is your own one-time link! %@ No comment provided by engineer. + + Connection link removed + Помилка підключення + conn error description + Connection not ready. Підключення не готове. @@ -2320,6 +2424,10 @@ This is your own one-time link! З'єднання No comment provided by engineer. + + Contact + No comment provided by engineer. + Contact address chat link info line @@ -2408,6 +2516,10 @@ This is your own one-time link! Копіювати No comment provided by engineer. + + Copy code + No comment provided by engineer. + Copy error Помилка копіювання @@ -2443,6 +2555,10 @@ This is your own one-time link! Створіть групу, використовуючи випадковий профіль. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + No comment provided by engineer. + Create file Створити файл @@ -2482,15 +2598,15 @@ This is your own one-time link! Create public channel No comment provided by engineer. - - Create public channel (BETA) - No comment provided by engineer. - Create queue Створити чергу server test step + + Create web preview. + No comment provided by engineer. + Create your address Створіть свою адресу @@ -3015,9 +3131,9 @@ alert button Настільні пристрої No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@. + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + Адреса сервера призначення %1$@ несумісна з налаштуваннями сервера пересилання %2$@. No comment provided by engineer. @@ -3025,9 +3141,9 @@ alert button Помилка сервера призначення: %@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - Версія сервера призначення %@ несумісна з версією сервера переадресації %@. + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + Версія сервера призначення %1$@ несумісна з версією сервера переадресації %2$@. No comment provided by engineer. @@ -3040,9 +3156,9 @@ alert button Деталі No comment provided by engineer. - - Develop - Розробник + + Developer + Інструменти для розробників No comment provided by engineer. @@ -3050,11 +3166,6 @@ alert button Можливості для розробників No comment provided by engineer. - - Developer tools - Інструменти для розробників - No comment provided by engineer. - Device Пристрій @@ -3198,6 +3309,10 @@ alert button Зробіть це пізніше No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. Не надсилайте історію новим користувачам. @@ -3232,6 +3347,10 @@ alert button Не пропускайте важливі повідомлення. No comment provided by engineer. + + Don't save + alert action + Don't show again Більше не показувати @@ -3312,6 +3431,10 @@ chat item action Easier to invite your friends 👋 No comment provided by engineer. + + Easier to read. + No comment provided by engineer. + Edit Редагувати @@ -3321,6 +3444,10 @@ chat item action Edit channel profile No comment provided by engineer. + + Edit description + No comment provided by engineer. + Edit group profile Редагування профілю групи @@ -3517,6 +3644,10 @@ chat item action Введіть правильну парольну фразу. No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… Введіть назву групи… @@ -3555,6 +3686,10 @@ chat item action Введіть назву пристрою… No comment provided by engineer. + + Enter webpage URL + No comment provided by engineer. + Enter welcome message… Введіть вітальне повідомлення… @@ -3573,7 +3708,7 @@ chat item action Error Помилка - conn error description + No comment provided by engineer. Error aborting address change @@ -3869,6 +4004,10 @@ chat item action Помилка збереження профілю групи No comment provided by engineer. + + Error saving name + alert title + Error saving passcode Помилка збереження пароля @@ -3923,6 +4062,10 @@ chat item action Помилка встановлення підтвердження доставлення! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel alert title @@ -4001,6 +4144,7 @@ chat item action Error: %@ Помилка: %@ alert message +conn error description file error text snd error text @@ -4143,6 +4287,14 @@ server test error Помилка файлового сервера: %@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status Статус файлу @@ -4438,6 +4590,10 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link relay test step @@ -4546,6 +4702,10 @@ Error: %2$@ Профіль групи було змінено. Якщо ви збережете його, оновлений профіль буде надіслано учасникам групи. alert message + + Group webpage + No comment provided by engineer. + Group welcome message Привітальне повідомлення групи @@ -4571,6 +4731,10 @@ Error: %2$@ Довідка No comment provided by engineer. + + Help & support + No comment provided by engineer. + Help admins moderating their groups. Допоможіть адміністраторам модерувати їхні групи. @@ -4650,6 +4814,10 @@ Error: %2$@ Як зробити No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it Як ним користуватися @@ -5049,6 +5217,10 @@ More improvements are coming soon! Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@). No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + No comment provided by engineer. + Italian interface Італійський інтерфейс @@ -5073,6 +5245,10 @@ More improvements are coming soon! Join channel No comment provided by engineer. + + Join channel %@ + new chat action + Join group Приєднуйтесь до групи @@ -5153,7 +5329,7 @@ This is your link for group %@! Learn more Дізнайтеся більше - No comment provided by engineer. + badge alert button Leave @@ -5193,6 +5369,14 @@ This is your link for group %@! Менше трафіку в мобільних мережах. No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + No comment provided by engineer. + + + Let people join via name registered with this channel link. + No comment provided by engineer. + Let someone connect to you No comment provided by engineer. @@ -5300,6 +5484,10 @@ This is your link for group %@! Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються. No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone Позначити видалено для всіх @@ -5367,21 +5555,6 @@ This is your link for group %@! Повідомлення учасників chat feature - - Member role will be changed to "%@". All chat members will be notified. - Роль учасника буде змінено на "%@". Усі учасники чату отримають сповіщення. - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - Роль учасника буде змінено на "%@". Учасник отримає нове запрошення. - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! Учасника буде видалено з чату – це неможливо скасувати! @@ -5525,6 +5698,14 @@ This is your link for group %@! Форма повідомлення No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. Джерело повідомлення залишається приватним. @@ -5692,6 +5873,10 @@ This is your link for group %@! Незабаром буде ще більше покращень! No comment provided by engineer. + + More privacy + No comment provided by engineer. + More reliable network connection. Більш надійне з'єднання з мережею. @@ -5732,6 +5917,10 @@ This is your link for group %@! Ім'я swipe action + + Name not found + No comment provided by engineer. + Network & servers Мережа та сервери @@ -6053,6 +6242,10 @@ The most secure encryption. Немає серверів для отримання повідомлень. servers error + + No servers to resolve names. + servers warning + No servers to send files. Немає серверів для надсилання файлів. @@ -6068,6 +6261,10 @@ The most secure encryption. Немає непрочитаних чатів No comment provided by engineer. + + No valid link + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. No comment provided by engineer. @@ -6076,6 +6273,10 @@ The most secure encryption. Non-profit governance No comment provided by engineer. + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + No comment provided by engineer. + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. No comment provided by engineer. @@ -6294,6 +6495,10 @@ Requires compatible VPN. Тільки ваш контакт може надсилати голосові повідомлення. No comment provided by engineer. + + Only your page above can show the preview. + No comment provided by engineer. + Open Відкрито @@ -6470,8 +6675,8 @@ alert button Owner No comment provided by engineer. - - Owners + + Owners & contributors No comment provided by engineer. @@ -6661,10 +6866,6 @@ Error: %@ Будь ласка, спробуйте вимкнути та знову увімкнути сповіщення. token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи. @@ -6723,11 +6924,6 @@ Error: %@ Раніше підключені сервери No comment provided by engineer. - - Privacy & security - Конфіденційність і безпека - No comment provided by engineer. - Privacy for your customers. Конфіденційність для ваших клієнтів. @@ -6813,7 +7009,8 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + alert message +alert title Prohibit audio/video calls. @@ -6929,6 +7126,10 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 No comment provided by engineer. + + Public names for your channel or business. + No comment provided by engineer. + Push notifications Push-сповіщення @@ -6967,7 +7168,7 @@ Enable in *Network & servers* settings. Read more Читати далі - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7215,11 +7416,19 @@ swipe action Видалити учасника? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? Видалити парольну фразу з брелока? No comment provided by engineer. + + Remove relay + No comment provided by engineer. + Remove relay? alert title @@ -7323,6 +7532,10 @@ swipe action Звіти No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required Потрібно @@ -7368,6 +7581,10 @@ swipe action Повернутися до теми користувача No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустіть програму, щоб створити новий профіль чату @@ -7448,6 +7665,25 @@ swipe action Роль No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + Роль учасника буде змінено на "%@". Усі учасники чату отримають сповіщення. + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + Роль учасника буде змінено на "%@". Учасник отримає нове запрошення. + No comment provided by engineer. + Run chat Запустити чат @@ -7480,7 +7716,8 @@ swipe action Save Зберегти - alert button + alert action +alert button chat item action @@ -7497,6 +7734,10 @@ chat item action Save (and notify subscribers) alert button + + Save SimpleX name? + alert title + Save admission settings? Зберегти налаштування входу? @@ -7512,6 +7753,10 @@ chat item action Зберегти та повідомити учасників групи No comment provided by engineer. + + Save and notify members + No comment provided by engineer. + Save and notify subscribers No comment provided by engineer. @@ -7579,6 +7824,10 @@ chat item action Зберегти сервери? alert title + + Save webpage settings? + alert title + Save welcome message? Зберегти вітальне повідомлення? @@ -7871,11 +8120,6 @@ chat item action Відправник скасував передачу файлу. alert message - - Sender may have deleted the connection request. - Можливо, відправник видалив запит на підключення. - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. alert message @@ -7970,6 +8214,10 @@ chat item action Сервер No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. Сервер додано до оператора %@. @@ -8273,6 +8521,10 @@ chat item action Показати опції розробника No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages Показати останні повідомлення @@ -8303,6 +8555,31 @@ chat item action Показати: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8398,6 +8675,18 @@ chat item action Посилання SimpleX заборонені No comment provided by engineer. + + SimpleX name + No comment provided by engineer. + + + SimpleX name error + No comment provided by engineer. + + + SimpleX name not verified + alert title + SimpleX one-time invitation Одноразове запрошення SimpleX @@ -8408,6 +8697,10 @@ chat item action Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address simplex link type @@ -8661,9 +8954,8 @@ Relay address was used to set up this relay for the channel. Підписки ігноруються No comment provided by engineer. - - Support SimpleX Chat - Підтримка чату SimpleX + + Support the project No comment provided by engineer. @@ -8854,6 +9146,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. Адреса буде короткою, і ваш профіль буде доступний за цією адресою. @@ -8883,6 +9191,14 @@ It can happen because of some bug or when the connection is compromised.Спроба змінити пароль до бази даних не була завершена. No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. Відсканований вами код не є QR-кодом посилання SimpleX. @@ -8976,6 +9292,11 @@ your contacts and groups. Другу галочку ми пропустили! ✅ No comment provided by engineer. + + The sender deleted the connection request. + Можливо, відправник видалив запит на підключення. + No comment provided by engineer. + The sender will NOT be notified Відправник НЕ буде повідомлений @@ -9029,6 +9350,10 @@ your contacts and groups. Їх можна перевизначити в налаштуваннях контактів і груп. No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться. @@ -9049,6 +9374,10 @@ your contacts and groups. Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені. No comment provided by engineer. + + This badge could not be verified and may not be genuine. + badge alert + This chat is protected by end-to-end encryption. Цей чат захищений наскрізним шифруванням. @@ -9201,6 +9530,10 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону. No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**. @@ -9235,6 +9568,10 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях. No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. Увімкніть інкогніто при підключенні. @@ -9322,6 +9659,10 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? No comment provided by engineer. + + Unconfirmed name + No comment provided by engineer. + Undelivered messages Недоставлені повідомлення @@ -9382,13 +9723,6 @@ You will be prompted to complete authentication before this feature is enabled.< Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань. No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це. -Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею. - No comment provided by engineer. - Unlink Роз'єднати зв'язок @@ -9419,18 +9753,14 @@ To connect, please ask your contact to create another connection link and check Непрочитане swipe action - - Unsupported channel name - alert title - Unsupported connection link Несумісне посилання для підключення conn error description - - Unsupported contact name - alert title + + Unverified badge + badge alert title Up to 100 last messages are sent to new members. @@ -9484,7 +9814,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? Змінити адресу? - alert message + alert message +alert title Upgrade and open chat @@ -9658,6 +9989,10 @@ To connect, please ask your contact to create another connection link and check Використовувати веб-порт No comment provided by engineer. + + Used chat relays do not support webpages. + No comment provided by engineer. + User selection Вибір користувача @@ -9677,6 +10012,10 @@ To connect, please ask your contact to create another connection link and check Verify relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop Перевірте код на робочому столі @@ -9702,6 +10041,10 @@ To connect, please ask your contact to create another connection link and check Перевірте пароль до бази даних No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase Підтвердіть парольну фразу @@ -9857,6 +10200,14 @@ To connect, please ask your contact to create another connection link and check Сервери WebRTC ICE No comment provided by engineer. + + Webpage code + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + alert message + Welcome %@! Ласкаво просимо %@! @@ -10076,9 +10427,8 @@ Repeat join request? Ви можете увімкнути пізніше в Налаштуваннях No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми. + + You can enable them later via app Your privacy settings. No comment provided by engineer. @@ -10140,6 +10490,10 @@ Repeat join request? Ви все ще можете переглянути розмову з %@ у списку чатів. No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + badge alert + You can turn on SimpleX Lock via Settings. Увімкнути SimpleX Lock можна в Налаштуваннях. @@ -10325,6 +10679,10 @@ Repeat connection request? Ваша адреса SimpleX No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact Ваш діловий контакт @@ -10339,11 +10697,6 @@ Repeat connection request? Your channel No comment provided by engineer. - - Your chat database - Ваша база даних чату - No comment provided by engineer. - Your chat database is not encrypted - set passphrase to encrypt it. Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її. @@ -10374,6 +10727,13 @@ Repeat connection request? Ваш контакт No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це. +Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею. + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@). @@ -10538,6 +10898,10 @@ Relays can access channel messages. прийняв(ла) вас rcv group event chat item + + acknowledged roster + No comment provided by engineer. + active No comment provided by engineer. @@ -10800,6 +11164,10 @@ marked deleted chat item preview text контакт повинен прийняти… No comment provided by engineer. + + contributor + member role + creator творець @@ -11003,6 +11371,10 @@ pref value години time unit + + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS Keychain використовується для безпечного зберігання пароля - це дає змогу отримувати миттєві повідомлення. @@ -11095,6 +11467,7 @@ pref value link + посилання No comment provided by engineer. @@ -11437,6 +11810,10 @@ last received msg: %2$@ закреслено No comment provided by engineer. + + subscriber + member role + this contact цей контакт @@ -11486,11 +11863,6 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ relay hostname @@ -11642,7 +12014,7 @@ last received msg: %2$@
- +
@@ -11677,9 +12049,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11701,7 +12088,7 @@ last received msg: %2$@
- +
@@ -11733,7 +12120,7 @@ last received msg: %2$@
- +
@@ -11755,7 +12142,7 @@ last received msg: %2$@
- +
@@ -11938,9 +12325,8 @@ last received msg: %2$@ Неправильна ключова фраза до бази даних No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock. + + You can allow sharing in Your privacy / SimpleX Lock settings. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json index a93c702952..c6053c573c 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "uk", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file 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 8823f17204..1f6067fe53 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 @@ -2,7 +2,7 @@
- +
@@ -35,6 +35,11 @@ #秘密# No comment provided by engineer. + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ 曾是 SimpleX Chat 支持者。徽章已于 %2$@ 过期。 + badge alert + %@ %@ @@ -67,12 +72,12 @@ %@ and %@ connected - %@ 和%@ 以建立连接 + %@ 和%@ 已建立连接 No comment provided by engineer. %1$@ at %2$@: - @ %2$@: + %1$@ 于 %2$@: copied message info, <sender> at <time> @@ -85,6 +90,11 @@ %@ 已下载 No comment provided by engineer. + + %@ invested in SimpleX Chat crowdfunding. + %@ 出资支持了 SimpleX Chat 的众筹。 + badge alert + %@ is connected! %@ 已连接! @@ -110,6 +120,11 @@ 服务器 No comment provided by engineer. + + %@ supports SimpleX Chat. + %@ 是 SimpleX Chat 支持者。 + badge alert + %@ uploaded %@ 已上传 @@ -185,18 +200,36 @@ %d 月 time interval + + %d owner + %d位所有者 + channel owners count + + + %d owners + %d位所有者 + channel owners count + + + %d owners & contributors + %d位所有者和贡献者 + channel members count + %d relays failed + %d 个中继失败 channel relay bar channel subscriber relay bar %d relays not active + %d 个中继未启用 channel relay bar channel subscriber relay bar %d relays removed + %d 个中继已移除 channel relay bar channel subscriber relay bar @@ -217,10 +250,12 @@ channel subscriber relay bar
%d subscriber + %d 位订阅者 channel subscriber count %d subscribers + %d 位订阅者 channel subscriber count @@ -230,36 +265,44 @@ channel subscriber relay bar %1$d/%2$d relays active + %1$d/%2$d 个中继已启用 channel creation progress channel relay bar progress %1$d/%2$d relays active, %3$d errors + %1$d/%2$d 个中继已启用,%3$d 个错误 channel relay bar %1$d/%2$d relays active, %3$d failed + %1$d/%2$d 个中继已启用,%3$d 个失败 channel creation progress with errors channel relay bar %1$d/%2$d relays active, %3$d removed + %1$d/%2$d 个中继已启用,%3$d 个已移除 channel relay bar %1$d/%2$d relays connected + %1$d/%2$d 个中继已连接 channel subscriber relay bar progress %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d 个中继已连接,%3$d 个错误 channel subscriber relay bar %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d 个中继已连接,%3$d 个失败 channel subscriber relay bar %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d 个中继已连接,%3$d 个已移除 channel subscriber relay bar @@ -274,6 +317,7 @@ channel relay bar %lld channel events + %lld 个频道事件 No comment provided by engineer. @@ -378,6 +422,7 @@ channel relay bar (from owner) + (来自所有者) chat link info line @@ -385,10 +430,6 @@ channel relay bar (新) No comment provided by engineer. - - (signed) - chat link info line - (this device v%@) (此设备 v%@) @@ -436,6 +477,7 @@ channel relay bar **Test relay** to retrieve its name. + **测试中继**,获取其名称。 No comment provided by engineer. @@ -485,6 +527,9 @@ channel relay bar - opt-in to send link previews. - prevent hyperlink phishing. - remove link tracking. + - 选择是否发送链接预览。 +- 防止超链接钓鱼。 +- 移除链接跟踪。 No comment provided by engineer. @@ -577,7 +622,7 @@ time interval <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> <p>你好!</p> -<p><a href="%@">通过 SimpleX Chat </a></p>与我联系 +<p><a href="%@">通过 SimpleX Chat 联系我</a></p> email text @@ -587,6 +632,7 @@ time interval A link for one person to connect + 供一人连接的链接 No comment provided by engineer. @@ -717,10 +763,20 @@ swipe action Add + 添加 No comment provided by engineer. Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + 将地址添加到你的个人资料,让你的 SimpleX 联系人可以与其他人分享。个人资料更新将发送给你的 SimpleX 联系人。 + No comment provided by engineer. + + + Add contributors. + No comment provided by engineer. + + + Add description No comment provided by engineer. @@ -743,12 +799,19 @@ swipe action 添加个人资料 No comment provided by engineer. + + Add relay + 添加中继 + No comment provided by engineer. + Add relays + 添加中继 No comment provided by engineer. Add relays to restore message delivery. + 添加中继来恢复消息传送。 No comment provided by engineer. @@ -766,6 +829,11 @@ swipe action 添加团队成员 No comment provided by engineer. + + Add this code to your webpage. It will display the preview of your channel / group. + 将此代码添加到你的网页。它会显示你的频道 / 群组预览。 + No comment provided by engineer. + Add to another device 添加另一设备 @@ -846,6 +914,11 @@ swipe action 高级网络设置 No comment provided by engineer. + + Advanced options + 高级选项 + No comment provided by engineer. + Advanced settings 高级设置 @@ -918,10 +991,12 @@ swipe action All relays failed + 所有中继均失败 No comment provided by engineer. All relays removed + 所有中继均已移除 No comment provided by engineer. @@ -954,6 +1029,11 @@ swipe action 允许 No comment provided by engineer. + + Allow anyone to embed + 允许任何人嵌入 + No comment provided by engineer. + Allow calls only if your contact allows them. 仅当您的联系人允许时才允许呼叫。 @@ -986,6 +1066,7 @@ swipe action Allow members to chat with admins. + 允许成员与管理员聊天。 No comment provided by engineer. @@ -1005,6 +1086,7 @@ swipe action Allow sending direct messages to subscribers. + 允许向订阅者发送直接消息。 No comment provided by engineer. @@ -1019,6 +1101,7 @@ swipe action Allow subscribers to chat with admins. + 允许订阅者与管理员聊天。 No comment provided by engineer. @@ -1126,6 +1209,11 @@ swipe action 接听来电 No comment provided by engineer. + + Any webpage can show the preview. + 任何网页都可以显示预览。 + No comment provided by engineer. + App build: %@ 应用程序构建:%@ @@ -1168,6 +1256,7 @@ swipe action App update required + 需要更新应用程序 alert title @@ -1335,9 +1424,16 @@ swipe action 错误消息散列 No comment provided by engineer. + + Badge cannot be verified + 无法验证徽章 + badge alert title + Be free in your network + 在你的网络中 +保持自由 No comment provided by engineer. @@ -1355,6 +1451,10 @@ in your network 更佳的通话 No comment provided by engineer. + + Better channels 📢 + No comment provided by engineer. + Better groups 更佳的群组 @@ -1447,6 +1547,7 @@ in your network Block subscriber for all? + 要为所有人封锁订阅者吗? No comment provided by engineer. @@ -1501,10 +1602,12 @@ in your network Bottom bar + 底部栏 No comment provided by engineer. Broadcast + 广播 compose placeholder for channel owner @@ -1542,11 +1645,6 @@ in your network 通话已结束! No comment provided by engineer. - - Calls - 通话 - No comment provided by engineer. - Calls prohibited! 禁止来电! @@ -1596,10 +1694,12 @@ new chat action Cancel and delete channel + 取消并删除频道 No comment provided by engineer. Cancel creating channel? + 要取消创建频道吗? alert title @@ -1657,11 +1757,6 @@ new chat action 更改锁定模式 authentication reason - - Change member role? - 更改成员角色? - No comment provided by engineer. - Change passcode 更改密码 @@ -1682,6 +1777,11 @@ new chat action 改变角色 No comment provided by engineer. + + Change role? + 改变角色? + No comment provided by engineer. + Change self-destruct mode 更改自毁模式 @@ -1695,63 +1795,87 @@ set passcode view Channel + 频道 + No comment provided by engineer. + + + Channel SimpleX name No comment provided by engineer. Channel display name + 频道显示名称 No comment provided by engineer. Channel full name (optional) + 频道全名(可选) No comment provided by engineer. Channel has no active relays. Please try to join later. + 频道没有已启用的中继。请稍后再尝试加入。 alert message alert subtitle Channel image + 频道图片 No comment provided by engineer. Channel link + 频道链接 chat link info line Channel preferences + 频道偏好设置 No comment provided by engineer. Channel profile + 频道资料 No comment provided by engineer. Channel profile is stored on subscribers' devices and on the chat relays. + 频道资料会存储在订阅者的设备和聊天中继上。 No comment provided by engineer. Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + 频道资料已更改。如果保存,更新后的资料将发送给频道订阅者。 alert message Channel temporarily unavailable + 频道暂时不可用 alert title + + Channel webpage + 频道网页 + No comment provided by engineer. + Channel will be deleted for all subscribers - this cannot be undone! + 将为所有订阅者删除频道-此操作无法撤销! No comment provided by engineer. Channel will be deleted for you - this cannot be undone! + 频道将为你删除,且无法撤消! No comment provided by engineer. Channel will start working with %1$d of %2$d relays. Continue? + 频道将以 %1$d/%2$d 个中继开始运行。要继续吗? alert message Channels + 频道 No comment provided by engineer. @@ -1779,6 +1903,11 @@ alert subtitle 聊天控制台 No comment provided by engineer. + + Chat data + 聊天数据 + No comment provided by engineer. + Chat database 聊天数据库 @@ -1841,18 +1970,22 @@ alert subtitle Chat relay + 聊天中继 No comment provided by engineer. Chat relays + 聊天中继 No comment provided by engineer. Chat relays forward messages in channels you create. + 聊天中继会转发你创建的频道中的消息。 No comment provided by engineer. Chat relays forward messages to channel subscribers. + 聊天中继会将消息转发给频道订阅者。 No comment provided by engineer. @@ -1883,7 +2016,7 @@ chat toolbar Chat with members before they join. - 在成员加入前和这些人聊天 + 在成员加入前与其聊天。 No comment provided by engineer. @@ -1893,10 +2026,12 @@ chat toolbar Chats with admins are prohibited. + 禁止与管理员聊天。 No comment provided by engineer. Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + 与管理员在公开频道中聊天没有端到端加密 — 请只在受信任聊天中继中使用。 alert message @@ -1906,6 +2041,7 @@ chat toolbar Chats with members are disabled + 禁止与成员聊天 No comment provided by engineer. @@ -1920,10 +2056,12 @@ chat toolbar Check relay address and try again. + 请检查中继地址并重试。 alert message Check relay name and try again. + 请检查中继名称并重试。 alert message @@ -2073,6 +2211,7 @@ chat toolbar Configure relays + 配置中继 No comment provided by engineer. @@ -2146,6 +2285,11 @@ server test step 更快地连接!🚀 No comment provided by engineer. + + Connect to %@ + 连接到%@ + new chat action + Connect to desktop 连接到桌面 @@ -2182,6 +2326,7 @@ This is your own one-time link! Connect via link or QR code + 通过链接或二维码连接 No comment provided by engineer. @@ -2239,14 +2384,6 @@ This is your own one-time link! 正连接到桌面 No comment provided by engineer. - - Connecting via channel name requires a newer app version. - alert message - - - Connecting via contact name requires a newer app version. - alert message - Connection 连接 @@ -2262,26 +2399,33 @@ This is your own one-time link! 连接被阻止 No comment provided by engineer. + + Connection blocked: %@ + 连接被阻止:%@ + conn error description + Connection error 连接错误 alert title - - Connection error (AUTH) - 连接错误(AUTH) - conn error description - Connection failed + 连接失败 No comment provided by engineer. Connection is blocked by server operator: %@ - 连接被运营方 %@ 阻止 + 连接已被运营方阻止: +%@ No comment provided by engineer. + + Connection link removed + 连接错误 + conn error description + Connection not ready. 连接未就绪。 @@ -2327,8 +2471,14 @@ This is your own one-time link! 连接 No comment provided by engineer. + + Contact + 联系人 + No comment provided by engineer. + Contact address + 联系地址 chat link info line @@ -2416,6 +2566,11 @@ This is your own one-time link! 复制 No comment provided by engineer. + + Copy code + 复制代码 + No comment provided by engineer. + Copy error 复制错误 @@ -2451,6 +2606,11 @@ This is your own one-time link! 使用随机身份创建群组. No comment provided by engineer. + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + 创建网页,在访客订阅前向他们显示你的频道预览。你可以自行托管,或使用任何静态托管服务。 + No comment provided by engineer. + Create file 创建文件 @@ -2488,10 +2648,7 @@ This is your own one-time link! Create public channel - No comment provided by engineer. - - - Create public channel (BETA) + 创建公开频道 No comment provided by engineer. @@ -2499,6 +2656,10 @@ This is your own one-time link! 创建队列 server test step + + Create web preview. + No comment provided by engineer. + Create your address 创建地址 @@ -2506,6 +2667,7 @@ This is your own one-time link! Create your link + 创建你的链接 No comment provided by engineer. @@ -2515,6 +2677,7 @@ This is your own one-time link! Create your public address + 创建你的公开地址 No comment provided by engineer. @@ -2539,6 +2702,7 @@ This is your own one-time link! Creating channel + 正在创建频道 No comment provided by engineer. @@ -2701,6 +2865,7 @@ This is your own one-time link! Decode link + 解码链接 relay test step @@ -2751,10 +2916,12 @@ swipe action Delete channel + 删除频道 No comment provided by engineer. Delete channel? + 要删除频道吗? No comment provided by engineer. @@ -2839,6 +3006,7 @@ swipe action Delete from history + 从历史记录中删除 No comment provided by engineer. @@ -2883,6 +3051,7 @@ swipe action Delete member messages? + 要删除成员消息吗? alert title @@ -2933,6 +3102,7 @@ alert button Delete relay + 删除中继 No comment provided by engineer. @@ -3025,9 +3195,9 @@ alert button 桌面设备 No comment provided by engineer. - - Destination server address of %@ is incompatible with forwarding server %@ settings. - 目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。 + + Destination server address of %1$@ is incompatible with forwarding server %2$@ settings. + 目标服务器地址 %1$@ 与转发服务器 %2$@ 设置不兼容。 No comment provided by engineer. @@ -3035,9 +3205,9 @@ alert button 目标服务器错误:%@ snd error text - - Destination server version of %@ is incompatible with forwarding server %@. - 目标服务器版本 %@ 与转发服务器 %@ 不兼容。 + + Destination server version of %1$@ is incompatible with forwarding server %2$@. + 目标服务器版本 %1$@ 与转发服务器 %2$@ 不兼容。 No comment provided by engineer. @@ -3050,9 +3220,9 @@ alert button 详细信息 No comment provided by engineer. - - Develop - 开发 + + Developer + 开发者工具 No comment provided by engineer. @@ -3060,11 +3230,6 @@ alert button 开发者选项 No comment provided by engineer. - - Developer tools - 开发者工具 - No comment provided by engineer. - Device 设备 @@ -3102,10 +3267,12 @@ alert button Direct messages between subscribers are prohibited. + 禁止订阅者之间发送直接消息。 No comment provided by engineer. Disable + 停用 alert button @@ -3208,6 +3375,10 @@ alert button 稍后再做 No comment provided by engineer. + + Do not require signing messages. + No comment provided by engineer. + Do not send history to new members. 不给新成员发送历史消息。 @@ -3215,6 +3386,7 @@ alert button Do not send history to new subscribers. + 不要将历史记录发送给新订阅者。 No comment provided by engineer. @@ -3242,6 +3414,10 @@ alert button 不错过重要消息。 No comment provided by engineer. + + Don't save + alert action + Don't show again 不再显示 @@ -3320,6 +3496,11 @@ chat item action Easier to invite your friends 👋 + 邀请好友更简单 👋 + No comment provided by engineer. + + + Easier to read. No comment provided by engineer. @@ -3329,6 +3510,11 @@ chat item action Edit channel profile + 编辑频道简介 + No comment provided by engineer. + + + Edit description No comment provided by engineer. @@ -3368,6 +3554,7 @@ chat item action Enable at least one chat relay in Network & Servers. + 请在「网络与服务器」中启用至少一个聊天中继。 channel creation warning @@ -3382,6 +3569,7 @@ chat item action Enable chats with admins? + 要启用与管理员聊天吗? alert title @@ -3406,6 +3594,7 @@ chat item action Enable link previews? + 要启用链接预览吗? alert title @@ -3520,6 +3709,7 @@ chat item action Enter channel name… + 输入频道名称… No comment provided by engineer. @@ -3527,6 +3717,10 @@ chat item action 输入正确密码。 No comment provided by engineer. + + Enter description (optional) + placeholder + Enter group name… 输入组名称… @@ -3549,10 +3743,12 @@ chat item action Enter profile name... + 输入个人资料名称... No comment provided by engineer. Enter relay name… + 输入中继名称… No comment provided by engineer. @@ -3565,6 +3761,11 @@ chat item action 输入此设备名… No comment provided by engineer. + + Enter webpage URL + 请输入网址 + No comment provided by engineer. + Enter welcome message… 输入欢迎消息…… @@ -3583,7 +3784,7 @@ chat item action Error 错误 - conn error description + No comment provided by engineer. Error aborting address change @@ -3612,10 +3813,12 @@ chat item action Error adding relay + 添加中继时出错 alert title Error adding relays + 添加中继时出错 alert title @@ -3670,6 +3873,7 @@ chat item action Error connecting to the server used to receive messages from this connection: %@ + 连接用于接收此连接消息的服务器时出错:%@ subscription status explanation @@ -3679,6 +3883,7 @@ chat item action Error creating channel + 创建频道时出错 alert title @@ -3748,6 +3953,7 @@ chat item action Error deleting message + 删除消息时出错 alert title @@ -3867,6 +4073,7 @@ chat item action Error saving channel profile + 保存频道资料时出错 No comment provided by engineer. @@ -3879,6 +4086,10 @@ chat item action 保存群组资料错误 No comment provided by engineer. + + Error saving name + alert title + Error saving passcode 保存密码错误 @@ -3934,8 +4145,13 @@ chat item action 设置送达回执出错! No comment provided by engineer. + + Error sharing address + alert title + Error sharing channel + 分享频道时出错 alert title @@ -4012,6 +4228,7 @@ chat item action Error: %@ 错误: %@ alert message +conn error description file error text snd error text @@ -4155,6 +4372,14 @@ server test error 文件服务器错误:%@ file error text + + File servers + No comment provided by engineer. + + + File servers: %@ + copied message info + File status 文件状态 @@ -4262,7 +4487,7 @@ server test error Fingerprint in server address does not match certificate. - 服务器地址中的证书指纹可能不正确 + 服务器地址中的指纹与证书不符。 relay test error server test error @@ -4308,6 +4533,7 @@ server test error For anyone to reach you + 让任何人都能联系你 No comment provided by engineer. @@ -4455,8 +4681,13 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Get SimpleX name (BETA) + No comment provided by engineer. + Get link + 获取链接 relay test step @@ -4466,6 +4697,7 @@ Error: %2$@ Get started + 开始使用 No comment provided by engineer. @@ -4563,6 +4795,11 @@ Error: %2$@ 群资料已修改。如果你进行保存,修改后的群资料将发送给其他群成员。 alert message + + Group webpage + 群组网页 + No comment provided by engineer. + Group welcome message 群欢迎词 @@ -4588,6 +4825,11 @@ Error: %2$@ 帮助 No comment provided by engineer. + + Help & support + 帮助与支持 + No comment provided by engineer. + Help admins moderating their groups. 帮助管理员管理群组。 @@ -4640,6 +4882,7 @@ Error: %2$@ History is not sent to new subscribers. + 历史记录不会发送给新订阅者。 No comment provided by engineer. @@ -4667,6 +4910,10 @@ Error: %2$@ 如何 No comment provided by engineer. + + How to register a test name + No comment provided by engineer. + How to use it 如何使用它 @@ -4709,6 +4956,7 @@ Error: %2$@ If you joined or created channels, they will stop working permanently. + 如果你加入或创建了频道,它们将永久停止工作。 down migration warning @@ -4967,10 +5215,12 @@ More improvements are coming soon! Invalid relay address! + 中继地址无效! alert title Invalid relay name! + 中继名称无效! alert title @@ -5010,6 +5260,7 @@ More improvements are coming soon! Invite someone privately + 私下邀请某人 No comment provided by engineer. @@ -5068,6 +5319,11 @@ More improvements are coming soon! 您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。 No comment provided by engineer. + + It will be shown to subscribers and used to allow loading the preview. + 它会显示给订阅者,并用于允许加载预览。 + No comment provided by engineer. + Italian interface 意大利语界面 @@ -5090,8 +5346,14 @@ More improvements are coming soon! Join channel + 加入频道 No comment provided by engineer. + + Join channel %@ + 加入频道%@ + new chat action + Join group 加入群组 @@ -5172,7 +5434,7 @@ This is your link for group %@! Learn more 了解更多 - No comment provided by engineer. + badge alert button Leave @@ -5181,10 +5443,12 @@ This is your link for group %@! Leave channel + 离开频道 No comment provided by engineer. Leave channel? + 要离开频道吗? No comment provided by engineer. @@ -5212,8 +5476,19 @@ This is your link for group %@! 消耗更少的移动网络数据。 No comment provided by engineer. + + Let people connect to you via name registered with your SimpleX address. + 让别人通过用你的 SimpleX 地址注册的名称和你建立联系。 + No comment provided by engineer. + + + Let people join via name registered with this channel link. + 让别人通过用这个频道链接注册的名称加入。 + No comment provided by engineer. + Let someone connect to you + 让某人连接到你 No comment provided by engineer. @@ -5238,6 +5513,7 @@ This is your link for group %@! Link signature verified. + 链接签名已验证。 owner verification @@ -5320,6 +5596,10 @@ This is your link for group %@! 确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。 No comment provided by engineer. + + Manage your relays. + No comment provided by engineer. + Mark deleted for everyone 标记为所有人已删除 @@ -5382,6 +5662,7 @@ This is your link for group %@! Member messages will be deleted - this cannot be undone! + 成员消息将被删除,且无法撤消! alert message @@ -5389,21 +5670,6 @@ This is your link for group %@! 成员举报 chat feature - - Member role will be changed to "%@". All chat members will be notified. - 将变更成员角色为“%@”。所有成员都会收到通知。 - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - 成员角色将更改为 "%@"。所有群成员将收到通知。 - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - 成员角色将更改为 "%@"。该成员将收到一份新的邀请。 - No comment provided by engineer. - Member will be removed from chat - this cannot be undone! 将从聊天中删除成员 - 此操作无法撤销! @@ -5426,6 +5692,7 @@ This is your link for group %@! Members can chat with admins. + 成员可以与管理员聊天。 No comment provided by engineer. @@ -5495,6 +5762,7 @@ This is your link for group %@! Message error + 消息错误 No comment provided by engineer. @@ -5547,6 +5815,14 @@ This is your link for group %@! 消息形状 No comment provided by engineer. + + Message signing is not required. + No comment provided by engineer. + + + Message signing is required. + No comment provided by engineer. + Message source remains private. 消息来源保持私密。 @@ -5594,10 +5870,12 @@ This is your link for group %@! Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + 此频道中的消息**并非端到端加密**。聊天中继可以看到这些消息。 No comment provided by engineer. Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + 此频道中的消息并非端到端加密。聊天中继可以看到这些消息。 E2EE info chat item @@ -5632,6 +5910,7 @@ This is your link for group %@! Migrate + 迁移 No comment provided by engineer. @@ -5686,7 +5965,7 @@ This is your link for group %@! Migrations: - 迁移 + 迁移: No comment provided by engineer. @@ -5714,6 +5993,11 @@ This is your link for group %@! 更多改进即将推出! No comment provided by engineer. + + More privacy + 更多隐私 + No comment provided by engineer. + More reliable network connection. 更可靠的网络连接。 @@ -5754,6 +6038,11 @@ This is your link for group %@! 名称 swipe action + + Name not found + 未找到名称 + No comment provided by engineer. + Network & servers 网络和服务器 @@ -5761,6 +6050,7 @@ This is your link for group %@! Network commitments + 网络承诺 No comment provided by engineer. @@ -5775,6 +6065,7 @@ This is your link for group %@! Network error + 网络错误 conn error description @@ -5795,6 +6086,8 @@ This is your link for group %@! Network routers cannot know who talks to whom + 网络路由器无法知道 +谁在与谁通信 No comment provided by engineer. @@ -5814,6 +6107,7 @@ who talks to whom New 1-time link + 新的一次性链接 No comment provided by engineer. @@ -5843,6 +6137,7 @@ who talks to whom New chat relay + 新的聊天中继 No comment provided by engineer. @@ -5918,10 +6213,13 @@ who talks to whom No account. No phone. No email. No ID. The most secure encryption. + 无需账号。无需电话号码。无需电子邮件。无需 ID。 +最安全的加密。 No comment provided by engineer. No active relays + 没有已启用的中继 No comment provided by engineer. @@ -5931,14 +6229,17 @@ The most secure encryption. No available relays + 没有可用的中继 No comment provided by engineer. No chat relays + 没有聊天中继 No comment provided by engineer. No chat relays enabled. + 未启用聊天中继。 servers warning @@ -6058,6 +6359,7 @@ The most secure encryption. No relays + 没有中继 No comment provided by engineer. @@ -6075,6 +6377,11 @@ The most secure encryption. 无消息接收服务器。 servers error + + No servers to resolve names. + 没有解析名称的服务器。 + servers warning + No servers to send files. 无文件发送服务器。 @@ -6090,6 +6397,11 @@ The most secure encryption. 没有未读聊天 No comment provided by engineer. + + No valid link + 无有效链接 + No comment provided by engineer. + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. 没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。 @@ -6097,6 +6409,12 @@ The most secure encryption. Non-profit governance + 非营利治理 + No comment provided by engineer. + + + None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link. + 你的服务器没有一台被设定为解析 SimpleX 名称。配置服务器或使用连接链接。 No comment provided by engineer. @@ -6106,6 +6424,7 @@ The most secure encryption. Not all relays connected + 并非所有中继都已连接 alert title @@ -6186,6 +6505,7 @@ new chat action On your phone, not on servers. + 在你的手机上,而不是在服务器上。 No comment provided by engineer. @@ -6195,6 +6515,7 @@ new chat action One-time link + 一次性链接 chat link info line @@ -6218,6 +6539,7 @@ Requires compatible VPN. Only channel owners can change channel preferences. + 只有频道所有者才能更改频道偏好设置。 No comment provided by engineer. @@ -6320,6 +6642,11 @@ Requires compatible VPN. 只有您的联系人可以发送语音消息。 No comment provided by engineer. + + Only your page above can show the preview. + 只有你在上方设置的页面可以显示预览。 + No comment provided by engineer. + Open 打开 @@ -6338,6 +6665,7 @@ alert button Open channel + 打开频道 new chat action @@ -6362,6 +6690,7 @@ alert button Open external link? + 打开外部链接? alert title @@ -6386,6 +6715,7 @@ alert button Open new channel + 打开新频道 new chat action @@ -6405,7 +6735,7 @@ alert button Open to connect - 打开以连接 + 打开并连接 No comment provided by engineer. @@ -6438,6 +6768,10 @@ alert button - Be independent - Minimize metadata usage - Run verified open-source code + 运营商承诺: +- 保持独立 +- 尽量减少元数据使用 +- 运行经验证的开源代码 No comment provided by engineer. @@ -6462,6 +6796,7 @@ alert button Or show QR in person or via video call. + 或当面或通过视频通话显示二维码。 No comment provided by engineer. @@ -6476,6 +6811,7 @@ alert button Or use this QR - print or show online. + 或使用此二维码,可以打印或在线显示。 No comment provided by engineer. @@ -6497,14 +6833,17 @@ alert button Owner + 所有者 No comment provided by engineer. - - Owners + + Owners & contributors + 所有者和贡献者 No comment provided by engineer. Ownership: you can run your own relays. + 所有权:你可以运营自己的中继。 No comment provided by engineer. @@ -6564,6 +6903,7 @@ alert button Paste link / Scan + 粘贴链接 / 扫描 No comment provided by engineer. @@ -6690,10 +7030,6 @@ Error: %@ 请尝试禁用并重新启用通知。 token info - - Please upgrade the app. - alert message - Please wait for group moderators to review your request to join the group. 请等待群的协管审核你加入该群的请求。 @@ -6726,10 +7062,12 @@ Error: %@ Preset relay address + 预设中继地址 No comment provided by engineer. Preset relay name + 预设中继名称 No comment provided by engineer. @@ -6752,11 +7090,6 @@ Error: %@ 以前连接的服务器 No comment provided by engineer. - - Privacy & security - 隐私和安全 - No comment provided by engineer. - Privacy for your customers. 客户隐私。 @@ -6769,10 +7102,12 @@ Error: %@ Privacy: for owners and subscribers. + 隐私:保护所有者和订阅者。 No comment provided by engineer. Private and secure messaging. + 私密且安全的消息传递。 No comment provided by engineer. @@ -6842,7 +7177,9 @@ Error: %@ Profile update will be sent to your SimpleX contacts. - alert message + 个人资料更新将发送给你的 SimpleX 联系人。 + alert message +alert title Prohibit audio/video calls. @@ -6851,6 +7188,7 @@ Error: %@ Prohibit chats with admins. + 禁止与管理员聊天。 No comment provided by engineer. @@ -6885,6 +7223,7 @@ Error: %@ Prohibit sending direct messages to subscribers. + 禁止向订阅者发送直接消息。 No comment provided by engineer. @@ -6956,6 +7295,11 @@ Enable in *Network & servers* settings. Public channels - speak freely 🚀 + 公开频道 - 自由发言 🚀 + No comment provided by engineer. + + + Public names for your channel or business. No comment provided by engineer. @@ -6996,7 +7340,7 @@ Enable in *Network & servers* settings. Read more 阅读更多 - No comment provided by engineer. + profile description teaser Read more in User Guide. @@ -7135,6 +7479,7 @@ Enable in *Network & servers* settings. Register notification token? + 注册通知令牌? token info @@ -7166,22 +7511,27 @@ swipe action Relay + 中继 No comment provided by engineer. Relay address + 中继地址 alert title Relay connection failed + 中继连接失败 alert title Relay link + 中继链接 No comment provided by engineer. Relay results: + 中继结果: alert message @@ -7196,18 +7546,22 @@ swipe action Relay test failed! + 中继测试失败! No comment provided by engineer. Relay will be removed from channel - this cannot be undone! + 中继将从频道移除,且无法撤消! alert message Relays added: %@. + 已添加中继:%@。 alert message Reliability: many relays per channel. + 可靠性:每个频道使用多个中继。 No comment provided by engineer. @@ -7245,17 +7599,28 @@ swipe action 删除成员吗? alert title + + Remove name + No comment provided by engineer. + Remove passphrase from keychain? 从钥匙串中删除密码? No comment provided by engineer. + + Remove relay + 移除中继 + No comment provided by engineer. + Remove relay? + 要移除中继吗? alert title Remove subscriber? + 要移除订阅者吗? alert title @@ -7353,6 +7718,10 @@ swipe action 举报 No comment provided by engineer. + + Require signing messages. + No comment provided by engineer. + Required 必须 @@ -7398,6 +7767,10 @@ swipe action 重置为用户主题 No comment provided by engineer. + + Resolver error: %@ + No comment provided by engineer. + Restart the app to create a new chat profile 重新启动应用程序以创建新的聊天资料 @@ -7455,6 +7828,7 @@ swipe action Review members before admitting ("knocking"). + 在批准加入前审核成员(“敲门”)。 admission stage description @@ -7477,6 +7851,26 @@ swipe action 角色 No comment provided by engineer. + + Role will be changed to "%@". All chat members will be notified. + 将变更成员角色为“%@”。所有成员都会收到通知。 + No comment provided by engineer. + + + Role will be changed to "%@". All group members will be notified. + 成员角色将更改为 "%@"。所有群成员将收到通知。 + No comment provided by engineer. + + + Role will be changed to "%@". All subscribers will be notified. + 角色将被变更为“%@”。所有订阅者将会收到通知。 + No comment provided by engineer. + + + Role will be changed to "%@". The member will receive a new invitation. + 成员角色将更改为 "%@"。该成员将收到一份新的邀请。 + No comment provided by engineer. + Run chat 运行聊天 @@ -7494,6 +7888,7 @@ swipe action Safe web links + 安全的网页链接 No comment provided by engineer. @@ -7509,7 +7904,8 @@ swipe action Save 保存 - alert button + alert action +alert button chat item action @@ -7524,8 +7920,13 @@ chat item action Save (and notify subscribers) + 保存(并通知订阅者) alert button + + Save SimpleX name? + alert title + Save admission settings? 保存入群设置? @@ -7541,8 +7942,14 @@ chat item action 保存并通知群组成员 No comment provided by engineer. + + Save and notify members + 保存并通知成员 + No comment provided by engineer. + Save and notify subscribers + 保存并通知订阅者 No comment provided by engineer. @@ -7557,10 +7964,12 @@ chat item action Save channel profile + 保存频道资料 No comment provided by engineer. Save channel profile? + 要保存频道资料吗? alert title @@ -7608,6 +8017,11 @@ chat item action 保存服务器? alert title + + Save webpage settings? + 要保存网页设置吗? + alert title + Save welcome message? 保存欢迎信息? @@ -7745,6 +8159,7 @@ chat item action Security: owners hold channel keys. + 安全性:所有者持有频道密钥。 No comment provided by engineer. @@ -7879,6 +8294,7 @@ chat item action Send the link via any messenger - it's secure. Ask to paste into SimpleX. + 通过任何通讯应用发送链接,这是安全的。请对方粘贴到 SimpleX 中。 No comment provided by engineer. @@ -7893,6 +8309,7 @@ chat item action Send up to 100 last messages to new subscribers. + 最多将最近 100 条消息发送给新订阅者。 No comment provided by engineer. @@ -7905,13 +8322,9 @@ chat item action 发送人已取消文件传输。 alert message - - Sender may have deleted the connection request. - 发送人可能已删除连接请求。 - No comment provided by engineer. - Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + 发送链接预览可能会向该网站透露你的 IP 地址。你稍后可以在隐私设置中更改此设置。 alert message @@ -8004,6 +8417,10 @@ chat item action 服务器 No comment provided by engineer. + + Server %@ does not support name resolution. Configure servers, or use a connection link. + No comment provided by engineer. + Server added to operator %@. 服务器已添加到运营方 %@。 @@ -8041,16 +8458,17 @@ chat item action Server requires authorization to connect to relay, check password. + 服务器需要授权才能连接到中继,请检查密码。 relay test error Server requires authorization to create queues, check password. - 服务器需要授权才能创建队列,检查密码 + 服务器需要授权才能创建队列,请检查密码。 server test error Server requires authorization to upload, check password. - 服务器需要授权来上传,检查密码 + 服务器需要授权才能上传,请检查密码。 server test error @@ -8175,10 +8593,12 @@ chat item action Setup notifications + 设置通知 No comment provided by engineer. Setup routers + 设置路由器 No comment provided by engineer. @@ -8219,10 +8639,12 @@ chat item action Share address with SimpleX contacts? + 要与 SimpleX 联系人分享地址吗? alert title Share channel + 分享频道 No comment provided by engineer. @@ -8252,6 +8674,7 @@ chat item action Share relay address + 分享中继地址 No comment provided by engineer. @@ -8266,10 +8689,12 @@ chat item action Share via chat + 通过聊天分享 No comment provided by engineer. Share with SimpleX contacts + 与 SimpleX 联系人分享 No comment provided by engineer. @@ -8307,6 +8732,10 @@ chat item action 显示开发者选项 No comment provided by engineer. + + Show encryption + No comment provided by engineer. + Show last messages 显示最近的消息 @@ -8337,6 +8766,31 @@ chat item action 显示: No comment provided by engineer. + + Sign message + No comment provided by engineer. + + + Sign messages + chat feature + + + Signature missing + alert title +copied message info + + + Signed + copied message info + + + Signed & verified + copied message info + + + Signing proves you authored this message and can't be denied later. + No comment provided by engineer. + SimpleX SimpleX @@ -8432,6 +8886,21 @@ chat item action 不允许SimpleX 链接 No comment provided by engineer. + + SimpleX name + SimpleX 名称 + No comment provided by engineer. + + + SimpleX name error + SimpleX 名称错误 + No comment provided by engineer. + + + SimpleX name not verified + SimpleX 名称未验证 + alert title + SimpleX one-time invitation SimpleX 一次性邀请 @@ -8442,8 +8911,13 @@ chat item action SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. + + SimpleX public names (BETA) + No comment provided by engineer. + SimpleX relay address + SimpleX 中继地址 simplex link type @@ -8551,6 +9025,7 @@ report reason Status + 状态 No comment provided by engineer. @@ -8610,6 +9085,7 @@ report reason Storage + 存储 No comment provided by engineer. @@ -8629,59 +9105,74 @@ report reason Subscriber + 订阅者 No comment provided by engineer. Subscriber reports + 订阅者举报报告 chat feature Subscriber will be removed from channel - this cannot be undone! + 订阅者将从频道移除,且无法撤消! alert message Subscribers + 订阅者 No comment provided by engineer. Subscribers can add message reactions. + 订阅者可以添加消息回应。 No comment provided by engineer. Subscribers can chat with admins. + 订阅者可以与管理员聊天。 No comment provided by engineer. Subscribers can irreversibly delete sent messages. (24 hours) + 订阅者可以删除已发送的消息,且无法撤消。(24 小时) No comment provided by engineer. Subscribers can report messsages to moderators. + 订阅者可以向审核员举报消息。 No comment provided by engineer. Subscribers can send SimpleX links. + 订阅者可以发送 SimpleX 链接。 No comment provided by engineer. Subscribers can send direct messages. + 订阅者可以发送直接消息。 No comment provided by engineer. Subscribers can send disappearing messages. + 订阅者可以发送自动销毁消息。 No comment provided by engineer. Subscribers can send files and media. + 订阅者可以发送文件和媒体。 No comment provided by engineer. Subscribers can send voice messages. + 订阅者可以发送语音消息。 No comment provided by engineer. Subscribers use relay link to connect to the channel. Relay address was used to set up this relay for the channel. + 订阅者使用中继链接连接到频道。 +中继地址曾用于为此频道设置此中继。 No comment provided by engineer. @@ -8694,9 +9185,9 @@ Relay address was used to set up this relay for the channel. 忽略订阅 No comment provided by engineer. - - Support SimpleX Chat - 支持 SimpleX Chat + + Support the project + 支持项目 No comment provided by engineer. @@ -8766,6 +9257,7 @@ Relay address was used to set up this relay for the channel. Talk to someone + 与某人聊天 No comment provided by engineer. @@ -8785,6 +9277,7 @@ Relay address was used to set up this relay for the channel. Tap Join channel + 点按“加入频道” No comment provided by engineer. @@ -8819,6 +9312,7 @@ Relay address was used to set up this relay for the channel. Tap to open + 点按即可打开 No comment provided by engineer. @@ -8849,6 +9343,7 @@ server test failure Test relay + 测试中继 No comment provided by engineer. @@ -8888,6 +9383,22 @@ It can happen because of some bug or when the connection is compromised. No comment provided by engineer. + + The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page. + alert message + + + The SimpleX name %@ is registered, but it has no valid link. + No comment provided by engineer. + + + The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner. + No comment provided by engineer. + + + The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page. + alert message + The address will be short, and your profile will be shared via the address. 地址不会长,将通过该简短地址分享个人资料。 @@ -8905,6 +9416,7 @@ It can happen because of some bug or when the connection is compromised. The app removed this message after %lld attempts to receive it. + 应用程序在尝试接收此消息 %lld 次后已将其移除。 No comment provided by engineer. @@ -8917,6 +9429,15 @@ It can happen because of some bug or when the connection is compromised.更改数据库密码的尝试未完成。 No comment provided by engineer. + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + 此徽章使用此版本应用程序无法识别的密钥签署。请更新应用程序来验证此徽章。 + badge alert + + + The channel required this message to be signed, but the signature is missing. + alert message + The code you scanned is not a SimpleX link QR code. 您扫描的码不是 SimpleX 链接的二维码。 @@ -8924,6 +9445,7 @@ It can happen because of some bug or when the connection is compromised. The connection reached the limit of undelivered messages + 连接已达到未送达消息数量上限 conn error description @@ -8954,6 +9476,8 @@ It can happen because of some bug or when the connection is compromised. The first network where you own your contacts and groups. + 第一个让你拥有 +自己的联系人和群组的网络。 No comment provided by engineer. @@ -8998,6 +9522,7 @@ your contacts and groups. The same conditions will apply to operator **%@**. + 相同条件将适用于运营商 **%@**。 No comment provided by engineer. @@ -9010,6 +9535,11 @@ your contacts and groups. 我们错过的第二个"√"!✅ No comment provided by engineer. + + The sender deleted the connection request. + 发送人可能已删除连接请求。 + No comment provided by engineer. + The sender will NOT be notified 发送者将不会收到通知 @@ -9022,6 +9552,7 @@ your contacts and groups. The servers for new files of your current chat profile **%@**. + 当前聊天个人资料 **%@** 的新文件服务器。 No comment provided by engineer. @@ -9064,6 +9595,10 @@ your contacts and groups. 可以在联系人和群组设置中覆盖它们。 No comment provided by engineer. + + This SimpleX name is not registered. Please check the name. + No comment provided by engineer. + This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. 此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。 @@ -9084,6 +9619,11 @@ your contacts and groups. 此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。 No comment provided by engineer. + + This badge could not be verified and may not be genuine. + 无法验证此徽章,可能并非真品。 + badge alert + This chat is protected by end-to-end encryption. 此聊天受端到端加密保护。 @@ -9116,19 +9656,23 @@ your contacts and groups. This group requires a newer version of the app. Please update the app to join. + 此群组需要较新的应用程序版本。请更新应用程序才能加入。 alert message alert subtitle This is a chat relay address, it cannot be used to connect. + 这是聊天中继地址,无法用于连接。 alert message This is the last active relay. Removing it will prevent message delivery to subscribers. + 这是最后一个已启用的中继。移除它会导致无法向订阅者发送消息。 alert message This is your link for channel %@! + 这是你的频道 %@ 链接! new chat action @@ -9183,6 +9727,7 @@ alert subtitle To make SimpleX Network last. + 让 SimpleX Network 长久延续。 No comment provided by engineer. @@ -9237,6 +9782,10 @@ You will be prompted to complete authentication before this feature is enabled.< 请授权使用麦克风以录制语音消息。 No comment provided by engineer. + + To resolve names + No comment provided by engineer. + To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. 要显示您的隐藏的个人资料,请在**您的聊天个人资料**页面的搜索字段中输入完整密码。 @@ -9272,6 +9821,10 @@ You will be prompted to complete authentication before this feature is enabled.< 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 No comment provided by engineer. + + To verify keys with this subscriber, compare (or scan) the code on your devices. + No comment provided by engineer. + Toggle incognito when connecting. 在连接时切换隐身模式。 @@ -9279,6 +9832,7 @@ You will be prompted to complete authentication before this feature is enabled.< Token status: %@. + 令牌状态:%@。 token status @@ -9288,6 +9842,7 @@ You will be prompted to complete authentication before this feature is enabled.< Top bar + 顶部栏 No comment provided by engineer. @@ -9357,6 +9912,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock subscriber for all? + 要为所有人解除封锁订阅者吗? + No comment provided by engineer. + + + Unconfirmed name No comment provided by engineer. @@ -9419,13 +9979,6 @@ You will be prompted to complete authentication before this feature is enabled.< 除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。 No comment provided by engineer. - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - 除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 -如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。 - No comment provided by engineer. - Unlink 取消链接 @@ -9456,18 +10009,15 @@ To connect, please ask your contact to create another connection link and check 未读 swipe action - - Unsupported channel name - alert title - Unsupported connection link 不支持的连接链接 conn error description - - Unsupported contact name - alert title + + Unverified badge + 未验证的徽章 + badge alert title Up to 100 last messages are sent to new members. @@ -9476,6 +10026,7 @@ To connect, please ask your contact to create another connection link and check Up to 100 last messages are sent to new subscribers. + 最多会将最近 100 条消息发送给新订阅者。 No comment provided by engineer. @@ -9521,7 +10072,8 @@ To connect, please ask your contact to create another connection link and check Upgrade address? 升级地址? - alert message + alert message +alert title Upgrade and open chat @@ -9620,6 +10172,7 @@ To connect, please ask your contact to create another connection link and check Use for new channels + 用于新频道 No comment provided by engineer. @@ -9664,6 +10217,7 @@ To connect, please ask your contact to create another connection link and check Use relay + 使用中继 No comment provided by engineer. @@ -9688,6 +10242,7 @@ To connect, please ask your contact to create another connection link and check Use this address in your social media profile, website, or email signature. + 在社交媒体资料、网站或电子邮件签名中使用该地址。 No comment provided by engineer. @@ -9695,6 +10250,11 @@ To connect, please ask your contact to create another connection link and check 使用 web 端口 No comment provided by engineer. + + Used chat relays do not support webpages. + 使用中的聊天中继不支持网页。 + No comment provided by engineer. + User selection 用户选择 @@ -9712,8 +10272,13 @@ To connect, please ask your contact to create another connection link and check Verify + 验证 relay test step + + Verify SimpleX names + No comment provided by engineer. + Verify code with desktop 用桌面端验证代码 @@ -9739,6 +10304,10 @@ To connect, please ask your contact to create another connection link and check 验证数据库密码短语 No comment provided by engineer. + + Verify name + No comment provided by engineer. + Verify passphrase 验证密码短语 @@ -9836,14 +10405,17 @@ To connect, please ask your contact to create another connection link and check Wait + 等待 alert action Wait response + 等待响应 relay test step Waiting for channel owner to add relays. + 正在等待频道所有者添加中继。 No comment provided by engineer. @@ -9888,6 +10460,7 @@ To connect, please ask your contact to create another connection link and check We made connecting simpler for new users. + 我们让连接对新用户更简单。 No comment provided by engineer. @@ -9895,6 +10468,16 @@ To connect, please ask your contact to create another connection link and check WebRTC ICE 服务器 No comment provided by engineer. + + Webpage code + 网页代码 + No comment provided by engineer. + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + 网页设置已更改。如果保存,更新后的设置将发送给订阅者。 + alert message + Welcome %@! 欢迎%@! @@ -9942,6 +10525,7 @@ To connect, please ask your contact to create another connection link and check Why SimpleX is built. + 为何打造 SimpleX。 No comment provided by engineer. @@ -10116,9 +10700,9 @@ Repeat join request? 您可以稍后在设置中启用它 No comment provided by engineer. - - You can enable them later via app Privacy & Security settings. - 您可以稍后通过应用程序的 "隐私与安全 "设置启用它们。 + + You can enable them later via app Your privacy settings. + 你可以稍后通过应用程序的你的隐私设置启用它们。 No comment provided by engineer. @@ -10158,6 +10742,7 @@ Repeat join request? You can share a link or a QR code - anybody will be able to join the channel. + 你可以分享链接或二维码,任何人都可以加入频道。 No comment provided by engineer. @@ -10180,6 +10765,11 @@ Repeat join request? 您仍然可以在聊天列表中查看与 %@的对话。 No comment provided by engineer. + + You can support SimpleX starting from v7 of the app. + 从 v7 版本起您可以支持 SimpleX。 + badge alert + You can turn on SimpleX Lock via Settings. 您可以通过设置开启 SimpleX 锁定。 @@ -10209,10 +10799,14 @@ Repeat join request? You commit to: - Only legal content in public groups - Respect other users - no spam + 你承诺: +- 只在公开群组中发布合法内容 +- 尊重其他用户 - 不发垃圾消息 No comment provided by engineer. You connected to the channel via this relay link. + 你已通过此中继链接连接到频道。 No comment provided by engineer. @@ -10284,11 +10878,12 @@ Repeat connection request? You should receive notifications. + 你应该会收到通知。 token info You were born without an account - 你生来就没有账户。 + 你生来就没有账户 No comment provided by engineer. @@ -10328,6 +10923,7 @@ Repeat connection request? You will stop receiving messages from this channel. Chat history will be preserved. + 你将停止收到来自该频道的消息。聊天记录将被保留。 No comment provided by engineer. @@ -10365,6 +10961,10 @@ Repeat connection request? 您的 SimpleX 地址 No comment provided by engineer. + + Your SimpleX name + No comment provided by engineer. + Your business contact 你的企业联系人 @@ -10377,11 +10977,7 @@ Repeat connection request? Your channel - No comment provided by engineer. - - - Your chat database - 您的聊天数据库 + 你的频道 No comment provided by engineer. @@ -10401,10 +10997,12 @@ Repeat connection request? Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + 你的聊天已移至 %@,但将你重定向到该个人资料时发生意外错误。 alert message Your connection was moved to %@ but an error happened when switching profile. + 你的连接已移至 %@,但切换个人资料时发生错误。 No comment provided by engineer. @@ -10412,6 +11010,13 @@ Repeat connection request? 你的联系人 No comment provided by engineer. + + Your contact removed this link, or it was a one-time link that was already used. +To connect, ask your contact to create a new link. + 除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 +如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。 + No comment provided by engineer. + Your contact sent a file that is larger than currently supported maximum size (%@). 您的联系人发送的文件大于当前支持的最大大小 (%@)。 @@ -10454,11 +11059,14 @@ Repeat connection request? Your network + 您的网络 No comment provided by engineer. Your new channel %1$@ is connected to %2$d of %3$d relays. If you cancel, the channel will be deleted - you can create it again. + 你的新频道 %1$@ 已连接到 %2$d/%3$d 个中继。 +如果取消,频道将被删除;你可以再次创建。 alert message @@ -10479,6 +11087,8 @@ If you cancel, the channel will be deleted - you can create it again. Your profile **%@** will be shared with channel relays and subscribers. Relays can access channel messages. + 你的个人资料 **%@** 将与频道中继和订阅者分享。 +中继可以访问频道消息。 No comment provided by engineer. @@ -10503,6 +11113,7 @@ Relays can access channel messages. Your public address + 你的公开地址 No comment provided by engineer. @@ -10512,10 +11123,12 @@ Relays can access channel messages. Your relay address + 你的中继地址 No comment provided by engineer. Your relay name + 你的中继名称 No comment provided by engineer. @@ -10555,10 +11168,12 @@ Relays can access channel messages. accepted + 已接受 No comment provided by engineer. accepted %@ + 已接受 %@ rcv group event chat item @@ -10576,8 +11191,14 @@ Relays can access channel messages. 接受了你 rcv group event chat item + + acknowledged roster + 已确认名单 + No comment provided by engineer. + active + 活跃 No comment provided by engineer. @@ -10693,6 +11314,7 @@ marked deleted chat item preview text can't broadcast + 无法广播 No comment provided by engineer. @@ -10732,10 +11354,12 @@ marked deleted chat item preview text channel + 频道 shown as sender role for channel messages channel profile updated + 频道资料已更新 snd group event chat item @@ -10838,6 +11462,10 @@ marked deleted chat item preview text 联系人应当接受… No comment provided by engineer. + + contributor + member role + creator 创建者 @@ -10886,6 +11514,7 @@ pref value deleted channel + 已删除频道 rcv group event chat item @@ -11000,6 +11629,7 @@ pref value error: %@ + 错误:%@ receive error chat item @@ -11009,6 +11639,7 @@ pref value failed + 失败 No comment provided by engineer. @@ -11041,6 +11672,11 @@ pref value 小时 time unit + + https:// + https:// + No comment provided by engineer. + iOS Keychain is used to securely store passphrase - it allows receiving push notifications. iOS钥匙串用于安全地存储密码——它允许接收推送通知。 @@ -11133,6 +11769,7 @@ pref value link + 链接 No comment provided by engineer. @@ -11207,6 +11844,7 @@ pref value new + No comment provided by engineer. @@ -11294,6 +11932,7 @@ time to disappear pending + 待处理 No comment provided by engineer. @@ -11333,6 +11972,7 @@ time to disappear relay + 中继 member role @@ -11347,10 +11987,12 @@ time to disappear removed (%d attempts) + 已移除(%d 次尝试) receive error chat item removed by operator + 已被运营商移除 No comment provided by engineer. @@ -11477,6 +12119,10 @@ last received msg: %2$@ 删去 No comment provided by engineer. + + subscriber + member role + this contact 这个联系人 @@ -11509,6 +12155,7 @@ last received msg: %2$@ updated channel profile + 频道更新了频道资料 rcv group event chat item @@ -11526,13 +12173,9 @@ last received msg: %2$@ v%@ No comment provided by engineer. - - v%@ (%@) - v%@ (%@) - No comment provided by engineer. - via %@ + 通过 %@ relay hostname @@ -11612,6 +12255,7 @@ last received msg: %2$@ you are subscriber + 你是订阅者 No comment provided by engineer. @@ -11676,13 +12320,14 @@ last received msg: %2$@ ⚠️ Signature verification failed: %@. + ⚠️ 签名验证失败:%@。 owner verification
- +
@@ -11717,9 +12362,24 @@ last received msg: %2$@
+ +
+ +
+ + + SimpleXChat + Bundle name + + + Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
- +
@@ -11741,7 +12401,7 @@ last received msg: %2$@
- +
@@ -11773,7 +12433,7 @@ last received msg: %2$@
- +
@@ -11795,7 +12455,7 @@ last received msg: %2$@
- +
@@ -11960,7 +12620,7 @@ last received msg: %2$@ Unknown database error: %@ - 未知数据库错误: %@ + 未知数据库错误:%@ No comment provided by engineer. @@ -11978,9 +12638,9 @@ last received msg: %2$@ 数据库密码错误 No comment provided by engineer. - - You can allow sharing in Privacy & Security / SimpleX Lock settings. - 您可以在 "隐私与安全"/"SimpleX Lock "设置中允许共享。 + + You can allow sharing in Your privacy / SimpleX Lock settings. + 你可以在“你的隐私”/“SimpleX 锁定”设置中允许共享。 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings index d34eb67fc7..b8ff778e25 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -1,12 +1,18 @@ /* Bundle name */ "CFBundleName" = "SimpleX"; + /* Privacy - Camera Usage Description */ "NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; + /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; + /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "SimpleX uses local network access to allow using user chat profile via desktop app on the same network."; + /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; + /* Privacy - Photo Library Additions Usage Description */ "NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; + diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings new file mode 100644 index 0000000000..c36c8c815d --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/en.lproj/SimpleXChat-InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle name */ +"CFBundleName" = "SimpleXChat"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json index 91977b0744..f8b8f54d3a 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "zh-Hans", "toolInfo" : { - "toolBuildNumber" : "16C5032a", + "toolBuildNumber" : "17F113", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "16.2" + "toolVersion" : "26.6" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 0e4e383b52..168d865b4b 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -1148,8 +1148,8 @@ 開發 No comment provided by engineer. - - Developer tools + + Developer 開發者工具 No comment provided by engineer. @@ -2012,13 +2012,13 @@ We will be adding server redundancy to prevent lost messages. 成員 No comment provided by engineer. - - Member role will be changed to "%@". All group members will be notified. + + Role will be changed to "%@". All group members will be notified. 成員的身份會修改為 "%@"。所有在群組內的成員都會接收到通知。 No comment provided by engineer. - - Member role will be changed to "%@". The member will receive a new invitation. + + Role will be changed to "%@". The member will receive a new invitation. 成員的身份會修改為 "%@"。該成員將接收到新的邀請。 No comment provided by engineer. @@ -2369,7 +2369,7 @@ We will be adding server redundancy to prevent lost messages. Fingerprint in server address does not match certificate. - 伺服器地址的憑證指紋可能不正確 + 伺服器地址中的指紋與憑證不符。 server test error @@ -2737,8 +2737,8 @@ We will be adding server redundancy to prevent lost messages. 傳送者已取消傳送檔案。 No comment provided by engineer. - - Sender may have deleted the connection request. + + The sender deleted the connection request. 傳送者似乎已經刪除了連接的請求。 No comment provided by engineer. @@ -2759,7 +2759,7 @@ We will be adding server redundancy to prevent lost messages. Server requires authorization to create queues, check password. - 伺服器需要授權才能建立佇列,請檢查密碼 + 伺服器需要授權才能建立佇列,請檢查密碼。 server test error @@ -2821,8 +2821,9 @@ We will be adding server redundancy to prevent lost messages. 分享一次性邀請連結 No comment provided by engineer. - + Show QR code + 顯示二維碼 No comment provided by engineer. @@ -5132,7 +5133,7 @@ Available in v5.1 Server requires authorization to upload, check password. - 伺服器需要認證後才能上傳,檢查密碼 + 伺服器需要授權才能上傳,請檢查密碼。 server test error @@ -6480,6 +6481,5122 @@ It can happen because of some bug or when the connection is compromised.Uploaded 已上傳 + + %1$@ supported SimpleX Chat. The badge expired on %2$@. + %1$@ 曾是 SimpleX Chat 支持者。徽章已於 %2$@ 過期。 + + + %1$@ at %2$@: + %1$@ 於 %2$@: + + + %@ invested in SimpleX Chat crowdfunding. + %@ 出資支持了 SimpleX Chat 的群眾募資。 + + + %@ supports SimpleX Chat. + %@ 是 SimpleX Chat 支持者。 + + + Some non-fatal errors occurred during import - you may see Chat console for more details. + 匯入期間發生了一些非致命錯誤——你可以查看聊天控制台以了解更多詳情。 + + + You can hide or mute a user profile - swipe it to the right. + 你可以隱藏或靜音使用者個人檔案——向右滑動即可。 + + + Error aborting address change + 中止地址變更時發生錯誤 + + + Favorite + 加入最愛 + + + Receiving address will be changed to a different server. Address change will complete after sender comes online. + 接收地址將變更至其他伺服器。地址變更會在傳送者上線後完成。 + + + Unfav. + 取消最愛 + + + Files and media + 檔案和媒體 + + + Files and media prohibited! + 已禁止傳送檔案和媒體! + + + No filtered chats + 沒有符合篩選條件的聊天 + + + Only group owners can enable files and media. + 只有群組擁有者可以啟用檔案和媒體。 + + + Prohibit sending files and media. + 禁止傳送檔案和媒體。 + + + Contacts + 聯絡人 + + + Delivery receipts are disabled! + 送達回條已停用! + + + Delivery receipts! + 送達回條! + + + Error synchronizing connection + 同步連線時發生錯誤 + + + Exporting database archive… + 正在匯出數據庫封存檔… + + + Fix + 修復 + + + Fix connection + 修復連線 + + + Fix connection? + 修復連線? + + + Fix not supported by contact + 聯絡人不支援修復功能 + + + Fix not supported by group member + 群組成員不支援修復功能 + + + In reply to + 回覆 + + + Migrating database archive… + 正在遷移數據庫封存檔… + + + No history + 沒有歷史記錄 + + + Protocol timeout per KB + 每 KB 協議超時 + + + React… + 回應… + + + Reconnect all connected servers to force message delivery. It uses additional traffic. + 重新連線所有已連線的伺服器,以強制傳送訊息。這會使用額外流量。 + + + Reconnect servers? + 要重新連線伺服器嗎? + + + Renegotiate + 重新協商 + + + Renegotiate encryption + 重新協商加密 + + + Renegotiate encryption? + 重新協商加密? + + + Send delivery receipts to + 傳送送達回條給 + + + Send receipts + 傳送回條 + + + The encryption is working and the new encryption agreement is not required. It may result in connection errors! + 加密運作正常,不需要新的加密協議。這可能會導致連線錯誤! + + + agreeing encryption for %@… + 正在為 %@ 協商加密… + + + agreeing encryption… + 正在協商加密… + + + changing address for %@… + 正在為 %@ 變更地址… + + + changing address… + 正在變更地址… + + + default (no) + 預設(否) + + + default (yes) + 預設(是) + + + encryption agreed + 已完成加密協商 + + + encryption agreed for %@ + 已為 %@ 完成加密協商 + + + encryption ok + 加密正常 + + + encryption ok for %@ + %@ 的加密正常 + + + encryption re-negotiation allowed + 允許重新協商加密 + + + encryption re-negotiation allowed for %@ + 允許為 %@ 重新協商加密 + + + encryption re-negotiation required + 需要重新協商加密 + + + encryption re-negotiation required for %@ + 需要為 %@ 重新協商加密 + + + Disable (keep overrides) + 停用(保留覆寫設定) + + + Disable for all + 全部停用 + + + Don't enable + 不要啟用 + + + Enable (keep overrides) + 啟用(保留覆寫設定) + + + Enable for all + 全部啟用 + + + Error enabling delivery receipts! + 啟用送達回條時發生錯誤! + + + Error setting delivery receipts! + 設定送達回條時發生錯誤! + + + Even when disabled in the conversation. + 即使在對話中已停用。 + + + Filter unread and favorite chats. + 篩選未讀和最愛的聊天。 + + + Find chats faster + 更快尋找聊天 + + + Fix encryption after restoring backups. + 還原備份後修復加密。 + + + Keep your connections + 保留你的連線 + + + Make one message disappear + 讓一則訊息消失 + + + Message delivery receipts! + 訊息送達回條! + + + Sending delivery receipts will be enabled for all contacts in all visible chat profiles. + 將為所有可見聊天個人檔案中的所有聯絡人啟用送達回條。 + + + Sending delivery receipts will be enabled for all contacts. + 將為所有聯絡人啟用送達回條。 + + + Sending receipts is disabled for %lld contacts + 已為 %lld 位聯絡人停用送達回條 + + + Sending receipts is enabled for %lld contacts + 已為 %lld 位聯絡人啟用送達回條 + + + You can enable later via Settings + 你可以稍後透過「設定」啟用 + + + A new random profile will be shared. + 將分享新的隨機個人檔案。 + + + Connect via one-time link + 透過一次性連結連線 + + + Delivery + 送達 + + + Incognito mode protects your privacy by using a new random profile for each contact. + 隱身模式會為每位聯絡人使用新的隨機個人檔案,以保護你的私隱。 + + + Invalid status + 狀態無效 + + + Most likely this connection is deleted. + 此連線很可能已被刪除。 + + + No delivery information + 沒有送達資訊 + + + Receipts are disabled + 送達回條已停用 + + + Reject (sender NOT notified) + 拒絕(不會通知傳送者) + + + Sending receipts is disabled for %lld groups + 已為 %lld 個群組停用送達回條 + + + Sending receipts is enabled for %lld groups + 已為 %lld 個群組啟用送達回條 + + + Small groups (max 20) + 小型群組(最多 20 人) + + + This group has over %lld members, delivery receipts are not sent. + 此群組有超過 %lld 位成員,不會傳送送達回條。 + + + Use current profile + 使用目前個人檔案 + + + Use new incognito profile + 使用新的隱身個人檔案 + + + You invited a contact + 你已邀請聯絡人 + + + Your profile **%@** will be shared. + 將分享你的個人檔案 **%@**。 + + + disabled + 已停用 + + + - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable. + - 連接到[目錄服務](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- 送達回條(最多 20 位成員)。 +- 更快且更穩定。 + + + Create new profile in [desktop app](https://simplex.chat/downloads/). 💻 + 在[桌面應用程式](https://simplex.chat/downloads/)中建立新的個人檔案。💻 + + + Discover and join groups + 探索並加入群組 + + + Encrypt stored files & media + 加密已儲存的檔案和媒體 + + + New desktop app! + 全新桌面應用程式! + + + Simplified incognito mode + 簡化的隱身模式 + + + Toggle incognito when connecting. + 連線時切換隱身模式。 + + + Error creating member contact + 建立成員聯絡人時發生錯誤 + + + Error sending member contact invitation + 傳送成員聯絡人邀請時發生錯誤 + + + Open + 開啟 + + + %lld messages moderated by %@ + %lld 則訊息已由 %@ 審核 + + + All new messages from %@ will be hidden! + 來自 %@ 的所有新訊息都會隱藏! + + + Connect to yourself? +This is your own SimpleX address! + 要連線到自己嗎? +這是你自己的 SimpleX 地址! + + + Connect to yourself? +This is your own one-time link! + 要連線到自己嗎? +這是你自己的一次性連結! + + + Connect via contact address + 透過聯絡人地址連線 + + + Connect with %@ + 與 %@ 連線 + + + Correct name to %@? + 要將名稱更正為 %@ 嗎? + + + Create group + 建立群組 + + + Create profile + 建立個人檔案 + + + Delete %lld messages? + 要刪除 %lld 則訊息嗎? + + + Delete and notify contact + 刪除並通知聯絡人 + + + Enter group name… + 輸入群組名稱… + + + Enter your name… + 輸入你的名稱… + + + Expand + 展開 + + + Fully decentralized – visible only to members. + 完全去中心化——僅成員可見。 + + + Group already exists + 群組已存在 + + + Group already exists! + 群組已存在! + + + Invalid name! + 名稱無效! + + + Join your group? +This is your link for group %@! + 要加入你的群組嗎? +這是你群組 %@ 的連結! + + + Messages from %@ will be shown! + 來自 %@ 的訊息會顯示! + + + Open group + 開啟群組 + + + Unblock + 解除封鎖 + + + Unblock member + 解除封鎖成員 + + + Unblock member? + 要解除封鎖此成員嗎? + + + You are already connecting to %@. + 你已在與 %@ 連線。 + + + You are already connecting via this one-time link! + 你已在透過這個一次性連結連線! + + + You are already in group %@. + 你已在群組 %@ 中。 + + + You are already joining the group %@. + 你已在加入群組 %@。 + + + You are already joining the group via this link. + 你已在透過此連結加入該群組。 + + + You are already joining the group! +Repeat join request? + 你已在加入群組! +要重複加入請求嗎? + + + You have already requested connection! +Repeat connection request? + 你已請求連線! +要重複連線請求嗎? + + + You will be connected when group link host's device is online, please wait or check later! + 群組連結主機的裝置上線後,你將會連線;請等待或稍後查看! + + + Your profile + 你的個人檔案 + + + and %lld other events + 和其他 %lld 個事件 + + + blocked + 已封鎖 + + + deleted contact + 已刪除的聯絡人 + + + Connect to desktop + 連線到桌上電腦 + + + Connected desktop + 已連線的桌上電腦 + + + Connected to desktop + 已連線到桌上電腦 + + + Connecting to desktop + 正在連線到桌上電腦 + + + Connection terminated + 連線已終止 + + + Desktop address + 桌面地址 + + + Desktop app version %@ is not compatible with this app. + 桌面應用程式版本 %@ 與此應用程式不相容。 + + + Desktop devices + 桌面裝置 + + + Disconnect desktop? + 要中斷桌上電腦連線嗎? + + + Encryption re-negotiation error + 加密重新協商錯誤 + + + Encryption re-negotiation failed. + 加密重新協商失敗。 + + + Enter this device name… + 輸入此裝置名稱… + + + Incompatible version + 版本不相容 + + + Keep the app open to use it from desktop + 保持應用程式開啟,以便從桌上電腦使用 + + + Linked desktop options + 已連結桌上電腦選項 + + + Linked desktops + 已連結的桌上電腦 + + + Paste desktop address + 貼上桌面地址 + + + Scan QR code from desktop + 掃描桌上電腦上的二維碼 + + + This device name + 此裝置名稱 + + + Unlink desktop? + 要取消連結桌上電腦嗎? + + + Create a group using a random profile. + 使用隨機個人檔案建立群組。 + + + Faster joining and more reliable messages. + 加入更快速,訊息更可靠。 + + + Incognito groups + 隱身群組 + + + Link mobile and desktop apps! 🔗 + 連結手機和桌面應用程式!🔗 + + + To hide unwanted messages. + 用於隱藏不想看到的訊息。 + + + Connect automatically + 自動連線 + + + Discover via local network + 透過本機網路探索 + + + Found desktop + 找到桌上電腦 + + + Not compatible! + 不相容! + + + Waiting for desktop... + 正在等待桌上電腦… + + + author + 作者 + + + Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. + 聊天已停止。如果你已在另一部裝置上使用過此數據庫,應先將其傳回再開始聊天。 + + + Encrypted message: app is stopped + 加密訊息:應用程式已停止 + + + Error opening chat + 開啟聊天時發生錯誤 + + + Invalid response + 回應無效 + + + Opening app… + 正在開啟應用程式… + + + Please contact developers. +Error: %@ + 請聯絡開發者。 +錯誤:%@ + + + Start chat? + 要開始聊天嗎? + + + Use only local notifications? + 只使用本機通知嗎? + + + You can make it visible to your SimpleX contacts via Settings. + 你可以透過「設定」讓你的 SimpleX 聯絡人看到它。 + + + Creating link… + 正在建立連結… + + + Enable camera access + 啟用相機取用權限 + + + Error scanning code: %@ + 掃描代碼時發生錯誤:%@ + + + Invalid QR code + 二維碼無效 + + + Invalid link + 連結無效 + + + Keep + 保留 + + + Keep unused invitation? + 要保留未使用的邀請嗎? + + + New chat + 新聊天 + + + OK + + + + Or scan QR code + 或掃描二維碼 + + + Or show this code + 或顯示此代碼 + + + Paste the link you received + 貼上你收到的連結 + + + Share this 1-time invite link + 分享此一次性邀請連結 + + + Tap to paste link + 點一下即可貼上連結 + + + Tap to scan + 點一下即可掃描 + + + The code you scanned is not a SimpleX link QR code. + 你掃描的代碼不是 SimpleX 連結二維碼。 + + + You can view invitation link again in connection details. + 你可以在連線詳情中再次查看邀請連結。 + + + Do not send history to new members. + 不要向新成員傳送歷史記錄。 + + + History is not sent to new members. + 歷史記錄不會傳送給新成員。 + + + Invalid display name! + 顯示名稱無效! + + + Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours) + 只有你可以永久刪除訊息(你的聯絡人可以將其標記為待刪除)。(24 小時) + + + Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours) + 只有你的聯絡人可以永久刪除訊息(你可以將其標記為待刪除)。(24 小時) + + + Send up to 100 last messages to new members. + 向新成員傳送最多 100 則最近訊息。 + + + This display name is invalid. Please choose another name. + 此顯示名稱無效。請選擇其他名稱。 + + + Visible history + 可見歷史記錄 + + + Clear private notes? + 要清除私密筆記嗎? + + + Created at + 建立於 + + + Created at: %@ + 建立於:%@ + + + Error creating message + 建立訊息時發生錯誤 + + + Improved message delivery + 改善訊息送達 + + + Join group conversations + 加入群組對話 + + + Paste link to connect! + 貼上連結即可連線! + + + Private notes + 私密筆記 + + + Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). + 最近歷史記錄和改進的[目錄機器人](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)。 + + + Search bar accepts invitation links. + 搜尋列可接受邀請連結。 + + + Turkish interface + 土耳其語介面 + + + With encrypted files and media. + 包含加密的檔案和媒體。 + + + With reduced battery usage. + 降低電池用量。 + + + contact %1$@ changed to %2$@ + 聯絡人 %1$@ 已變更為 %2$@ + + + member %1$@ changed to %2$@ + 成員 %1$@ 已變更為 %2$@ + + + removed profile picture + 已移除個人檔案圖片 + + + removed contact address + 已移除聯絡人地址 + + + set new contact address + 已設定新的聯絡人地址 + + + set new profile picture + 已設定新的個人檔案圖片 + + + updated profile + 已更新個人檔案 + + + unknown status + 未知狀態 + + + Block for all + 為所有人封鎖 + + + Block member for all? + 要為所有人封鎖此成員嗎? + + + Unblock for all + 為所有人解除封鎖 + + + Unblock member for all? + 要為所有人解除封鎖此成員嗎? + + + blocked %@ + 已封鎖 %@ + + + blocked by admin + 由管理員封鎖 + + + unblocked %@ + 已解除封鎖 %@ + + + you blocked %@ + 你已封鎖 %@ + + + you unblocked %@ + 你已解除封鎖 %@ + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **請注意**:基於安全保護,在兩部裝置上使用同一個數據庫會導致無法解密來自你連線對象的訊息。 + + + **Warning**: the archive will be removed. + **警告**:封存檔將被移除。 + + + Chat migrated! + 聊天已遷移! + + + Choose _Migrate from another device_ on the new device and scan QR code. + 在新裝置上選擇 _從另一部裝置遷移_ 並掃描二維碼。 + + + Confirm network settings + 確認網路設定 + + + Confirm that you remember database passphrase to migrate it. + 確認你記得用於遷移的數據庫密碼短語。 + + + Confirm upload + 確認上傳 + + + Creating archive link + 正在建立封存檔連結 + + + Delete database from this device + 從此裝置刪除數據庫 + + + Download failed + 下載失敗 + + + Downloading archive + 正在下載封存檔 + + + Downloading link details + 正在下載連結詳情 + + + Enter passphrase + 輸入密碼短語 + + + Error downloading the archive + 下載封存檔時發生錯誤 + + + Error saving settings + 儲存設定時發生錯誤 + + + Exported file doesn't exist + 匯出的檔案不存在 + + + Error uploading the archive + 上傳封存檔時發生錯誤 + + + Error verifying passphrase: + 驗證密碼短語時發生錯誤: + + + Finalize migration + 完成遷移 + + + Finalize migration on another device. + 在另一部裝置上完成遷移。 + + + Import failed + 匯入失敗 + + + Importing archive + 正在匯入封存檔 + + + In order to continue, chat should be stopped. + 必須停止聊天才能繼續。 + + + Invalid migration confirmation + 遷移確認無效 + + + Message too large + 訊息太大 + + + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. + 訊息、檔案和通話均受具備完美前向保密、可否認性和入侵復原的 **端對端加密** 保護。 + + + Migrate device + 遷移裝置 + + + Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. + 訊息、檔案和通話均受具備完美前向保密、可否認性和入侵復原的 **抗量子端對端加密** 保護。 + + + Migrate here + 遷移到此處 + + + Migrate to another device + 遷移到另一部裝置 + + + Migrating + 正在遷移 + + + Open migration to another device + 開啟遷移到另一部裝置 + + + Migration complete + 遷移完成 + + + Or paste archive link + 或貼上封存檔連結 + + + Or securely share this file link + 或安全分享此檔案連結 + + + Push server + 推送伺服器 + + + Please confirm that network settings are correct for this device. + 請確認此裝置的網路設定正確。 + + + Repeat download + 重新下載 + + + Repeat import + 重新匯入 + + + Repeat upload + 重新上傳 + + + Set passphrase + 設定密碼短語 + + + Stop chat + 停止聊天 + + + Stopping chat + 正在停止聊天 + + + This chat is protected by end-to-end encryption. + 此聊天受端對端加密保護。 + + + This chat is protected by quantum resistant end-to-end encryption. + 此聊天受抗量子端對端加密保護。 + + + Verify database passphrase + 驗證數據庫密碼短語 + + + Warning: starting chat on multiple devices is not supported and will cause message delivery failures + 警告:不支援在多部裝置上啟動聊天,這會導致訊息送達失敗 + + + Welcome message is too long + 歡迎訊息太長 + + + You **must not** use the same database on two devices. + 你 **不得** 在兩部裝置上使用同一個數據庫。 + + + You can give another try. + 你可以再試一次。 + + + quantum resistant e2e encryption + 抗量子端對端加密 + + + standard end-to-end encryption + 標準端對端加密 + + + Admins can block a member for all. + 管理員可以為所有人封鎖成員。 + + + Enable in direct chats (BETA)! + 在直接聊天中啟用(BETA)! + + + Hungarian interface + 匈牙利語介面 + + + Migrate to another device via QR code. + 透過二維碼遷移到另一部裝置。 + + + Picture-in-picture calls + 畫中畫通話 + + + Quantum resistant encryption + 抗量子加密 + + + Safer groups + 更安全的群組 + + + Download + 下載 + + + Enabled for + 啟用對象 + + + Files and media not allowed + 不允許檔案和媒體 + + + Forward + 轉發 + + + Forwarded + 已轉發 + + + Forwarded from + 轉發自 + + + Network connection + 網路連線 + + + No network connection + 沒有網路連線 + + + Other + 其他 + + + Prohibit sending SimpleX links. + 禁止傳送 SimpleX 連結。 + + + Recipient(s) can't see who this message is from. + 收件人無法看到此訊息來自誰。 + + + WiFi + WiFi + + + Wired ethernet + 有線乙太網路 + + + admins + 管理員 + + + all members + 所有成員 + + + forwarded + 已轉發 + + + owners + 擁有者 + + + saved from %@ + 已從 %@ 儲存 + + + you + + + + Forward and save messages + 轉發並儲存訊息 + + + In-call sounds + 通話中音效 + + + Message source remains private. + 訊息來源保持私密。 + + + More reliable network connection. + 更可靠的網路連線。 + + + Network management + 網路管理 + + + When connecting audio and video calls. + 連線語音和視訊通話時。 + + + Will be enabled in direct chats! + 將在直接聊天中啟用! + + + Profile images + 個人檔案圖片 + + + Square, circle, or anything in between. + 方形、圓形,或介於兩者之間的任何形狀。 + + + Allow downgrade + 允許降級 + + + Always use private routing. + 一律使用私密路由。 + + + Capacity exceeded - recipient did not receive previously sent messages. + 容量已超出;收件人未收到先前傳送的訊息。 + + + Confirm files from unknown servers. + 確認來自未知伺服器的檔案。 + + + Destination server error: %@ + 目的地伺服器錯誤:%@ + + + Do NOT send messages directly, even if your or destination server does not support private routing. + 不要直接傳送訊息,即使你的伺服器或目的地伺服器不支援私密路由。 + + + Do NOT use private routing. + 不要使用私密路由。 + + + Files + 檔案 + + + Forwarding server: %1$@ +Destination server error: %2$@ + 轉發伺服器:%1$@ +目的地伺服器錯誤:%2$@ + + + Forwarding server: %1$@ +Error: %2$@ + 轉發伺服器:%1$@ +錯誤:%2$@ + + + Message delivery warning + 訊息送達警告 + + + Network issues - message expired after many attempts to send it. + 網路問題;多次嘗試傳送後訊息已過期。 + + + Private message routing + 私密訊息路由 + + + Private message routing 🚀 + 私密訊息路由 🚀 + + + Private routing + 私密路由 + + + Protect IP address + 保護 IP 地址 + + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + 保護你的 IP 地址,不讓聯絡人選擇的訊息中繼看到。 +在 *網路和伺服器* 設定中啟用。 + + + Send messages directly when your or destination server does not support private routing. + 當你的伺服器或目的地伺服器不支援私密路由時,直接傳送訊息。 + + + Server address is incompatible with network settings. + 伺服器地址與網路設定不相容。 + + + Server version is incompatible with network settings. + 伺服器版本與網路設定不相容。 + + + Show message status + 顯示訊息狀態 + + + Show → on messages sent via private routing. + 在透過私密路由傳送的訊息上顯示 →。 + + + The app will ask to confirm downloads from unknown file servers (except .onion). + 應用程式會要求你確認來自未知檔案伺服器的下載(.onion 除外)。 + + + To protect your IP address, private routing uses your SMP servers to deliver messages. + 為了保護你的 IP 地址,私密路由會使用你的 SMP 伺服器傳送訊息。 + + + Unknown servers! + 未知伺服器! + + + Use private routing with unknown servers when IP address is not protected. + 當 IP 地址未受保護時,對未知伺服器使用私密路由。 + + + Use private routing with unknown servers. + 對未知伺服器使用私密路由。 + + + Without Tor or VPN, your IP address will be visible to file servers. + 如果沒有 Tor 或 VPN,檔案伺服器將能看到你的 IP 地址。 + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + 如果沒有 Tor 或 VPN,這些 XFTP 中繼將能看到你的 IP 地址:%@。 + + + Wrong key or unknown connection - most likely this connection is deleted. + 金鑰錯誤或未知連線;此連線很可能已被刪除。 + + + unprotected + 未受保護 + + + when IP hidden + IP 隱藏時 + + + Debug delivery + 偵錯送達 + + + Message queue info + 訊息佇列資訊 + + + server queue info: %1$@ + +last received msg: %2$@ + 伺服器佇列資訊:%1$@ + +最後收到的訊息:%2$@ + + + Accent + 強調色 + + + Acknowledged + 已確認 + + + Acknowledgement errors + 確認錯誤 + + + Additional accent + 其他強調色 + + + Additional accent 2 + 其他強調色 2 + + + Additional secondary + 其他次要色 + + + Black + 黑色 + + + Cannot forward message + 無法轉發訊息 + + + Chat colors + 聊天顏色 + + + Chat theme + 聊天主題 + + + Chunks deleted + 已刪除分塊 + + + Chunks downloaded + 已下載分塊 + + + Chunks uploaded + 已上傳分塊 + + + Color mode + 顏色模式 + + + Completed + 已完成 + + + Connected + 已連線 + + + Connected servers + 已連線的伺服器 + + + Connecting + 正在連線 + + + Connection with desktop stopped + 與桌上電腦的連線已停止 + + + Connections + 連線 + + + Copy error + 複製錯誤 + + + Created + 已建立 + + + Customize theme + 自訂主題 + + + Dark mode colors + 深色模式顏色 + + + Deleted + 已刪除 + + + Deletion errors + 刪除錯誤 + + + Detailed statistics + 詳細統計資料 + + + Details + 詳情 + + + Download errors + 下載錯誤 + + + Downloaded + 已下載 + + + Downloaded files + 已下載的檔案 + + + Error exporting theme: %@ + 匯出主題時發生錯誤:%@ + + + Error reconnecting server + 重新連線伺服器時發生錯誤 + + + Error reconnecting servers + 重新連線多個伺服器時發生錯誤 + + + Error resetting statistics + 重設統計資料時發生錯誤 + + + Errors + 錯誤 + + + Export theme + 匯出主題 + + + File error + 檔案錯誤 + + + File not found - most likely file was deleted or cancelled. + 找不到檔案;檔案很可能已被刪除或取消。 + + + File server error: %@ + 檔案伺服器錯誤:%@ + + + File status + 檔案狀態 + + + File status: %@ + 檔案狀態:%@ + + + Good afternoon! + 午安! + + + Good morning! + 早安! + + + Import theme + 匯入主題 + + + Interface colors + 介面顏色 + + + Member inactive + 成員未活躍 + + + Menus + 選單 + + + Message forwarded + 訊息已轉發 + + + Message may be delivered later if member becomes active. + 如果成員變為活躍,訊息稍後可能會送達。 + + + Message status + 訊息狀態 + + + Message status: %@ + 訊息狀態:%@ + + + Messages received + 已接收訊息 + + + Messages sent + 已傳送訊息 + + + No direct connection yet, message is forwarded by admin. + 尚無直接連線,訊息由管理員轉發。 + + + No info, try to reload + 沒有資訊,請嘗試重新載入 + + + Pending + 待處理 + + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + 請檢查手機和桌上電腦是否連線到同一本機網路,且桌上電腦防火牆允許此連線。 +如有其他問題,請分享給開發者。 + + + Previously connected servers + 曾連線的伺服器 + + + Profile theme + 個人檔案主題 + + + Proxied + 已代理 + + + Proxied servers + 已代理的伺服器 + + + Receive errors + 接收錯誤 + + + Received messages + 已接收訊息 + + + Received reply + 已接收回覆 + + + Received total + 接收總計 + + + Reconnect + 重新連線 + + + Reconnect all servers + 重新連線所有伺服器 + + + Reconnect all servers? + 要重新連線所有伺服器嗎? + + + Reconnect server to force message delivery. It uses additional traffic. + 重新連線伺服器,強制送達訊息。這會使用額外流量。 + + + Reconnect server? + 要重新連線伺服器嗎? + + + Remove image + 移除圖片 + + + SMP server + SMP 伺服器 + + + Secondary + 次要色 + + + Secured + 已受保護 + + + Selected chat preferences prohibit this message. + 所選聊天偏好設定禁止此訊息。 + + + Send errors + 傳送錯誤 + + + Sent messages + 已傳送訊息 + + + Sent reply + 已傳送回覆 + + + Sent total + 傳送總計 + + + Server address + 伺服器地址 + + + Server type + 伺服器類型 + + + Size + 大小 + + + Statistics + 統計資料 + + + Subscribed + 已訂閱 + + + Subscriptions ignored + 已忽略訂閱 + + + Temporary file error + 暫存檔案錯誤 + + + This link was used with another mobile device, please create a new link on the desktop. + 此連結已在另一部行動裝置上使用,請在桌上電腦上建立新的連結。 + + + Title + 標題 + + + Total + 總計 + + + Transport sessions + 傳輸工作階段 + + + User selection + 使用者選擇 + + + Wallpaper accent + 桌布強調色 + + + Wallpaper background + 桌布背景 + + + Wrong key or unknown file chunk address - most likely file is deleted. + 金鑰錯誤或未知檔案分塊地址;檔案很可能已被刪除。 + + + XFTP server + XFTP 伺服器 + + + You are not connected to these servers. Private routing is used to deliver messages to them. + 你未連線到這些伺服器。私密路由會用來向它們傳送訊息。 + + + attempts + 嘗試次數 + + + decryption errors + 解密錯誤 + + + duplicates + 重複項目 + + + expired + 已過期 + + + inactive + 未活躍 + + + other + 其他 + + + other errors + 其他錯誤 + + + Active connections + 活躍連線 + + + All profiles + 所有個人檔案 + + + Current profile + 目前個人檔案 + + + Message reception + 訊息接收 + + + Private routing error + 私密路由錯誤 + + + Server address is incompatible with network settings: %@. + 伺服器地址與網路設定不相容:%@。 + + + Server version is incompatible with your app: %@. + 伺服器版本與你的應用程式不相容:%@。 + + + Show percentage + 顯示百分比 + + + You can now chat with %@ + 你現在可以與 %@ 聊天 + + + Blur media + 模糊媒體 + + + Connection notifications + 連線通知 + + + Destination server address of %@ is incompatible with forwarding server %@ settings. + 目的地伺服器 %@ 的地址與轉發伺服器 %@ 的設定不相容。 + + + Destination server version of %@ is incompatible with forwarding server %@. + 目的地伺服器 %@ 的版本與轉發伺服器 %@ 不相容。 + + + Disabled + 已停用 + + + Enabled + 已啟用 + + + Error connecting to forwarding server %@. Please try later. + 連線到轉發伺服器 %@ 時發生錯誤。請稍後再試。 + + + Forwarding server %1$@ failed to connect to destination server %2$@. Please try later. + 轉發伺服器 %1$@ 無法連線到目的地伺服器 %2$@。請稍後再試。 + + + Forwarding server address is incompatible with network settings: %@. + 轉發伺服器地址與網路設定不相容:%@。 + + + Forwarding server version is incompatible with network settings: %@. + 轉發伺服器版本與網路設定不相容:%@。 + + + Medium + 中等 + + + Soft + 柔和 + + + Strong + 強烈 + + + Allow sharing + 允許分享 + + + Delete %lld messages of members? + 要刪除成員的 %lld 則訊息嗎? + + + Nothing selected + 未選擇任何項目 + + + Selected %lld + 已選擇 %lld + + + Share to SimpleX + 分享到 SimpleX + + + The messages will be deleted for all members. + 這些訊息將為所有成員刪除。 + + + The messages will be marked as moderated for all members. + 這些訊息將為所有成員標記為已審核。 + + + Media & file servers + 媒體和檔案伺服器 + + + Message servers + 訊息伺服器 + + + Onion hosts will be **required** for connection. +Requires compatible VPN. + 連線將**需要** Onion 主機。 +需要相容的 VPN。 + + + Onion hosts will be used when available. +Requires compatible VPN. + 可用時將使用 Onion 主機。 +需要相容的 VPN。 + + + Save and reconnect + 儲存並重新連線 + + + Some file(s) were not exported: + 部分檔案未匯出: + + + Some non-fatal errors occurred during import: + 匯入期間發生一些非致命錯誤: + + + TCP connection + TCP 連線 + + + Update settings? + 要更新設定嗎? + + + You may migrate the exported database. + 你可以遷移匯出的數據庫。 + + + You may save the exported archive. + 你可以儲存匯出的封存檔。 + + + unknown servers + 未知伺服器 + + + Archived contacts + 已封存的聯絡人 + + + Calls prohibited! + 已禁止通話! + + + Can't call contact + 無法與聯絡人通話 + + + Confirm contact deletion? + 確認刪除聯絡人嗎? + + + Connecting to contact, please wait or check later! + 正在連線到聯絡人,請等待或稍後查看! + + + Connection and servers status. + 連線和伺服器狀態。 + + + Contact deleted! + 聯絡人已刪除! + + + Contact is deleted. + 聯絡人已刪除。 + + + Contact will be deleted - this cannot be undone! + 聯絡人將被刪除,且無法復原! + + + Conversation deleted! + 對話已刪除! + + + Delete up to 20 messages at once. + 一次最多刪除 20 則訊息。 + + + Delete without notification + 刪除且不通知 + + + Developer options + 開發者選項 + + + It protects your IP address and connections. + 它會保護你的 IP 地址和連線。 + + + Keep conversation + 保留對話 + + + Only delete conversation + 僅刪除對話 + + + Please ask your contact to enable calls. + 請你的聯絡人啟用通話。 + + + You can send messages to %@ from Archived contacts. + 你可以從已封存的聯絡人向 %@ 傳送訊息。 + + + You can still view conversation with %@ in the list of chats. + 你仍可在聊天列表中查看與 %@ 的對話。 + + + You need to allow your contact to call to be able to call them. + 你需要允許聯絡人來電,才能與其通話。 + + + call + 通話 + + + invite + 邀請 + + + message + 訊息 + + + Archive contacts to chat later. + 封存聯絡人,稍後再聊天。 + + + Better networking + 更好的網路連線 + + + Blur for better privacy. + 模糊處理,提升私隱。 + + + Chat list + 聊天列表 + + + Color chats with the new themes. + 使用新主題為聊天上色。 + + + Connect to your friends faster. + 更快與朋友連線。 + + + New chat experience 🎉 + 全新聊天體驗 🎉 + + + New media options + 新的媒體選項 + + + Play from the chat list. + 從聊天列表播放。 + + + Reachable chat toolbar + 易於觸及的聊天工具列 + + + Reset all hints + 重設所有提示 + + + Share from other apps. + 從其他應用程式分享。 + + + Toolbar opacity + 工具列不透明度 + + + You can change it in Appearance settings. + 你可以在外觀設定中變更。 + + + Chat preferences were changed. + 聊天偏好設定已變更。 + + + Corner + 角落 + + + Error changing connection profile + 變更連線個人檔案時發生錯誤 + + + Error changing to incognito! + 切換至隱身模式時發生錯誤! + + + Error migrating settings + 遷移設定時發生錯誤 + + + Error switching profile + 切換個人檔案時發生錯誤 + + + Message shape + 訊息形狀 + + + Remove archive? + 要移除封存檔嗎? + + + Select chat profile + 選擇聊天個人檔案 + + + Share profile + 分享個人檔案 + + + Some app settings were not migrated. + 部分應用程式設定未遷移。 + + + Tail + 尾端 + + + Your chat preferences + 你的聊天偏好設定 + + + Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. + 你的個人檔案儲存在你的裝置上,且只會與你的聯絡人分享。SimpleX 伺服器無法看到你的個人檔案。 + + + Your profile was changed. If you save it, the updated profile will be sent to all your contacts. + 你的個人檔案已變更。如果儲存,更新後的個人檔案會傳送給你的所有聯絡人。 + + + Do not use credentials with proxy. + 不要將憑證用於代理。 + + + Download files + 下載檔案 + + + File errors: +%@ + 檔案錯誤: +%@ + + + Forward %d message(s)? + 要轉發 %d 則訊息嗎? + + + Forward messages + 轉發訊息 + + + Forward messages without files? + 要轉發不含檔案的訊息嗎? + + + Forwarding %lld messages + 正在轉發 %lld 則訊息 + + + IP address + IP 地址 + + + Messages were deleted after you selected them. + 這些訊息在你選取後已被刪除。 + + + Nothing to forward! + 沒有可轉發的內容! + + + Other file errors: +%@ + 其他檔案錯誤: +%@ + + + Password + 密碼 + + + Port + 連接埠 + + + Proxy requires password + 代理需要密碼 + + + Username + 使用者名稱 + + + Your credentials may be sent unencrypted. + 你的憑證可能會未加密傳送。 + + + App session + 應用程式工作階段 + + + Chat profile + 聊天個人檔案 + + + New SOCKS credentials will be used every time you start the app. + 每次啟動應用程式時都會使用新的 SOCKS 憑證。 + + + New SOCKS credentials will be used for each server. + 每個伺服器都會使用新的 SOCKS 憑證。 + + + No permission to record speech + 沒有錄製語音的權限 + + + No permission to record video + 沒有錄製影片的權限 + + + Server + 伺服器 + + + To record speech please grant permission to use Microphone. + 若要錄製語音,請授予麥克風取用權限。 + + + To record video please grant permission to use Camera. + 若要錄製影片,請授予相機取用權限。 + + + Better calls + 更好的通話 + + + Better message dates. + 更好的訊息日期。 + + + Better notifications + 更好的通知 + + + Better security ✅ + 更好的安全性 ✅ + + + Better user experience + 更好的使用者體驗 + + + Customizable message shape. + 可自訂訊息形狀。 + + + Delete or moderate up to 200 messages. + 刪除或審核最多 200 則訊息。 + + + Forward up to 20 messages at once. + 一次最多轉發 20 則訊息。 + + + Improved delivery, reduced traffic usage. +More improvements are coming soon! + 改善送達,降低流量用量。 +更多改進即將推出! + + + SimpleX protocols reviewed by Trail of Bits. + SimpleX 協議已由 Trail of Bits 審查。 + + + Switch audio and video during the call. + 通話期間切換語音和視訊。 + + + Switch chat profile for 1-time invitations. + 為一次性邀請切換聊天個人檔案。 + + + 1-time link can be used *with one contact only* - share in person or via any messenger. + 一次性連結只能*與一位聯絡人*使用;請親自分享或透過任何通訊應用程式分享。 + + + Accept conditions + 接受條件 + + + Accepted conditions + 已接受條件 + + + Added media & file servers + 已新增媒體和檔案伺服器 + + + Added message servers + 已新增訊息伺服器 + + + Address or 1-time link? + 地址或一次性連結? + + + Address settings + 地址設定 + + + All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages. + 所有訊息和檔案均以 **端對端加密** 傳送,直接訊息具備後量子安全性。 + + + Check messages every 20 min. + 每 20 分鐘檢查訊息。 + + + Check messages when allowed. + 在允許時檢查訊息。 + + + Conditions accepted on: %@. + 已於 %@ 接受條件。 + + + Conditions are accepted for the operator(s): **%@**. + 已接受以下營運商的條件:**%@**。 + + + Conditions of use + 使用條件 + + + Conditions will be accepted for the operator(s): **%@**. + 將接受以下營運商的條件:**%@**。 + + + Conditions will be accepted on: %@. + 將於 %@ 接受條件。 + + + Conditions will be automatically accepted for enabled operators on: %@. + 將於 %@ 自動接受已啟用營運商的條件。 + + + Connection security + 連線安全性 + + + Create 1-time link + 建立一次性連結 + + + Current conditions text couldn't be loaded, you can review conditions via this link: + 無法載入目前條件文字,你可以透過此連結查看條件: + + + Delivered even when Apple drops them. + 即使 Apple 丟棄通知,也能送達。 + + + E2E encrypted notifications. + 端對端加密通知。 + + + Error accepting conditions + 接受條件時發生錯誤 + + + Error adding server + 新增伺服器時發生錯誤 + + + Error loading servers + 載入伺服器時發生錯誤 + + + Error saving servers + 儲存伺服器時發生錯誤 + + + Error updating server + 更新伺服器時發生錯誤 + + + Errors in servers configuration. + 伺服器設定中有錯誤。 + + + For chat profile %@: + 針對聊天個人檔案 %@: + + + For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server. + 例如,如果你的聯絡人透過 SimpleX Chat 伺服器接收訊息,你的應用程式會透過 Flux 伺服器傳送訊息。 + + + For private routing + 用於私密路由 + + + For social media + 用於社交媒體 + + + How it affects privacy + 它如何影響私隱 + + + How it helps privacy + 它如何提升私隱 + + + Instant + 即時 + + + More reliable notifications + 更可靠的通知 + + + Network decentralization + 網路去中心化 + + + Network operator + 網路營運商 + + + New events + 新事件 + + + New server + 新伺服器 + + + No media & file servers. + 沒有媒體和檔案伺服器。 + + + No message servers. + 沒有訊息伺服器。 + + + No push server + 沒有推送伺服器 + + + No servers for private message routing. + 沒有用於私密訊息路由的伺服器。 + + + No servers to receive files. + 沒有用於接收檔案的伺服器。 + + + No servers to receive messages. + 沒有用於接收訊息的伺服器。 + + + No servers to send files. + 沒有用於傳送檔案的伺服器。 + + + Notifications privacy + 通知私隱 + + + Open changes + 開啟變更 + + + Open conditions + 開啟條件 + + + Operator + 營運商 + + + Operator server + 營運商伺服器 + + + Or to share privately + 或私下分享 + + + Periodic + 定期 + + + Preset servers + 預設伺服器 + + + Server added to operator %@. + 已將伺服器新增至營運商 %@。 + + + Server operator changed. + 伺服器營運商已變更。 + + + Server operators + 伺服器營運商 + + + Server protocol changed. + 伺服器協議已變更。 + + + Share 1-time link with a friend + 與朋友分享一次性連結 + + + Share address publicly + 公開分享地址 + + + SimpleX address and 1-time links are safe to share via any messenger. + SimpleX 地址和一次性連結可安全地透過任何通訊應用程式分享。 + + + SimpleX address or 1-time link? + SimpleX 地址或一次性連結? + + + Some servers failed the test: +%@ + 部分伺服器測試失敗: +%@ + + + The app protects your privacy by using different operators in each conversation. + 應用程式會透過在每個對話中使用不同的營運商來保護你的私隱。 + + + The connection reached the limit of undelivered messages, your contact may be offline. + 此連線已達未送達訊息上限,你的聯絡人可能離線。 + + + The second preset operator in the app! + 應用程式中的第二個預設營運商! + + + The servers for new files of your current chat profile **%@**. + 你目前聊天個人檔案 **%@** 的新檔案伺服器。 + + + To protect against your link being replaced, you can compare contact security codes. + 為防止你的連結被替換,你可以比較聯絡人的安全碼。 + + + To receive + 用於接收 + + + To send + 用於傳送 + + + To use the servers of **%@**, accept conditions of use. + 若要使用 **%@** 的伺服器,請接受使用條件。 + + + Undelivered messages + 未送達訊息 + + + Use for files + 用於檔案 + + + View conditions + 查看條件 + + + View updated conditions + 查看更新後的條件 + + + When more than one operator is enabled, none of them has metadata to learn who communicates with whom. + 啟用多個營運商時,任何一個營運商都沒有可得知誰與誰通訊的中繼資料。 + + + You can configure servers via settings. + 你可以在設定中配置伺服器。 + + + You can set connection name, to remember who the link was shared with. + 你可以設定連線名稱,方便記住連結分享給了誰。 + + + Your servers + 你的伺服器 + + + Add friends + 新增朋友 + + + Add team members + 新增團隊成員 + + + Add your team members to the conversations. + 將你的團隊成員新增到對話中。 + + + Business address + 商務地址 + + + Business chats + 商務聊天 + + + Chat + 聊天 + + + Chat already exists + 聊天已存在 + + + Chat already exists! + 聊天已存在! + + + Chat will be deleted for all members - this cannot be undone! + 聊天將為所有成員刪除,且無法復原! + + + Chat will be deleted for you - this cannot be undone! + 聊天將為你刪除,且無法復原! + + + Delete chat + 刪除聊天 + + + Delete chat? + 要刪除聊天嗎? + + + Direct messages between members are prohibited in this chat. + 此聊天中禁止成員之間直接傳送訊息。 + + + Files and media are prohibited. + 已禁止檔案和媒體。 + + + Invite to chat + 邀請加入聊天 + + + Leave chat + 離開聊天 + + + Leave chat? + 要離開聊天嗎? + + + Role will be changed to "%@". All chat members will be notified. + 成員角色將變更為「%@」。所有聊天成員都會收到通知。 + + + Member will be removed from chat - this cannot be undone! + 成員將從聊天中移除,且無法復原! + + + Members can irreversibly delete sent messages. (24 hours) + 成員可以刪除已傳送訊息,且無法復原。(24 小時) + + + Members can send SimpleX links. + 成員可以傳送 SimpleX 連結。 + + + Members can send files and media. + 成員可以傳送檔案和媒體。 + + + Only chat owners can change preferences. + 只有聊天擁有者可以變更偏好設定。 + + + Or import archive file + 或匯入封存檔 + + + Privacy for your customers. + 保障你的客戶私隱。 + + + You are already connected with %@. + 你已經與 %@ 連線。 + + + You will stop receiving messages from this chat. Chat history will be preserved. + 你將停止接收此聊天的訊息。聊天記錄會保留。 + + + Change chat profiles + 變更聊天個人檔案 + + + About operators + 關於營運商 + + + accepted invitation + 已接受邀請 + + + Conditions are already accepted for these operator(s): **%@**. + 已接受這些營運商的條件:**%@**。 + + + SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app. + SimpleX Chat 與 Flux 達成協議,將 Flux 營運的伺服器納入應用程式。 + + + All data is kept private on your device. + 所有數據都會在你的裝置上保持私密。 + + + 1 year + 1 年 + + + Add list + 新增列表 + + + Add to list + 新增至列表 + + + All + 全部 + + + Another reason + 其他原因 + + + App group: + 應用程式群組: + + + Archive + 封存 + + + Archive report + 封存檢舉報告 + + + Archive report? + 要封存檢舉報告嗎? + + + Businesses + 商家 + + + Change automatic message deletion? + 要變更自動刪除訊息嗎? + + + Clear or delete group? + 要清除或刪除群組嗎? + + + Community guidelines violation + 違反社群指引 + + + Connection blocked + 連線已被封鎖 + + + Connection is blocked by server operator: +%@ + 連線已被伺服器營運商封鎖: +%@ + + + Connection not ready. + 連線尚未就緒。 + + + Connection requires encryption renegotiation. + 連線需要重新協商加密。 + + + Content violates conditions of use + 內容違反使用條件 + + + Create list + 建立列表 + + + Delete chat messages from your device. + 從你的裝置刪除聊天訊息。 + + + Delete list? + 要刪除列表嗎? + + + Delete report + 刪除檢舉報告 + + + Disable automatic message deletion? + 要停用自動刪除訊息嗎? + + + Disable delete messages + 停用自動刪除訊息 + + + Documents: + 文件: + + + Done + 完成 + + + Encryption renegotiation in progress. + 正在重新協商加密。 + + + Error creating list + 建立列表時發生錯誤 + + + Error creating report + 建立檢舉報告時發生錯誤 + + + Error reordering lists + 重新排序列表時發生錯誤 + + + Error saving chat list + 儲存聊天列表時發生錯誤 + + + Favorites + 最愛 + + + Groups + 群組 + + + Inappropriate content + 不當內容 + + + Inappropriate profile + 不當個人檔案 + + + List + 列表 + + + List name and emoji should be different for all lists. + 所有列表的名稱和表情符號都應不同。 + + + List name... + 列表名稱... + + + Messages in this chat will never be deleted. + 此聊天中的訊息永遠不會被刪除。 + + + More + 更多 + + + No chats + 沒有聊天 + + + No chats found + 找不到聊天 + + + No chats in list %@ + 列表 %@ 中沒有聊天 + + + No unread chats + 沒有未讀聊天 + + + Notes + 備註 + + + Only sender and moderators see it + 只有傳送者和審核員看得到 + + + Only you and moderators see it + 只有你和審核員看得到 + + + Report content: only group moderators will see it. + 檢舉內容:只有群組審核員會看到。 + + + Report member profile: only group moderators will see it. + 檢舉成員個人檔案:只有群組審核員會看到。 + + + Report other: only group moderators will see it. + 檢舉其他事項:只有群組審核員會看到。 + + + Report reason? + 檢舉原因? + + + Report spam: only group moderators will see it. + 檢舉垃圾訊息:只有群組審核員會看到。 + + + Report violation: only group moderators will see it. + 檢舉違規:只有群組審核員會看到。 + + + Set chat name… + 設定聊天名稱… + + + Spam + 垃圾訊息 + + + archived report + 已封存檢舉報告 + + + moderator + 審核員 + + + Active + 有效 + + + All reports will be archived for you. + 所有檢舉報告都會為你封存。 + + + Allow to report messsages to moderators. + 允許向審核員檢舉訊息。 + + + Archive %lld reports? + 要封存 %lld 份檢舉報告嗎? + + + Archive all reports? + 要封存所有檢舉報告嗎? + + + Archive reports + 封存檢舉報告 + + + Clear group? + 要清除群組嗎? + + + Confirmed + 已確認 + + + Error checking token status + 檢查權杖狀態時發生錯誤 + + + Error registering for notifications + 註冊通知時發生錯誤 + + + Error testing server connection + 測試伺服器連線時發生錯誤 + + + Expired + 已過期 + + + For all moderators + 對所有審核員 + + + For me + 只對我 + + + Invalid + 無效 + + + Invalid (bad token) + 無效(權杖錯誤) + + + Invalid (expired) + 無效(已過期) + + + Invalid (unregistered) + 無效(未註冊) + + + Invalid (wrong topic) + 無效(主題錯誤) + + + Member reports + 成員檢舉報告 + + + Members can report messsages to moderators. + 成員可以向審核員檢舉訊息。 + + + Mute all + 全部靜音 + + + New + 新建 + + + No token! + 沒有權杖! + + + Notifications error + 通知錯誤 + + + Notifications status + 通知狀態 + + + Please try to disable and re-enable notfications. + 請嘗試停用再重新啟用通知。 + + + Please wait for token activation to complete. + 請等待權杖啟用完成。 + + + Please wait for token to be registered. + 請等待權杖完成註冊。 + + + Prohibit reporting messages to moderators. + 禁止向審核員檢舉訊息。 + + + Register + 註冊 + + + Register notification token? + 要註冊通知權杖嗎? + + + Registered + 已註冊 + + + Reporting messages to moderators is prohibited. + 已禁止向審核員檢舉訊息。 + + + TCP port for messaging + 用於訊息傳遞的 TCP 連接埠 + + + Token status: %@. + 權杖狀態:%@。 + + + Use TCP port %@ when no port is specified. + 未指定連接埠時使用 TCP 連接埠 %@。 + + + Use web port + 使用 Web 連接埠 + + + You should receive notifications. + 你應該會收到通知。 + + + Better groups performance + 提升群組效能 + + + Better privacy and security + 提升私隱和安全性 + + + Don't miss important messages. + 不要錯過重要訊息。 + + + Faster deletion of groups. + 更快刪除群組。 + + + Faster sending messages. + 更快傳送訊息。 + + + Get notified when mentioned. + 被提及時收到通知。 + + + Help admins moderating their groups. + 協助管理員審核群組。 + + + Mention members 👋 + 提及成員 👋 + + + No message + 沒有訊息 + + + Organize chats into lists + 將聊天整理到列表中 + + + Private media file names. + 私密媒體檔案名稱。 + + + Send private reports + 傳送私密檢舉報告 + + + This message was deleted or not received yet. + 此訊息已刪除或尚未收到。 + + + Updated conditions + 更新後的條件 + + + pending + 待處理 + + + pending approval + 待核准 + + + rejected + 已拒絕 + + + All chats will be removed from the list %@, and the list deleted. + 所有聊天都會從列表 %@ 中移除,且該列表會被刪除。 + + + File is blocked by server operator: +%@. + 檔案已被伺服器營運商封鎖: +%@。 + + + Enable Flux in Network & servers settings for better metadata privacy. + 在「網路與伺服器」設定中啟用 Flux,以提升中繼資料私隱。 + + + Privacy policy and conditions of use. + 私隱政策和使用條件。 + + + Short link + 短連結 + + + SimpleX channel link + SimpleX 頻道連結 + + + This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link. + 此連結需要較新的應用程式版本。請升級應用程式,或請你的聯絡人傳送相容的連結。 + + + All servers + 所有伺服器 + + + Use TCP port 443 for preset servers only. + 僅對預設伺服器使用 TCP 連接埠 443。 + + + Open link? + 要開啟連結嗎? + + + Accept as member + 以成員身分接受 + + + Accept as observer + 以觀察者身分接受 + + + Accept member + 接受成員 + + + Chat with admins + 與管理員聊天 + + + Chat with member + 與成員聊天 + + + Chats with members + 與成員的聊天 + + + Delete chat with member? + 要刪除與成員的聊天嗎? + + + Error accepting member + 接受成員時發生錯誤 + + + Member admission + 成員加入審批 + + + Member will join the group, accept member? + 成員將加入群組,要接受成員嗎? + + + New member wants to join the group. + 新成員想加入群組。 + + + No chats with members + 沒有與成員的聊天 + + + Please wait for group moderators to review your request to join the group. + 請等待群組審核員審核你的加入群組申請。 + + + Reject member? + 要拒絕成員嗎? + + + Report sent to moderators + 檢舉報告已傳送給審核員 + + + Review members + 審核成員 + + + Review members before admitting ("knocking"). + 接納前先審核成員(「敲門」)。 + + + Save admission settings? + 要儲存加入審批設定嗎? + + + Set member admission + 設定成員加入審批 + + + You can view your reports in Chat with admins. + 你可以在「與管理員聊天」中查看你的檢舉報告。 + + + accepted %@ + 已接受 %@ + + + accepted you + 已接受你 + + + all + 全部 + + + can't send messages + 無法傳送訊息 + + + contact deleted + 聯絡人已刪除 + + + contact disabled + 聯絡人已停用 + + + contact not ready + 聯絡人尚未就緒 + + + group is deleted + 群組已刪除 + + + member has old version + 成員使用舊版本 + + + not synchronized + 未同步 + + + pending review + 等待審核 + + + removed from group + 已從群組移除 + + + request to join rejected + 加入群組申請已被拒絕 + + + review + 審核 + + + reviewed by admins + 管理員已審核 + + + you accepted this member + 你已接受此成員 + + + Error adding short link + 新增短連結時發生錯誤 + + + Group profile was changed. If you save it, the updated profile will be sent to group members. + 群組檔案已變更。如果儲存,更新後的檔案會傳送給群組成員。 + + + Save (and notify members) + 儲存(並通知成員) + + + Save group profile? + 要儲存群組檔案嗎? + + + Accept contact request + 接受聯絡請求 + + + Add message + 新增訊息 + + + Empty message! + 訊息為空! + + + Error changing chat profile + 變更聊天個人檔案時發生錯誤 + + + Error rejecting contact request + 拒絕聯絡請求時發生錯誤 + + + Messages are protected by **end-to-end encryption**. + 訊息受 **端對端加密** 保護。 + + + Open new chat + 開啟新聊天 + + + Open new group + 開啟新群組 + + + Open to accept + 開啟並接受 + + + Open to connect + 開啟並連線 + + + Open to join + 開啟並加入 + + + Send contact request? + 要傳送聯絡請求嗎? + + + Send request + 傳送請求 + + + Send request without message + 傳送不含訊息的請求 + + + SimpleX address settings + SimpleX 地址設定 + + + You will be able to send messages **only after your request is accepted**. + 你**只有在請求被接受後**才能傳送訊息。 + + + Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile. + 你的聊天已移至 %@,但重新導向至個人檔案時發生未預期的錯誤。 + + + contact should accept… + 聯絡人需要接受… + + + group + 群組 + + + request is sent + 請求已傳送 + + + Can't change profile + 無法變更個人檔案 + + + To use another profile after connection attempt, delete the chat and use the link again. + 若要在嘗試連線後使用另一個個人檔案,請刪除此聊天並再次使用連結。 + + + Chat with members before they join. + 在成員加入前與他們聊天。 + + + Connect faster! 🚀 + 更快連線!🚀 + + + Less traffic on mobile networks. + 減少行動網路流量。 + + + Message instantly once you tap Connect. + 點按「連線」後即可即時傳送訊息。 + + + New group role: Moderator + 新群組角色:審核員 + + + No private routing session + 沒有私密路由工作階段 + + + Private routing timeout + 私密路由逾時 + + + Protocol background timeout + 通訊協定背景逾時 + + + Removes messages and blocks members. + 移除訊息並封鎖成員。 + + + Review group members + 審核群組成員 + + + Send your private feedback to groups. + 向群組傳送你的私密回饋。 + + + TCP connection bg timeout + TCP 連線背景逾時 + + + Loading profile… + 正在載入個人檔案… + + + Your connection was moved to %@ but an error happened when switching profile. + 你的連線已移至 %@,但切換個人檔案時發生錯誤。 + + + Bio + 簡介 + + + Bio too large + 簡介過大 + + + Business connection + 商務連線 + + + Description too large + 描述過大 + + + Short description + 簡短描述 + + + Tap Connect to chat + 點按「連線」即可聊天 + + + Tap Connect to send request + 點按「連線」即可傳送請求 + + + Tap Join group + 點按「加入群組」 + + + Your business contact + 你的商務聯絡人 + + + Your contact + 你的聯絡人 + + + Your group + 你的群組 + + + Create your address + 建立你的地址 + + + Enable disappearing messages by default. + 預設啟用自動銷毀訊息。 + + + Keep your chats clean + 保持聊天整潔 + + + Set profile bio and welcome message. + 設定個人檔案簡介和歡迎訊息。 + + + Share your address + 分享你的地址 + + + Short SimpleX address + SimpleX 短地址 + + + Time to disappear is set only for new contacts. + 銷毀時間只會為新聯絡人設定。 + + + Use incognito profile + 使用隱身個人檔案 + + + Welcome your contacts 👋 + 歡迎你的聯絡人 👋 + + + Share old address + 分享舊地址 + + + Share old link + 分享舊連結 + + + The address will be short, and your profile will be shared via the address. + 地址將會是短地址,且你的個人檔案會透過該地址分享。 + + + The link will be short, and group profile will be shared via the link. + 連結將會是短連結,且群組檔案會透過該連結分享。 + + + Upgrade + 升級 + + + Upgrade address + 升級地址 + + + Upgrade address? + 要升級地址嗎? + + + Upgrade group link? + 要升級群組連結嗎? + + + Upgrade link + 升級連結 + + + Upgrade your address + 升級你的地址 + + + Contact requests from groups + 來自群組的聯絡請求 + + + Error setting auto-accept + 設定自動接受時發生錯誤 + + + Member is deleted - can't accept request + 成員已刪除,無法接受請求 + + + This setting is for your current profile **%@**. + 此設定適用於你目前的個人檔案 **%@**。 + + + requested connection + 已請求連線 + + + requested connection from group %@ + 已從群組 %@ 請求連線 + + + Allow files and media only if your contact allows them. + 只有你的聯絡人允許時,才允許傳送檔案和媒體。 + + + Allow your contacts to send files and media. + 允許你的聯絡人傳送檔案和媒體。 + + + Bot + 機器人 + + + Both you and your contact can send files and media. + 你和你的聯絡人都可以傳送檔案和媒體。 + + + Files and media are prohibited in this chat. + 此聊天中禁止檔案和媒體。 + + + Only you can send files and media. + 只有你可以傳送檔案和媒體。 + + + Only your contact can send files and media. + 只有你的聯絡人可以傳送檔案和媒體。 + + + Open to use bot + 開啟並使用機器人 + + + Tap Connect to use bot + 點按「連線」即可使用機器人 + + + To send commands you must be connected. + 你必須連線後才能傳送指令。 + + + Deprecated options + 已棄用選項 + + + Open clean link + 開啟清理後的連結 + + + Open full link + 開啟完整連結 + + + Remove link tracking + 移除連結追蹤 + + + Member %@ + 成員 %@ + + + Error deleting chat + 刪除聊天時發生錯誤 + + + Error: %@. + 錯誤:%@。 + + + Fingerprint in destination server address does not match certificate: %@. + 目的地伺服器地址中的指紋與憑證不符:%@。 + + + Fingerprint in forwarding server address does not match certificate: %@. + 轉發伺服器地址中的指紋與憑證不符:%@。 + + + Fingerprint in server address does not match certificate: %@. + 伺服器地址中的指紋與憑證不符:%@。 + + + Error connecting to the server used to receive messages from this connection: %@ + 連線至用於接收此連線訊息的伺服器時發生錯誤:%@ + + + Trying to connect to the server used to receive messages from this connection. + 正在嘗試連線至用於接收此連線訊息的伺服器。 + + + You are connected to the server used to receive messages from this connection. + 你已連線至用於接收此連線訊息的伺服器。 + + + You are not connected to the server used to receive messages from this connection (no subscription). + 你未連線至用於接收此連線訊息的伺服器(沒有訂閱)。 + + + no subscription + 沒有訂閱 + + + All messages + 所有訊息 + + + Audio call + 語音通話 + + + Delete member messages + 刪除成員訊息 + + + Delete member messages? + 要刪除成員訊息嗎? + + + Filter + 篩選 + + + Images + 圖片 + + + Invite member + 邀請成員 + + + Links + 連結 + + + Member messages will be deleted - this cannot be undone! + 成員訊息將被刪除,且無法復原! + + + Remove and delete messages + 移除並刪除訊息 + + + Search files + 搜尋檔案 + + + Search images + 搜尋圖片 + + + Search links + 搜尋連結 + + + Search videos + 搜尋影片 + + + Search voice messages + 搜尋語音訊息 + + + Videos + 影片 + + + Connection failed + 連線失敗 + + + If you joined or created channels, they will stop working permanently. + 如果你已加入或建立頻道,這些頻道將永久停止運作。 + + + failed + 失敗 + + + %d subscriber + %d 位訂閱者 + + + %d subscribers + %d 位訂閱者 + + + %1$d/%2$d relays active + %1$d/%2$d 個中繼已啟用 + + + %1$d/%2$d relays active, %3$d failed + %1$d/%2$d 個中繼已啟用,%3$d 個失敗 + + + %1$d/%2$d relays connected + %1$d/%2$d 個中繼已連線 + + + %1$d/%2$d relays connected, %3$d errors + %1$d/%2$d 個中繼已連線,%3$d 個錯誤 + + + %lld channel events + %lld 個頻道事件 + + + **Test relay** to retrieve its name. + **測試中繼**以取得其名稱。 + + + Block subscriber for all? + 要為所有人封鎖訂閱者嗎? + + + Broadcast + 廣播 + + + Channel + 頻道 + + + Channel display name + 頻道顯示名稱 + + + Channel full name (optional) + 頻道完整名稱(選填) + + + Channel image + 頻道圖片 + + + Channel link + 頻道連結 + + + Channel profile + 頻道檔案 + + + Channel profile is stored on subscribers' devices and on the chat relays. + 頻道檔案會儲存在訂閱者的裝置和聊天中繼上。 + + + Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers. + 頻道檔案已變更。如果儲存,更新後的檔案會傳送給頻道訂閱者。 + + + Channel will be deleted for all subscribers - this cannot be undone! + 頻道將為所有訂閱者刪除,且無法復原! + + + Channel will be deleted for you - this cannot be undone! + 頻道將為你刪除,且無法復原! + + + Chat relay + 聊天中繼 + + + Chat relays + 聊天中繼 + + + Chat relays forward messages in channels you create. + 聊天中繼會轉發你建立的頻道中的訊息。 + + + Chat relays forward messages to channel subscribers. + 聊天中繼會將訊息轉發給頻道訂閱者。 + + + Check relay address and try again. + 請檢查中繼地址並再試一次。 + + + Check relay name and try again. + 請檢查中繼名稱並再試一次。 + + + Configure relays + 配置中繼 + + + Create public channel + 建立公開頻道 + + + Create public channel (BETA) + 建立公開頻道(BETA) + + + Creating channel + 正在建立頻道 + + + Decode link + 解碼連結 + + + Delete channel + 刪除頻道 + + + Delete channel? + 要刪除頻道嗎? + + + Delete relay + 刪除中繼 + + + Edit channel profile + 編輯頻道檔案 + + + Enable at least one chat relay in Network & Servers. + 請在「網路與伺服器」中啟用至少一個聊天中繼。 + + + Enter channel name… + 輸入頻道名稱… + + + Enter relay name… + 輸入中繼名稱… + + + Error adding relay + 新增中繼時發生錯誤 + + + Error creating channel + 建立頻道時發生錯誤 + + + Error saving channel profile + 儲存頻道檔案時發生錯誤 + + + Get link + 取得連結 + + + Invalid relay address! + 中繼地址無效! + + + Invalid relay name! + 中繼名稱無效! + + + Join channel + 加入頻道 + + + Leave channel + 離開頻道 + + + Leave channel? + 要離開頻道嗎? + + + Message error + 訊息錯誤 + + + New chat relay + 新聊天中繼 + + + No chat relays + 沒有聊天中繼 + + + No chat relays enabled. + 未啟用聊天中繼。 + + + Not all relays connected + 並非所有中繼都已連線 + + + Open channel + 開啟頻道 + + + Open new channel + 開啟新頻道 + + + Owner + 擁有者 + + + Owners & contributors + 擁有者 + + + Preset relay address + 預設中繼地址 + + + Preset relay name + 預設中繼名稱 + + + Relay + 中繼 + + + Relay address + 中繼地址 + + + Relay connection failed + 中繼連線失敗 + + + Relay link + 中繼連結 + + + Relay test failed! + 中繼測試失敗! + + + Remove subscriber? + 要移除訂閱者嗎? + + + Save (and notify subscribers) + 儲存(並通知訂閱者) + + + Save channel profile + 儲存頻道檔案 + + + Save channel profile? + 要儲存頻道檔案嗎? + + + Server requires authorization to connect to relay, check password. + 伺服器需要授權才能連線至中繼,請檢查密碼。 + + + Share relay address + 分享中繼地址 + + + SimpleX relay address + SimpleX 中繼地址 + + + Subscriber + 訂閱者 + + + Subscriber will be removed from channel - this cannot be undone! + 訂閱者將從頻道移除,且無法復原! + + + Subscribers + 訂閱者 + + + Subscribers use relay link to connect to the channel. +Relay address was used to set up this relay for the channel. + 訂閱者使用中繼連結連線至頻道。 +中繼地址曾用於為此頻道設定此中繼。 + + + Tap Join channel + 點按「加入頻道」 + + + Test relay + 測試中繼 + + + The app removed this message after %lld attempts to receive it. + 應用程式在嘗試接收此訊息 %lld 次後將其移除。 + + + This is a chat relay address, it cannot be used to connect. + 這是聊天中繼地址,無法用於連線。 + + + This is your link for channel %@! + 這是你頻道 %@ 的連結! + + + Unblock subscriber for all? + 要為所有人解除封鎖訂閱者嗎? + + + Use for new channels + 用於新頻道 + + + Use relay + 使用中繼 + + + Verify + 驗證 + + + Wait + 等待 + + + Wait response + 等待回應 + + + You can share a link or a QR code - anybody will be able to join the channel. + 你可以分享連結或二維碼,任何人都可以加入頻道。 + + + You connected to the channel via this relay link. + 你已透過此中繼連結連線至頻道。 + + + You will stop receiving messages from this channel. Chat history will be preserved. + 你將停止接收此頻道的訊息。聊天記錄會保留。 + + + Your channel + 你的頻道 + + + Your profile **%@** will be shared with channel relays and subscribers. +Relays can access channel messages. + 你的個人檔案 **%@** 將與頻道中繼和訂閱者分享。 +中繼可以存取頻道訊息。 + + + Your relay address + 你的中繼地址 + + + Your relay name + 你的中繼名稱 + + + accepted + 已接受 + + + active + 已啟用 + + + channel + 頻道 + + + channel profile updated + 頻道檔案已更新 + + + deleted channel + 已刪除頻道 + + + error: %@ + 錯誤:%@ + + + link + 連結 + + + new + + + + relay + 中繼 + + + removed (%d attempts) + 已移除(%d 次嘗試) + + + updated channel profile + 已更新頻道檔案 + + + via %@ + 透過 %@ + + + you are subscriber + 你是訂閱者 + + + %d relays failed + %d 個中繼失敗 + + + %d relays not active + %d 個中繼未啟用 + + + %d relays removed + %d 個中繼已移除 + + + %1$d/%2$d relays active, %3$d errors + %1$d/%2$d 個中繼已啟用,%3$d 個錯誤 + + + %1$d/%2$d relays active, %3$d removed + %1$d/%2$d 個中繼已啟用,%3$d 個已移除 + + + %1$d/%2$d relays connected, %3$d failed + %1$d/%2$d 個中繼已連線,%3$d 個失敗 + + + %1$d/%2$d relays connected, %3$d removed + %1$d/%2$d 個中繼已連線,%3$d 個已移除 + + + (from owner) + (來自擁有者) + + + (signed) + (已簽署) + + + - opt-in to send link previews. +- prevent hyperlink phishing. +- remove link tracking. + - 選擇是否傳送連結預覽。 +- 防止超連結釣魚。 +- 移除連結追蹤。 + + + A link for one person to connect + 供一人連線的連結 + + + Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts. + 將地址加入你的個人檔案,讓你的 SimpleX 聯絡人可以與其他人分享。個人檔案更新將傳送給你的 SimpleX 聯絡人。 + + + All relays failed + 所有中繼均失敗 + + + All relays removed + 所有中繼均已移除 + + + Allow members to chat with admins. + 允許成員與管理員聊天。 + + + Allow sending direct messages to subscribers. + 允許向訂閱者傳送直接訊息。 + + + Allow subscribers to chat with admins. + 允許訂閱者與管理員聊天。 + + + Be free +in your network + 在你的網路中 +保持自由 + + + Be free in your network. + 在你的網路中保持自由。 + + + Because we destroyed the power to know who you are. So that your power can never be taken. + 因為我們消除了得知你身分的能力,讓你的自主權永遠不會被奪走。 + + + Channel has no active relays. Please try to join later. + 頻道沒有啟用中的中繼。請稍後再嘗試加入。 + + + Channel preferences + 頻道偏好設定 + + + Channel temporarily unavailable + 頻道暫時無法使用 + + + Channels + 頻道 + + + Chats with admins are prohibited. + 禁止與管理員聊天。 + + + Chats with admins in public channels have no E2E encryption - use only with trusted chat relays. + 公開頻道中與管理員的聊天沒有 E2E 加密,僅應與可信任的聊天中繼一起使用。 + + + Chats with members are disabled + 與成員聊天已停用 + + + Connect via link or QR code + 透過連結或二維碼連線 + + + Contact address + 聯絡地址 + + + Contribute + 貢獻 + + + Create your public address + 建立你的公開地址 + + + Direct messages between subscribers are prohibited. + 禁止訂閱者之間直接傳送訊息。 + + + Disable + 停用 + + + Do not send history to new subscribers. + 不要將歷史記錄傳送給新訂閱者。 + + + Easier to invite your friends 👋 + 更容易邀請你的朋友 👋 + + + Enable chats with admins? + 要啟用與管理員聊天嗎? + + + Enable link previews? + 要啟用連結預覽嗎? + + + Enter profile name... + 輸入個人檔案名稱... + + + Error sharing channel + 分享頻道時發生錯誤 + + + For anyone to reach you + 讓任何人都能聯絡你 + + + Get started + 開始使用 + + + History is not sent to new subscribers. + 歷史記錄不會傳送給新訂閱者。 + + + Install SimpleX Chat for terminal + 安裝 SimpleX Chat 終端機版 + + + Invite someone privately + 私下邀請某人 + + + Let someone connect to you + 讓某人與你連線 + + + Link signature verified. + 連結簽章已驗證。 + + + Members can chat with admins. + 成員可以與管理員聊天。 + + + Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages. + 此頻道中的訊息**並非端對端加密**。聊天中繼可以看到這些訊息。 + + + Messages in this channel are not end-to-end encrypted. Chat relays can see these messages. + 此頻道中的訊息並非端對端加密。聊天中繼可以看到這些訊息。 + + + Migrate + 遷移 + + + Network error + 網路錯誤 + + + Network routers cannot know +who talks to whom + 網路路由器無法知道 +誰在與誰通訊 + + + New 1-time link + 新的一次性連結 + + + No account. No phone. No email. No ID. +The most secure encryption. + 無需帳戶。無需電話號碼。無需電子郵件。無需 ID。 +最安全的加密。 + + + No active relays + 沒有啟用中的中繼 + + + Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life. + 沒有人追蹤你的對話。沒有人描繪你的去向。私隱從來不是一項功能,而是一種生活方式。 + + + Non-profit governance + 非營利治理 + + + Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign. + 這不是裝在別人門上的一把更好的鎖。也不是一位更友善、尊重你私隱卻仍記錄所有訪客的房東。你不是客人。這裡是你的家。沒有人可以擅自進入;你完全自主。 + + + One-time link + 一次性連結 + + + Only channel owners can change channel preferences. + 只有頻道擁有者才能更改頻道偏好設定。 + + + Open external link? + 開啟外部連結? + + + Operators commit to: +- Be independent +- Minimize metadata usage +- Run verified open-source code + 營運商承諾: +- 保持獨立 +- 盡量減少中繼資料使用 +- 執行經驗證的開源程式碼 + + + Or show QR in person or via video call. + 或當面或透過視訊通話顯示二維碼。 + + + Or use this QR - print or show online. + 或使用此二維碼,可以列印或在線上顯示。 + + + Ownership: you can run your own relays. + 擁有權:你可以營運自己的中繼。 + + + Paste link / Scan + 貼上連結 / 掃描 + + + Privacy: for owners and subscribers. + 私隱:保障擁有者和訂閱者。 + + + Private and secure messaging. + 私密且安全的訊息傳遞。 + + + Profile update will be sent to your SimpleX contacts. + 個人檔案更新將傳送給你的 SimpleX 聯絡人。 + + + Prohibit chats with admins. + 禁止與管理員聊天。 + + + Prohibit sending direct messages to subscribers. + 禁止向訂閱者傳送直接訊息。 + + + Public channels - speak freely 🚀 + 公開頻道 - 自由發言 🚀 + + + Read more in User Guide. + 在使用者指南中閱讀更多內容。 + + + Relay results: + 中繼結果: + + + Reliability: many relays per channel. + 可靠性:每個頻道使用多個中繼。 + + + Safe web links + 安全的網頁連結 + + + Save and notify subscribers + 儲存並通知訂閱者 + + + Security: owners hold channel keys. + 安全性:擁有者持有頻道金鑰。 + + + Send the link via any messenger - it's secure. Ask to paste into SimpleX. + 透過任何通訊應用程式傳送連結,這是安全的。請對方貼到 SimpleX 中。 + + + Send up to 100 last messages to new subscribers. + 最多將最近 100 則訊息傳送給新訂閱者。 + + + Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later. + 傳送連結預覽可能會向該網站透露你的 IP 地址。你稍後可以在「私隱」設定中變更此設定。 + + + Setup notifications + 設定通知 + + + Setup routers + 設定路由器 + + + Share address with SimpleX contacts? + 要與 SimpleX 聯絡人分享地址嗎? + + + Share channel + 分享頻道 + + + Share via chat + 透過聊天分享 + + + Share with SimpleX contacts + 與 SimpleX 聯絡人分享 + + + Star on GitHub + 在 GitHub 上加星 + + + Subscriber reports + 訂閱者檢舉報告 + + + Subscribers can add message reactions. + 訂閱者可以新增訊息回應。 + + + Subscribers can chat with admins. + 訂閱者可以與管理員聊天。 + + + Subscribers can irreversibly delete sent messages. (24 hours) + 訂閱者可以刪除已傳送的訊息,且無法復原。(24 小時) + + + Subscribers can report messsages to moderators. + 訂閱者可以向審核員檢舉訊息。 + + + Subscribers can send SimpleX links. + 訂閱者可以傳送 SimpleX 連結。 + + + Subscribers can send direct messages. + 訂閱者可以傳送直接訊息。 + + + Subscribers can send disappearing messages. + 訂閱者可以傳送自動銷毀訊息。 + + + Subscribers can send files and media. + 訂閱者可以傳送檔案和媒體。 + + + Subscribers can send voice messages. + 訂閱者可以傳送語音訊息。 + + + Talk to someone + 與某人聊天 + + + Tap to open + 點一下即可開啟 + + + The connection reached the limit of undelivered messages + 連線已達未送達訊息數量上限 + + + The first network where you own +your contacts and groups. + 第一個讓你擁有 +自己的聯絡人和群組的網路。 + + + The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it. + 人類最古老的自由 - 在不被監視的情況下與另一個人交談 - 建立在不會背棄它的基礎設施上。 + + + Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible. + 後來我們轉到線上,每個平台都要求你交出一部分自己:你的姓名、電話號碼、朋友。我們接受了這樣的代價:與他人交談,就要讓別人知道我們在和誰交談。每一代人和技術都是如此:電話、電子郵件、通訊應用程式、社交媒體。這似乎是唯一可行的方式。 + + + There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected. + 還有另一種方式。一個不需要電話號碼、使用者名稱、帳戶,也沒有任何形式使用者身分的網路。一個在不知道誰與誰連線的情況下,仍能連接人們並傳遞加密訊息的網路。 + + + To make SimpleX Network last. + 讓 SimpleX Network 長久延續。 + + + Up to 100 last messages are sent to new subscribers. + 最多會將最近 100 則訊息傳送給新訂閱者。 + + + Use this address in your social media profile, website, or email signature. + 在你的社交媒體個人檔案、網站或電子郵件簽名中使用此地址。 + + + Waiting for channel owner to add relays. + 正在等待頻道擁有者新增中繼。 + + + We made connecting simpler for new users. + 我們讓新使用者更容易連線。 + + + Why SimpleX is built. + SimpleX 為何而建。 + + + You commit to: +- Only legal content in public groups +- Respect other users - no spam + 你承諾: +- 只在公開群組中發布合法內容 +- 尊重其他使用者 - 不發垃圾訊息 + + + You were born without an account + 你生來就不需要帳戶 + + + Your conversations belong to you, as it had always been before the Internet. The network is not a place you visit. It is a place you create and own. And nobody can take it from you, whether you make it private or public. + 你的對話屬於你,就像網際網路出現前一直如此。網路不是你造訪的地方,而是你建立並擁有的地方。無論你把它設為私密或公開,都沒有人能從你手中奪走。 + + + Your network + 你的網路 + + + Your public address + 你的公開地址 + + + can't broadcast + 無法廣播 + + + removed by operator + 已由營運商移除 + + + ⚠️ Signature verification failed: %@. + ⚠️ 簽章驗證失敗:%@。 + + + Bottom bar + 底部列 + + + Create your link + 建立你的連結 + + + Network commitments + 網路承諾 + + + On your phone, not on servers. + 在你的手機上,而不是在伺服器上。 + + + Top bar + 頂部列 + + + Add + 新增 + + + Add relay + 新增中繼 + + + Add relays + 新增中繼 + + + Add relays to restore message delivery. + 新增中繼來恢復訊息傳送。 + + + Add this code to your webpage. It will display the preview of your channel / group. + 將此程式碼加入你的網頁。它會顯示你的頻道 / 群組預覽。 + + + Advanced options + 進階選項 + + + Allow anyone to embed + 允許任何人嵌入 + + + Any webpage can show the preview. + 任何網頁都可以顯示預覽。 + + + App update required + 需要更新應用程式 + + + Badge cannot be verified + 無法驗證徽章 + + + Cancel and delete channel + 取消並刪除頻道 + + + Cancel creating channel? + 要取消建立頻道嗎? + + + Channel webpage + 頻道網頁 + + + Channel will start working with %1$d of %2$d relays. Continue? + 頻道將以 %1$d/%2$d 個中繼開始運作。要繼續嗎? + + + Chat data + 聊天資料 + + + Connecting via channel name requires a newer app version. + 透過頻道名稱連線需要較新的應用程式版本。 + + + Connecting via contact name requires a newer app version. + 透過聯絡人名稱連線需要較新的應用程式版本。 + + + Contact + 聯絡人 + + + Copy code + 複製程式碼 + + + Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting. + 建立網頁,在訪客訂閱前向他們顯示你的頻道預覽。你可以自行託管,或使用任何靜態託管服務。 + + + Delete from history + 從歷史記錄中刪除 + + + Enter webpage URL + 輸入網頁 URL + + + Error adding relays + 新增中繼時發生錯誤 + + + Error deleting message + 刪除訊息時發生錯誤 + + + Group webpage + 群組網頁 + + + Help & support + 說明與支援 + + + It will be shown to subscribers and used to allow loading the preview. + 它會顯示給訂閱者,並用來允許載入預覽。 + + + More privacy + 更多私隱 + + + No available relays + 沒有可用的中繼 + + + No relays + 沒有中繼 + + + Only your page above can show the preview. + 只有你在上方設定的頁面可以顯示預覽。 + + + Please upgrade the app. + 請升級應用程式。 + + + Relay will be removed from channel - this cannot be undone! + 中繼將從頻道移除,且無法復原! + + + Relays added: %@. + 已新增中繼:%@。 + + + Remove relay + 移除中繼 + + + Remove relay? + 要移除中繼嗎? + + + Save and notify members + 儲存並通知成員 + + + Save webpage settings? + 要儲存網頁設定嗎? + + + Status + 狀態 + + + Support the project + 支持專案 + + + The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge. + 此徽章使用此版本應用程式無法識別的金鑰簽署。請更新應用程式來驗證此徽章。 + + + This badge could not be verified and may not be genuine. + 無法驗證此徽章,可能並非真品。 + + + This group requires a newer version of the app. Please update the app to join. + 此群組需要較新的應用程式版本。請更新應用程式才能加入。 + + + This is the last active relay. Removing it will prevent message delivery to subscribers. + 這是最後一個啟用中的中繼。移除它會導致無法向訂閱者傳送訊息。 + + + Unsupported channel name + 不支援的頻道名稱 + + + Unsupported contact name + 不支援的聯絡人名稱 + + + Unverified badge + 未驗證的徽章 + + + Used chat relays do not support webpages. + 使用中的聊天中繼不支援網頁。 + + + Webpage code + 網頁程式碼 + + + Webpage settings were changed. If you save, the updated settings will be sent to subscribers. + 網頁設定已變更。如果儲存,更新後的設定將傳送給訂閱者。 + + + You can enable them later via app Your privacy settings. + 你稍後可以透過應用程式的「你的私隱」設定啟用它們。 + + + You can support SimpleX starting from v7 of the app. + 自應用程式 v7 起,你可以支持 SimpleX。 + + + Your new channel %1$@ is connected to %2$d of %3$d relays. +If you cancel, the channel will be deleted - you can create it again. + 你的新頻道 %1$@ 已連線到 %2$d/%3$d 個中繼。 +如果取消,頻道將被刪除;你可以再次建立。 + + + acknowledged roster + 已確認名單 + + + https:// + https:// +
@@ -6494,24 +11611,28 @@ It can happen because of some bug or when the connection is compromised. SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - SimpleX 需要相機的啟用權限去掃描二維碼以連接其他用戶和接收視訊通話。 + SimpleX 需要相機取用權限,才能掃描二維碼與其他使用者連線,並用於視訊通話。 Privacy - Camera Usage Description SimpleX uses Face ID for local authentication - SimpleX 用 Face ID 去進行本機認證 + SimpleX 使用 Face ID 進行本機認證 Privacy - Face ID Usage Description SimpleX needs microphone access for audio and video calls, and to record voice messages. - SimpleX 需要麥克風的啟用權限去接收語音和視訊通話,以及錄製語音訊息。 + SimpleX 需要麥克風取用權限,才能進行語音和視訊通話,並錄製語音訊息。 Privacy - Microphone Usage Description SimpleX needs access to Photo Library for saving captured and received media - SimpleX 需要圖片庫的啟用權限去儲存已截取和已接收的媒體 + SimpleX 需要照片圖庫取用權限,才能儲存拍攝和接收的媒體 Privacy - Photo Library Additions Usage Description + + SimpleX uses local network access to allow using user chat profile via desktop app on the same network. + SimpleX 需要本機網路取用權限,讓同一網路上的桌面應用程式能使用你的聊天個人檔案。 + @@ -6536,4 +11657,196 @@ It can happen because of some bug or when the connection is compromised. + + + + %d new events + %d 個新事件 + + + From: %@ + 來自:%@ + + + From %d chat(s) + 來自 %d 個聊天 + + + New events + 新事件 + + + New messages + 新訊息 + + + + + + + SimpleX SE + SimpleX SE + + + Copyright © 2024 SimpleX Chat. All rights reserved. + 版權所有© 2024 SimpleX Chat。保留所有權利。 + + + SimpleX SE + SimpleX SE + + + + + + + App is locked! + 應用程式已鎖定! + + + Cancel + 取消 + + + Cannot access keychain to save database password + 無法存取鑰匙圈以儲存數據庫密碼 + + + Cannot forward message + 無法轉發訊息 + + + Currently maximum supported file size is %@. + 目前支援的最大檔案大小為 %@。 + + + Database downgrade required + 需要降級數據庫 + + + Database error + 數據庫錯誤 + + + Database passphrase is required to open chat. + 需要數據庫密碼才能開啟聊天。 + + + Error preparing file + 準備檔案時發生錯誤 + + + Error preparing message + 準備訊息時發生錯誤 + + + Error: %@ + 錯誤:%@ + + + File error + 檔案錯誤 + + + Incompatible database version + 數據庫版本不相容 + + + Invalid migration confirmation + 遷移確認無效 + + + Keychain error + 鑰匙圈錯誤 + + + Large file! + 檔案過大! + + + No active profile + 沒有使用中的個人檔案 + + + Ok + + + + Open the app to downgrade the database. + 請開啟應用程式來降級數據庫。 + + + Open the app to upgrade the database. + 請開啟應用程式來升級數據庫。 + + + Passphrase + 密碼 + + + Please create a profile in the SimpleX app + 請在 SimpleX 應用程式中建立個人檔案 + + + Selected chat preferences prohibit this message. + 所選聊天偏好設定不允許傳送此訊息。 + + + Sending a message takes longer than expected. + 傳送訊息所需時間比預期更長。 + + + Sending message… + 正在傳送訊息… + + + Share + 分享 + + + Slow network? + 網路速度慢嗎? + + + Unknown database error: %@ + 未知數據庫錯誤:%@ + + + Unsupported format + 不支援的格式 + + + Wait + 等待 + + + Wrong database passphrase + 數據庫密碼錯誤 + + + You can allow sharing in Your privacy / SimpleX Lock settings. + 你可以在「你的私隱」/「SimpleX 鎖定」設定中允許分享。 + + + %@ + %@ + + + Comment + 評論 + + + Database encrypted! + 數據庫已加密! + + + Database passphrase is different from saved in the keychain. + 數據庫密碼與鑰匙圈中儲存的密碼不同。 + + + Database upgrade required + 需要升級數據庫 + + +
diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings index 999bb3608f..387be3ae26 100644 --- a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -1,6 +1,9 @@ /* notification body */ "%d new events" = "%d nouveaux événements"; +/* notification body */ +"From %d chat(s)" = "De %d discussion(s)"; + /* notification body */ "From: %@" = "De : %@"; diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 18f3e2c344..9790e5944f 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -75,7 +75,7 @@ class ShareModel: ObservableObject { func setup(context: NSExtensionContext) { if appLocalAuthEnabledGroupDefault.get() && !allowShareExtensionGroupDefault.get() { - errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.") + errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Your privacy / SimpleX Lock settings.") return } if let item = context.inputItems.first as? NSExtensionItem, diff --git a/apps/ios/SimpleX SE/de.lproj/Localizable.strings b/apps/ios/SimpleX SE/de.lproj/Localizable.strings index df368686e8..d957769421 100644 --- a/apps/ios/SimpleX SE/de.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Falsches Datenbank-Passwort"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Sie können das Teilen in Ihren Privatsphäre‑ / SimpleX-Sperre‑Einstellungen erlauben."; diff --git a/apps/ios/SimpleX SE/es.lproj/Localizable.strings b/apps/ios/SimpleX SE/es.lproj/Localizable.strings index 4cc5029537..bb103354eb 100644 --- a/apps/ios/SimpleX SE/es.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/es.lproj/Localizable.strings @@ -98,7 +98,7 @@ "Unknown database error: %@" = "Error desconocido en la base de datos: %@"; /* No comment provided by engineer. */ -"Unsupported format" = "Formato sin soporte"; +"Unsupported format" = "Formato no compatible"; /* No comment provided by engineer. */ "Wait" = "Espera"; @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Contraseña incorrecta de la base de datos"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Puedes habilitar el uso compartido en el menú Privacidad / Bloqueo Simplex."; diff --git a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings index 46a458b471..df67d6b28b 100644 --- a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans Votre vie privée / Réglages de verrouillage SimpleX."; diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index 3aad39c5d1..0d0e9e1498 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "A megosztást az Adatvédelem / SimpleX-zár menüben engedélyezheti."; diff --git a/apps/ios/SimpleX SE/it.lproj/Localizable.strings b/apps/ios/SimpleX SE/it.lproj/Localizable.strings index e3d34650a3..54ace83c72 100644 --- a/apps/ios/SimpleX SE/it.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/it.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Password del database sbagliata"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Puoi consentire la condivisione nelle impostazioni \"La tua privacy\" / \"SimpleX Lock\"."; diff --git a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings index e5d2487b54..ad1dfaf162 100644 --- a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings @@ -106,6 +106,3 @@ /* No comment provided by engineer. */ "Wrong database passphrase" = "Verkeerde database wachtwoord"; -/* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock."; - diff --git a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings index c563431c28..4711006d33 100644 --- a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings @@ -106,6 +106,3 @@ /* No comment provided by engineer. */ "Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; -/* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX."; - diff --git a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings index e4c8c000d4..0b942b1b27 100644 --- a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Неправильный пароль базы данных"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Вы можете разрешить отправку в настройках Конфиденциальность / Блокировка SimpleX."; diff --git a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings index baef71c127..7f810f260d 100644 --- a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings @@ -106,6 +106,3 @@ /* No comment provided by engineer. */ "Wrong database passphrase" = "Yanlış veritabanı parolası"; -/* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz."; - diff --git a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings index a6da81185e..5814def4f4 100644 --- a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings @@ -106,6 +106,3 @@ /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильна ключова фраза до бази даних"; -/* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock."; - diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings index 362e2edb74..f1fe10645f 100644 --- a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -95,7 +95,7 @@ "Slow network?" = "网络速度慢?"; /* No comment provided by engineer. */ -"Unknown database error: %@" = "未知数据库错误: %@"; +"Unknown database error: %@" = "未知数据库错误:%@"; /* No comment provided by engineer. */ "Unsupported format" = "不支持的格式"; @@ -107,5 +107,5 @@ "Wrong database passphrase" = "数据库密码错误"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "您可以在 \"隐私与安全\"/\"SimpleX Lock \"设置中允许共享。"; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "你可以在“你的隐私”/“SimpleX 锁定”设置中允许共享。"; diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0e585fbe15..0f7dab1eba 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -59,7 +59,6 @@ 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; - CE11BADE0000000000000002 /* NameBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE11BADE0000000000000001 /* NameBadge.swift */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; }; @@ -184,8 +183,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.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.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 */; }; @@ -227,6 +226,7 @@ B728945B2D0C62BF00F7A19A /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = B728945A2D0C62BF00F7A19A /* ElegantEmojiPicker */; }; B73EFE532CE5FA3500C778EA /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */; }; B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; }; + CE11BADE0000000000000002 /* NameBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE11BADE0000000000000001 /* NameBadge.swift */; }; CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; }; CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; }; CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; }; @@ -264,6 +264,7 @@ E5DDBE6E2DC4106800A0EFF0 /* AppAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */; }; E5DDBE702DC4217900A0EFF0 /* NSEAPITypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */; }; E5E418012F83D2CA00252B9E /* OnboardingCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E418002F83D2CA00252B9E /* OnboardingCards.swift */; }; + E5E418022F83D2CA00252B9E /* ChannelWebAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E418032F83D2CA00252B9E /* ChannelWebAccessView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -414,7 +415,6 @@ 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; - CE11BADE0000000000000001 /* NameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameBadge.swift; sourceTree = ""; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = ""; }; 5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; @@ -563,8 +563,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.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.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 = ""; }; @@ -604,6 +604,7 @@ B70CE9E52D4BE5930080F36D /* GroupMentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMentions.swift; sourceTree = ""; }; B73EFE522CE5FA3500C778EA /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = ""; }; + CE11BADE0000000000000001 /* NameBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameBadge.swift; sourceTree = ""; }; CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = ""; }; CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = ""; }; CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = ""; }; @@ -688,6 +689,7 @@ E5DDBE6D2DC4106200A0EFF0 /* AppAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAPITypes.swift; sourceTree = ""; }; E5DDBE6F2DC4217900A0EFF0 /* NSEAPITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEAPITypes.swift; sourceTree = ""; }; E5E418002F83D2CA00252B9E /* OnboardingCards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingCards.swift; sourceTree = ""; }; + E5E418032F83D2CA00252B9E /* ChannelWebAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelWebAccessView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -733,8 +735,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -820,8 +822,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.6.1-AHNtWMpWy1qCojVTgyCNik.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.11-SNj2VtVeH9ARktfFtATBo.a */, ); path = Libraries; sourceTree = ""; @@ -1178,6 +1180,7 @@ 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */, 6495D7032F48CFC50060512B /* ChannelMembersView.swift */, 6495D7052F48CFFD0060512B /* ChannelRelaysView.swift */, + E5E418032F83D2CA00252B9E /* ChannelWebAccessView.swift */, 6495D7072F48D0000060512B /* AddGroupRelayView.swift */, ); path = Group; @@ -1640,6 +1643,7 @@ 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */, 647B15E82F4C8D2500EB431E /* AddChannelView.swift in Sources */, 6495D7062F48CFFD0060512B /* ChannelRelaysView.swift in Sources */, + E5E418022F83D2CA00252B9E /* ChannelWebAccessView.swift in Sources */, 6495D7082F48D0000060512B /* AddGroupRelayView.swift in Sources */, 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */, @@ -2077,7 +2081,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2102,7 +2106,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2127,7 +2131,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2152,7 +2156,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000"; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; @@ -2169,11 +2173,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2189,11 +2193,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2214,7 +2218,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2229,7 +2233,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2251,7 +2255,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2266,7 +2270,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2288,7 +2292,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2314,7 +2318,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2339,7 +2343,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2366,7 +2370,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2393,7 +2397,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2408,7 +2412,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2427,7 +2431,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 337; + CURRENT_PROJECT_VERSION = 345; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2442,7 +2446,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.5.6; + MARKETING_VERSION = 7.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 5f1d8ef6c2..636ba99927 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -84,10 +84,8 @@ extension ChatAPIResult { // Spec: spec/api.md#decodeAPIResult public func decodeAPIResult(_ d: Data) -> APIResult { -// print("decodeAPIResult \(String(describing: R.self))") do { -// return try withStackSizeLimit { try jsonDecoder.decode(APIResult.self, from: d) } - return try jsonDecoder.decode(APIResult.self, from: d) + return try withLargeStack { try jsonDecoder.decode(APIResult.self, from: d) } } catch {} if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { if let (_, jErr) = getOWSF(j, "error") { @@ -106,31 +104,50 @@ public func decodeAPIResult(_ d: Data) -> APIResult { // Default stack size for the main thread is 1mb, for secondary threads - 512 kb. // This function can be used to test what size is used (or to increase available stack size). // Stack size must be a multiple of system page size (16kb). -//private let stackSizeLimit: Int = 256 * 1024 -// -//private func withStackSizeLimit(_ f: @escaping () throws -> T) throws -> T { -// let semaphore = DispatchSemaphore(value: 0) -// var result: Result? -// let thread = Thread { -// do { -// result = .success(try f()) -// } catch { -// result = .failure(error) -// } -// semaphore.signal() -// } -// -// thread.stackSize = stackSizeLimit -// thread.qualityOfService = Thread.current.qualityOfService -// thread.start() -// -// semaphore.wait() -// -// switch result! { -// case let .success(r): return r -// case let .failure(e): throw e -// } -//} +private let stackSizeLimit: Int = 16 * 1024 * 1024 + +private final class LargeStackRunner: NSObject { + static let shared = LargeStackRunner() + + private let serialize = NSLock() // serialize submitters + private let jobReady = DispatchSemaphore(value: 0) + private let jobDone = DispatchSemaphore(value: 0) + private var job: (() -> Void)? + private var thread: Thread? = nil + + private override init() { + super.init() + let t = Thread(target: self, selector: #selector(loop), object: nil) + t.stackSize = stackSizeLimit + t.name = "chat.simplex.decoding" + t.qualityOfService = .default + t.start() + thread = t + } + + @objc private func loop() { + while true { + jobReady.wait() + job?() + jobDone.signal() + } + } + + func run(_ f: @escaping () throws -> T) throws -> T { + serialize.lock() + defer { serialize.unlock() } + var result: Result! + job = { result = Result(catching: f) } + jobReady.signal() + jobDone.wait() + job = nil + return try result.get() + } +} + +func withLargeStack(_ work: @escaping () throws -> T) throws -> T { + try LargeStackRunner.shared.run(work) +} public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? { if let jApiChats = jResp["apiChats"] as? NSDictionary, @@ -167,6 +184,10 @@ public struct CreatedConnLink: Decodable, Hashable { public func simplexChatUri(short: Bool = true) -> String { short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink) } + + public var cmdString: String { + connFullLink + (connShortLink.map { " \($0)"} ?? "") + } } public func simplexChatLink(_ uri: String) -> String { @@ -741,6 +762,8 @@ public enum ChatErrorType: Decodable, Hashable { case chatNotStopped case chatStoreChanged case invalidConnReq + case simplexDomainNotReady(simplexDomain: SimplexDomain, simplexDomainError: SimplexDomainError) + case notResolvedLocally case unsupportedConnReq case invalidChatMessage(connection: Connection, message: String) case connReqMessageProhibited @@ -896,6 +919,13 @@ public enum AgentErrorType: Decodable, Hashable { case INTERNAL(internalErr: String) case CRITICAL(offerRestart: Bool, criticalErr: String) case INACTIVE + case NO_NAME_SERVERS +} + +public enum NameErrorType: Decodable, Hashable { + case NO_RESOLVER + case NOT_FOUND + case RESOLVER(resolverErr: String) } public enum CommandErrorType: Decodable, Hashable { @@ -937,6 +967,7 @@ public enum ProtocolErrorType: Decodable, Hashable { case LARGE_MSG case EXPIRED case INTERNAL + case NAME(nameErr: NameErrorType) } public enum ProxyError: Decodable, Hashable { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index bfe25c6d42..e6940b11d4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -34,6 +34,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var image: String? { get { profile.image } } public var localAlias: String { get { "" } } @@ -116,22 +117,27 @@ public struct Profile: Codable, NamedChat, Hashable { displayName: String, fullName: String, shortDescr: String? = nil, + description: String? = nil, image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, - peerType: ChatPeerType? = nil + peerType: ChatPeerType? = nil, + contactDomain: SimplexDomainClaim? = nil ) { self.displayName = displayName self.fullName = fullName self.shortDescr = shortDescr + self.description = description self.image = image self.contactLink = contactLink self.preferences = preferences + self.contactDomain = contactDomain } public var displayName: String public var fullName: String public var shortDescr: String? + public var description: String? public var image: String? public var contactLink: String? public var preferences: Preferences? @@ -139,6 +145,9 @@ public struct Profile: Codable, NamedChat, Hashable { // the badge proof from the wire profile - opaque to the UI, round-tripped to the core (apiPrepareContact) public var badge: BadgeProof? public var localAlias: String { get { "" } } + public var contactDomain: SimplexDomainClaim? + + public var profileDescription: String? { description } var profileViewName: String { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" @@ -156,35 +165,46 @@ public struct LocalProfile: Codable, NamedChat, Hashable { displayName: String, fullName: String, shortDescr: String? = nil, + description: String? = nil, image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, peerType: ChatPeerType? = nil, localBadge: LocalBadge? = nil, - localAlias: String + localAlias: String, + contactDomain: SimplexDomainClaim? = nil, + contactDomainVerified: Bool? = nil ) { self.profileId = profileId self.displayName = displayName self.fullName = fullName self.shortDescr = shortDescr + self.description = description self.image = image self.contactLink = contactLink self.preferences = preferences self.peerType = peerType self.localBadge = localBadge self.localAlias = localAlias + self.contactDomain = contactDomain + self.contactDomainVerified = contactDomainVerified } public var profileId: Int64 public var displayName: String public var fullName: String public var shortDescr: String? + public var description: String? public var image: String? public var contactLink: String? public var preferences: Preferences? public var peerType: ChatPeerType? public var localBadge: LocalBadge? public var localAlias: String + public var contactDomain: SimplexDomainClaim? + public var contactDomainVerified: Bool? + + public var profileDescription: String? { description } var profileViewName: String { localAlias == "" @@ -276,6 +296,7 @@ public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias displayName: profile.displayName, fullName: profile.fullName, shortDescr: profile.shortDescr, + description: profile.description, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, @@ -289,6 +310,7 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { displayName: profile.displayName, fullName: profile.fullName, shortDescr: profile.shortDescr, + description: profile.description, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, @@ -314,11 +336,14 @@ public protocol NamedChat { var displayName: String { get } var fullName: String { get } var shortDescr: String? { get } + var profileDescription: String? { get } var image: String? { get } var localAlias: String { get } } extension NamedChat { + public var profileDescription: String? { nil } + public var chatViewName: String { localAlias == "" ? displayName + (fullName == "" || fullName == displayName ? "" : " / \(fullName)") @@ -937,6 +962,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case reports case history case support + case signMessages public var id: Self { self } @@ -959,6 +985,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .reports: false case .history: false case .support: false + case .signMessages: false } } @@ -978,6 +1005,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { : NSLocalizedString("Member reports", comment: "chat feature") case .history: return NSLocalizedString("Visible history", comment: "chat feature") case .support: return NSLocalizedString("Chat with admins", comment: "chat feature") + case .signMessages: return NSLocalizedString("Sign messages", comment: "chat feature") } } @@ -993,6 +1021,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .reports: return "flag" case .history: return "clock" case .support: return "questionmark.circle" + case .signMessages: return "checkmark.seal" } } @@ -1008,6 +1037,7 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { case .reports: return "flag.fill" case .history: return "clock.fill" case .support: return "questionmark.circle.fill" + case .signMessages: return "checkmark.seal.fill" } } @@ -1081,6 +1111,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { : "Allow members to chat with admins." case .off: return "Prohibit chats with admins." } + case .signMessages: + switch enabled { + case .on: return "Require signing messages." + case .off: return "Do not require signing messages." + } } } else { switch self { @@ -1158,6 +1193,11 @@ public enum GroupFeature: String, Decodable, Feature, Hashable { : "Members can chat with admins." case .off: return "Chats with admins are prohibited." } + case .signMessages: + switch enabled { + case .on: return "Message signing is required." + case .off: return "Message signing is not required." + } } } } @@ -1313,6 +1353,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var reports: GroupPreference public var history: GroupPreference public var support: GroupPreference + public var signMessages: GroupPreference public var commands: [ChatBotCommand] public init( @@ -1326,6 +1367,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { reports: GroupPreference, history: GroupPreference, support: GroupPreference, + signMessages: GroupPreference, commands: [ChatBotCommand] ) { self.timedMessages = timedMessages @@ -1338,6 +1380,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { self.reports = reports self.history = history self.support = support + self.signMessages = signMessages self.commands = commands } @@ -1352,6 +1395,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { reports: GroupPreference(enable: .on), history: GroupPreference(enable: .on), support: GroupPreference(enable: .on), + signMessages: GroupPreference(enable: .off), commands: [] ) } @@ -1367,6 +1411,7 @@ public struct GroupPreferences: Codable, Hashable { public var reports: GroupPreference? public var history: GroupPreference? public var support: GroupPreference? + public var signMessages: GroupPreference? public var commands: [ChatBotCommand]? public init( @@ -1380,6 +1425,7 @@ public struct GroupPreferences: Codable, Hashable { reports: GroupPreference? = nil, history: GroupPreference? = nil, support: GroupPreference? = nil, + signMessages: GroupPreference? = nil, commands: [ChatBotCommand]? = nil ) { self.timedMessages = timedMessages @@ -1392,6 +1438,7 @@ public struct GroupPreferences: Codable, Hashable { self.reports = reports self.history = history self.support = support + self.signMessages = signMessages self.commands = commands } @@ -1421,12 +1468,14 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group simplexLinks: fullPreferences.simplexLinks, reports: fullPreferences.reports, history: fullPreferences.history, + signMessages: fullPreferences.signMessages, commands: fullPreferences.commands ) } public struct GroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled + public var role: GroupMemberRole? public var on: Bool { enable == .on @@ -1444,8 +1493,9 @@ public struct GroupPreference: Codable, Equatable, Hashable { } } - public init(enable: GroupFeatureEnabled) { + public init(enable: GroupFeatureEnabled, role: GroupMemberRole? = nil) { self.enable = enable + self.role = role } } @@ -1574,6 +1624,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var profileDescription: String? { + switch self { + case let .direct(contact): contact.profile.description + case let .group(groupInfo, _): groupInfo.profileDescription + case .local: nil + case let .contactRequest(contactRequest): contactRequest.profile.description + case .contactConnection: nil + case .invalidJSON: nil + } + } + public var image: String? { get { switch self { @@ -1718,11 +1779,11 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if groupInfo.membership.memberActive { switch(groupChatScope) { case .none: - if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") } if groupInfo.membership.memberRole == .observer { return groupInfo.useRelays ? ("you are subscriber", nil) : ("you are observer", "Please contact group admin.") } + if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) } return nil case let .some(.memberSupport(groupMember_: .some(supportMember))): if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending { @@ -2076,6 +2137,14 @@ public func sameChatScope(_ scope1: GroupChatScope, _ scope2: GroupChatScope) -> } } +public func draftChatId(_ chatId: ChatId?, _ scope: GroupChatScope?) -> ChatId? { + guard let chatId, let scope else { return chatId } + return switch scope { + case let .memberSupport(groupMemberId_): "\(chatId) support:\(groupMemberId_?.description ?? "")" + case .reports: "\(chatId) reports" + } +} + public enum GroupChatScopeInfo: Decodable, Hashable { case memberSupport(groupMember_: GroupMember?) case reports // surrogate scope used for matching new items to opened Reports "chat scope" in UI, this type is not present in backend @@ -2134,6 +2203,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var image: String? { get { profile.image } } public var contactLink: String? { get { profile.contactLink } } public var localAlias: String { profile.localAlias } @@ -2352,6 +2422,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { var ready: Bool { get { true } } public var displayName: String { get { profile.displayName } } public var shortDescr: String? { profile.shortDescr } + public var profileDescription: String? { profile.description } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } public var localAlias: String { "" } @@ -2505,7 +2576,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupId: Int64 public var useRelays: Bool public var relayOwnStatus: RelayStatus? = nil - var localDisplayName: GroupName + public var localDisplayName: GroupName public var groupProfile: GroupProfile public var businessChat: BusinessChatInfo? public var fullGroupPreferences: FullGroupPreferences @@ -2528,10 +2599,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var shortDescr: String? { groupProfile.shortDescr } + public var profileDescription: String? { businessChat != nil ? groupProfile.description : nil } public var image: String? { get { groupProfile.image } } public var chatTags: [Int64] public var chatItemTTL: Int64? public var localAlias: String + public var groupDomainVerified: Bool? public var isOwner: Bool { return membership.memberRole == .owner && membership.memberCurrent @@ -2612,12 +2685,37 @@ public enum GroupType: Codable, Hashable { } public struct PublicGroupAccess: Codable, Hashable { + public init(groupWebPage: String? = nil, groupDomainClaim: SimplexDomainClaim? = nil, domainWebPage: Bool = false, allowEmbedding: Bool = false) { + self.groupWebPage = groupWebPage + self.groupDomainClaim = groupDomainClaim + self.domainWebPage = domainWebPage + self.allowEmbedding = allowEmbedding + } + public var groupWebPage: String? - public var groupDomain: String? + public var groupDomainClaim: SimplexDomainClaim? public var domainWebPage: Bool = false public var allowEmbedding: Bool = false } +public struct SimplexDomainClaim: Codable, Hashable { + public init(domain: String, proof: SimplexDomainProof? = nil) { + self.domain = domain + self.proof = proof + } + public var domain: String + public var proof: SimplexDomainProof? + + public var shortName: String { + domain.hasSuffix(".simplex") ? String(domain.dropLast(".simplex".count)) : domain + } +} + +public enum SimplexDomainError: Decodable, Hashable { + case noValidLink + case unknownDomain +} + public struct RelayCapabilities: Codable, Hashable { public var webDomain: String? } @@ -2728,10 +2826,36 @@ public struct GroupShortLinkData: Codable, Hashable { public var publicGroupData: PublicGroupData? } +public enum MsgSigStatus: String, Decodable, Equatable, Hashable { + case verified + case signedNoKey +} + +public enum MsgVerified: Decodable, Equatable, Hashable { + case signed(sigStatus: MsgSigStatus) + case sigMissing + + public var verified: Bool { + if case let .signed(sigStatus) = self { return sigStatus == .verified } + return false + } + + public var sigMissingInfo: (String, String)? { + switch self { + case .sigMissing: return ( + NSLocalizedString("Signature missing", comment: "alert title"), + NSLocalizedString("The channel required this message to be signed, but the signature is missing.", comment: "alert message") + ) + default: return nil + } + } +} + public enum RelayStatus: String, Decodable, Equatable, Hashable { case new case invited case accepted + case acknowledgedRoster case active case inactive case rejected @@ -2807,6 +2931,7 @@ extension RelayStatus { case .new: "new" case .invited: "invited" case .accepted: "accepted" + case .acknowledgedRoster: "acknowledged roster" case .active: "active" case .inactive: "inactive" case .rejected: "rejected" @@ -2818,6 +2943,7 @@ public struct BusinessChatInfo: Decodable, Hashable { public var chatType: BusinessChatType public var businessId: String public var customerId: String + public var businessDomain: SimplexDomainClaim? } public enum BusinessChatType: String, Codable, Hashable { @@ -2843,6 +2969,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var supportChat: GroupSupportChat? public var memberChatVRange: VersionRange public var relayLink: String? + public var memberVerifiedCode: SecurityCode? public var id: String { "#\(groupId) @\(groupMemberId)" } public var ready: Bool { get { activeConn?.connStatus == .ready } } @@ -2864,7 +2991,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public var image: String? { get { memberProfile.image } } public var contactLink: String? { get { memberProfile.contactLink } } public var nameBadge: LocalBadge? { memberProfile.localBadge } - public var verified: Bool { activeConn?.connectionCode != nil } + public var verified: Bool { memberVerifiedCode != nil || activeConn?.connectionCode != nil } public var blocked: Bool { blockedByAdmin || !memberSettings.showMessages } var directChatId: ChatId? { @@ -2970,8 +3097,16 @@ public struct GroupMember: Identifiable, Decodable, Hashable { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { if memberRole == .relay || !canBeRemoved(groupInfo: groupInfo) || memberStatus == .memRemoved || memberStatus == .memLeft || memberPending { return nil } + if groupInfo.useRelays && !groupInfo.isOwner { return nil } let userRole = groupInfo.membership.memberRole - return GroupMemberRole.supportedRoles.filter { $0 <= userRole } + if groupInfo.useRelays { + // TODO [relays]: for now owners can only set observer/member in channels. + // Restore the full Owner-excluded picker when moderator/admin promotion is supported: + // return GroupMemberRole.supportedRoles.filter { $0 <= userRole && $0 != .owner } + return [.observer, .member] + } else { + return GroupMemberRole.supportedRoles.filter { $0 <= userRole } + } } public func canBlockForAll(groupInfo: GroupInfo) -> Bool { @@ -3059,12 +3194,16 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .moderator, .admin, .owner] - public var text: String { + public func text(isChannel: Bool) -> String { switch self { case .relay: return NSLocalizedString("relay", comment: "member role") - case .observer: return NSLocalizedString("observer", comment: "member role") + case .observer: return isChannel + ? NSLocalizedString("subscriber", comment: "member role") + : NSLocalizedString("observer", comment: "member role") case .author: return NSLocalizedString("author", comment: "member role") - case .member: return NSLocalizedString("member", comment: "member role") + case .member: return isChannel + ? NSLocalizedString("contributor", comment: "member role") + : NSLocalizedString("member", comment: "member role") case .moderator: return NSLocalizedString("moderator", comment: "member role") case .admin: return NSLocalizedString("admin", comment: "member role") case .owner: return NSLocalizedString("owner", comment: "member role") @@ -3689,7 +3828,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable { userMention: false, deletable: false, editable: false, - showGroupAsSender: false + showGroupAsSender: false, + msgVerified: nil ), content: .sndMsgContent(msgContent: .report(text: text, reason: reason)), quotedItem: CIQuote.getSample(item.id, item.meta.createdAt, item.text, chatDir: item.chatDir), @@ -3713,7 +3853,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable { userMention: false, deletable: false, editable: false, - showGroupAsSender: false + showGroupAsSender: false, + msgVerified: nil ), content: .rcvDeleted(deleteMode: .cidmBroadcast), quotedItem: nil, @@ -3737,7 +3878,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable { userMention: false, deletable: false, editable: false, - showGroupAsSender: false + showGroupAsSender: false, + msgVerified: nil ), content: .sndMsgContent(msgContent: .text("")), quotedItem: nil, @@ -3816,6 +3958,7 @@ public struct CIMeta: Decodable, Hashable { public var deletable: Bool public var editable: Bool public var showGroupAsSender: Bool + public var msgVerified: MsgVerified? public var timestampText: Text { Text(formatTimestampMeta(itemTs)) } public var recent: Bool { updatedAt + 10 > .now } @@ -3841,7 +3984,8 @@ public struct CIMeta: Decodable, Hashable { userMention: false, deletable: deletable, editable: editable, - showGroupAsSender: false + showGroupAsSender: false, + msgVerified: nil ) } @@ -3859,7 +4003,8 @@ public struct CIMeta: Decodable, Hashable { userMention: false, deletable: false, editable: false, - showGroupAsSender: false + showGroupAsSender: false, + msgVerified: nil ) } } @@ -5096,11 +5241,7 @@ public enum MsgChatLink: Equatable, Hashable { NSLocalizedString("One-time link", comment: "chat link info line") } if signed { - s += " " + ( - self.isPublicGroup - ? NSLocalizedString("(from owner)", comment: "chat link info line") - : NSLocalizedString("(signed)", comment: "chat link info line") - ) + s += " " + NSLocalizedString("(from owner)", comment: "chat link info line") } return s } @@ -5203,10 +5344,14 @@ public enum Format: Decodable, Equatable, Hashable { case simplexName(nameInfo: SimplexNameInfo) case command(commandStr: String) case mention(memberName: String) + case modal(modalName: String, text: String) case email case phone case unknown + // client-only format that opens a modal when tapped, see openMarkdownModal + public static let modalDescription = "description" + public var isSimplexLink: Bool { get { switch (self) { @@ -5235,28 +5380,69 @@ public enum SimplexLinkType: String, Decodable, Hashable { } } -public struct SimplexNameInfo: Decodable, Equatable, Hashable { +public struct SimplexNameInfo: Codable, Equatable, Hashable { public var nameType: SimplexNameType - public var nameDomain: SimplexNameDomain + public var nameDomain: SimplexDomain + + // mirrors backend shortNameInfoStr: "#name" for a simplex public group, else prefix + full domain + public var shortStr: String { + if nameType == .publicGroup && nameDomain.nameTLD == .simplex && nameDomain.subDomain.isEmpty { + return "#" + nameDomain.domain + } else { + return (nameType == .publicGroup ? "#" : "@") + nameDomain.fullDomainName + } + } + + public init(nameType: SimplexNameType, nameDomain: SimplexDomain) { + self.nameType = nameType + self.nameDomain = nameDomain + } } -public struct SimplexNameDomain: Decodable, Equatable, Hashable { +public struct SimplexDomain: Codable, Equatable, Hashable { public var nameTLD: SimplexTLD public var domain: String public var subDomain: [String] + + // mirrors backend fullDomainName: reverse(subDomain) ++ [domain] ++ tld + public var fullDomainName: String { + let tld: [String] + switch nameTLD { + case .simplex: tld = ["simplex"] + case .testing: tld = ["testing"] + case .web: tld = [] + } + return (subDomain.reversed() + [domain] + tld).joined(separator: ".") + } + + public var cmdString: String { + "domain=\(fullDomainName)" + } + + public init(nameTLD: SimplexTLD, domain: String, subDomain: [String]) { + self.nameTLD = nameTLD + self.domain = domain + self.subDomain = subDomain + } } -public enum SimplexTLD: String, Decodable, Hashable { +public enum SimplexTLD: String, Codable, Hashable { case simplex case testing case web } -public enum SimplexNameType: String, Decodable, Hashable { +public enum SimplexNameType: String, Codable, Hashable { case publicGroup case contact } +public struct SimplexDomainProof: Codable, Hashable { + public var linkOwnerId: String? + public var presHeader: String + public var signature: String +} + public enum FormatColor: String, Decodable, Hashable { case red = "red" case green = "green" @@ -5585,7 +5771,7 @@ public enum RcvGroupEvent: Decodable, Hashable { case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item") case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item") case let .memberRole(_, profile, role): - return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text) + return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text(isChannel: isChannel)) case let .memberBlocked(_, profile, blocked): if blocked { return String.localizedStringWithFormat(NSLocalizedString("blocked %@", comment: "rcv group event chat item"), profile.profileViewName) @@ -5593,7 +5779,7 @@ public enum RcvGroupEvent: Decodable, Hashable { return String.localizedStringWithFormat(NSLocalizedString("unblocked %@", comment: "rcv group event chat item"), profile.profileViewName) } case let .userRole(role): - return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text) + return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text(isChannel: isChannel)) case let .memberDeleted(_, profile): return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName) case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item") @@ -5639,9 +5825,9 @@ public enum SndGroupEvent: Decodable, Hashable { func text(isChannel: Bool) -> String { switch self { case let .memberRole(_, profile, role): - return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text) + return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text(isChannel: isChannel)) case let .userRole(role): - return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text) + return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text(isChannel: isChannel)) case let .memberBlocked(_, profile, blocked): if blocked { return String.localizedStringWithFormat(NSLocalizedString("you blocked %@", comment: "snd group event chat item"), profile.profileViewName) @@ -5855,6 +6041,7 @@ public struct ChatItemInfo: Decodable, Hashable { public var itemVersions: [ChatItemVersion] public var memberDeliveryStatuses: [MemberDeliveryStatus]? public var forwardedFromChatItem: AChatItem? + public var fileXftpServers: [String]? } public struct ChatItemVersion: Decodable, Hashable { diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 7de7f3704d..45320c5b93 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -27,6 +27,7 @@ extension ChatLike { case .history: p.history.on case .support: p.support.on case .reports: p.reports.on + case .signMessages: p.signMessages.on } } else { return true diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index 2920c2383c..fb39ccd88e 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -34,20 +34,16 @@ public struct ErrorAlert: Error { } public init(_ error: any Error) { - self = if let e = error as? ChatError { - ErrorAlert(e) + self = if let chatError = error as? ChatError { + if let a = getNetworkErrorAlert(chatError) { + ErrorAlert(title: "\(a.title)", message: a.message.map { "\($0)" }) + } else { + ErrorAlert("\(chatErrorString(chatError))") + } } else { ErrorAlert("\(error.localizedDescription)") } } - - public init(_ chatError: ChatError) { - self = if let networkErrorAlert = getNetworkErrorAlert(chatError) { - networkErrorAlert - } else { - ErrorAlert("\(chatErrorString(chatError))") - } - } } extension LocalizedStringKey: @unchecked Sendable { } @@ -83,18 +79,33 @@ extension View { } } -public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { +public func getNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String?)? { switch e { case let .errorAgent(.BROKER(addr, .TIMEOUT)): - ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection timeout", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK(.unknownCAError))): - ErrorAlert(title: "Connection error", message: "Fingerprint in server address does not match certificate: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in server address does not match certificate: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .NETWORK)): - ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .HOST)): - ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server address is incompatible with network settings: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.BROKER(addr, .TRANSPORT(.version))): - ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + ( + title: NSLocalizedString("Connection error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Server version is incompatible with your app: %@.", comment: ""), serverHostname(addr)) + ) case let .errorAgent(.SMP(serverAddress, .PROXY(proxyErr))): smpProxyErrorAlert(proxyErr, serverAddress) case let .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr)))): @@ -103,39 +114,72 @@ public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? { } } -private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in forwarding server address does not match certificate: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in forwarding server address does not match certificate: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server address is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server version is incompatible with network settings: %@.", comment: ""), serverHostname(srvAddr)) + ) default: - return nil + nil } } -private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> (title: String, message: String?)? { switch proxyErr { case .BROKER(brokerErr: .TIMEOUT): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK(.unknownCAError)): - return ErrorAlert(title: "Private routing error", message: "Fingerprint in destination server address does not match certificate: \(serverHostname(relayServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Fingerprint in destination server address does not match certificate: %@.", comment: ""), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .NETWORK): - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .NO_SESSION: - return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: ""), serverHostname(proxyServer), serverHostname(relayServer)) + ) case .BROKER(brokerErr: .HOST): - return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server address of %1$@ is incompatible with forwarding server %2$@ settings.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) case .BROKER(brokerErr: .TRANSPORT(.version)): - return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + ( + title: NSLocalizedString("Private routing error", comment: ""), + message: String.localizedStringWithFormat(NSLocalizedString("Destination server version of %1$@ is incompatible with forwarding server %2$@.", comment: ""), serverHostname(relayServer), serverHostname(proxyServer)) + ) default: - return nil + nil } } diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index ec869e05b4..7956ef1c17 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -888,9 +888,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "повикване…"; -/* No comment provided by engineer. */ -"Calls" = "Обаждания"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Обажданията са забранени!"; @@ -956,9 +953,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Промяна на режима на заключване"; -/* No comment provided by engineer. */ -"Change member role?" = "Промяна на ролята на члена?"; - /* authentication reason */ "Change passcode" = "Промени kодa за достъп"; @@ -1301,15 +1295,15 @@ server test step */ /* alert title */ "Connection error" = "Грешка при свързване"; -/* conn error description */ -"Connection error (AUTH)" = "Грешка при свързване (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "установена е връзка"; /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Връзката е блокирана от оператора на сървъра:\n%@"; +/* conn error description */ +"Connection link removed" = "Грешка при свързване"; + /* No comment provided by engineer. */ "Connection not ready." = "Връзката не е готова."; @@ -1656,10 +1650,7 @@ alert button */ "Desktop devices" = "Настолни устройства"; /* No comment provided by engineer. */ -"Develop" = "Разработване"; - -/* No comment provided by engineer. */ -"Developer tools" = "Инструменти за разработчици"; +"Developer" = "Инструменти за разработчици"; /* No comment provided by engineer. */ "Device" = "Устройство"; @@ -1941,7 +1932,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "грешка"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Грешка при свързване със сървъра"; /* No comment provided by engineer. */ @@ -2101,6 +2092,7 @@ chat item action */ "Error: " = "Грешка: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Грешка: %@"; @@ -2628,7 +2620,7 @@ server test error */ /* No comment provided by engineer. */ "Large file!" = "Голям файл!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Научете повече"; /* swipe action */ @@ -2721,12 +2713,6 @@ server test error */ /* rcv group event chat item */ "member connected" = "свързан"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Ролята на члена ще бъде променена на \"%@\". Всички членове на групата ще бъдат уведомени."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Ролята на члена ще бъде променена на \"%@\". Членът ще получи нова покана."; - /* alert message */ "Member will be removed from group - this cannot be undone!" = "Членът ще бъде премахнат от групата - това не може да бъде отменено!"; @@ -3216,9 +3202,6 @@ alert button */ /* No comment provided by engineer. */ "Preview" = "Визуализация"; -/* No comment provided by engineer. */ -"Privacy & security" = "Поверителност и сигурност"; - /* No comment provided by engineer. */ "Private filenames" = "Поверителни имена на файлове"; @@ -3297,7 +3280,7 @@ alert button */ /* swipe action */ "Read" = "Прочетено"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Прочетете още"; /* No comment provided by engineer. */ @@ -3473,13 +3456,20 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Роля"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Ролята на члена ще бъде променена на \"%@\". Всички членове на групата ще бъдат уведомени."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Ролята на члена ще бъде променена на \"%@\". Членът ще получи нова покана."; + /* No comment provided by engineer. */ "Run chat" = "Стартиране на чат"; /* No comment provided by engineer. */ "Safer groups" = "По-безопасни групи"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Запази"; @@ -3636,9 +3626,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Подателят отмени прехвърлянето на файла."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Подателят може да е изтрил заявката за връзка."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили."; @@ -3886,9 +3873,6 @@ chat item action */ /* No comment provided by engineer. */ "Submit" = "Изпрати"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Подкрепете SimpleX Chat"; - /* No comment provided by engineer. */ "System" = "Системен"; @@ -3992,6 +3976,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Подателят може да е изтрил заявката за връзка."; + /* alert message */ "The sender will NOT be notified" = "Подателят НЯМА да бъде уведомен"; @@ -4145,9 +4132,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Освен ако не използвате интерфейса за повикване на iOS, активирайте режима \"Не безпокой\", за да избегнете прекъсвания."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте.\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка."; - /* No comment provided by engineer. */ "Unlink" = "Забрави"; @@ -4235,9 +4219,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify code with desktop" = "Потвърди кода с настолното устройство"; @@ -4451,9 +4432,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Можете да активирате по-късно през Настройки"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението."; - /* No comment provided by engineer. */ "You can give another try." = "Можете да опитате още веднъж."; @@ -4583,15 +4561,15 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Вашите обаждания"; -/* No comment provided by engineer. */ -"Your chat database" = "Вашата база данни"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Вашата база данни не е криптирана - задайте парола, за да я криптирате."; /* No comment provided by engineer. */ "Your chat profiles" = "Вашите чат профили"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте.\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@)."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index fc4b3f0fc6..165177876c 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -758,9 +758,6 @@ swipe action */ /* call status */ "calling…" = "volání…"; -/* No comment provided by engineer. */ -"Calls" = "Hovory"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Volání zakázáno!"; @@ -826,9 +823,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Změnit zamykání"; -/* No comment provided by engineer. */ -"Change member role?" = "Změnit roli člena?"; - /* authentication reason */ "Change passcode" = "Změnit heslo"; @@ -1011,12 +1005,12 @@ server test step */ /* alert title */ "Connection error" = "Chyba připojení"; -/* conn error description */ -"Connection error (AUTH)" = "Chyba spojení (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "spojení navázáno"; +/* conn error description */ +"Connection link removed" = "Chyba spojení"; + /* No comment provided by engineer. */ "Connection request sent!" = "Požadavek na připojení byl odeslán!"; @@ -1303,10 +1297,7 @@ alert button */ "Description" = "Popis"; /* No comment provided by engineer. */ -"Develop" = "Vyvinout"; - -/* No comment provided by engineer. */ -"Developer tools" = "Nástroje pro vývojáře"; +"Developer" = "Nástroje pro vývojáře"; /* No comment provided by engineer. */ "Device" = "Zařízení"; @@ -1536,7 +1527,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "chyba"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Chyba"; /* No comment provided by engineer. */ @@ -1675,6 +1666,7 @@ alert button */ "Error: " = "Chyba: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Chyba: %@"; @@ -2097,7 +2089,7 @@ server test error */ /* No comment provided by engineer. */ "Large file!" = "Velký soubor!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Zjistit více"; /* swipe action */ @@ -2178,12 +2170,6 @@ server test error */ /* rcv group event chat item */ "member connected" = "připojeno"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Role člena se změní na \"%@\". Všichni členové skupiny budou upozorněni."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Role člena se změní na \"%@\". Člen obdrží novou pozvánku."; - /* alert message */ "Member will be removed from group - this cannot be undone!" = "Člen bude odstraněn ze skupiny - toto nelze vzít zpět!"; @@ -2577,9 +2563,6 @@ alert button */ /* No comment provided by engineer. */ "Preview" = "Náhled"; -/* No comment provided by engineer. */ -"Privacy & security" = "Ochrana osobních údajů a zabezpečení"; - /* No comment provided by engineer. */ "Private filenames" = "Soukromé názvy souborů"; @@ -2640,7 +2623,7 @@ alert button */ /* swipe action */ "Read" = "Číst"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Přečíst více"; /* No comment provided by engineer. */ @@ -2792,10 +2775,17 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Role"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Role člena se změní na \"%@\". Všichni členové skupiny budou upozorněni."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Role člena se změní na \"%@\". Člen obdrží novou pozvánku."; + /* No comment provided by engineer. */ "Run chat" = "Spustit chat"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Uložit"; @@ -2925,9 +2915,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Odesílatel zrušil přenos souboru."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Odesílatel možná smazal požadavek připojení."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu."; @@ -3130,9 +3117,6 @@ chat item action */ /* No comment provided by engineer. */ "Submit" = "Odeslat"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Podpořte SimpleX Chat"; - /* No comment provided by engineer. */ "System" = "Systém"; @@ -3227,6 +3211,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Odesílatel možná smazal požadavek připojení."; + /* alert message */ "The sender will NOT be notified" = "Odesílatel NEBUDE informován"; @@ -3344,9 +3331,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji.\nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti."; - /* No comment provided by engineer. */ "Unlock" = "Odemknout"; @@ -3404,9 +3388,6 @@ server test failure */ /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Používat servery SimpleX Chat."; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify connection security" = "Ověření zabezpečení připojení"; @@ -3542,9 +3523,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Můžete povolit později v Nastavení"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace"; - /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava."; @@ -3659,15 +3637,15 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Vaše hovory"; -/* No comment provided by engineer. */ -"Your chat database" = "Vaše chatovací databáze"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Vaše chat databáze není šifrována – nastavte přístupovou frázi pro její šifrování."; /* No comment provided by engineer. */ "Your chat profiles" = "Vaše profily chatu"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji.\nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@)."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 2c4e37791b..41f64e5400 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -28,9 +28,6 @@ /* No comment provided by engineer. */ "(new)" = "(Neu)"; -/* chat link info line */ -"(signed)" = "(signiert)"; - /* No comment provided by engineer. */ "(this device v%@)" = "(Dieses Gerät hat v%@)"; @@ -121,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ heruntergeladen"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ hat sich am SimpleX Chat-Crowdfunding beteiligt."; + /* notification title */ "%@ is connected!" = "%@ ist mit Ihnen verbunden!"; @@ -136,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ Server"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ unterstützt SimpleX Chat."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ hochgeladen"; @@ -154,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ hat SimpleX Chat unterstützt. Das Abzeichen ist am %2$@ abgelaufen."; + /* time interval */ "%d days" = "%d Tage"; @@ -181,6 +187,15 @@ /* time interval */ "%d months" = "%d Monate"; +/* channel owners count */ +"%d owner" = "%d Eigentümer"; + +/* channel owners count */ +"%d owners" = "%d Eigentümer"; + +/* channel members count */ +"%d owners & contributors" = "%d Eigentümer und Mitwirkende"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d Relais fehlgeschlagen"; @@ -451,6 +466,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "Bestätigt"; +/* No comment provided by engineer. */ +"acknowledged roster" = "Bestätigter Relaisbestand"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "Fehler bei der Bestätigung"; @@ -463,9 +481,18 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Aktive Verbindungen"; +/* No comment provided by engineer. */ +"Add" = "Hinzufügen"; + /* No comment provided by engineer. */ "Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet."; +/* No comment provided by engineer. */ +"Add contributors." = "Mitwirkende hinzufügen."; + +/* No comment provided by engineer. */ +"Add description" = "Beschreibung hinzufügen"; + /* No comment provided by engineer. */ "Add friends" = "Freunde aufnehmen"; @@ -478,6 +505,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Profil hinzufügen"; +/* No comment provided by engineer. */ +"Add relay" = "Relais hinzufügen"; + +/* No comment provided by engineer. */ +"Add relays" = "Relais hinzufügen"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Relais hinzufügen, um die Nachrichtenübermittlung wiederherzustellen."; + /* No comment provided by engineer. */ "Add server" = "Server hinzufügen"; @@ -487,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Team-Mitglieder aufnehmen"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Fügen Sie diesen Code in Ihre Webseite ein. Er zeigt die Vorschau Ihres Kanals / Ihrer Gruppe an."; + /* No comment provided by engineer. */ "Add to another device" = "Einem anderen Gerät hinzufügen"; @@ -541,6 +580,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Erweiterte Netzwerkeinstellungen"; +/* No comment provided by engineer. */ +"Advanced options" = "Erweiterte Optionen"; + /* No comment provided by engineer. */ "Advanced settings" = "Erweiterte Einstellungen"; @@ -619,6 +661,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Erlauben"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Einbetten für alle erlauben"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; @@ -730,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Anruf annehmen"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Eine Vorschau ist auf jeder Webseite möglich."; + /* No comment provided by engineer. */ "App build: %@" = "App Build: %@"; @@ -754,6 +802,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "App-Sitzung"; +/* alert title */ +"App update required" = "Aktualisierung der App erforderlich"; + /* No comment provided by engineer. */ "App version" = "App Version"; @@ -809,7 +860,7 @@ swipe action */ "attempts" = "Versuche"; /* No comment provided by engineer. */ -"Audio & video calls" = "Audio- & Videoanrufe"; +"Audio & video calls" = "Audio- und Videoanrufe"; /* No comment provided by engineer. */ "Audio and video calls" = "Audio- und Videoanrufe"; @@ -871,6 +922,9 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Falsche Nachrichten-ID"; +/* badge alert title */ +"Badge cannot be verified" = "Abzeichen ist nicht verifizierbar"; + /* No comment provided by engineer. */ "Be free\nin your network" = "Seien Sie frei\nin Ihrem Netzwerk"; @@ -883,6 +937,9 @@ swipe action */ /* No comment provided by engineer. */ "Better calls" = "Verbesserte Anrufe"; +/* No comment provided by engineer. */ +"Better channels 📢" = "Verbesserte Kanäle 📢"; + /* No comment provided by engineer. */ "Better groups" = "Bessere Gruppen"; @@ -1022,9 +1079,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "Anrufen…"; -/* No comment provided by engineer. */ -"Calls" = "Anrufe"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Anrufe nicht zugelassen!"; @@ -1060,6 +1114,12 @@ alert button new chat action */ "Cancel" = "Abbrechen"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Kanal abbrechen und löschen"; + +/* alert title */ +"Cancel creating channel?" = "Kanalerstellung abbrechen?"; + /* No comment provided by engineer. */ "Cancel migration" = "Migration abbrechen"; @@ -1096,9 +1156,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Sperr-Modus ändern"; -/* No comment provided by engineer. */ -"Change member role?" = "Die Mitgliederrolle ändern?"; - /* authentication reason */ "Change passcode" = "Zugangscode ändern"; @@ -1111,6 +1168,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "Rolle ändern"; +/* No comment provided by engineer. */ +"Change role?" = "Rolle ändern?"; + /* authentication reason */ "Change self-destruct mode" = "Selbstzerstörungs-Modus ändern"; @@ -1170,15 +1230,24 @@ alert subtitle */ /* alert message */ "Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Das Kanalprofil wurde geändert. Beim Speichern wird das aktualisierte Profil an die Abonnenten des Kanals gesendet."; +/* No comment provided by engineer. */ +"Channel SimpleX name" = "Im Kanal genutzter SimpleX-Name"; + /* alert title */ "Channel temporarily unavailable" = "Der Kanal ist vorübergehend nicht erreichbar"; +/* No comment provided by engineer. */ +"Channel webpage" = "Kanal-Webseite"; + /* No comment provided by engineer. */ "Channel will be deleted for all subscribers - this cannot be undone!" = "Der Kanal wird für alle Abonnenten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "Channel will be deleted for you - this cannot be undone!" = "Der Kanal wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "Der Kanal wird mit %1$d von %2$d Relais gestartet. Fortfahren?"; + /* No comment provided by engineer. */ "Channels" = "Kanäle"; @@ -1197,6 +1266,9 @@ alert subtitle */ /* No comment provided by engineer. */ "Chat console" = "Chat-Konsole"; +/* No comment provided by engineer. */ +"Chat data" = "Chat-Daten"; + /* No comment provided by engineer. */ "Chat database" = "Chat-Datenbank"; @@ -1430,6 +1502,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "Schneller miteinander verbinden! 🚀"; +/* new chat action */ +"Connect to %@" = "Mit %@ verbinden"; + /* No comment provided by engineer. */ "Connect to desktop" = "Mit dem Desktop verbinden"; @@ -1520,12 +1595,12 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "Verbindung blockiert"; +/* conn error description */ +"Connection blocked: %@" = "Verbindung blockiert: %@"; + /* alert title */ "Connection error" = "Verbindungsfehler"; -/* conn error description */ -"Connection error (AUTH)" = "Verbindungsfehler (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; @@ -1535,6 +1610,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Die Verbindung wurde vom Serverbetreiber blockiert:\n%@"; +/* conn error description */ +"Connection link removed" = "Verbindungsfehler"; + /* No comment provided by engineer. */ "Connection not ready." = "Verbindung noch nicht bereit."; @@ -1565,6 +1643,9 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Verbindungen"; +/* No comment provided by engineer. */ +"Contact" = "Kontakt"; + /* profile update event chat item */ "contact %@ changed to %@" = "Der Kontaktname wurde von %1$@ auf %2$@ geändert"; @@ -1634,12 +1715,18 @@ server test step */ /* No comment provided by engineer. */ "Contribute" = "Unterstützen Sie uns"; +/* member role */ +"contributor" = "Mitwirkender"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Chat-Inhalte entfernt!"; /* No comment provided by engineer. */ "Copy" = "Kopieren"; +/* No comment provided by engineer. */ +"Copy code" = "Code kopieren"; + /* No comment provided by engineer. */ "Copy error" = "Fehlermeldung kopieren"; @@ -1658,6 +1745,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "Gruppe mit einem zufälligen Profil erstellen."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "Erstellen Sie eine Webseite, die Besuchern Ihren Kanal als Vorschau zeigt, bevor sie ihn abonnieren. Hosten Sie die Seite selbst oder nutzen Sie beliebiges statisches Hosting."; + /* server test step */ "Create file" = "Datei erstellen"; @@ -1682,15 +1772,15 @@ server test step */ /* No comment provided by engineer. */ "Create public channel" = "Öffentlichen Kanal erstellen"; -/* No comment provided by engineer. */ -"Create public channel (BETA)" = "Öffentlichen Kanal erstellen (BETA)"; - /* server test step */ "Create queue" = "Warteschlange erstellen"; /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-Adresse erstellen"; +/* No comment provided by engineer. */ +"Create web preview." = "Eine Web-Vorschau erstellen."; + /* No comment provided by engineer. */ "Create your address" = "Ihre Adresse erstellen"; @@ -1791,7 +1881,7 @@ server test step */ "Database passphrase" = "Datenbank-Passwort"; /* No comment provided by engineer. */ -"Database passphrase & export" = "Datenbank-Passwort & -Export"; +"Database passphrase & export" = "Datenbank-Passwort und -Export"; /* No comment provided by engineer. */ "Database passphrase is different from saved in the keychain." = "Das Datenbank-Passwort unterscheidet sich von dem im Schlüsselbund gespeicherten."; @@ -1918,6 +2008,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Nur bei mir löschen"; +/* No comment provided by engineer. */ +"Delete from history" = "Aus dem Nachrichtenverlauf löschen"; + /* No comment provided by engineer. */ "Delete group" = "Gruppe löschen"; @@ -2043,13 +2136,13 @@ alert button */ "Desktop devices" = "Desktop-Geräte"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Die Adresse des Zielservers von %1$@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %2$@."; /* snd error text */ "Destination server error: %@" = "Zielserver-Fehler: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "Die Version des Zielservers %1$@ ist nicht kompatibel mit dem Weiterleitungsserver %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Detaillierte Statistiken"; @@ -2058,14 +2151,11 @@ alert button */ "Details" = "Details"; /* No comment provided by engineer. */ -"Develop" = "Entwicklung"; +"Developer" = "Entwicklertools"; /* No comment provided by engineer. */ "Developer options" = "Optionen für Entwickler"; -/* No comment provided by engineer. */ -"Developer tools" = "Entwicklertools"; - /* No comment provided by engineer. */ "Device" = "Gerät"; @@ -2153,6 +2243,9 @@ alert button */ /* No comment provided by engineer. */ "Do it later" = "Später wiederholen"; +/* No comment provided by engineer. */ +"Do not require signing messages." = "Signatur für Nachrichten nicht erforderlich."; + /* No comment provided by engineer. */ "Do not send history to new members." = "Den Nachrichtenverlauf nicht an neue Mitglieder senden."; @@ -2183,6 +2276,9 @@ alert button */ /* No comment provided by engineer. */ "Don't miss important messages." = "Verpassen Sie keine wichtigen Nachrichten."; +/* alert action */ +"Don't save" = "Nicht speichern"; + /* alert action */ "Don't show again" = "Nicht nochmals anzeigen"; @@ -2241,12 +2337,18 @@ chat item action */ /* No comment provided by engineer. */ "Easier to invite your friends 👋" = "Freunde einladen – jetzt noch einfacher 👋"; +/* No comment provided by engineer. */ +"Easier to read." = "Einfacher zu lesen."; + /* chat item action */ "Edit" = "Bearbeiten"; /* No comment provided by engineer. */ "Edit channel profile" = "Kanalprofil bearbeiten"; +/* No comment provided by engineer. */ +"Edit description" = "Beschreibung bearbeiten"; + /* No comment provided by engineer. */ "Edit group profile" = "Gruppenprofil bearbeiten"; @@ -2260,7 +2362,7 @@ chat item action */ "Enable (keep overrides)" = "Aktivieren (vorgenommene Einstellungen bleiben erhalten)"; /* channel creation warning */ -"Enable at least one chat relay in Network & Servers." = "Aktivieren Sie mindestens ein Chat‑Relais unter 'Netzwerk & Server'."; +"Enable at least one chat relay in Network & Servers." = "Aktivieren Sie mindestens ein Chat‑Relais unter \"Netzwerk und Server\"."; /* alert title */ "Enable automatic message deletion?" = "Automatisches Löschen von Nachrichten aktivieren?"; @@ -2332,7 +2434,7 @@ chat item action */ "Encrypt local files" = "Lokale Dateien verschlüsseln"; /* No comment provided by engineer. */ -"Encrypt stored files & media" = "Gespeicherte Dateien & Medien verschlüsseln"; +"Encrypt stored files & media" = "Gespeicherte Dateien und Medien verschlüsseln"; /* No comment provided by engineer. */ "Encrypted database" = "Verschlüsselte Datenbank"; @@ -2403,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter correct passphrase." = "Geben Sie das korrekte Passwort ein."; +/* placeholder */ +"Enter description (optional)" = "Beschreibung eingeben (optional)"; + /* No comment provided by engineer. */ "Enter group name…" = "Geben Sie den Gruppennamen ein…"; @@ -2430,6 +2535,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter this device name…" = "Geben Sie diesen Gerätenamen ein…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "URL der Webseite eingeben"; + /* placeholder */ "Enter welcome message…" = "Geben Sie eine Begrüßungsmeldung ein …"; @@ -2442,7 +2550,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "Fehler"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Fehler"; /* No comment provided by engineer. */ @@ -2463,6 +2571,9 @@ chat item action */ /* alert title */ "Error adding relay" = "Fehler beim Hinzufügen des Relais"; +/* alert title */ +"Error adding relays" = "Fehler beim Hinzufügen von Relais"; + /* alert title */ "Error adding server" = "Fehler beim Hinzufügen des Servers"; @@ -2541,6 +2652,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Fehler beim Löschen der Datenbank"; +/* alert title */ +"Error deleting message" = "Fehler beim Löschen der Nachricht"; + /* alert title */ "Error deleting old database" = "Fehler beim Löschen der alten Datenbank"; @@ -2619,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error saving ICE servers" = "Fehler beim Speichern der ICE-Server"; +/* alert title */ +"Error saving name" = "Fehler beim Speichern des Namens"; + /* No comment provided by engineer. */ "Error saving passcode" = "Fehler beim Speichern des Zugangscodes"; @@ -2638,7 +2755,7 @@ chat item action */ "Error scanning code: %@" = "Fehler beim Scannen des Codes: %@"; /* No comment provided by engineer. */ -"Error sending email" = "Fehler beim Senden der eMail"; +"Error sending email" = "Fehler beim Senden der E-Mail"; /* No comment provided by engineer. */ "Error sending member contact invitation" = "Fehler beim Senden einer Mitglied-Kontakt-Einladung"; @@ -2652,6 +2769,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; +/* alert title */ +"Error sharing address" = "Fehler beim Teilen der Adresse"; + /* alert title */ "Error sharing channel" = "Fehler beim Teilen des Kanals"; @@ -2701,6 +2821,7 @@ chat item action */ "error: %@" = "Fehler: %@"; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Fehler: %@"; @@ -2793,6 +2914,12 @@ server test error */ /* file error text */ "File server error: %@" = "Datei-Server Fehler: %@"; +/* No comment provided by engineer. */ +"File servers" = "Datei-Server"; + +/* copied message info */ +"File servers: %@" = "Datei-Server: %@"; + /* No comment provided by engineer. */ "File status" = "Datei-Status"; @@ -2815,7 +2942,7 @@ server test error */ "Files" = "Dateien"; /* No comment provided by engineer. */ -"Files & media" = "Dateien & Medien"; +"Files & media" = "Dateien und Medien"; /* chat feature */ "Files and media" = "Dateien und Medien"; @@ -2978,6 +3105,9 @@ servers warning */ /* No comment provided by engineer. */ "Get notified when mentioned." = "Bei Erwähnung benachrichtigt werden."; +/* No comment provided by engineer. */ +"Get SimpleX name (BETA)" = "SimpleX-Name erhalten (BETA)"; + /* No comment provided by engineer. */ "Get started" = "Jetzt starten"; @@ -3053,6 +3183,9 @@ servers warning */ /* alert message */ "Group profile was changed. If you save it, the updated profile will be sent to group members." = "Das Gruppenprofil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an die Gruppenmitglieder gesendet."; +/* No comment provided by engineer. */ +"Group webpage" = "Webseite der Gruppe"; + /* No comment provided by engineer. */ "Group welcome message" = "Gruppen-Begrüßungsmeldung"; @@ -3068,6 +3201,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "Hilfe"; +/* No comment provided by engineer. */ +"Help & support" = "Hilfe & Unterstützung"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "Helfen Sie Administratoren bei der Moderation ihrer Gruppen."; @@ -3119,12 +3255,18 @@ servers warning */ /* No comment provided by engineer. */ "How to" = "Anleitung"; +/* No comment provided by engineer. */ +"How to register a test name" = "Wie man einen Test-Namen registriert"; + /* No comment provided by engineer. */ "How to use it" = "Wie man SimpleX nutzt"; /* No comment provided by engineer. */ "How to use your servers" = "Wie Sie Ihre Server nutzen"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "Ungarische Bedienoberfläche"; @@ -3404,6 +3546,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Dies wird Abonnenten angezeigt und zum Laden der Vorschau genutzt."; + /* No comment provided by engineer. */ "Italian interface" = "Italienische Bedienoberfläche"; @@ -3422,6 +3567,9 @@ servers warning */ /* No comment provided by engineer. */ "Join channel" = "Kanal beitreten"; +/* new chat action */ +"Join channel %@" = "Kanal %@ beitreten"; + /* new chat sheet title */ "Join group" = "Treten Sie der Gruppe bei"; @@ -3464,7 +3612,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Große Datei!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Mehr erfahren"; /* swipe action */ @@ -3494,6 +3642,12 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Weniger Datenverkehr in mobilen Netzen."; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "Lassen Sie sich über den mit Ihrer SimpleX‑Adresse registrierten Namen verbinden."; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "Ermöglichen Sie Beitritte über den mit diesem Kanal‑Link registrierten Namen."; + /* No comment provided by engineer. */ "Let someone connect to you" = "Jemand mit Ihnen verbinden lassen"; @@ -3566,6 +3720,9 @@ servers warning */ /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind."; +/* No comment provided by engineer. */ +"Manage your relays." = "Ihre Relais verwalten."; + /* No comment provided by engineer. */ "Mark deleted for everyone" = "Für Alle als gelöscht markieren"; @@ -3623,15 +3780,6 @@ servers warning */ /* chat feature */ "Member reports" = "Mitglieder-Meldungen"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Die Rolle des Mitglieds wird auf \"%@\" geändert. Alle Chat-Mitglieder werden darüber informiert."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!"; @@ -3725,6 +3873,12 @@ servers warning */ /* No comment provided by engineer. */ "Message shape" = "Nachrichten-Form"; +/* No comment provided by engineer. */ +"Message signing is not required." = "Nachrichten müssen nicht signiert werden."; + +/* No comment provided by engineer. */ +"Message signing is required." = "Nachrichten müssen signiert werden."; + /* No comment provided by engineer. */ "Message source remains private." = "Die Nachrichtenquelle bleibt privat."; @@ -3845,6 +3999,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Weitere Verbesserungen sind bald verfügbar!"; +/* No comment provided by engineer. */ +"More privacy" = "Weitere Privatsphäre"; + /* No comment provided by engineer. */ "More reliable network connection." = "Zuverlässigere Netzwerkverbindung."; @@ -3870,7 +4027,10 @@ servers warning */ "Name" = "Name"; /* No comment provided by engineer. */ -"Network & servers" = "Netzwerk & Server"; +"Name not found" = "Name wurde nicht gefunden"; + +/* No comment provided by engineer. */ +"Network & servers" = "Netzwerk und Server"; /* No comment provided by engineer. */ "Network commitments" = "Netzwerk Verpflichtungen"; @@ -3981,7 +4141,7 @@ servers warning */ "No" = "Nein"; /* No comment provided by engineer. */ -"No account. No phone. No email. No ID.\nThe most secure encryption." = "Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung."; +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Kein Benutzerkonto. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung."; /* No comment provided by engineer. */ "No active relays" = "Keine aktiven Relais"; @@ -3989,6 +4149,9 @@ servers warning */ /* Authentication unavailable */ "No app password" = "Kein App-Passwort"; +/* No comment provided by engineer. */ +"No available relays" = "Keine verfügbaren Relais"; + /* No comment provided by engineer. */ "No chat relays" = "Keine Chat-Relais"; @@ -4067,6 +4230,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "Keine herunter- oder hochgeladenen Dateien"; +/* No comment provided by engineer. */ +"No relays" = "Keine Relais"; + /* servers error */ "No servers for private message routing." = "Keine Router für privates Nachrichten-Routing."; @@ -4076,6 +4242,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "Keine Server für den Empfang von Nachrichten."; +/* servers warning */ +"No servers to resolve names." = "Keine Server für die Namensauflösung konfiguriert."; + /* servers error */ "No servers to send files." = "Keine Server für das Versenden von Dateien."; @@ -4091,12 +4260,18 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "Keine ungelesenen Chats"; +/* No comment provided by engineer. */ +"No valid link" = "Kein gültiger Link"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich."; /* No comment provided by engineer. */ "Non-profit governance" = "Non‑Profit‑Governance"; +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "Keiner Ihrer Server ist zum Auflösen von SimpleX‑Namen konfiguriert. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink."; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän."; @@ -4249,6 +4424,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Nur Ihre oben genannte Seite kann die Vorschau anzeigen."; + /* alert action alert button */ "Open" = "Öffnen"; @@ -4371,7 +4549,7 @@ alert button */ "owners" = "Eigentümer"; /* No comment provided by engineer. */ -"Owners" = "Eigentümer"; +"Owners & contributors" = "Eigentümer und Mitwirkende"; /* No comment provided by engineer. */ "Ownership: you can run your own relays." = "Volle Kontrolle: Sie können Ihre eigenen Relais betreiben."; @@ -4532,9 +4710,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Bisher verbundene Server"; -/* No comment provided by engineer. */ -"Privacy & security" = "Datenschutz & Sicherheit"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Schutz der Privatsphäre Ihrer Kunden."; @@ -4586,7 +4761,8 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Profil-Design"; -/* alert message */ +/* alert message +alert title */ "Profile update will be sent to your SimpleX contacts." = "Profil-Aktualisierung wird an Ihre SimpleX-Kontakte gesendet."; /* No comment provided by engineer. */ @@ -4635,7 +4811,7 @@ alert button */ "Protect your chat profiles with a password!" = "Ihre Chat-Profile mit einem Passwort schützen!"; /* No comment provided by engineer. */ -"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen."; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk und Server* Einstellungen."; /* No comment provided by engineer. */ "Protocol background timeout" = "Protokoll Hintergrund-Zeitüberschreitung"; @@ -4658,6 +4834,9 @@ alert button */ /* No comment provided by engineer. */ "Public channels - speak freely 🚀" = "Öffentliche Kanäle – frei sprechen 🚀"; +/* No comment provided by engineer. */ +"Public names for your channel or business." = "Öffentliche Namen für Ihren Kanal oder Ihr Unternehmen."; + /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; @@ -4682,7 +4861,7 @@ alert button */ /* swipe action */ "Read" = "Gelesen"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Mehr erfahren"; /* No comment provided by engineer. */ @@ -4825,6 +5004,12 @@ swipe action */ /* No comment provided by engineer. */ "Relay test failed!" = "Relais-Test fehlgeschlagen!"; +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "Relais wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* alert message */ +"Relays added: %@." = "Relais hinzugefügt: %@."; + /* No comment provided by engineer. */ "Reliability: many relays per channel." = "Zuverlässigkeit: Mehrere Relais pro Kanal."; @@ -4849,9 +5034,18 @@ swipe action */ /* alert title */ "Remove member?" = "Das Mitglied entfernen?"; +/* No comment provided by engineer. */ +"Remove name" = "Name entfernen"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Passwort aus dem Schlüsselbund entfernen?"; +/* No comment provided by engineer. */ +"Remove relay" = "Relais entfernen"; + +/* alert title */ +"Remove relay?" = "Relais entfernen?"; + /* alert title */ "Remove subscriber?" = "Abonnent entfernen?"; @@ -4951,6 +5145,9 @@ swipe action */ /* chat list item title */ "requested to connect" = "Zur Verbindung aufgefordert"; +/* No comment provided by engineer. */ +"Require signing messages." = "Signatur für Nachrichten erforderlich."; + /* No comment provided by engineer. */ "Required" = "Erforderlich"; @@ -4978,6 +5175,9 @@ swipe action */ /* No comment provided by engineer. */ "Reset to user theme" = "Auf das Benutzer-spezifische Design zurücksetzen"; +/* No comment provided by engineer. */ +"Resolver error: %@" = "Namensauflösungs-Fehler: %@"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Um ein neues Chat-Profil zu erstellen, starten Sie die App neu"; @@ -5032,6 +5232,18 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rolle"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Die Rolle des Mitglieds wird auf \"%@\" geändert. Alle Chat-Mitglieder werden darüber informiert."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Gruppenmitglieder werden benachrichtigt."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "Die Rolle wird auf \"%@\" geändert. Alle Abonnenten werden benachrichtigt."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; + /* No comment provided by engineer. */ "Run chat" = "Chat starten"; @@ -5044,7 +5256,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Speichern"; @@ -5066,6 +5279,9 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Speichern und Gruppenmitglieder benachrichtigen"; +/* No comment provided by engineer. */ +"Save and notify members" = "Speichern und Mitglieder benachrichtigen"; + /* No comment provided by engineer. */ "Save and notify subscribers" = "Speichern und Abonnenten benachrichtigen"; @@ -5108,6 +5324,12 @@ chat item action */ /* alert title */ "Save servers?" = "Alle Server speichern?"; +/* alert title */ +"Save SimpleX name?" = "SimpleX-Name speichern?"; + +/* alert title */ +"Save webpage settings?" = "Webseiten-Einstellungen sichern?"; + /* No comment provided by engineer. */ "Save welcome message?" = "Begrüßungsmeldung speichern?"; @@ -5309,9 +5531,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Der Absender hat möglicherweise die Verbindungsanfrage gelöscht."; - /* alert message */ "Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Das Senden einer Link-Vorschau kann Ihre IP‑Adresse an die Website übermitteln. Sie können dies später in den Datenschutzeinstellungen ändern."; @@ -5369,6 +5588,9 @@ chat item action */ /* No comment provided by engineer. */ "Server" = "Server"; +/* No comment provided by engineer. */ +"Server %@ does not support name resolution. Configure servers, or use a connection link." = "Der Server %@ unterstützt keine Namensauflösung. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink."; + /* alert message */ "Server added to operator %@." = "Der Server wurde dem Betreiber %@ hinzugefügt."; @@ -5565,6 +5787,9 @@ chat item action */ /* No comment provided by engineer. */ "Show developer options" = "Entwickleroptionen anzeigen"; +/* No comment provided by engineer. */ +"Show encryption" = "Verschlüsselung anzeigen"; + /* No comment provided by engineer. */ "Show last messages" = "Letzte Nachrichten anzeigen"; @@ -5583,6 +5808,25 @@ chat item action */ /* No comment provided by engineer. */ "Show:" = "Anzeigen:"; +/* No comment provided by engineer. */ +"Sign message" = "Nachricht signieren"; + +/* chat feature */ +"Sign messages" = "Nachrichten signieren"; + +/* alert title +copied message info */ +"Signature missing" = "Signatur fehlt"; + +/* copied message info */ +"Signed" = "Signiert"; + +/* copied message info */ +"Signed & verified" = "Signiert und verifiziert"; + +/* No comment provided by engineer. */ +"Signing proves you authored this message and can't be denied later." = "Die Signatur bestätigt, dass Sie diese Nachricht verfasst haben und sie später nicht abstreiten können."; + /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -5640,12 +5884,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "SimpleX-Sperre aktiviert"; +/* No comment provided by engineer. */ +"SimpleX name" = "SimpleX-Name"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "Fehler beim SimpleX-Namen"; + +/* alert title */ +"SimpleX name not verified" = "SimpleX-Name ist nicht verifiziert"; + /* simplex link type */ "SimpleX one-time invitation" = "SimpleX-Einmal-Einladung"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Die SimpleX-Protokolle wurden von Trail of Bits überprüft."; +/* No comment provided by engineer. */ +"SimpleX public names (BETA)" = "Öffentliche SimpleX-Namen (BETA)"; + /* simplex link type */ "SimpleX relay address" = "SimpleX Relais-Adresse"; @@ -5722,6 +5978,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "Statistiken"; +/* No comment provided by engineer. */ +"Status" = "Status"; + /* No comment provided by engineer. */ "Stop" = "Beenden"; @@ -5770,6 +6029,9 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Abonniert"; +/* member role */ +"subscriber" = "Abonnent"; + /* No comment provided by engineer. */ "Subscriber" = "Abonnent"; @@ -5819,7 +6081,7 @@ report reason */ "Subscriptions ignored" = "Nicht beachtete Abonnements"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "Unterstützung von SimpleX Chat"; +"Support the project" = "Unterstützen Sie das Projekt"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "Während des Anrufs zwischen Audio und Video wechseln"; @@ -5928,10 +6190,10 @@ server test failure */ "Thank you for installing SimpleX Chat!" = "Vielen Dank, dass Sie SimpleX Chat installiert haben!"; /* No comment provided by engineer. */ -"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Dank der Nutzer - [Tragen Sie per Weblate bei](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Dank der Nutzer - [Wirken Sie per Weblate mit](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; /* No comment provided by engineer. */ -"Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Tragen Sie per Weblate bei!"; +"Thanks to the users – contribute via Weblate!" = "Dank der Nutzer - Wirken Sie per Weblate mit!"; /* alert message */ "The address will be short, and your profile will be shared via the address." = "Die Adresse wird gekürzt sein, und Ihr Profil wird über die Adresse geteilt."; @@ -5951,6 +6213,12 @@ server test failure */ /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden."; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "Das Abzeichen ist mit einem Schlüssel signiert, den diese App‑Version nicht erkennt. Aktualisieren Sie die App, um dieses Abzeichen zu verifizieren."; + +/* alert message */ +"The channel required this message to be signed, but the signature is missing." = "Der Kanal verlangt für diese Nachricht eine Signatur, welche aber fehlt."; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Der von Ihnen gescannte Code ist kein SimpleX-Link-QR-Code."; @@ -6011,6 +6279,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Wir haben das zweite Häkchen vermisst! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Der Absender hat möglicherweise die Verbindungsanfrage gelöscht."; + /* alert message */ "The sender will NOT be notified" = "Der Absender wird NICHT benachrichtigt"; @@ -6020,6 +6291,18 @@ server test failure */ /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "Medien- und Datei-Server für neue Daten über Ihr aktuelles Chat-Profil **%@**."; +/* alert message */ +"The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." = "Der SimpleX‑Name @%@ wurde ohne SimpleX-Adresse registriert. Fügen Sie die SimpleX-Adresse über die Registrierungsseite hinzu."; + +/* alert message */ +"The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." = "Der SimpleX‑Name #%@ wurde ohne Kanal‑Link registriert. Fügen Sie den Kanal‑Link über die Registrierungsseite hinzu."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but it has no valid link." = "Der SimpleX-Name %@ wurde registriert, hat aber keinen gültigen Link."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." = "Der SimpleX‑Name %@ wurde registriert, jedoch nicht in Ihrem Profil hinterlegt. Bitte zu Ihrer Adresse oder zum Kanalprofil hinzufügen, sofern Sie der Besitzer sind."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; @@ -6033,7 +6316,7 @@ server test failure */ "Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein."; /* No comment provided by engineer. */ -"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist."; +"There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzerkonten, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist."; /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Diese Nutzungsbedingungen gelten auch für: **%@**."; @@ -6054,7 +6337,10 @@ server test failure */ "This action cannot be undone - the messages sent and received in this chat earlier than selected will be deleted." = "Dieser Vorgang kann nicht rückgängig gemacht werden - die in diesem Chat früher als ausgewählt gesendeten und empfangenen Nachrichten werden gelöscht."; /* No comment provided by engineer. */ -"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden!"; +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Diese Aktion kann nicht rückgängig gemacht werden."; + +/* badge alert */ +"This badge could not be verified and may not be genuine." = "Dieses Abzeichen konnte nicht verifiziert werden und ist möglicherweise nicht echt."; /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Dieser Chat ist durch Ende-zu-Ende-Verschlüsselung geschützt."; @@ -6077,9 +6363,16 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "Diese Gruppe existiert nicht mehr."; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "Diese Gruppe erfordert eine neuere App‑Version. Bitte aktualisieren Sie die App, um beizutreten."; + /* alert message */ "This is a chat relay address, it cannot be used to connect." = "Dies ist eine Chat‑Relais-Adresse, welche nicht zum Verbinden verwendet werden kann."; +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "Dies ist das letzte aktive Relais. Wenn Sie es entfernen, können keine Nachrichten mehr an Abonnenten zugestellt werden."; + /* new chat action */ "This is your link for channel %@!" = "Dies ist Ihr Link für den Kanal %@!"; @@ -6098,6 +6391,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting is for your current profile **%@**." = "Diese Einstellung gilt für Ihr aktuelles Profil **%@**."; +/* No comment provided by engineer. */ +"This SimpleX name is not registered. Please check the name." = "Dieser SimpleX-Name wurde nicht registriert. Bitte überprüfen Sie den Namen."; + /* 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."; @@ -6146,6 +6442,9 @@ server test failure */ /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können."; +/* No comment provided by engineer. */ +"To resolve names" = "Für Namensauflösung"; + /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite **Ihre Chat-Profile** ein, um Ihr verborgenes Profil zu sehen."; @@ -6167,6 +6466,9 @@ server test failure */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen."; +/* No comment provided by engineer. */ +"To verify keys with this subscriber, compare (or scan) the code on your devices." = "Für die Überprüfung der Schlüssel mit diesem Abonnenten vergleichen oder scannen Sie den Code auf Ihren Geräten."; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognito beim Verbinden einschalten."; @@ -6224,6 +6526,9 @@ server test failure */ /* rcv group event chat item */ "unblocked %@" = "hat %@ freigegeben"; +/* No comment provided by engineer. */ +"Unconfirmed name" = "Unbestätigter Name"; + /* No comment provided by engineer. */ "Undelivered messages" = "Nicht ausgelieferte Nachrichten"; @@ -6269,9 +6574,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben."; - /* No comment provided by engineer. */ "Unlink" = "Entkoppeln"; @@ -6296,6 +6598,9 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "Verbindungs-Link wird nicht unterstützt"; +/* badge alert title */ +"Unverified badge" = "Abzeichen nicht verifiziert"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Bis zu 100 der letzten Nachrichten werden an neue Mitglieder gesendet."; @@ -6335,7 +6640,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Adresse aktualisieren"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Adresse aktualisieren?"; /* No comment provided by engineer. */ @@ -6443,6 +6749,9 @@ server test failure */ /* No comment provided by engineer. */ "Use web port" = "Web-Port nutzen"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "Die verwendeten Chat‑Relais unterstützen keine Webseiten."; + /* No comment provided by engineer. */ "User selection" = "Benutzer-Auswahl"; @@ -6455,9 +6764,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* relay test step */ "Verify" = "Überprüfen"; @@ -6476,12 +6782,18 @@ server test failure */ /* No comment provided by engineer. */ "Verify database passphrase" = "Überprüfen Sie das Datenbank-Passwort"; +/* No comment provided by engineer. */ +"Verify name" = "Name überprüfen"; + /* No comment provided by engineer. */ "Verify passphrase" = "Überprüfen Sie das Passwort"; /* No comment provided by engineer. */ "Verify security code" = "Sicherheitscode überprüfen"; +/* No comment provided by engineer. */ +"Verify SimpleX names" = "SimpleX-Namen überprüfen"; + /* relay hostname */ "via %@" = "via %@"; @@ -6599,6 +6911,12 @@ server test failure */ /* No comment provided by engineer. */ "We made connecting simpler for new users." = "Wir haben das Verbinden für neue Nutzer vereinfacht."; +/* No comment provided by engineer. */ +"Webpage code" = "Webseiten-Code"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "Die Webseiten-Einstellungen wurden geändert. Wenn Sie sie abspeichern, werden die aktualisierten Einstellungen an die Abonnenten gesendet."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE-Server"; @@ -6759,7 +7077,7 @@ server test failure */ "You can enable later via Settings" = "Sie können diese später in den Einstellungen aktivieren"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren."; +"You can enable them later via app Your privacy settings." = "Sie können diese später in Ihren Privatsphäre-Einstellungen der App aktivieren."; /* No comment provided by engineer. */ "You can give another try." = "Sie können es nochmal probieren."; @@ -6797,6 +7115,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "Sie können in der Chat-Liste weiterhin die Unterhaltung mit %@ einsehen."; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "Sie können SimpleX ab der App-Version v7 unterstützen."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren."; @@ -6888,7 +7209,7 @@ server test failure */ "you unblocked %@" = "Sie haben %@ freigegeben"; /* No comment provided by engineer. */ -"You were born without an account" = "Sie wurden ohne eine Benutzerkennung geboren."; +"You were born without an account" = "Sie wurden ohne ein Benutzerkonto geboren."; /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "Sie können erst dann Nachrichten versenden, **sobald Ihre Anfrage angenommen wurde**."; @@ -6941,9 +7262,6 @@ server test failure */ /* No comment provided by engineer. */ "Your channel" = "Ihr Kanal"; -/* No comment provided by engineer. */ -"Your chat database" = "Chat-Datenbank"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen."; @@ -6962,6 +7280,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Ihr Kontakt"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@)."; @@ -6992,6 +7313,9 @@ server test failure */ /* No comment provided by engineer. */ "Your network" = "Ihr Netzwerk"; +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "Ihr neuer Kanal %1$@ ist mit %2$d von %3$d Relais verbunden.\nWenn Sie abbrechen, wird der Kanal gelöscht. Sie können ihn später erneut erstellen."; + /* No comment provided by engineer. */ "Your preferences" = "Ihre Präferenzen"; @@ -7040,3 +7364,6 @@ server test failure */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ihre SimpleX-Adresse"; +/* No comment provided by engineer. */ +"Your SimpleX name" = "Ihr SimpleX-Name"; + diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index cf03ae6dbf..a6d4e9d20c 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -28,9 +28,6 @@ /* No comment provided by engineer. */ "(new)" = "(nuevo)"; -/* chat link info line */ -"(signed)" = "(firmado)"; - /* No comment provided by engineer. */ "(this device v%@)" = "(este dispositivo v%@)"; @@ -77,7 +74,7 @@ "**Warning**: the archive will be removed." = "**Atención**: el archivo será eliminado."; /* No comment provided by engineer. */ -"*bold*" = "\\*bold*"; +"*bold*" = "\\*negrita*"; /* copied message info title, # */ "# %@" = "# %@"; @@ -121,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ descargado"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ ha participado en la financiación colectiva de SimpleX Chat."; + /* notification title */ "%@ is connected!" = "%@ ¡está conectado!"; @@ -136,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ servidores"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ apoya a SimpleX Chat."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ subido"; @@ -154,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ ha apoyado a SimpleX Chat. La insignia caducó el %2$@."; + /* time interval */ "%d days" = "%d día(s)"; @@ -181,6 +187,15 @@ /* time interval */ "%d months" = "%d mes(es)"; +/* channel owners count */ +"%d owner" = "%d propietario"; + +/* channel owners count */ +"%d owners" = "%d propietarios"; + +/* channel members count */ +"%d owners & contributors" = "%d propietarios & colaboradores"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d servidores han fallado"; @@ -451,6 +466,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "Confirmaciones"; +/* No comment provided by engineer. */ +"acknowledged roster" = "lista confirmada"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "Errores de confirmación"; @@ -463,9 +481,18 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Conexiones activas"; +/* No comment provided by engineer. */ +"Add" = "Añadir"; + /* No comment provided by engineer. */ "Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Añade la dirección a tu perfil para que tus contactos SimpleX puedan compartirla con otros. La actualización del perfil se enviará a tus contactos SimpleX."; +/* No comment provided by engineer. */ +"Add contributors." = "Añade colaboradores."; + +/* No comment provided by engineer. */ +"Add description" = "Añadir descripción"; + /* No comment provided by engineer. */ "Add friends" = "Añadir amigos"; @@ -478,6 +505,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Añadir perfil"; +/* No comment provided by engineer. */ +"Add relay" = "Añadir servidor"; + +/* No comment provided by engineer. */ +"Add relays" = "Añadir servidores"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Añade servidores para restaurar la entrega de mensajes."; + /* No comment provided by engineer. */ "Add server" = "Añadir servidor"; @@ -487,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Añadir miembros del equipo"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Añade este código a tu web. Mostrará una vista previa de tu canal o grupo."; + /* No comment provided by engineer. */ "Add to another device" = "Añadir a otro dispositivo"; @@ -541,6 +580,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Configuración avanzada de red"; +/* No comment provided by engineer. */ +"Advanced options" = "Opciones avanzadas"; + /* No comment provided by engineer. */ "Advanced settings" = "Configuración avanzada"; @@ -619,6 +661,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Se permite"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Permitir que cualquiera pueda añadirlo a su web"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Se permiten las llamadas pero sólo si tu contacto también las permite."; @@ -638,7 +683,7 @@ swipe action */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas)"; /* No comment provided by engineer. */ -"Allow members to chat with admins." = "Permitir que los miembros chateen con administradores."; +"Allow members to chat with admins." = "Permite que los miembros chateen con los administradores."; /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite."; @@ -659,7 +704,7 @@ swipe action */ "Allow sharing" = "Permitir compartir"; /* No comment provided by engineer. */ -"Allow subscribers to chat with admins." = "Permitir que los suscriptores chateen con administradores."; +"Allow subscribers to chat with admins." = "Permite que los suscriptores chateen con los administradores."; /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Se permite la eliminación irreversible de mensajes. (24 horas)"; @@ -730,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Responder llamada"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Cualquier página web puede mostrar la vista previa."; + /* No comment provided by engineer. */ "App build: %@" = "Compilación app: %@"; @@ -754,6 +802,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "por sesión"; +/* alert title */ +"App update required" = "Es necesario actualizar la aplicación"; + /* No comment provided by engineer. */ "App version" = "Versión de la aplicación"; @@ -871,6 +922,9 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "ID de mensaje incorrecto"; +/* badge alert title */ +"Badge cannot be verified" = "No se pudo verificar la insignia"; + /* No comment provided by engineer. */ "Be free\nin your network" = "Se libre\nen tu red"; @@ -883,6 +937,9 @@ swipe action */ /* No comment provided by engineer. */ "Better calls" = "Llamadas mejoradas"; +/* No comment provided by engineer. */ +"Better channels 📢" = "Canales mejorados 📢"; + /* No comment provided by engineer. */ "Better groups" = "Grupos mejorados"; @@ -1022,9 +1079,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "llamando…"; -/* No comment provided by engineer. */ -"Calls" = "Llamadas"; - /* No comment provided by engineer. */ "Calls prohibited!" = "¡Llamadas no permitidas!"; @@ -1060,6 +1114,12 @@ alert button new chat action */ "Cancel" = "Cancelar"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Cancelar y eliminar el canal"; + +/* alert title */ +"Cancel creating channel?" = "¿Cancelar la creación del canal?"; + /* No comment provided by engineer. */ "Cancel migration" = "Cancelar migración"; @@ -1096,9 +1156,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Cambiar el modo de bloqueo"; -/* No comment provided by engineer. */ -"Change member role?" = "¿Cambiar rol?"; - /* authentication reason */ "Change passcode" = "Cambiar código de acceso"; @@ -1111,6 +1168,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "Cambiar rol"; +/* No comment provided by engineer. */ +"Change role?" = "¿Cambiar el rol?"; + /* authentication reason */ "Change self-destruct mode" = "Cambiar el modo de autodestrucción"; @@ -1170,15 +1230,24 @@ alert subtitle */ /* alert message */ "Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "El perfil del canal ha sido modificado. Si lo guardas, el perfil actualizado será enviado a los suscriptores."; +/* No comment provided by engineer. */ +"Channel SimpleX name" = "Nombre SimpleX del canal"; + /* alert title */ "Channel temporarily unavailable" = "Canales no disponibles temporalmente"; +/* No comment provided by engineer. */ +"Channel webpage" = "Web del canal"; + /* No comment provided by engineer. */ "Channel will be deleted for all subscribers - this cannot be undone!" = "El canal será eliminado para todos los suscriptores. ¡No puede deshacerse!"; /* No comment provided by engineer. */ "Channel will be deleted for you - this cannot be undone!" = "El canal será eliminado para tí. ¡No puede deshacerse!"; +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "El canal comenzará a funcionar con %1$d de %2$d servidores. ¿Deseas continuar?"; + /* No comment provided by engineer. */ "Channels" = "Canales"; @@ -1197,6 +1266,9 @@ alert subtitle */ /* No comment provided by engineer. */ "Chat console" = "Consola de Chat"; +/* No comment provided by engineer. */ +"Chat data" = "Datos del chat"; + /* No comment provided by engineer. */ "Chat database" = "Base de datos de SimpleX"; @@ -1346,7 +1418,7 @@ chat toolbar */ "colored" = "coloreado"; /* report reason */ -"Community guidelines violation" = "Violación de las normas de la comunidad"; +"Community guidelines violation" = "Violación de las normas"; /* server test step */ "Compare file" = "Comparar archivo"; @@ -1430,6 +1502,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "¡Conéctate más rápido! 🚀"; +/* new chat action */ +"Connect to %@" = "Conectar con %@"; + /* No comment provided by engineer. */ "Connect to desktop" = "Conectar con ordenador"; @@ -1476,7 +1551,7 @@ server test step */ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ -"connecting" = "conectando..."; +"connecting" = "conectando"; /* No comment provided by engineer. */ "Connecting" = "Conectando"; @@ -1503,7 +1578,7 @@ server test step */ "Connecting server… (error: %@)" = "Conectando con el servidor... (error: %@)"; /* No comment provided by engineer. */ -"Connecting to contact, please wait or check later!" = "Conectando con el contacto, por favor espera o revisa más tarde."; +"Connecting to contact, please wait or check later!" = "Se está estableciendo la conexión con el contacto. ¡Por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ "Connecting to desktop" = "Conectando con ordenador"; @@ -1520,12 +1595,12 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "Conexión bloqueada"; +/* conn error description */ +"Connection blocked: %@" = "Conexión bloqueada: %@"; + /* alert title */ "Connection error" = "Error conexión"; -/* conn error description */ -"Connection error (AUTH)" = "Error de conexión (Autenticación)"; - /* chat list item title (it should not be shown */ "connection established" = "conexión establecida"; @@ -1535,6 +1610,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Conexión bloqueada por el operador del servidor:\n%@"; +/* conn error description */ +"Connection link removed" = "Error de conexión"; + /* No comment provided by engineer. */ "Connection not ready." = "Conexión no establecida."; @@ -1565,6 +1643,9 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Conexiones"; +/* No comment provided by engineer. */ +"Contact" = "Contacto"; + /* profile update event chat item */ "contact %@ changed to %@" = "el contacto %1$@ ha cambiado a %2$@"; @@ -1602,7 +1683,7 @@ server test step */ "Contact is deleted." = "El contacto está eliminado."; /* No comment provided by engineer. */ -"Contact name" = "Contacto"; +"Contact name" = "Nombre de contacto"; /* No comment provided by engineer. */ "contact not ready" = "en espera de ser aceptado"; @@ -1634,12 +1715,18 @@ server test step */ /* No comment provided by engineer. */ "Contribute" = "Contribuye"; +/* member role */ +"contributor" = "colaborador"; + /* No comment provided by engineer. */ "Conversation deleted!" = "¡Conversación eliminada!"; /* No comment provided by engineer. */ "Copy" = "Copiar"; +/* No comment provided by engineer. */ +"Copy code" = "Copiar código"; + /* No comment provided by engineer. */ "Copy error" = "Copiar error"; @@ -1658,6 +1745,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "Crear grupo usando perfil aleatorio."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "Crea una página web para mostrar la vista previa de tu canal a las visitas antes de suscribirse. Alójala tú mismo o usa cualquier servicio de alojamiento estático."; + /* server test step */ "Create file" = "Crear archivo"; @@ -1674,7 +1764,7 @@ server test step */ "Create list" = "Crear lista"; /* No comment provided by engineer. */ -"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea perfil nuevo en la [aplicación para PC](https://simplex.Descargas/de chat/). 💻"; +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea un nuevo perfil en la [aplicación de escritorio](https://simplex.chat/downloads/). 💻"; /* No comment provided by engineer. */ "Create profile" = "Crear perfil"; @@ -1682,15 +1772,15 @@ server test step */ /* No comment provided by engineer. */ "Create public channel" = "Crear canal público"; -/* No comment provided by engineer. */ -"Create public channel (BETA)" = "Crear canal público (BETA)"; - /* server test step */ "Create queue" = "Crear cola"; /* No comment provided by engineer. */ "Create SimpleX address" = "Crear dirección SimpleX"; +/* No comment provided by engineer. */ +"Create web preview." = "Crea previsualizaciones web."; + /* No comment provided by engineer. */ "Create your address" = "Crea tu dirección"; @@ -1918,6 +2008,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Eliminar para mí"; +/* No comment provided by engineer. */ +"Delete from history" = "Eliminar del historial"; + /* No comment provided by engineer. */ "Delete group" = "Eliminar grupo"; @@ -2043,13 +2136,13 @@ alert button */ "Desktop devices" = "Ordenadores"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "La dirección del servidor de destino de %1$@ es incompatible con la configuración del servidor de reenvío %2$@."; /* snd error text */ "Destination server error: %@" = "Error del servidor de destino: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "La versión del servidor de destino de %1$@ es incompatible con el servidor de reenvío %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Estadísticas detalladas"; @@ -2058,14 +2151,11 @@ alert button */ "Details" = "Detalles"; /* No comment provided by engineer. */ -"Develop" = "Desarrollo"; +"Developer" = "Herramientas desarrollo"; /* No comment provided by engineer. */ "Developer options" = "Opciones desarrollador"; -/* No comment provided by engineer. */ -"Developer tools" = "Herramientas desarrollo"; - /* No comment provided by engineer. */ "Device" = "Dispositivo"; @@ -2094,7 +2184,7 @@ alert button */ "Direct messages between members are prohibited." = "Los mensajes directos entre miembros del grupo no están permitidos."; /* No comment provided by engineer. */ -"Direct messages between subscribers are prohibited." = "Los mensajes directos entre suscriptores del canal no están permitidos."; +"Direct messages between subscribers are prohibited." = "Los mensajes directos entre suscriptores no están permitidos."; /* alert button */ "Disable" = "Desactivar"; @@ -2153,6 +2243,9 @@ alert button */ /* No comment provided by engineer. */ "Do it later" = "Hacer más tarde"; +/* No comment provided by engineer. */ +"Do not require signing messages." = "No requerir la firma de mensajes."; + /* No comment provided by engineer. */ "Do not send history to new members." = "No se envía el historial a los miembros nuevos."; @@ -2183,6 +2276,9 @@ alert button */ /* No comment provided by engineer. */ "Don't miss important messages." = "No pierdas los mensajes importantes."; +/* alert action */ +"Don't save" = "No guardar"; + /* alert action */ "Don't show again" = "No volver a mostrar"; @@ -2241,12 +2337,18 @@ chat item action */ /* No comment provided by engineer. */ "Easier to invite your friends 👋" = "Invitar a tus amigos es más fácil 👋"; +/* No comment provided by engineer. */ +"Easier to read." = "Fácil de leer."; + /* chat item action */ "Edit" = "Editar"; /* No comment provided by engineer. */ "Edit channel profile" = "Editar perfil del canal"; +/* No comment provided by engineer. */ +"Edit description" = "Editar descripción"; + /* No comment provided by engineer. */ "Edit group profile" = "Editar perfil de grupo"; @@ -2403,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter correct passphrase." = "Introduce la contraseña correcta."; +/* placeholder */ +"Enter description (optional)" = "Introduce descripción (opcional)"; + /* No comment provided by engineer. */ "Enter group name…" = "Nombre del grupo…"; @@ -2430,6 +2535,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter this device name…" = "Nombre de este dispositivo…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "Introduce la URL de la web"; + /* placeholder */ "Enter welcome message…" = "Deja un mensaje de bienvenida…"; @@ -2442,7 +2550,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "error"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Error"; /* No comment provided by engineer. */ @@ -2463,6 +2571,9 @@ chat item action */ /* alert title */ "Error adding relay" = "Error al añadir el servidor"; +/* alert title */ +"Error adding relays" = "Error al añadir servidores"; + /* alert title */ "Error adding server" = "Error al añadir servidor"; @@ -2541,6 +2652,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Error al eliminar base de datos"; +/* alert title */ +"Error deleting message" = "Error al eliminar el mensaje"; + /* alert title */ "Error deleting old database" = "Error al eliminar base de datos antigua"; @@ -2619,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error saving ICE servers" = "Error al guardar servidores ICE"; +/* alert title */ +"Error saving name" = "Error al guardar el nombre"; + /* No comment provided by engineer. */ "Error saving passcode" = "Error al guardar código de acceso"; @@ -2652,6 +2769,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "¡Error al configurar confirmaciones de entrega!"; +/* alert title */ +"Error sharing address" = "Error compartiendo dirección"; + /* alert title */ "Error sharing channel" = "Error al compartir el canal"; @@ -2701,6 +2821,7 @@ chat item action */ "error: %@" = "error: %@"; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Error: %@"; @@ -2793,6 +2914,12 @@ server test error */ /* file error text */ "File server error: %@" = "Error del servidor de archivos: %@"; +/* No comment provided by engineer. */ +"File servers" = "Servidores de archivos"; + +/* copied message info */ +"File servers: %@" = "Servidores de archivos: %@"; + /* No comment provided by engineer. */ "File status" = "Estado del archivo"; @@ -2978,6 +3105,9 @@ servers warning */ /* No comment provided by engineer. */ "Get notified when mentioned." = "Las menciones ahora se notifican."; +/* No comment provided by engineer. */ +"Get SimpleX name (BETA)" = "Obtener nombre SimpleX (BETA)"; + /* No comment provided by engineer. */ "Get started" = "Empezar"; @@ -3053,6 +3183,9 @@ servers warning */ /* alert message */ "Group profile was changed. If you save it, the updated profile will be sent to group members." = "El perfil del grupo ha cambiado. Si lo guardas, el perfil actualizado se enviará a los miembros del grupo."; +/* No comment provided by engineer. */ +"Group webpage" = "Web del grupo"; + /* No comment provided by engineer. */ "Group welcome message" = "Mensaje de bienvenida en grupos"; @@ -3068,6 +3201,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "Ayuda"; +/* No comment provided by engineer. */ +"Help & support" = "Ayuda y asistencia"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "Ayuda a los admins a moderar sus grupos."; @@ -3119,12 +3255,18 @@ servers warning */ /* No comment provided by engineer. */ "How to" = "Cómo"; +/* No comment provided by engineer. */ +"How to register a test name" = "Cómo registrar un nombre de prueba"; + /* No comment provided by engineer. */ "How to use it" = "Guía de uso"; /* No comment provided by engineer. */ "How to use your servers" = "Cómo usar los servidores"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "Interfaz en húngaro"; @@ -3404,6 +3546,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Se mostrará a los suscriptores y se usará para permitir la carga de la vista previa."; + /* No comment provided by engineer. */ "Italian interface" = "Interfaz en italiano"; @@ -3422,6 +3567,9 @@ servers warning */ /* No comment provided by engineer. */ "Join channel" = "Unirme al canal"; +/* new chat action */ +"Join channel %@" = "Unirme al canal %@"; + /* new chat sheet title */ "Join group" = "Unirme al grupo"; @@ -3464,7 +3612,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "¡Archivo grande!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Más información"; /* swipe action */ @@ -3494,6 +3642,12 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Menos tráfico en redes móviles."; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "Permitir el contacto mediante tu nombre registrado contra tu dirección SimpleX."; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "Permitir unirse al canal mediante el nombre registrado con este enlace."; + /* No comment provided by engineer. */ "Let someone connect to you" = "Conecta con alguien"; @@ -3566,6 +3720,9 @@ servers warning */ /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas."; +/* No comment provided by engineer. */ +"Manage your relays." = "Gestiona tus servidores."; + /* No comment provided by engineer. */ "Mark deleted for everyone" = "Marcar como eliminado para todos"; @@ -3623,15 +3780,6 @@ servers warning */ /* chat feature */ "Member reports" = "Informes de miembros"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\". Se notificará en el chat."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "El miembro será eliminado del chat. ¡No puede deshacerse!"; @@ -3672,7 +3820,7 @@ servers warning */ "Mention members 👋" = "Menciona a miembros 👋"; /* No comment provided by engineer. */ -"Menus" = "Menus"; +"Menus" = "Menús"; /* No comment provided by engineer. */ "message" = "mensaje"; @@ -3725,6 +3873,12 @@ servers warning */ /* No comment provided by engineer. */ "Message shape" = "Forma del mensaje"; +/* No comment provided by engineer. */ +"Message signing is not required." = "Los mensajes firmados no son obligatorios."; + +/* No comment provided by engineer. */ +"Message signing is required." = "Los mensajes firmados son obligatorios."; + /* No comment provided by engineer. */ "Message source remains private." = "El autor del mensaje se mantiene privado."; @@ -3845,6 +3999,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "¡Pronto habrá más mejoras!"; +/* No comment provided by engineer. */ +"More privacy" = "Más privacidad"; + /* No comment provided by engineer. */ "More reliable network connection." = "Conexión de red más fiable."; @@ -3869,6 +4026,9 @@ servers warning */ /* swipe action */ "Name" = "Nombre"; +/* No comment provided by engineer. */ +"Name not found" = "Nombre no encontrado"; + /* No comment provided by engineer. */ "Network & servers" = "Servidores y Redes"; @@ -3930,7 +4090,7 @@ servers warning */ "New contact:" = "Contacto nuevo:"; /* No comment provided by engineer. */ -"New desktop app!" = "Nueva aplicación para PC!"; +"New desktop app!" = "¡Nueva aplicación de escritorio!"; /* No comment provided by engineer. */ "New display name" = "Nuevo nombre mostrado"; @@ -3989,6 +4149,9 @@ servers warning */ /* Authentication unavailable */ "No app password" = "Sin contraseña de la aplicación"; +/* No comment provided by engineer. */ +"No available relays" = "Sin servidores disponibles"; + /* No comment provided by engineer. */ "No chat relays" = "Sin servidores de chat"; @@ -4067,6 +4230,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "Sin archivos recibidos o enviados"; +/* No comment provided by engineer. */ +"No relays" = "Sin servidores"; + /* servers error */ "No servers for private message routing." = "Sin servidores para enrutamiento privado."; @@ -4076,6 +4242,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "Sin servidores para recibir mensajes."; +/* servers warning */ +"No servers to resolve names." = "Sin servidores para resolver nombres."; + /* servers error */ "No servers to send files." = "Sin servidores para enviar archivos."; @@ -4091,12 +4260,18 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "Ningún chat sin leer"; +/* No comment provided by engineer. */ +"No valid link" = "Ningún enlace válido"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nadie monitorizaba tus conversaciones. Nadie registraba tus ubicaciones. La privacidad nunca fue un lujo, era la manera de vivir."; /* No comment provided by engineer. */ "Non-profit governance" = "Gobernanza no lucrativa"; +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "No tienes servidores configurados para resolver nombres SimpleX. Hazlo, o usa un enlace para conectarte."; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "No un candado mejorado en la puerta de otro. No un terrateniente que respeta tu privacidad pero sigue guardando un registro de tus visitantes. Tu no eres el invitado. Estás en tu casa y ningún rey podrá entrar. Tu eres el soberano."; @@ -4187,7 +4362,7 @@ new chat action */ "Onion hosts will not be used." = "No se usarán hosts .onion."; /* No comment provided by engineer. */ -"Only channel owners can change channel preferences." = "Sólo los propietarios pueden modificar las preferencias de los canales."; +"Only channel owners can change channel preferences." = "Sólo los propietarios pueden modificar las preferencias del canal."; /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Sólo los propietarios del chat pueden cambiar las preferencias."; @@ -4249,6 +4424,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Sólo tu contacto puede enviar mensajes de voz."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Solo la página superior puede mostrar la vista previa."; + /* alert action alert button */ "Open" = "Abrir"; @@ -4371,7 +4549,7 @@ alert button */ "owners" = "propietarios"; /* No comment provided by engineer. */ -"Owners" = "Propietarios"; +"Owners & contributors" = "Propietarios y colaboradores"; /* No comment provided by engineer. */ "Ownership: you can run your own relays." = "En propiedad: puedes poner en marcha tus propios servidores."; @@ -4532,9 +4710,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Servidores conectados previamente"; -/* No comment provided by engineer. */ -"Privacy & security" = "Seguridad y Privacidad"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Privacidad para tus clientes."; @@ -4586,7 +4761,8 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Tema del perfil"; -/* alert message */ +/* alert message +alert title */ "Profile update will be sent to your SimpleX contacts." = "La actualización del perfil se enviará a tus contactos SimpleX."; /* No comment provided by engineer. */ @@ -4658,6 +4834,9 @@ alert button */ /* No comment provided by engineer. */ "Public channels - speak freely 🚀" = "Canales públicos - habla con libertad 🚀"; +/* No comment provided by engineer. */ +"Public names for your channel or business." = "Nombres públicos para tu canal o negocio."; + /* No comment provided by engineer. */ "Push notifications" = "Notificaciones push"; @@ -4682,7 +4861,7 @@ alert button */ /* swipe action */ "Read" = "Leer"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Saber más"; /* No comment provided by engineer. */ @@ -4761,7 +4940,7 @@ alert button */ "Reconnect servers?" = "¿Reconectar servidores?"; /* No comment provided by engineer. */ -"Record updated at" = "Registro actualiz."; +"Record updated at" = "Registro actualizado a las"; /* copied message info */ "Record updated at: %@" = "Registro actualiz: %@"; @@ -4825,6 +5004,12 @@ swipe action */ /* No comment provided by engineer. */ "Relay test failed!" = "¡El test del servidor ha fallado!"; +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "El servidor será eliminado del canal. ¡No puede deshacerse!"; + +/* alert message */ +"Relays added: %@." = "Servidores añadidos: %@."; + /* No comment provided by engineer. */ "Reliability: many relays per channel." = "Fiabilidad: muchos servidores por canal."; @@ -4849,11 +5034,20 @@ swipe action */ /* alert title */ "Remove member?" = "¿Expulsar miembro?"; +/* No comment provided by engineer. */ +"Remove name" = "Eliminar nombre"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "¿Eliminar contraseña de Keychain?"; +/* No comment provided by engineer. */ +"Remove relay" = "Quitar servidor"; + /* alert title */ -"Remove subscriber?" = "¿Eliminar suscriptor?"; +"Remove relay?" = "¿Eliminar el servidor?"; + +/* alert title */ +"Remove subscriber?" = "¿Eliminar el suscriptor?"; /* No comment provided by engineer. */ "removed" = "expulsado"; @@ -4951,6 +5145,9 @@ swipe action */ /* chat list item title */ "requested to connect" = "solicitado para conectar"; +/* No comment provided by engineer. */ +"Require signing messages." = "Requerir la firma de mensajes."; + /* No comment provided by engineer. */ "Required" = "Obligatorio"; @@ -4978,6 +5175,9 @@ swipe action */ /* No comment provided by engineer. */ "Reset to user theme" = "Restablecer al tema del usuario"; +/* No comment provided by engineer. */ +"Resolver error: %@" = "Error de resolución: %@"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Reinicia la aplicación para crear un perfil nuevo"; @@ -5032,6 +5232,18 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rol"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará en el chat."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará en el grupo."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "El rol cambiará a \"%@\" y se notificará a los suscriptores."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "El rol del miembro cambiará a \"%@\" y recibirá una invitación nueva."; + /* No comment provided by engineer. */ "Run chat" = "Ejecutar SimpleX"; @@ -5044,7 +5256,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Guardar"; @@ -5066,6 +5279,9 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Guardar y notificar grupo"; +/* No comment provided by engineer. */ +"Save and notify members" = "Guardar e informar miembros"; + /* No comment provided by engineer. */ "Save and notify subscribers" = "Guardar y notificar suscriptores"; @@ -5108,6 +5324,12 @@ chat item action */ /* alert title */ "Save servers?" = "¿Guardar servidores?"; +/* alert title */ +"Save SimpleX name?" = "¿Guardar el nombre SimpleX?"; + +/* alert title */ +"Save webpage settings?" = "¿Guardar la configuración web?"; + /* No comment provided by engineer. */ "Save welcome message?" = "¿Guardar mensaje de bienvenida?"; @@ -5298,10 +5520,10 @@ chat item action */ "Send them from gallery or custom keyboards." = "Envíalos desde la galería o desde teclados personalizados."; /* No comment provided by engineer. */ -"Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos."; +"Send up to 100 last messages to new members." = "Se envían los 100 últimos mensajes a los miembros nuevos."; /* No comment provided by engineer. */ -"Send up to 100 last messages to new subscribers." = "Se envían hasta 100 mensajes más recientes a los suscriptores nuevos."; +"Send up to 100 last messages to new subscribers." = "Se envían los 100 últimos mensajes a los suscriptores nuevos."; /* No comment provided by engineer. */ "Send your private feedback to groups." = "Envía tu comentario privado a los grupos."; @@ -5309,9 +5531,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "El remitente puede haber eliminado la solicitud de conexión."; - /* alert message */ "Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Enviar una previsualización del enlace puede revelar tu dirección IP al sitio web. Puedes cambiarlo más tarde en los ajustes de privacidad."; @@ -5369,6 +5588,9 @@ chat item action */ /* No comment provided by engineer. */ "Server" = "Servidor"; +/* No comment provided by engineer. */ +"Server %@ does not support name resolution. Configure servers, or use a connection link." = "El servidor %@ no admite la resolución de nombres. Configura un servidor, o usa un enlace para conectarte."; + /* alert message */ "Server added to operator %@." = "Servidor añadido al operador %@."; @@ -5565,6 +5787,9 @@ chat item action */ /* No comment provided by engineer. */ "Show developer options" = "Mostrar opciones de desarrollador"; +/* No comment provided by engineer. */ +"Show encryption" = "Mostrar cifrado"; + /* No comment provided by engineer. */ "Show last messages" = "Mostrar último mensaje"; @@ -5583,6 +5808,25 @@ chat item action */ /* No comment provided by engineer. */ "Show:" = "Muestra:"; +/* No comment provided by engineer. */ +"Sign message" = "Firmar mensaje"; + +/* chat feature */ +"Sign messages" = "Firmar mensajes"; + +/* alert title +copied message info */ +"Signature missing" = "Falta la firma"; + +/* copied message info */ +"Signed" = "Firmado"; + +/* copied message info */ +"Signed & verified" = "Firmado y verificado"; + +/* No comment provided by engineer. */ +"Signing proves you authored this message and can't be denied later." = "La firma prueba que eres el autor del mensaje sin posibilidad de repudio."; + /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -5640,12 +5884,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "Bloqueo SimpleX activado"; +/* No comment provided by engineer. */ +"SimpleX name" = "Nombre SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "Error del nombre SimpleX"; + +/* alert title */ +"SimpleX name not verified" = "Nombre SimpleX no verificado"; + /* simplex link type */ "SimpleX one-time invitation" = "Invitación SimpleX de un uso"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolos de SimpleX auditados por Trail of Bits."; +/* No comment provided by engineer. */ +"SimpleX public names (BETA)" = "Nombres públicos SimpleX (BETA)"; + /* simplex link type */ "SimpleX relay address" = "Dirección de servidor SimpleX"; @@ -5722,6 +5978,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "Estadísticas"; +/* No comment provided by engineer. */ +"Status" = "Estado"; + /* No comment provided by engineer. */ "Stop" = "Parar"; @@ -5770,6 +6029,9 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Suscritas"; +/* member role */ +"subscriber" = "suscriptor"; + /* No comment provided by engineer. */ "Subscriber" = "Suscriptor"; @@ -5789,25 +6051,25 @@ report reason */ "Subscribers can chat with admins." = "Los suscriptores pueden chatear con los administradores."; /* No comment provided by engineer. */ -"Subscribers can irreversibly delete sent messages. (24 hours)" = "Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas)"; +"Subscribers can irreversibly delete sent messages. (24 hours)" = "Los suscriptores pueden eliminar mensajes de forma irreversible. (24 horas)"; /* No comment provided by engineer. */ "Subscribers can report messsages to moderators." = "Los suscriptores pueden informar de mensajes a los moderadores."; /* No comment provided by engineer. */ -"Subscribers can send direct messages." = "Los suscriptores del canal pueden enviar mensajes directos."; +"Subscribers can send direct messages." = "Los suscriptores pueden enviar mensajes directos."; /* No comment provided by engineer. */ -"Subscribers can send disappearing messages." = "Los suscriptores del canal pueden enviar mensajes temporales."; +"Subscribers can send disappearing messages." = "Los suscriptores pueden enviar mensajes temporales."; /* No comment provided by engineer. */ -"Subscribers can send files and media." = "Los suscriptores del canal pueden enviar archivos y multimedia."; +"Subscribers can send files and media." = "Los suscriptores pueden enviar archivos y multimedia."; /* No comment provided by engineer. */ -"Subscribers can send SimpleX links." = "Los suscriptores del canal pueden enviar enlaces SimpleX."; +"Subscribers can send SimpleX links." = "Los suscriptores pueden enviar enlaces SimpleX."; /* No comment provided by engineer. */ -"Subscribers can send voice messages." = "Los suscriptores del canal pueden enviar mensajes de voz."; +"Subscribers can send voice messages." = "Los suscriptores pueden enviar mensajes de voz."; /* No comment provided by engineer. */ "Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "Los suscriptores usan el enlace del servidor para conectarse a los canales.\nLa dirección del servidor se usó para establecer el servidor para el canal."; @@ -5819,7 +6081,7 @@ report reason */ "Subscriptions ignored" = "Suscripciones ignoradas"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "Soporte SimpleX Chat"; +"Support the project" = "Apoya el proyecto"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "Intercambia audio y video durante la llamada."; @@ -5951,6 +6213,12 @@ server test failure */ /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "El intento de cambiar la contraseña de la base de datos no se ha completado."; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "La insignia está firmada con una clave que esta versión de la app no reconoce. Actualiza la app para verificar la insignia."; + +/* alert message */ +"The channel required this message to be signed, but the signature is missing." = "El canal requiere que el mensaje esté firmado, pero falta la firma."; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "El código QR escaneado no es un enlace de SimpleX."; @@ -6011,6 +6279,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "¡El doble check que nos faltaba! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "El remitente puede haber eliminado la solicitud de conexión."; + /* alert message */ "The sender will NOT be notified" = "El remitente NO será notificado"; @@ -6020,6 +6291,18 @@ server test failure */ /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "Servidores para enviar archivos en tu perfil **%@**."; +/* alert message */ +"The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." = "El nombre SimpleX @%@ está registrado sin una dirección SimpleX. Añádela en la página de registro."; + +/* alert message */ +"The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." = "El nombre SimpleX #%@ está registrado sin un enlace de canal. Añádelo en la página de registro."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but it has no valid link." = "El nombre SimpleX %@ está registrado, pero no tiene un enlace válido."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." = "El nombre SimpleX %@ está registrado, pero no se ha añadido a un perfil. Por favor, si eres el propietario, añádelo a tu dirección o al perfil del canal."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace de SimpleX."; @@ -6056,6 +6339,9 @@ server test failure */ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente."; +/* badge alert */ +"This badge could not be verified and may not be genuine." = "No se ha podido verificar la insignia, podría no ser auténtica."; + /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Este chat está protegido por cifrado de extremo a extremo."; @@ -6077,9 +6363,16 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "Este grupo ya no existe."; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "Este grupo requiere una versión más reciente de la app. Por favor, actualizala para unirte."; + /* alert message */ "This is a chat relay address, it cannot be used to connect." = "Esto es una dirección de servidor, no puede usarse para conectar."; +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "Este es el último servidor activo. Si lo eliminas, se impedirá la entrega de mensajes a los suscriptores."; + /* new chat action */ "This is your link for channel %@!" = "Este es tu enlace para el canal %@!"; @@ -6098,6 +6391,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting is for your current profile **%@**." = "Esta configuración se aplica al perfil actual **%@**."; +/* No comment provided by engineer. */ +"This SimpleX name is not registered. Please check the name." = "El nombre SimpleX no está registrado. Por favor, comprueba el nombre."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Mensajes temporales activados sólo para los contactos nuevos."; @@ -6146,6 +6442,9 @@ server test failure */ /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Para grabar el mensaje de voz concede permiso para usar el micrófono."; +/* No comment provided by engineer. */ +"To resolve names" = "Para resolver nombres"; + /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**."; @@ -6167,6 +6466,9 @@ server test failure */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos."; +/* No comment provided by engineer. */ +"To verify keys with this subscriber, compare (or scan) the code on your devices." = "Para verificar las claves con este suscriptor, compara (o escanea) el código en ambos dispositivos."; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Activa incógnito al conectar."; @@ -6224,6 +6526,9 @@ server test failure */ /* rcv group event chat item */ "unblocked %@" = "ha desbloqueado a %@"; +/* No comment provided by engineer. */ +"Unconfirmed name" = "Nombre sin confirmar"; + /* No comment provided by engineer. */ "Undelivered messages" = "Mensajes no entregados"; @@ -6269,9 +6574,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red."; - /* No comment provided by engineer. */ "Unlink" = "Desenlazar"; @@ -6296,6 +6598,9 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "Enlace de conexión no compatible"; +/* badge alert title */ +"Unverified badge" = "Insignia sin verificar"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Hasta 100 últimos mensajes son enviados a los miembros nuevos."; @@ -6335,7 +6640,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Actualizar dirección"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "¿Actualizar la dirección?"; /* No comment provided by engineer. */ @@ -6443,6 +6749,9 @@ server test failure */ /* No comment provided by engineer. */ "Use web port" = "Usar puerto web"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "Los servidores usados no admiten páginas web."; + /* No comment provided by engineer. */ "User selection" = "Selección de usuarios"; @@ -6455,9 +6764,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* relay test step */ "Verify" = "Verificar"; @@ -6476,12 +6782,18 @@ server test failure */ /* No comment provided by engineer. */ "Verify database passphrase" = "Verificar la contraseña de la base de datos"; +/* No comment provided by engineer. */ +"Verify name" = "Verificar nombre"; + /* No comment provided by engineer. */ "Verify passphrase" = "Verificar frase de contraseña"; /* No comment provided by engineer. */ "Verify security code" = "Comprobar código de seguridad"; +/* No comment provided by engineer. */ +"Verify SimpleX names" = "Verificar nombres SimpleX"; + /* relay hostname */ "via %@" = "mediante %@"; @@ -6516,7 +6828,7 @@ server test failure */ "Video will be received when your contact completes uploading it." = "El video se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde."; +"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando tu contacto esté conectado. ¡Por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ "Videos" = "Vídeos"; @@ -6599,6 +6911,12 @@ server test failure */ /* No comment provided by engineer. */ "We made connecting simpler for new users." = "Hemos simplificado la conexión para los usuarios nuevos."; +/* No comment provided by engineer. */ +"Webpage code" = "Código web"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "Se han modificado los ajustes de la página web. Si guardas los cambios, los nuevos ajustes se enviarán a los suscriptores."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "Servidores WebRTC ICE"; @@ -6759,7 +7077,7 @@ server test failure */ "You can enable later via Settings" = "Puedes activar más tarde en Configuración"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; +"You can enable them later via app Your privacy settings." = "Puedes habilitarlos más tarde en el menú privacidad."; /* No comment provided by engineer. */ "You can give another try." = "Puedes intentarlo de nuevo."; @@ -6797,6 +7115,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "Aún puedes ver la conversación con %@ en la lista de chats."; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "Puedes apoyar SimpleX desde la versión 7."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puedes activar el Bloqueo SimpleX a través de Configuración."; @@ -6864,7 +7185,7 @@ server test failure */ "You need to allow your contact to call to be able to call them." = "Debes permitir que tus contacto te llamen para poder llamarles."; /* No comment provided by engineer. */ -"You need to allow your contact to send voice messages to be able to send them." = "Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos."; +"You need to allow your contact to send voice messages to be able to send them." = "Para poder enviar mensajes de voz, antes debes permitir que tu contacto pueda enviarlos."; /* No comment provided by engineer. */ "You rejected group invitation" = "Has rechazado la invitación del grupo"; @@ -6894,16 +7215,16 @@ server test failure */ "You will be able to send messages **only after your request is accepted**." = "Podrás enviar mensajes **después de que tu solicitud sea aceptada**."; /* No comment provided by engineer. */ -"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde."; +"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del administrador del grupo esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ -"You will be connected when group link host's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde."; +"You will be connected when group link host's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del anfitrión del enlace de grupo esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ -"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde."; +"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando se acepte tu solicitud de conexión. ¡Por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ -"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde."; +"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo de tu contacto esté conectado; ¡por favor, espera o vuelve a intentarlo más tarde!"; /* No comment provided by engineer. */ "You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano."; @@ -6941,9 +7262,6 @@ server test failure */ /* No comment provided by engineer. */ "Your channel" = "Tu canal"; -/* No comment provided by engineer. */ -"Your chat database" = "Base de datos"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "La base de datos no está cifrada - establece una contraseña para cifrarla."; @@ -6962,6 +7280,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Mi contacto"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "El contacto ha enviado un archivo mayor al máximo admitido (%@)."; @@ -6992,6 +7313,9 @@ server test failure */ /* No comment provided by engineer. */ "Your network" = "Tu red"; +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "Tu canal %1$@ está conectado a %2$d de %3$d servidores.\nSi cancelas, el canal se eliminará. Puedes volver a crearlo."; + /* No comment provided by engineer. */ "Your preferences" = "Mis preferencias"; @@ -7040,3 +7364,6 @@ server test failure */ /* No comment provided by engineer. */ "Your SimpleX address" = "Mi dirección SimpleX"; +/* No comment provided by engineer. */ +"Your SimpleX name" = "Mi nombre SimpleX"; + diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 3b1bd6523c..dfe1b6479d 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -488,9 +488,6 @@ swipe action */ /* call status */ "calling…" = "soittaa…"; -/* No comment provided by engineer. */ -"Calls" = "Puhelut"; - /* No comment provided by engineer. */ "Can't invite contact!" = "Kontaktia ei voi kutsua!"; @@ -520,9 +517,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Vaihda lukitustilaa"; -/* No comment provided by engineer. */ -"Change member role?" = "Vaihda jäsenroolia?"; - /* authentication reason */ "Change passcode" = "Vaihda pääsykoodi"; @@ -687,12 +681,12 @@ server test step */ /* alert title */ "Connection error" = "Yhteysvirhe"; -/* conn error description */ -"Connection error (AUTH)" = "Yhteysvirhe (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "yhteys luotu"; +/* conn error description */ +"Connection link removed" = "Yhteysvirhe"; + /* No comment provided by engineer. */ "Connection request sent!" = "Yhteyspyyntö lähetetty!"; @@ -979,10 +973,7 @@ alert button */ "Description" = "Kuvaus"; /* No comment provided by engineer. */ -"Develop" = "Kehitä"; - -/* No comment provided by engineer. */ -"Developer tools" = "Kehittäjätyökalut"; +"Developer" = "Kehittäjätyökalut"; /* No comment provided by engineer. */ "Device" = "Laite"; @@ -1209,7 +1200,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "virhe"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Virhe"; /* No comment provided by engineer. */ @@ -1342,6 +1333,7 @@ alert button */ "Error: " = "Virhe: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Virhe: %@"; @@ -1764,7 +1756,7 @@ server test error */ /* No comment provided by engineer. */ "Large file!" = "Suuri tiedosto!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Lue lisää"; /* swipe action */ @@ -1845,12 +1837,6 @@ server test error */ /* rcv group event chat item */ "member connected" = "yhdistetty"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Jäsenen rooli muuttuu muotoon \"%@\". Kaikille ryhmän jäsenille ilmoitetaan asiasta."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Jäsenen rooli muutetaan muotoon \"%@\". Jäsen saa uuden kutsun."; - /* alert message */ "Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; @@ -2231,9 +2217,6 @@ new chat action */ /* No comment provided by engineer. */ "Preview" = "Esikatselu"; -/* No comment provided by engineer. */ -"Privacy & security" = "Yksityisyys ja turvallisuus"; - /* No comment provided by engineer. */ "Private filenames" = "Yksityiset tiedostonimet"; @@ -2294,7 +2277,7 @@ new chat action */ /* swipe action */ "Read" = "Lue"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Lue lisää"; /* No comment provided by engineer. */ @@ -2446,10 +2429,17 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rooli"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Jäsenen rooli muuttuu muotoon \"%@\". Kaikille ryhmän jäsenille ilmoitetaan asiasta."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Jäsenen rooli muutetaan muotoon \"%@\". Jäsen saa uuden kutsun."; + /* No comment provided by engineer. */ "Run chat" = "Käynnistä chat"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Tallenna"; @@ -2576,9 +2566,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Lähettäjä peruutti tiedoston siirron."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Lähettäjä on saattanut poistaa yhteyspyynnön."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa."; @@ -2778,9 +2765,6 @@ chat item action */ /* No comment provided by engineer. */ "Submit" = "Lähetä"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "SimpleX Chat tuki"; - /* No comment provided by engineer. */ "System" = "Järjestelmä"; @@ -2872,6 +2856,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Lähettäjä on saattanut poistaa yhteyspyynnön."; + /* alert message */ "The sender will NOT be notified" = "Lähettäjälle EI ilmoiteta"; @@ -2980,9 +2967,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä.\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa."; - /* No comment provided by engineer. */ "Unlock" = "Avaa"; @@ -3040,9 +3024,6 @@ server test failure */ /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Käyttää SimpleX Chat -palvelimia."; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify connection security" = "Tarkista yhteyden suojaus"; @@ -3178,9 +3159,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Voit ottaa käyttöön myöhemmin asetusten kautta"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista."; - /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; @@ -3292,15 +3270,15 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Puhelusi"; -/* No comment provided by engineer. */ -"Your chat database" = "Keskustelut-tietokantasi"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi."; /* No comment provided by engineer. */ "Your chat profiles" = "Keskusteluprofiilisi"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä.\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@)."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 91cd6f3078..0952cacb90 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- une diffusion plus stable des messages.\n- des groupes un peu plus performants.\n- et bien d'autres choses encore !"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- choisir d'envoyer des aperçus de lien.\n- empêcher l'hameçonnage par hyperlien.\n- retirer le traçage par liens."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- option pour notifier les contacts supprimés.\n- noms de profil avec espaces.\n- et plus encore !"; @@ -19,6 +22,9 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 coloré!"; +/* chat link info line */ +"(from owner)" = "(du propriétaire)"; + /* No comment provided by engineer. */ "(new)" = "(nouveau)"; @@ -26,7 +32,7 @@ "(this device v%@)" = "(cet appareil v%@)"; /* No comment provided by engineer. */ -"[Send us email](mailto:chat@simplex.chat)" = "[Contact par mail](mailto:chat@simplex.chat)"; +"[Send us email](mailto:chat@simplex.chat)" = "[Envoyez-nous un courriel](mailto:chat@simplex.chat)"; /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Ajouter un contact** : pour créer un nouveau lien d'invitation."; @@ -44,7 +50,7 @@ "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." = "**Vie privée** : vérification de nouveaux messages toute les 20 minutes. Le token de l'appareil est partagé avec le serveur SimpleX, mais pas le nombre de messages ou de contacts."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages periodiquement en arrière plan (dépend de l'utilisation de l'app)."; +"**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." = "**Confidentiel** : ne pas utiliser le serveur de notifications SimpleX, vérification de nouveaux messages périodiquement en arrière plan (dépend de l'utilisation de l'appli)."; /* No comment provided by engineer. */ "**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Remarque** : l'utilisation de la même base de données sur deux appareils interrompt le déchiffrement des messages provenant de vos connexions, par mesure de sécurité."; @@ -58,6 +64,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Scanner / Coller** : pour vous connecter via un lien que vous avez reçu."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Tester le relais** pour récupérer son nom."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Avertissement** : les notifications push instantanées nécessitent une phrase secrète enregistrée dans la keychain."; @@ -109,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ téléchargé"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ a investi dans le financement participatif de SimpleX Chat."; + /* notification title */ "%@ is connected!" = "%@ est connecté·e !"; @@ -124,11 +136,14 @@ /* No comment provided by engineer. */ "%@ servers" = "Serveurs %@"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ soutient SimpleX Chat."; + /* No comment provided by engineer. */ -"%@ uploaded" = "%@ envoyé"; +"%@ uploaded" = "%@ téléversé"; /* notification title */ -"%@ wants to connect!" = "%@ veut se connecter !"; +"%@ wants to connect!" = "%@ veut se connecter !"; /* format for date separator in chat */ "%@, %@" = "%1$@, %2$@"; @@ -142,6 +157,9 @@ /* copied message info */ "%@:" = "%@ :"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ a soutenu SimpleX Chat. Le badge a expiré le %2$@."; + /* time interval */ "%d days" = "%d jours"; @@ -169,8 +187,16 @@ /* time interval */ "%d months" = "%d mois"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d relais inactifs"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d relais supprimé(s)"; + /* time interval */ -"%d sec" = "%d sec"; +"%d sec" = "%d s"; /* delete after time */ "%d seconds(s)" = "%d seconde(s)"; @@ -178,15 +204,37 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d message·s sauté·s"; +/* channel subscriber count */ +"%d subscriber" = "%d abonné·e"; + +/* channel subscriber count */ +"%d subscribers" = "%d abonné·es"; + /* time interval */ "%d weeks" = "%d semaines"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d relais actifs"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d relais actifs, %3$d erreurs"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d relais connectés"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d relais connectés, %3$d erreurs"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld évènements du canal"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld contact·s sélectionné·s"; @@ -251,11 +299,14 @@ "`a + b`" = "\\`a + b`"; /* email text */ -"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Bonjour !</p>\n<p><a href=\"%@\">Contactez-moi via SimpleX Chat</a></p>"; +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Bonjour !</p>\n<p><a href=\"%@\">Contactez-moi via SimpleX Chat</a></p>"; /* No comment provided by engineer. */ "~strike~" = "\\~barré~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ Échec de la vérification de la signature : %@."; + /* time to disappear */ "0 sec" = "0 sec"; @@ -301,6 +352,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Encore quelques points"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Un lien pour qu'une personne se connecte"; + /* notification title */ "A new contact" = "Un nouveau contact"; @@ -308,7 +362,7 @@ time interval */ "A new random profile will be shared." = "Un nouveau profil aléatoire sera partagé."; /* No comment provided by engineer. */ -"A separate TCP connection will be used **for each chat profile you have in the app**." = "Une connexion TCP distincte sera utilisée **pour chaque profil de chat que vous avez dans l'application**."; +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Une connexion TCP distincte sera utilisée **pour chaque profil de discussion que vous avez dans l'application**."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Une connexion TCP distincte sera utilisée **pour chaque contact et membre de groupe**.\n**Veuillez noter** : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer."; @@ -365,6 +419,12 @@ swipe action */ /* alert title */ "Accept member" = "Accepter le membre"; +/* No comment provided by engineer. */ +"accepted" = "accepté"; + +/* rcv group event chat item */ +"accepted %@" = "%@ accepté"; + /* call status */ "accepted call" = "appel accepté"; @@ -374,18 +434,30 @@ swipe action */ /* chat list item title */ "accepted invitation" = "invitation acceptée"; +/* rcv group event chat item */ +"accepted you" = "vous a accepté·e"; + /* No comment provided by engineer. */ "Acknowledged" = "Reçu avec accusé de réception"; /* No comment provided by engineer. */ "Acknowledgement errors" = "Erreur d'accusé de réception"; +/* No comment provided by engineer. */ +"active" = "actif"; + /* token status text */ "Active" = "Actif"; /* No comment provided by engineer. */ "Active connections" = "Connections actives"; +/* No comment provided by engineer. */ +"Add" = "Ajouter"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Ajoutez une adresse à votre profil afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts."; + /* No comment provided by engineer. */ "Add friends" = "Ajouter des amis"; @@ -398,6 +470,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Ajouter un profil"; +/* No comment provided by engineer. */ +"Add relay" = "Ajouter un relais"; + +/* No comment provided by engineer. */ +"Add relays" = "Ajouter des relais"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Ajouter un relais pour restaurer la livraison des messages."; + /* No comment provided by engineer. */ "Add server" = "Ajouter un serveur"; @@ -407,6 +488,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Ajouter des membres à l'équipe"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Ajoutez ce code sur votre page Web. Il affichera l'aperçu de votre canal / groupe."; + /* No comment provided by engineer. */ "Add to another device" = "Ajouter à un autre appareil"; @@ -461,6 +545,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Paramètres réseau avancés"; +/* No comment provided by engineer. */ +"Advanced options" = "Options avancées"; + /* No comment provided by engineer. */ "Advanced settings" = "Paramètres avancés"; @@ -480,7 +567,7 @@ swipe action */ "All chats and messages will be deleted - this cannot be undone!" = "Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !"; /* alert message */ -"All chats will be removed from the list %@, and the list deleted." = "Tous les chats seront supprimés de la liste %@, et la liste sera supprimée."; +"All chats will be removed from the list %@, and the list deleted." = "Toutes les discussions seront supprimées de la liste %@ et la liste sera supprimée."; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi."; @@ -494,6 +581,9 @@ swipe action */ /* feature role */ "all members" = "tous les membres"; +/* No comment provided by engineer. */ +"All messages" = "Tous les messages"; + /* No comment provided by engineer. */ "All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Tous les messages et fichiers sont envoyés **chiffrés de bout en bout**, avec une sécurité post-quantique dans les messages directs."; @@ -509,6 +599,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Tous les profiles"; +/* No comment provided by engineer. */ +"All relays failed" = "Tous les relais échoués"; + +/* No comment provided by engineer. */ +"All relays removed" = "Tous les relais retirés"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Tous les rapports seront archivés pour vous."; @@ -527,6 +623,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Autoriser"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Autoriser n'importe qui à incorporer"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Autoriser les appels que si votre contact les autorise."; @@ -545,6 +644,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Autoriser les membres à discuter avec les administrateurs."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Autoriser les réactions aux messages uniquement si votre contact les autorise."; @@ -554,12 +656,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Autoriser l'envoi de messages directs aux membres."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Autoriser l'envoi de messages directs aux abonné·es."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Autorise l’envoi de messages éphémères."; /* No comment provided by engineer. */ "Allow sharing" = "Autoriser le partage"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Autoriser tous les abonné·es à discuter avec les admins."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Autoriser la suppression irréversible de messages envoyés. (24 heures)"; @@ -618,7 +726,7 @@ swipe action */ "Always use relay" = "Se connecter via relais"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Un profil de chat vierge portant le nom fourni est créé et l'application s'ouvre normalement."; +"An empty chat profile with the provided name is created, and the app opens as usual." = "Un profil de discussion vierge portant le nom fourni est créé et l'application s'ouvre normalement."; /* No comment provided by engineer. */ "and %lld other events" = "et %lld autres événements"; @@ -629,6 +737,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Répondre à l'appel"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "N'importe quelle page Web peut afficher l'aperçu."; + /* No comment provided by engineer. */ "App build: %@" = "Build de l'app : %@"; @@ -638,6 +749,9 @@ swipe action */ /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "L'application chiffre les nouveaux fichiers locaux (sauf les vidéos)."; +/* No comment provided by engineer. */ +"App group:" = "Groupe de l'appli :"; + /* No comment provided by engineer. */ "App icon" = "Icône de l'app"; @@ -650,6 +764,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "Session de l'app"; +/* alert title */ +"App update required" = "Mise à jour de l'appli nécessaire"; + /* No comment provided by engineer. */ "App version" = "Version de l'app"; @@ -707,6 +824,9 @@ swipe action */ /* No comment provided by engineer. */ "Audio and video calls" = "Appels audio et vidéo"; +/* No comment provided by engineer. */ +"Audio call" = "Appel audio"; + /* No comment provided by engineer. */ "audio call (not e2e encrypted)" = "appel audio (sans chiffrement)"; @@ -761,6 +881,12 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Mauvais ID de message"; +/* badge alert title */ +"Badge cannot be verified" = "Le badge ne peut pas être vérifié"; + +/* No comment provided by engineer. */ +"Be free in your network." = "Soyez libre dans votre réseau."; + /* No comment provided by engineer. */ "Better calls" = "Appels améliorés"; @@ -791,6 +917,12 @@ swipe action */ /* No comment provided by engineer. */ "Better user experience" = "Une meilleure expérience pour l'utilisateur"; +/* No comment provided by engineer. */ +"Bio" = "Biographie"; + +/* alert title */ +"Bio too large" = "Biographie trop longue"; + /* No comment provided by engineer. */ "Black" = "Noir"; @@ -850,7 +982,10 @@ marked deleted chat item preview text */ "Both you and your contact can send voice messages." = "Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux."; /* No comment provided by engineer. */ -"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !"; +"Bottom bar" = "Barre inférieure"; + +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgare, finnois, thaï et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !"; /* chat link info line */ "Business address" = "Adresse professionnelle"; @@ -858,6 +993,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Business chats" = "Discussions professionnelles"; +/* No comment provided by engineer. */ +"Business connection" = "Connexion pro"; + /* No comment provided by engineer. */ "Businesses" = "Entreprises"; @@ -879,9 +1017,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "appel…"; -/* No comment provided by engineer. */ -"Calls" = "Appels"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Les appels ne sont pas autorisés !"; @@ -894,6 +1029,9 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Can't call member" = "Impossible d'appeler le membre"; +/* alert title */ +"Can't change profile" = "Impossible de changer le profil"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Impossible d'inviter le contact !"; @@ -908,6 +1046,12 @@ alert button new chat action */ "Cancel" = "Annuler"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Annuler et supprimer le canal"; + +/* alert title */ +"Cancel creating channel?" = "Annuler la création du canal ?"; + /* No comment provided by engineer. */ "Cancel migration" = "Annuler le transfert"; @@ -944,9 +1088,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Modifier le mode de verrouillage"; -/* No comment provided by engineer. */ -"Change member role?" = "Changer le rôle du membre ?"; - /* authentication reason */ "Change passcode" = "Modifier le code d'accès"; @@ -981,6 +1122,61 @@ set passcode view */ /* chat item text */ "changing address…" = "changement d'adresse…"; +/* shown as sender role for channel messages */ +"channel" = "canal"; + +/* No comment provided by engineer. */ +"Channel" = "Canal"; + +/* No comment provided by engineer. */ +"Channel display name" = "Nom d'affichage du canal"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "Nom complet du canal (optionnel)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "Le canal n'a aucun relais actif. Veuillez réessayer de vous connecter plus tard."; + +/* No comment provided by engineer. */ +"Channel image" = "Image du canal"; + +/* chat link info line */ +"Channel link" = "Lien du canal"; + +/* No comment provided by engineer. */ +"Channel preferences" = "Préférences du canal"; + +/* No comment provided by engineer. */ +"Channel profile" = "Profil du canal"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "Le profil du canal est stocké sur les périphériques des abonné·es et sur les relais de discussion."; + +/* snd group event chat item */ +"channel profile updated" = "profil du canal mis à jour"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Le profil a été changé. Si vous l'enregistrez, le profil mis à jour sera envoyé aux abonné·es du canal."; + +/* alert title */ +"Channel temporarily unavailable" = "Canal temporairement indisponible"; + +/* No comment provided by engineer. */ +"Channel webpage" = "Page Web du canal"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "Le canal sera supprimé pour tous les abonné·es ; ceci ne peut pas être annulé !"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "Le canal sera supprimé pour vous ; ceci ne peut pas être annulé !"; + +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "Le canal commencera à fonctionner avec %1$d relais sur %2$d. Continuer ?"; + +/* No comment provided by engineer. */ +"Channels" = "Canaux"; + /* No comment provided by engineer. */ "Chat" = "Discussions"; @@ -996,6 +1192,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat console" = "Console du chat"; +/* No comment provided by engineer. */ +"Chat data" = "Données de la discussion"; + /* No comment provided by engineer. */ "Chat database" = "Base de données du chat"; @@ -1015,7 +1214,7 @@ set passcode view */ "Chat is stopped" = "Le chat est arrêté"; /* No comment provided by engineer. */ -"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat."; +"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "La discussion est arrêtée. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer la discussion."; /* No comment provided by engineer. */ "Chat list" = "Liste de discussion"; @@ -1032,6 +1231,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "Profil d'utilisateur"; +/* No comment provided by engineer. */ +"Chat relay" = "Relais de la discussion"; + +/* No comment provided by engineer. */ +"Chat relays" = "Relais de la discussion"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "Les relais de discussion transmettent les messages dans les canaux que vous créez."; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "Les relais de discussion transmettent les messages aux abonné·es du canal."; + /* No comment provided by engineer. */ "Chat theme" = "Thème de chat"; @@ -1041,15 +1252,43 @@ set passcode view */ /* No comment provided by engineer. */ "Chat will be deleted for you - this cannot be undone!" = "Le discussion sera supprimé pour vous - il n'est pas possible de revenir en arrière !"; +/* chat feature +chat toolbar */ +"Chat with admins" = "Discuter avec les admins"; + +/* No comment provided by engineer. */ +"Chat with member" = "Discuter avec un membre"; + +/* No comment provided by engineer. */ +"Chat with members before they join." = "Discuter avec les membres avant qu'ils rejoignent le canal."; + /* No comment provided by engineer. */ "Chats" = "Discussions"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "Les discussions avec les admins sont interdites."; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "Les discussions avec les admins dans les canaux publics n'ont pas de chiffrement E2E ; à utiliser uniquement avec des relais de discussion fiables."; + +/* No comment provided by engineer. */ +"Chats with members" = "Discussions avec les membres"; + +/* No comment provided by engineer. */ +"Chats with members are disabled" = "Les discussions avec les membres sont désactivées"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "Consulter les messages toutes les 20 minutes."; /* No comment provided by engineer. */ "Check messages when allowed." = "Consulter les messages quand c'est possible."; +/* alert message */ +"Check relay address and try again." = "Vérifiez l'adresse du relais et réessayez."; + +/* alert message */ +"Check relay name and try again." = "Vérifiez le nom du relais et réessayez."; + /* alert title */ "Check server address and try again." = "Vérifiez l'adresse du serveur et réessayez."; @@ -1143,6 +1382,9 @@ set passcode view */ /* No comment provided by engineer. */ "Configure ICE servers" = "Configurer les serveurs ICE"; +/* No comment provided by engineer. */ +"Configure relays" = "Configurer les relais"; + /* No comment provided by engineer. */ "Confirm" = "Confirmer"; @@ -1183,6 +1425,9 @@ server test step */ /* No comment provided by engineer. */ "Connect automatically" = "Connexion automatique"; +/* No comment provided by engineer. */ +"Connect faster! 🚀" = "Connectez-vous plus vite ! 🚀"; + /* No comment provided by engineer. */ "Connect to desktop" = "Connexion au bureau"; @@ -1204,6 +1449,9 @@ server test step */ /* new chat sheet title */ "Connect via link" = "Se connecter via un lien"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "Se connecter via un lien ou un code QR"; + /* new chat sheet title */ "Connect via one-time link" = "Se connecter via un lien unique"; @@ -1273,15 +1521,18 @@ server test step */ /* alert title */ "Connection error" = "Erreur de connexion"; -/* conn error description */ -"Connection error (AUTH)" = "Erreur de connexion (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "connexion établie"; +/* No comment provided by engineer. */ +"Connection failed" = "La connexion a échoué"; + /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "La connexion est bloquée par l'opérateur du serveur :\n%@"; +/* conn error description */ +"Connection link removed" = "Erreur de connexion"; + /* No comment provided by engineer. */ "Connection not ready." = "La connexion n'est pas prête."; @@ -1312,18 +1563,30 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Connexions"; +/* No comment provided by engineer. */ +"Contact" = "Contact"; + /* profile update event chat item */ "contact %@ changed to %@" = "le contact %1$@ est devenu %2$@"; +/* chat link info line */ +"Contact address" = "Adresse du contact"; + /* No comment provided by engineer. */ "Contact allows" = "Votre contact autorise"; /* No comment provided by engineer. */ "Contact already exists" = "Contact déjà existant"; +/* No comment provided by engineer. */ +"contact deleted" = "contact supprimé"; + /* No comment provided by engineer. */ "Contact deleted!" = "Contact supprimé !"; +/* No comment provided by engineer. */ +"contact disabled" = "contact désactivé"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Ce contact a le chiffrement de bout en bout"; @@ -1342,9 +1605,15 @@ server test step */ /* No comment provided by engineer. */ "Contact name" = "Nom du contact"; +/* No comment provided by engineer. */ +"contact not ready" = "contact non prêt"; + /* No comment provided by engineer. */ "Contact preferences" = "Préférences de contact"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Demandes de contact des groupes"; + /* 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 !"; @@ -1369,6 +1638,9 @@ server test step */ /* No comment provided by engineer. */ "Copy" = "Copier"; +/* No comment provided by engineer. */ +"Copy code" = "Copier le code"; + /* No comment provided by engineer. */ "Copy error" = "Erreur de copie"; @@ -1408,15 +1680,27 @@ server test step */ /* No comment provided by engineer. */ "Create profile" = "Créer le profil"; +/* No comment provided by engineer. */ +"Create public channel" = "Créer un canal public"; + /* server test step */ "Create queue" = "Créer une file d'attente"; /* No comment provided by engineer. */ "Create SimpleX address" = "Créer une adresse SimpleX"; +/* No comment provided by engineer. */ +"Create your address" = "Créer votre adresse"; + +/* No comment provided by engineer. */ +"Create your link" = "Créer votre lien"; + /* No comment provided by engineer. */ "Create your profile" = "Créez votre profil"; +/* No comment provided by engineer. */ +"Create your public address" = "Créer votre adresse publique"; + /* No comment provided by engineer. */ "Created" = "Créées"; @@ -1429,6 +1713,9 @@ server test step */ /* No comment provided by engineer. */ "Creating archive link" = "Création d'un lien d'archive"; +/* No comment provided by engineer. */ +"Creating channel" = "Création du canal"; + /* No comment provided by engineer. */ "Creating link…" = "Création d'un lien…"; @@ -1531,6 +1818,9 @@ server test step */ /* No comment provided by engineer. */ "Debug delivery" = "Livraison de débogage"; +/* relay test step */ +"Decode link" = "Décoder le lien"; + /* message decrypt error item */ "Decryption error" = "Erreur de déchiffrement"; @@ -1572,6 +1862,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "Supprimer et en informer le contact"; +/* No comment provided by engineer. */ +"Delete channel" = "Supprimer le canal"; + +/* No comment provided by engineer. */ +"Delete channel?" = "Supprimer le canal ?"; + /* No comment provided by engineer. */ "Delete chat" = "Supprimer la discussion"; @@ -1584,6 +1880,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete chat profile?" = "Supprimer le profil du chat ?"; +/* alert title */ +"Delete chat with member?" = "Supprimer la discussion avec le membre ?"; + /* No comment provided by engineer. */ "Delete chat?" = "Supprimer la discussion ?"; @@ -1617,6 +1916,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Supprimer pour moi"; +/* No comment provided by engineer. */ +"Delete from history" = "Supprimer de l'historique"; + /* No comment provided by engineer. */ "Delete group" = "Supprimer le groupe"; @@ -1638,6 +1940,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete member message?" = "Supprimer le message de ce membre ?"; +/* No comment provided by engineer. */ +"Delete member messages" = "Supprimer les messages du membre"; + +/* alert title */ +"Delete member messages?" = "Supprimer les messages des membres ?"; + /* No comment provided by engineer. */ "Delete message?" = "Supprimer le message ?"; @@ -1666,6 +1974,9 @@ alert button */ /* server test step */ "Delete queue" = "Supprimer la file d'attente"; +/* No comment provided by engineer. */ +"Delete relay" = "Supprimer le relais"; + /* No comment provided by engineer. */ "Delete report" = "Supprimer le rapport"; @@ -1711,9 +2022,15 @@ alert button */ /* No comment provided by engineer. */ "Delivery receipts!" = "Justificatifs de réception !"; +/* No comment provided by engineer. */ +"Deprecated options" = "Options obsolètes"; + /* No comment provided by engineer. */ "Description" = "Description"; +/* alert title */ +"Description too large" = "Description trop longue"; + /* No comment provided by engineer. */ "Desktop address" = "Adresse de bureau"; @@ -1724,13 +2041,13 @@ alert button */ "Desktop devices" = "Appareils de bureau"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "L'adresse du serveur de destination %1$@ est incompatible avec les paramètres du serveur de redirection %2$@."; /* snd error text */ "Destination server error: %@" = "Erreur du serveur de destination : %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "La version du serveur de destination %@ est incompatible avec le serveur de redirection %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "La version du serveur de destination %1$@ est incompatible avec le serveur de redirection %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Statistiques détaillées"; @@ -1739,14 +2056,11 @@ alert button */ "Details" = "Détails"; /* No comment provided by engineer. */ -"Develop" = "Développer"; +"Developer" = "Outils du développeur"; /* No comment provided by engineer. */ "Developer options" = "Options pour les développeurs"; -/* No comment provided by engineer. */ -"Developer tools" = "Outils du développeur"; - /* No comment provided by engineer. */ "Device" = "Appareil"; @@ -1774,6 +2088,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "Les messages directs entre membres sont interdits dans ce groupe."; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "Les messages directs entre les abonné·es sont interdits."; + +/* alert button */ +"Disable" = "Désactiver"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "Désactiver (conserver les remplacements)"; @@ -1831,6 +2151,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "Ne pas envoyer d'historique aux nouveaux membres."; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "Ne pas envoyer l'historique à de nouveaux abonné·es."; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; @@ -1910,24 +2233,42 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "Notifications chiffrées E2E."; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "Plus facile d'inviter vos ami·es 👋"; + /* chat item action */ "Edit" = "Modifier"; +/* No comment provided by engineer. */ +"Edit channel profile" = "Modifier le profil du canal"; + /* No comment provided by engineer. */ "Edit group profile" = "Modifier le profil du groupe"; +/* No comment provided by engineer. */ +"Empty message!" = "Message vide !"; + /* alert button */ "Enable" = "Activer"; /* No comment provided by engineer. */ "Enable (keep overrides)" = "Activer (conserver les remplacements)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "Activez au moins un relais de discussion dans Réseaux et serveurs."; + /* alert title */ "Enable automatic message deletion?" = "Activer la suppression automatique des messages ?"; /* No comment provided by engineer. */ "Enable camera access" = "Autoriser l'accès à la caméra"; +/* alert title */ +"Enable chats with admins?" = "Activer les discussions avec les admins ?"; + +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "Activer les messages éphémères par défaut."; + /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Activez Flux dans les paramètres du réseau et des serveurs pour une meilleure confidentialité des métadonnées."; @@ -1940,6 +2281,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "Activer les notifications instantanées ?"; +/* alert title */ +"Enable link previews?" = "Activer les aperçus de lien ?"; + /* No comment provided by engineer. */ "Enable lock" = "Activer le verrouillage"; @@ -2048,6 +2392,9 @@ chat item action */ /* call status */ "ended call %@" = "appel terminé %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "Entrez le nom du canal…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "Entrez la phrase secrète correcte."; @@ -2066,12 +2413,21 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "Entrez ci-dessus le mot de passe pour afficher le profil !"; +/* No comment provided by engineer. */ +"Enter profile name..." = "Entrez le nom du profil…"; + +/* No comment provided by engineer. */ +"Enter relay name…" = "Entrez le nom du relais…"; + /* No comment provided by engineer. */ "Enter server manually" = "Entrer un serveur manuellement"; /* No comment provided by engineer. */ "Enter this device name…" = "Entrez le nom de l'appareil…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "Entrez l'URL de la page Web"; + /* placeholder */ "Enter welcome message…" = "Entrez un message de bienvenue…"; @@ -2084,7 +2440,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "erreur"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Erreur"; /* No comment provided by engineer. */ @@ -2096,15 +2452,30 @@ chat item action */ /* No comment provided by engineer. */ "Error accepting contact request" = "Erreur de validation de la demande de contact"; +/* alert title */ +"Error accepting member" = "Erreur lors de l'acceptation du membre"; + /* No comment provided by engineer. */ "Error adding member(s)" = "Erreur lors de l'ajout de membre·s"; +/* alert title */ +"Error adding relay" = "Erreur lors de l'ajout du relais"; + +/* alert title */ +"Error adding relays" = "Erreur lors de l'ajout des relais"; + /* alert title */ "Error adding server" = "Erreur lors de l'ajout du serveur"; +/* No comment provided by engineer. */ +"Error adding short link" = "Erreur lors de l'ajout d'un lien court"; + /* No comment provided by engineer. */ "Error changing address" = "Erreur de changement d'adresse"; +/* alert title */ +"Error changing chat profile" = "Erreur lors du changement du profil de discussion"; + /* No comment provided by engineer. */ "Error changing connection profile" = "Erreur lors du changement de profil de connexion"; @@ -2123,9 +2494,15 @@ chat item action */ /* alert message */ "Error connecting to forwarding server %@. Please try later." = "Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard."; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "Erreur de connexion au serveur utilisé pour recevoir des messages de cette connexion : %@"; + /* No comment provided by engineer. */ "Error creating address" = "Erreur lors de la création de l'adresse"; +/* alert title */ +"Error creating channel" = "Erreur lors de la création du canal"; + /* No comment provided by engineer. */ "Error creating group" = "Erreur lors de la création du groupe"; @@ -2150,6 +2527,9 @@ chat item action */ /* No comment provided by engineer. */ "Error decrypting file" = "Erreur lors du déchiffrement du fichier"; +/* alert title */ +"Error deleting chat" = "Erreur lors de la suppression de la discussion"; + /* alert title */ "Error deleting chat database" = "Erreur lors de la suppression de la base de données du chat"; @@ -2162,6 +2542,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Erreur lors de la suppression de la base de données"; +/* alert title */ +"Error deleting message" = "Erreur lors de la suppression du message"; + /* alert title */ "Error deleting old database" = "Erreur lors de la suppression de l'ancienne base de données"; @@ -2216,6 +2599,9 @@ chat item action */ /* alert title */ "Error registering for notifications" = "Erreur lors de l'inscription aux notifications"; +/* alert title */ +"Error rejecting contact request" = "Erreur lors du rejet de la demande de contact"; + /* alert title */ "Error removing member" = "Erreur lors de la suppression d'un membre"; @@ -2225,6 +2611,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "Erreur de réinitialisation des statistiques"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "Erreur lors de l'enregistrement du profil du canal"; + /* alert title */ "Error saving chat list" = "Erreur lors de l'enregistrement de la liste des chats"; @@ -2253,7 +2642,7 @@ chat item action */ "Error scanning code: %@" = "Erreur lors du scan du code : %@"; /* No comment provided by engineer. */ -"Error sending email" = "Erreur lors de l'envoi de l'e-mail"; +"Error sending email" = "Erreur lors de l'envoi du courriel"; /* No comment provided by engineer. */ "Error sending member contact invitation" = "Erreur lors de l'envoi de l'invitation de contact d'un membre"; @@ -2261,9 +2650,15 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Erreur lors de l'envoi du message"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Erreur lors de la définition de l'acceptation automatique"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Erreur lors de la configuration des accusés de réception !"; +/* alert title */ +"Error sharing channel" = "Erreur lors du partage du canal"; + /* No comment provided by engineer. */ "Error starting chat" = "Erreur lors du démarrage du chat"; @@ -2298,7 +2693,7 @@ chat item action */ "Error updating user privacy" = "Erreur de mise à jour de la confidentialité de l'utilisateur"; /* No comment provided by engineer. */ -"Error uploading the archive" = "Erreur lors de l'envoi de l'archive"; +"Error uploading the archive" = "Erreur lors du téléversement de l'archive"; /* No comment provided by engineer. */ "Error verifying passphrase:" = "Erreur lors de la vérification de la phrase secrète :"; @@ -2307,10 +2702,15 @@ chat item action */ "Error: " = "Erreur : "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Erreur : %@"; +/* relay test error +server test error */ +"Error: %@." = "Erreur : %@."; + /* No comment provided by engineer. */ "Error: no database file" = "Erreur : pas de fichier de base de données"; @@ -2419,6 +2819,9 @@ snd error text */ /* chat feature */ "Files and media" = "Fichiers et médias"; +/* No comment provided by engineer. */ +"Files and media are prohibited in this chat." = "Les fichiers et médias sont interdits dans cette discussion."; + /* No comment provided by engineer. */ "Files and media are prohibited." = "Les fichiers et les médias sont interdits dans ce groupe."; @@ -2428,6 +2831,9 @@ snd error text */ /* No comment provided by engineer. */ "Files and media prohibited!" = "Fichiers et médias interdits !"; +/* No comment provided by engineer. */ +"Filter" = "Filtre"; + /* No comment provided by engineer. */ "Filter unread and favorite chats." = "Filtrer les messages non lus et favoris."; @@ -2443,6 +2849,9 @@ snd error text */ /* No comment provided by engineer. */ "Find chats faster" = "Recherche de message plus rapide"; +/* No comment provided by engineer. */ +"Fingerprint in destination server address does not match certificate: %@." = "L'empreinte dans l'adresse du serveur de destination ne correspond pas au certificat : %@."; + /* relay test error server test error */ "Fingerprint in server address does not match certificate." = "Il est possible que l'empreinte du certificat dans l'adresse du serveur soit incorrecte"; @@ -2465,6 +2874,9 @@ server test error */ /* No comment provided by engineer. */ "Fix not supported by group member" = "Correction non prise en charge par un membre du groupe"; +/* No comment provided by engineer. */ +"For anyone to reach you" = "Pour que n'importe qui puisse vous contacter"; + /* servers error servers warning */ "For chat profile %@:" = "Pour le profil de discussion %@ :"; @@ -2475,6 +2887,9 @@ servers warning */ /* No comment provided by engineer. */ "For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server." = "Par exemple, si votre contact reçoit des messages via un serveur SimpleX Chat, votre application les transmettra via un serveur Flux."; +/* No comment provided by engineer. */ +"For me" = "Pour moi"; + /* No comment provided by engineer. */ "For private routing" = "Pour le routage privé"; @@ -2547,6 +2962,15 @@ servers warning */ /* No comment provided by engineer. */ "Further reduced battery usage" = "Réduction accrue de l'utilisation de la batterie"; +/* relay test step */ +"Get link" = "Obtenir un lien"; + +/* No comment provided by engineer. */ +"Get notified when mentioned." = "Soyez averti·e quand vous êtes mentionné·e."; + +/* No comment provided by engineer. */ +"Get started" = "Commençons"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs et stickers"; @@ -2556,6 +2980,9 @@ servers warning */ /* message preview */ "Good morning!" = "Bonjour !"; +/* shown on group welcome message */ +"group" = "groupe"; + /* No comment provided by engineer. */ "Group" = "Groupe"; @@ -2610,6 +3037,12 @@ servers warning */ /* snd group event chat item */ "group profile updated" = "mise à jour du profil de groupe"; +/* alert message */ +"Group profile was changed. If you save it, the updated profile will be sent to group members." = "Le profil du groupe a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé aux membres du groupe."; + +/* No comment provided by engineer. */ +"Group webpage" = "Page Web du groupe"; + /* No comment provided by engineer. */ "Group welcome message" = "Message d'accueil du groupe"; @@ -2619,9 +3052,18 @@ servers warning */ /* No comment provided by engineer. */ "Group will be deleted for you - this cannot be undone!" = "Le groupe va être supprimé pour vous - impossible de revenir en arrière !"; +/* No comment provided by engineer. */ +"Groups" = "Groupes"; + /* No comment provided by engineer. */ "Help" = "Aide"; +/* No comment provided by engineer. */ +"Help & support" = "Aide et assistance"; + +/* No comment provided by engineer. */ +"Help admins moderating their groups." = "Aidez les admins à modérer leurs groupes."; + /* No comment provided by engineer. */ "Hidden" = "Caché"; @@ -2649,6 +3091,9 @@ servers warning */ /* No comment provided by engineer. */ "History is not sent to new members." = "L'historique n'est pas envoyé aux nouveaux membres."; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "L'historique n'est pas envoyé aux nouveaux abonné·es."; + /* time unit */ "hours" = "heures"; @@ -2658,6 +3103,9 @@ servers warning */ /* No comment provided by engineer. */ "How it helps privacy" = "Comment il contribue à la protection de la vie privée"; +/* alert button */ +"How it works" = "Comment ça marche"; + /* No comment provided by engineer. */ "How SimpleX works" = "Comment SimpleX fonctionne"; @@ -2685,8 +3133,11 @@ servers warning */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "Si vous entrez votre code d'autodestruction à l'ouverture de l'application :"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "Si vous avez rejoint ou créé des canaux, ils arrêteront de fonctionner définitivement."; + /* No comment provided by engineer. */ -"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Si vous avez besoin d'utiliser le chat maintenant appuyez sur **le faire plus tard** (vous pourrez migrer la base de données quand vous relancerez l'app)."; +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Si vous avez besoin d'utiliser le chat maintenant appuyez sur **le faire plus tard** (vous pourrez migrer la base de données quand vous relancerez l'appli)."; /* No comment provided by engineer. */ "Ignore" = "Ignorer"; @@ -2697,6 +3148,9 @@ servers warning */ /* No comment provided by engineer. */ "Image will be received when your contact is online, please wait or check later!" = "L'image sera reçue quand votre contact sera en ligne, merci d'attendre ou de revenir plus tard !"; +/* No comment provided by engineer. */ +"Images" = "Images"; + /* No comment provided by engineer. */ "Immediately" = "Immédiatement"; @@ -2742,6 +3196,12 @@ servers warning */ /* No comment provided by engineer. */ "inactive" = "inactif"; +/* report reason */ +"Inappropriate content" = "Contenu inapproprié"; + +/* report reason */ +"Inappropriate profile" = "Profil inapproprié"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2808,6 +3268,21 @@ servers warning */ /* No comment provided by engineer. */ "Interface colors" = "Couleurs d'interface"; +/* token status text */ +"Invalid" = "Invalide"; + +/* token status text */ +"Invalid (bad token)" = "Invalide (mauvais jeton)"; + +/* token status text */ +"Invalid (expired)" = "Invalide (expiré)"; + +/* token status text */ +"Invalid (unregistered)" = "Invalide (non enregistré)"; + +/* token status text */ +"Invalid (wrong topic)" = "Invalide (mauvais sujet)"; + /* invalid chat data */ "invalid chat" = "chat invalide"; @@ -2835,6 +3310,12 @@ servers warning */ /* No comment provided by engineer. */ "Invalid QR code" = "Code QR invalide"; +/* alert title */ +"Invalid relay address!" = "Adresse de relais invalide !"; + +/* alert title */ +"Invalid relay name!" = "Nom du relais invalide !"; + /* No comment provided by engineer. */ "Invalid response" = "Réponse invalide"; @@ -2856,9 +3337,15 @@ servers warning */ /* No comment provided by engineer. */ "Invite friends" = "Inviter des amis"; +/* No comment provided by engineer. */ +"Invite member" = "Inviter un membre"; + /* No comment provided by engineer. */ "Invite members" = "Inviter des membres"; +/* No comment provided by engineer. */ +"Invite someone privately" = "Inviter quelqu'un en privé"; + /* No comment provided by engineer. */ "Invite to chat" = "Inviter à discuter"; @@ -2910,6 +3397,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Ceci sera montré aux abonné·es et utilisé pour permettre le chargement de l'aperçu."; + /* No comment provided by engineer. */ "Italian interface" = "Interface en italien"; @@ -2925,6 +3415,9 @@ servers warning */ /* No comment provided by engineer. */ "Join as %@" = "rejoindre entant que %@"; +/* No comment provided by engineer. */ +"Join channel" = "Rejoindre le canal"; + /* new chat sheet title */ "Join group" = "Rejoindre le groupe"; @@ -2952,6 +3445,9 @@ servers warning */ /* alert title */ "Keep unused invitation?" = "Conserver l'invitation inutilisée ?"; +/* No comment provided by engineer. */ +"Keep your chats clean" = "Gardez vos discussions propres"; + /* No comment provided by engineer. */ "Keep your connections" = "Conserver vos connexions"; @@ -2964,12 +3460,18 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Fichier trop lourd !"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "En savoir plus"; /* swipe action */ "Leave" = "Quitter"; +/* No comment provided by engineer. */ +"Leave channel" = "Quitter le canal"; + +/* No comment provided by engineer. */ +"Leave channel?" = "Quitter le canal ?"; + /* No comment provided by engineer. */ "Leave chat" = "Quitter la discussion"; @@ -2985,6 +3487,12 @@ servers warning */ /* rcv group event chat item */ "left" = "a quitté"; +/* No comment provided by engineer. */ +"Less traffic on mobile networks." = "Moins de transferts de données sur les réseaux mobiles."; + +/* No comment provided by engineer. */ +"Let someone connect to you" = "Laisser quelqu'un se connecter à vous"; + /* email subject */ "Let's talk in SimpleX Chat" = "Discutons sur SimpleX Chat"; @@ -2994,15 +3502,33 @@ servers warning */ /* No comment provided by engineer. */ "Limitations" = "Limitations"; +/* No comment provided by engineer. */ +"link" = "lien"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Liez vos applications mobiles et de bureau ! 🔗"; +/* owner verification */ +"Link signature verified." = "Signature du lien vérifiée."; + /* No comment provided by engineer. */ "Linked desktop options" = "Options de bureau lié"; /* No comment provided by engineer. */ "Linked desktops" = "Bureaux liés"; +/* No comment provided by engineer. */ +"Links" = "Liens"; + +/* swipe action */ +"List" = "Liste"; + +/* No comment provided by engineer. */ +"List name and emoji should be different for all lists." = "Le nom de la liste et les émojis devraient être différents pour toutes les listes."; + +/* No comment provided by engineer. */ +"List name..." = "Nom de la liste…"; + /* No comment provided by engineer. */ "LIVE" = "LIVE"; @@ -3012,6 +3538,9 @@ servers warning */ /* No comment provided by engineer. */ "Live messages" = "Messages dynamiques"; +/* in progress text */ +"Loading profile…" = "Chargement du profil…"; + /* No comment provided by engineer. */ "Local name" = "Nom local"; @@ -3063,23 +3592,32 @@ servers warning */ /* No comment provided by engineer. */ "Member" = "Membre"; +/* past/unknown group member */ +"Member %@" = "Membre %@"; + /* profile update event chat item */ "member %@ changed to %@" = "le membre %1$@ est devenu %2$@"; +/* No comment provided by engineer. */ +"Member admission" = "Admission du membre"; + /* rcv group event chat item */ "member connected" = "est connecté·e"; +/* No comment provided by engineer. */ +"member has old version" = "le membre a une ancienne version"; + /* item status text */ "Member inactive" = "Membre inactif"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés."; +"Member is deleted - can't accept request" = "Le membre est supprimé ; impossible d'accepter la demande"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Le rôle du membre sera changé pour \"%@\". Tous les membres du groupe en seront informés."; +/* alert message */ +"Member messages will be deleted - this cannot be undone!" = "Les messages des membres seront supprimés ; ceci ne peut pas être annulé !"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Le rôle du membre sera changé pour \"%@\". Ce membre recevra une nouvelle invitation."; +/* chat feature */ +"Member reports" = "Signalements des membres"; /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Le membre sera retiré de la discussion - cela ne peut pas être annulé !"; @@ -3087,12 +3625,21 @@ servers warning */ /* alert message */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; +/* alert message */ +"Member will join the group, accept member?" = "Le membre rejoindra le groupe ; accepter le membre ?"; + /* No comment provided by engineer. */ "Members can add message reactions." = "Les membres du groupe peuvent ajouter des réactions aux messages."; +/* No comment provided by engineer. */ +"Members can chat with admins." = "Les membres peuvent discuter avec les admins."; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés. (24 heures)"; +/* No comment provided by engineer. */ +"Members can report messsages to moderators." = "Les membres peuvent signaler les messages aux modérateur·ices."; + /* No comment provided by engineer. */ "Members can send direct messages." = "Les membres du groupe peuvent envoyer des messages directs."; @@ -3108,6 +3655,9 @@ servers warning */ /* No comment provided by engineer. */ "Members can send voice messages." = "Les membres du groupe peuvent envoyer des messages vocaux."; +/* No comment provided by engineer. */ +"Mention members 👋" = "Mentionnez les membres 👋"; + /* No comment provided by engineer. */ "Menus" = "Menus"; @@ -3126,9 +3676,15 @@ servers warning */ /* No comment provided by engineer. */ "Message draft" = "Brouillon de message"; +/* No comment provided by engineer. */ +"Message error" = "Erreur du message"; + /* item status text */ "Message forwarded" = "Message transféré"; +/* No comment provided by engineer. */ +"Message instantly once you tap Connect." = "Parlez instantanément dès que vous appuyez sur Connecter."; + /* item status description */ "Message may be delivered later if member becomes active." = "Le message peut être transmis plus tard si le membre devient actif."; @@ -3177,9 +3733,21 @@ servers warning */ /* No comment provided by engineer. */ "Messages & files" = "Messages"; +/* No comment provided by engineer. */ +"Messages are protected by **end-to-end encryption**." = "Les messages sont protégés par le **chiffrement de bout-en-bout**."; + /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Les messages de %@ seront affichés !"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "Les messages dans ce canal **ne sont pas chiffrés de bout-en-bout**. Les relais de discussion peuvent voir ces messages."; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "Les messages dans ce canal ne sont pas chiffrés de bout-en-bout. Les relais de discussion peuvent voir ces messages."; + +/* alert message */ +"Messages in this chat will never be deleted." = "Les messages dans cette discussion ne seront jamais supprimés."; + /* No comment provided by engineer. */ "Messages received" = "Messages reçus"; @@ -3195,6 +3763,9 @@ servers warning */ /* No comment provided by engineer. */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Les messages, fichiers et appels sont protégés par un chiffrement **e2e résistant post-quantique** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction."; +/* No comment provided by engineer. */ +"Migrate" = "Migrer"; + /* No comment provided by engineer. */ "Migrate device" = "Transférer l'appareil"; @@ -3220,7 +3791,7 @@ servers warning */ "Migration error:" = "Erreur de migration :"; /* No comment provided by engineer. */ -"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Echec de la migration. Appuyez sur **Passer** ci-dessous pour continuer à utiliser la base de données actuelle. Veuillez signaler le problème aux développeurs de l'app par chat ou par e-mail [chat@simplex.chat](mailto:chat@simplex.chat)."; +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Échec de la migration. Appuyez sur **Passer** ci-dessous pour continuer à utiliser la base de données actuelle. Veuillez signaler le problème aux développeurs de l'appli par discussion ou par courriel [chat@simplex.chat](mailto:chat@simplex.chat)."; /* No comment provided by engineer. */ "Migration is completed" = "La migration est terminée"; @@ -3249,12 +3820,21 @@ servers warning */ /* marked deleted chat item preview text */ "moderated by %@" = "modéré par %@"; +/* member role */ +"moderator" = "modérateur·ice"; + /* time unit */ "months" = "mois"; +/* swipe action */ +"More" = "Plus"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "Plus d'améliorations à venir !"; +/* No comment provided by engineer. */ +"More privacy" = "Plus de vie privée"; + /* No comment provided by engineer. */ "More reliable network connection." = "Connexion réseau plus fiable."; @@ -3270,6 +3850,9 @@ servers warning */ /* notification label action */ "Mute" = "Muet"; +/* notification label action */ +"Mute all" = "Tout en sourdine"; + /* No comment provided by engineer. */ "Muted when inactive!" = "Mute en cas d'inactivité !"; @@ -3279,12 +3862,18 @@ servers warning */ /* No comment provided by engineer. */ "Network & servers" = "Réseau et serveurs"; +/* No comment provided by engineer. */ +"Network commitments" = "Engagements réseau"; + /* No comment provided by engineer. */ "Network connection" = "Connexion au réseau"; /* No comment provided by engineer. */ "Network decentralization" = "Décentralisation du réseau"; +/* conn error description */ +"Network error" = "Erreur réseau"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi."; @@ -3294,6 +3883,9 @@ servers warning */ /* No comment provided by engineer. */ "Network operator" = "Opérateur de réseau"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "Les routeurs réseau ne peuvent pas savoir\nqui parle à qui"; + /* No comment provided by engineer. */ "Network settings" = "Paramètres réseau"; @@ -3304,11 +3896,23 @@ servers warning */ "never" = "jamais"; /* No comment provided by engineer. */ -"New chat" = "Nouveau chat"; +"new" = "nouveau"; + +/* token status text */ +"New" = "Nouveau"; + +/* No comment provided by engineer. */ +"New 1-time link" = "Nouveau lien unique"; + +/* No comment provided by engineer. */ +"New chat" = "Nouvelle discussion"; /* No comment provided by engineer. */ "New chat experience 🎉" = "Nouvelle expérience de discussion 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "Nouveau relais de discussion"; + /* notification */ "New contact request" = "Nouvelle demande de contact"; @@ -3333,6 +3937,9 @@ servers warning */ /* No comment provided by engineer. */ "New member role" = "Nouveau rôle"; +/* rcv group event chat item */ +"New member wants to join the group." = "Un nouveau membre veut rejoindre le groupe."; + /* notification */ "new message" = "nouveau message"; @@ -3360,9 +3967,36 @@ servers warning */ /* No comment provided by engineer. */ "No" = "Non"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "Pas de compte. Pas de téléphone. Pas de courriel. Pas d'identifiant.\nLe chiffrement le plus sûr."; + +/* No comment provided by engineer. */ +"No active relays" = "Aucun relais actif"; + /* Authentication unavailable */ "No app password" = "Pas de mot de passe pour l'app"; +/* No comment provided by engineer. */ +"No available relays" = "Aucun relais disponible"; + +/* No comment provided by engineer. */ +"No chat relays" = "Aucun relais de discussion"; + +/* servers warning */ +"No chat relays enabled." = "Aucun relais de discussion disponible."; + +/* No comment provided by engineer. */ +"No chats" = "Aucune discussion"; + +/* No comment provided by engineer. */ +"No chats found" = "Aucune discussion trouvée"; + +/* No comment provided by engineer. */ +"No chats in list %@" = "Aucune discussion dans la liste %@"; + +/* No comment provided by engineer. */ +"No chats with members" = "Aucune discussion avec les membres"; + /* No comment provided by engineer. */ "No contacts selected" = "Aucun contact sélectionné"; @@ -3396,6 +4030,9 @@ servers warning */ /* servers error */ "No media & file servers." = "Pas de serveurs de médias et de fichiers."; +/* No comment provided by engineer. */ +"No message" = "Aucun message"; + /* servers error */ "No message servers." = "Pas de serveurs de messages."; @@ -3411,12 +4048,18 @@ servers warning */ /* No comment provided by engineer. */ "No permission to record voice message" = "Pas l'autorisation d'enregistrer un message vocal"; +/* alert title */ +"No private routing session" = "Aucune session de routage privée"; + /* No comment provided by engineer. */ "No push server" = "No push server"; /* No comment provided by engineer. */ "No received or sent files" = "Aucun fichier reçu ou envoyé"; +/* No comment provided by engineer. */ +"No relays" = "Aucun relais"; + /* servers error */ "No servers for private message routing." = "Pas de serveurs pour le routage privé des messages."; @@ -3429,12 +4072,39 @@ servers warning */ /* servers error */ "No servers to send files." = "Pas de serveurs pour envoyer des fichiers."; +/* No comment provided by engineer. */ +"no subscription" = "aucun abonnement"; + /* copied message info in history */ "no text" = "aucun texte"; +/* alert title */ +"No token!" = "Aucun jeton !"; + +/* No comment provided by engineer. */ +"No unread chats" = "Aucune discussion non lue"; + +/* No comment provided by engineer. */ +"Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Personne ne suivait vos conversations. Personne ne dessinait de carte d'où vous étiez. La vie privée n'était jamais une caractéristique – c'était le mode de vie."; + +/* No comment provided by engineer. */ +"Non-profit governance" = "Gouvernance à but non lucratif"; + +/* No comment provided by engineer. */ +"Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Ce n’est pas une meilleure serrure sur la porte de quelqu’un d’autre. Ce n’est pas un propriétaire plus aimable qui respecte votre vie privée, mais qui tient tout de même un registre de tous les visiteurs. Vous n’êtes pas un·e invité·e, vous êtes chez vous. Aucun roi ne peut y entrer : c’est vous le souverain."; + +/* alert title */ +"Not all relays connected" = "Les relais ne sont pas tous connectés"; + /* No comment provided by engineer. */ "Not compatible!" = "Non compatible !"; +/* No comment provided by engineer. */ +"not synchronized" = "non synchronisé"; + +/* No comment provided by engineer. */ +"Notes" = "Notes"; + /* No comment provided by engineer. */ "Nothing selected" = "Aucune sélection"; @@ -3447,9 +4117,15 @@ servers warning */ /* No comment provided by engineer. */ "Notifications are disabled!" = "Les notifications sont désactivées !"; +/* alert title */ +"Notifications error" = "Erreur de notification"; + /* No comment provided by engineer. */ "Notifications privacy" = "Notifications sécurisées"; +/* alert title */ +"Notifications status" = "État des notifications"; + /* No comment provided by engineer. */ "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Désormais, les administrateurs peuvent :\n- supprimer les messages des membres.\n- désactiver des membres (rôle \"observateur\")"; @@ -3460,10 +4136,10 @@ servers warning */ group pref value member criteria value time to disappear */ -"off" = "off"; +"off" = "désactivé"; /* blur media */ -"Off" = "Off"; +"Off" = "Désactivé"; /* feature offered item */ "offered %@" = "propose %@"; @@ -3474,7 +4150,7 @@ time to disappear */ /* alert action alert button new chat action */ -"Ok" = "Ok"; +"Ok" = "D'accord"; /* alert button */ "OK" = "OK"; @@ -3483,11 +4159,17 @@ new chat action */ "Old database" = "Ancienne base de données"; /* group pref value */ -"on" = "on"; +"on" = "activé"; + +/* No comment provided by engineer. */ +"On your phone, not on servers." = "Sur votre téléphone, pas sur les serveurs."; /* No comment provided by engineer. */ "One-time invitation link" = "Lien d'invitation unique"; +/* chat link info line */ +"One-time link" = "Lien unique"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Les hôtes .onion seront **nécessaires** pour la connexion.\nNécessite l'activation d'un VPN."; @@ -3497,6 +4179,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "Les hôtes .onion ne seront pas utilisés."; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "Seuls les propriétaires du canal peuvent changer les préférences du canal."; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "Seuls les propriétaires peuvent modifier les préférences."; @@ -3515,6 +4200,12 @@ new chat action */ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "Seuls les propriétaires de groupes peuvent activer les messages vocaux."; +/* No comment provided by engineer. */ +"Only sender and moderators see it" = "Seul l'expéditeur et les modérateurs le voient"; + +/* No comment provided by engineer. */ +"Only you and moderators see it" = "Seuls vous et les modérateurs le voyez"; + /* No comment provided by engineer. */ "Only you can add message reactions." = "Vous seul pouvez ajouter des réactions aux messages."; @@ -3527,6 +4218,9 @@ new chat action */ /* No comment provided by engineer. */ "Only you can send disappearing messages." = "Seulement vous pouvez envoyer des messages éphémères."; +/* No comment provided by engineer. */ +"Only you can send files and media." = "Seul·e vous pouvez envoyer des fichiers et des médias."; + /* No comment provided by engineer. */ "Only you can send voice messages." = "Vous seul pouvez envoyer des messages vocaux."; @@ -3542,9 +4236,15 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send disappearing messages." = "Seulement votre contact peut envoyer des messages éphémères."; +/* No comment provided by engineer. */ +"Only your contact can send files and media." = "Seul votre contact peut envoyer des fichiers et des médias."; + /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Seul votre contact peut envoyer des messages vocaux."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Seule votre page ci-dessus peut afficher l'aperçu."; + /* alert action alert button */ "Open" = "Ouvrir"; @@ -3552,24 +4252,60 @@ alert button */ /* No comment provided by engineer. */ "Open changes" = "Ouvrir les modifications"; +/* new chat action */ +"Open channel" = "Ouvrir le canal"; + /* new chat action */ "Open chat" = "Ouvrir le chat"; /* authentication reason */ "Open chat console" = "Ouvrir la console du chat"; +/* alert action */ +"Open clean link" = "Ouvrir le lien nettoyé"; + /* No comment provided by engineer. */ "Open conditions" = "Ouvrir les conditions"; +/* alert title */ +"Open external link?" = "Ouvrir le lien externe ?"; + +/* alert action */ +"Open full link" = "Ouvrir le lien complet"; + /* new chat action */ "Open group" = "Ouvrir le groupe"; +/* alert title */ +"Open link?" = "Ouvrir le lien ?"; + /* authentication reason */ "Open migration to another device" = "Ouvrir le transfert vers un autre appareil"; +/* new chat action */ +"Open new channel" = "Ouvrir un nouveau canal"; + +/* new chat action */ +"Open new chat" = "Ouvrir une nouvelle discussion"; + +/* new chat action */ +"Open new group" = "Ouvrir le nouveau groupe"; + /* No comment provided by engineer. */ "Open Settings" = "Ouvrir les Paramètres"; +/* No comment provided by engineer. */ +"Open to accept" = "Ouvrir pour accepter"; + +/* No comment provided by engineer. */ +"Open to connect" = "Ouvrir pour connecter"; + +/* No comment provided by engineer. */ +"Open to join" = "Ouvrir pour rejoindre"; + +/* No comment provided by engineer. */ +"Open to use bot" = "Ouvrir pour utiliser le bot"; + /* No comment provided by engineer. */ "Opening app…" = "Ouverture de l'app…"; @@ -3579,6 +4315,9 @@ alert button */ /* alert title */ "Operator server" = "Serveur de l'opérateur"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "Les opérateurs s'engagent à :\n- Être indépendants\n- Minimiser l'utilisation des métadonnées\n- Exécuter un code ouvert vérifié"; + /* No comment provided by engineer. */ "Or import archive file" = "Ou importer un fichier d'archive"; @@ -3591,12 +4330,21 @@ alert button */ /* No comment provided by engineer. */ "Or securely share this file link" = "Ou partagez en toute sécurité le lien de ce fichier"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "Ou montrez le code QR en personne ou via un appel vidéo."; + /* No comment provided by engineer. */ "Or show this code" = "Ou montrez ce code"; /* No comment provided by engineer. */ "Or to share privately" = "Ou à partager en privé"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "Ou utilisez ce code QR : imprimez-le ou affichez-le en ligne."; + +/* No comment provided by engineer. */ +"Organize chats into lists" = "Organisez des discussions en listes"; + /* No comment provided by engineer. */ "other" = "autre"; @@ -3612,9 +4360,15 @@ alert button */ /* member role */ "owner" = "propriétaire"; +/* No comment provided by engineer. */ +"Owner" = "Propriétaire"; + /* feature role */ "owners" = "propriétaires"; +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "Propriétaire : vous pouvez exécuter vos propres relais."; + /* No comment provided by engineer. */ "Passcode" = "Code d'accès"; @@ -3642,6 +4396,9 @@ alert button */ /* No comment provided by engineer. */ "Paste image" = "Coller l'image"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "Coller le lien / Scanner"; + /* No comment provided by engineer. */ "Paste link to connect!" = "Collez le lien pour vous connecter !"; @@ -3651,9 +4408,18 @@ alert button */ /* No comment provided by engineer. */ "peer-to-peer" = "pair-à-pair"; +/* No comment provided by engineer. */ +"pending" = "en attente"; + /* No comment provided by engineer. */ "Pending" = "En attente"; +/* No comment provided by engineer. */ +"pending approval" = "en attente d'approbation"; + +/* No comment provided by engineer. */ +"pending review" = "en attente de révision"; + /* No comment provided by engineer. */ "Periodic" = "Périodique"; @@ -3720,6 +4486,18 @@ alert button */ /* No comment provided by engineer. */ "Please store passphrase securely, you will NOT be able to change it if you lose it." = "Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez."; +/* token info */ +"Please try to disable and re-enable notfications." = "Veuillez essayer de désactiver et réactiver les notifications."; + +/* snd group event chat item */ +"Please wait for group moderators to review your request to join the group." = "Veuillez attendre que les modérateur·ices de groupe examinent votre demande pour rejoindre le groupe."; + +/* token info */ +"Please wait for token activation to complete." = "Veuillez attendre la fin de l'activation du jeton."; + +/* token info */ +"Please wait for token to be registered." = "Veuillez attendre que le jeton soit enregistré."; + /* No comment provided by engineer. */ "Polish interface" = "Interface en polonais"; @@ -3729,6 +4507,12 @@ alert button */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "Conserver le brouillon du dernier message, avec les pièces jointes."; +/* No comment provided by engineer. */ +"Preset relay address" = "Adresse de relais prédéfinie"; + +/* No comment provided by engineer. */ +"Preset relay name" = "Nom de relais prédéfini"; + /* No comment provided by engineer. */ "Preset server address" = "Adresse du serveur prédéfinie"; @@ -3741,15 +4525,24 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Serveurs précédemment connectés"; -/* No comment provided by engineer. */ -"Privacy & security" = "Vie privée et sécurité"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Respect de la vie privée de vos clients."; +/* No comment provided by engineer. */ +"Privacy policy and conditions of use." = "Politique de confidentialité et conditions d'utilisation."; + +/* No comment provided by engineer. */ +"Privacy: for owners and subscribers." = "Confidentialité : pour les propriétaires et les abonné·es."; + +/* No comment provided by engineer. */ +"Private and secure messaging." = "Messagerie privée et sécurisée."; + /* No comment provided by engineer. */ "Private filenames" = "Noms de fichiers privés"; +/* No comment provided by engineer. */ +"Private media file names." = "Noms de fichiers multimédias privés."; + /* No comment provided by engineer. */ "Private message routing" = "Routage privé des messages"; @@ -3765,6 +4558,9 @@ alert button */ /* alert title */ "Private routing error" = "Erreur de routage privé"; +/* alert title */ +"Private routing timeout" = "Temps de routage privé"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil et connexions au serveur"; @@ -3780,9 +4576,16 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Thème de profil"; +/* alert message +alert title */ +"Profile update will be sent to your SimpleX contacts." = "La mise à jour du profil sera envoyée à vos contacts SimpleX."; + /* No comment provided by engineer. */ "Prohibit audio/video calls." = "Interdire les appels audio/vidéo."; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "Interdire les conversations avec les admins."; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Interdire la suppression irréversible des messages."; @@ -3792,9 +4595,15 @@ alert button */ /* No comment provided by engineer. */ "Prohibit messages reactions." = "Interdire les réactions aux messages."; +/* No comment provided by engineer. */ +"Prohibit reporting messages to moderators." = "Interdire de signaler des messages aux modérateur·ices."; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "Interdire l'envoi de messages directs aux membres."; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "Interdire l'envoi de messages directs aux abonné·es."; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Interdire l’envoi de messages éphémères."; @@ -3808,17 +4617,20 @@ alert button */ "Prohibit sending voice messages." = "Interdire l'envoi de messages vocaux."; /* No comment provided by engineer. */ -"Protect app screen" = "Protéger l'écran de l'app"; +"Protect app screen" = "Protéger l'écran de l'appli"; /* No comment provided by engineer. */ "Protect IP address" = "Protéger l'adresse IP"; /* No comment provided by engineer. */ -"Protect your chat profiles with a password!" = "Protégez vos profils de chat par un mot de passe !"; +"Protect your chat profiles with a password!" = "Protégez vos profils de discussion par un mot de passe !"; /* No comment provided by engineer. */ "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Protégez votre adresse IP des relais de messagerie choisis par vos contacts.\nActivez-le dans les paramètres *Réseau et serveurs*."; +/* No comment provided by engineer. */ +"Protocol background timeout" = "Expiration du protocole en arrière-plan"; + /* No comment provided by engineer. */ "Protocol timeout" = "Délai du protocole"; @@ -3834,6 +4646,9 @@ alert button */ /* No comment provided by engineer. */ "Proxy requires password" = "Le proxy est protégé par un mot de passe"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "Les canaux publics – parlez librement 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "Notifications push"; @@ -3847,7 +4662,7 @@ alert button */ "Quantum resistant encryption" = "Chiffrement résistant post-quantique"; /* No comment provided by engineer. */ -"Rate the app" = "Évaluer l'app"; +"Rate the app" = "Évaluer l'appli"; /* No comment provided by engineer. */ "Reachable chat toolbar" = "Barre d'outils accessible"; @@ -3858,7 +4673,7 @@ alert button */ /* swipe action */ "Read" = "Lire"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "En savoir plus"; /* No comment provided by engineer. */ @@ -3945,6 +4760,15 @@ alert button */ /* No comment provided by engineer. */ "Reduced battery usage" = "Réduction de la consommation de batterie"; +/* No comment provided by engineer. */ +"Register" = "Inscrire"; + +/* token info */ +"Register notification token?" = "Inscrire le jeton de notification ?"; + +/* token status text */ +"Registered" = "Inscrit"; + /* alert action reject incoming call via notification swipe action */ @@ -3956,11 +4780,35 @@ swipe action */ /* alert title */ "Reject contact request" = "Rejeter la demande de contact"; +/* alert title */ +"Reject member?" = "Rejeter le membre ?"; + +/* No comment provided by engineer. */ +"rejected" = "rejeté"; + /* call status */ "rejected call" = "appel rejeté"; +/* member role */ +"relay" = "relais"; + /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP."; +"Relay" = "Relais"; + +/* alert title */ +"Relay address" = "Adresse de relais"; + +/* alert title */ +"Relay connection failed" = "Échec de la connexion au relais"; + +/* No comment provided by engineer. */ +"Relay link" = "Lien de relais"; + +/* alert message */ +"Relay results:" = "Résultats du relais :"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Le serveur du relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP."; /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "Le serveur relais protège votre adresse IP, mais il peut observer la durée de l'appel."; @@ -3974,6 +4822,9 @@ swipe action */ /* No comment provided by engineer. */ "Remove image" = "Enlever l'image"; +/* No comment provided by engineer. */ +"Remove link tracking" = "Retirer le traçage par lien"; + /* No comment provided by engineer. */ "Remove member" = "Retirer le membre"; @@ -3984,14 +4835,23 @@ swipe action */ "Remove passphrase from keychain?" = "Supprimer la phrase secrète de la keychain ?"; /* No comment provided by engineer. */ -"removed" = "supprimé"; +"removed" = "retiré"; + +/* receive error chat item */ +"removed (%d attempts)" = "retiré (%d tentatives)"; /* rcv group event chat item */ "removed %@" = "a retiré %@"; +/* No comment provided by engineer. */ +"removed by operator" = "retiré par un opérateur"; + /* profile update event chat item */ "removed contact address" = "suppression de l'adresse de contact"; +/* No comment provided by engineer. */ +"removed from group" = "retiré du groupe"; + /* profile update event chat item */ "removed profile picture" = "suppression de la photo de profil"; @@ -4019,6 +4879,51 @@ swipe action */ /* chat item action */ "Reply" = "Répondre"; +/* chat item action */ +"Report" = "Signaler"; + +/* report reason */ +"Report content: only group moderators will see it." = "Contenu du signalement : seuls les modérateurs de groupe le verront."; + +/* report reason */ +"Report member profile: only group moderators will see it." = "Signaler le profil d'un membre : seuls les modérateurs de groupe le verront."; + +/* report reason */ +"Report other: only group moderators will see it." = "Signaler autre chose : seuls les modérateurs de groupe le verront."; + +/* No comment provided by engineer. */ +"Report reason?" = "Motif du signalement ?"; + +/* alert title */ +"Report sent to moderators" = "Signalement envoyé aux modérateurs"; + +/* report reason */ +"Report spam: only group moderators will see it." = "Signaler du spam : seuls les modérateurs de groupe le verront."; + +/* report reason */ +"Report violation: only group moderators will see it." = "Signaler une violation : seuls les modérateurs de groupe le verront."; + +/* report in notification */ +"Report: %@" = "Signalement : %@"; + +/* No comment provided by engineer. */ +"Reporting messages to moderators is prohibited." = "Signaler des messages aux modérateur·ices est interdit."; + +/* No comment provided by engineer. */ +"Reports" = "Signalements"; + +/* No comment provided by engineer. */ +"request is sent" = "la demande est envoyée"; + +/* No comment provided by engineer. */ +"request to join rejected" = "demande de connexion rejetée"; + +/* rcv group event chat item */ +"requested connection" = "a demandé une connexion"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "connexion demandée du groupe %@"; + /* chat list item title */ "requested to connect" = "demande à se connecter"; @@ -4050,7 +4955,7 @@ swipe action */ "Reset to user theme" = "Réinitialisation au thème de l'utilisateur"; /* No comment provided by engineer. */ -"Restart the app to create a new chat profile" = "Redémarrez l'application pour créer un nouveau profil de chat"; +"Restart the app to create a new chat profile" = "Redémarrez l'appli pour créer un nouveau profil de discussion"; /* No comment provided by engineer. */ "Restart the app to use imported chat database" = "Redémarrez l'application pour utiliser la base de données de chat importée"; @@ -4073,9 +4978,24 @@ swipe action */ /* chat item action */ "Reveal" = "Révéler"; +/* No comment provided by engineer. */ +"review" = "révision"; + /* No comment provided by engineer. */ "Review conditions" = "Vérifier les conditions"; +/* No comment provided by engineer. */ +"Review group members" = "Contrôler les membres du groupe"; + +/* admission stage */ +"Review members" = "Contrôler les membres"; + +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "Contrôler les membres avant de les admettre (« toquer »)."; + +/* No comment provided by engineer. */ +"reviewed by admins" = "révisé par les admins"; + /* No comment provided by engineer. */ "Revoke" = "Révoquer"; @@ -4088,37 +5008,77 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rôle"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Le rôle du membre sera modifié pour « %@ ». Tous les membres du chat seront notifiés."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Le rôle du membre sera changé pour \"%@\". Tous les membres du groupe en seront informés."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Le rôle du membre sera changé pour \"%@\". Ce membre recevra une nouvelle invitation."; + /* No comment provided by engineer. */ "Run chat" = "Exécuter le chat"; +/* No comment provided by engineer. */ +"Safe web links" = "Liens Web sûrs"; + /* No comment provided by engineer. */ "Safely receive files" = "Réception de fichiers en toute sécurité"; /* No comment provided by engineer. */ "Safer groups" = "Groupes plus sûrs"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Enregistrer"; /* alert button */ "Save (and notify contacts)" = "Enregistrer (et en informer les contacts)"; +/* alert button */ +"Save (and notify members)" = "Enregistrer (et notifier les membres)"; + +/* alert button */ +"Save (and notify subscribers)" = "Enregistrer (et notifier les abonné·es)"; + +/* alert title */ +"Save admission settings?" = "Enregistrer les réglages d'admission ?"; + /* alert button */ "Save and notify contact" = "Enregistrer et en informer le contact"; /* No comment provided by engineer. */ "Save and notify group members" = "Enregistrer et en informer les membres du groupe"; +/* No comment provided by engineer. */ +"Save and notify members" = "Enregistrer (et notifier les membres)"; + +/* No comment provided by engineer. */ +"Save and notify subscribers" = "Enregistrer et notifier les abonné·es"; + /* No comment provided by engineer. */ "Save and reconnect" = "Sauvegarder et se reconnecter"; /* No comment provided by engineer. */ "Save and update group profile" = "Enregistrer et mettre à jour le profil du groupe"; +/* No comment provided by engineer. */ +"Save channel profile" = "Enregistrer le profil du canal"; + +/* alert title */ +"Save channel profile?" = "Enregistrer le profil du canal ?"; + /* No comment provided by engineer. */ "Save group profile" = "Enregistrer le profil du groupe"; +/* alert title */ +"Save group profile?" = "Enregistrer le profil du groupe ?"; + +/* No comment provided by engineer. */ +"Save list" = "Enregistrer la liste"; + /* No comment provided by engineer. */ "Save passphrase and open chat" = "Enregistrer la phrase secrète et ouvrir le chat"; @@ -4137,6 +5097,9 @@ chat item action */ /* alert title */ "Save servers?" = "Enregistrer les serveurs ?"; +/* alert title */ +"Save webpage settings?" = "Enregistrer les paramètres de la page Web ?"; + /* No comment provided by engineer. */ "Save welcome message?" = "Enregistrer le message d'accueil ?"; @@ -4194,9 +5157,24 @@ chat item action */ /* No comment provided by engineer. */ "Search bar accepts invitation links." = "La barre de recherche accepte les liens d'invitation."; +/* No comment provided by engineer. */ +"Search files" = "Rechercher des fichiers"; + +/* No comment provided by engineer. */ +"Search images" = "Recherche des images"; + +/* No comment provided by engineer. */ +"Search links" = "Rechercher des liens"; + /* No comment provided by engineer. */ "Search or paste SimpleX link" = "Rechercher ou coller un lien SimpleX"; +/* No comment provided by engineer. */ +"Search videos" = "Rechercher des vidéos"; + +/* No comment provided by engineer. */ +"Search voice messages" = "Rechercher des messages vocaux"; + /* network option */ "sec" = "sec"; @@ -4224,6 +5202,9 @@ chat item action */ /* chat item text */ "security code changed" = "code de sécurité modifié"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "Sécurité : les propriétaires détiennent des clés de canal."; + /* chat item action */ "Select" = "Choisir"; @@ -4254,6 +5235,9 @@ chat item action */ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez"; +/* No comment provided by engineer. */ +"Send contact request?" = "Envoyer une demande de contact ?"; + /* No comment provided by engineer. */ "Send delivery receipts to" = "Envoyer les accusés de réception à"; @@ -4284,23 +5268,41 @@ chat item action */ /* No comment provided by engineer. */ "Send notifications" = "Envoi de notifications"; +/* No comment provided by engineer. */ +"Send private reports" = "Envoyer des signalements privés"; + /* No comment provided by engineer. */ "Send questions and ideas" = "Envoyez vos questions et idées"; /* No comment provided by engineer. */ "Send receipts" = "Envoi de justificatifs"; +/* No comment provided by engineer. */ +"Send request" = "Envoyer la demande"; + +/* No comment provided by engineer. */ +"Send request without message" = "Envoyer la demande sans message"; + +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "Envoyez le lien par n'importe quelle messagerie – il est sécurisé. Demandez de coller dans SimpleX."; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Envoyez-les depuis la phototèque ou des claviers personnalisés."; /* No comment provided by engineer. */ "Send up to 100 last messages to new members." = "Envoi des 100 derniers messages aux nouveaux membres."; +/* No comment provided by engineer. */ +"Send up to 100 last messages to new subscribers." = "Envoyer jusqu'aux 100 derniers messages aux nouveaux abonné·es."; + +/* No comment provided by engineer. */ +"Send your private feedback to groups." = "Envoyer vos remarques privées aux groupes."; + /* alert message */ "Sender cancelled file transfer." = "L'expéditeur a annulé le transfert de fichiers."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "L'expéditeur a peut-être supprimé la demande de connexion."; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "L'envoi d'un aperçu de lien peut révéler votre adresse IP au site Web. Vous pouvez modifier ceci dans les paramètres de confidentialité plus tard."; /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "L'envoi d'accusés de réception sera activé pour tous les contacts dans tous les profils de chat visibles."; @@ -4380,6 +5382,9 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "info sur la file d'attente du serveur : %1$@\n\ndernier message reçu : %2$@"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "Le serveur demande l'autorisation de se connecter au relais, vérifier le mot de passe."; + /* server test error */ "Server requires authorization to create queues, check password." = "Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe"; @@ -4413,6 +5418,9 @@ chat item action */ /* No comment provided by engineer. */ "Set 1 day" = "Définir 1 jour"; +/* No comment provided by engineer. */ +"Set chat name…" = "Paramétrer le nom de la discussion…"; + /* No comment provided by engineer. */ "Set contact name…" = "Définir le nom du contact…"; @@ -4425,6 +5433,12 @@ chat item action */ /* No comment provided by engineer. */ "Set it instead of system authentication." = "Il permet de remplacer l'authentification du système."; +/* No comment provided by engineer. */ +"Set member admission" = "Paramétrer l'admission des membres"; + +/* No comment provided by engineer. */ +"Set message expiration in chats." = "Paramétrer l'expiration des messages dans les discussions."; + /* profile update event chat item */ "set new contact address" = "a changé d'adresse de contact"; @@ -4440,6 +5454,9 @@ chat item action */ /* No comment provided by engineer. */ "Set passphrase to export" = "Définir la phrase secrète pour l'export"; +/* No comment provided by engineer. */ +"Set profile bio and welcome message." = "Définir la biographie du profil et le message d'accueil."; + /* No comment provided by engineer. */ "Set the message shown to new members!" = "Choisissez un message à l'attention des nouveaux membres !"; @@ -4452,6 +5469,12 @@ chat item action */ /* alert message */ "Settings were changed." = "Les paramètres ont été modifiés."; +/* No comment provided by engineer. */ +"Setup notifications" = "Configurer les notifications"; + +/* No comment provided by engineer. */ +"Setup routers" = "Configurer les routeurs"; + /* No comment provided by engineer. */ "Shape profile images" = "Images de profil modelable"; @@ -4471,15 +5494,30 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "Partager publiquement votre adresse"; +/* alert title */ +"Share address with SimpleX contacts?" = "Partager l'adresse avec les contacts SimpleX ?"; + +/* No comment provided by engineer. */ +"Share channel" = "Partager le canal"; + /* No comment provided by engineer. */ "Share from other apps." = "Partager depuis d'autres applications."; /* No comment provided by engineer. */ "Share link" = "Partager le lien"; +/* alert button */ +"Share old address" = "Partager l'ancienne adresse"; + +/* alert button */ +"Share old link" = "Partager l'ancien lien"; + /* No comment provided by engineer. */ "Share profile" = "Partager le profil"; +/* No comment provided by engineer. */ +"Share relay address" = "Partager l'adresse du relais"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "Partagez votre adresse SimpleX sur les réseaux sociaux."; @@ -4489,6 +5527,24 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "Partager sur SimpleX"; +/* No comment provided by engineer. */ +"Share via chat" = "Partager via la discussion"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "Partager avec les contacts SimpleX"; + +/* No comment provided by engineer. */ +"Share your address" = "Partager votre adresse"; + +/* No comment provided by engineer. */ +"Short description" = "Brève description"; + +/* No comment provided by engineer. */ +"Short link" = "Lien court"; + +/* No comment provided by engineer. */ +"Short SimpleX address" = "Adresse SimpleX courte"; + /* No comment provided by engineer. */ "Show → on messages sent via private routing." = "Afficher → sur les messages envoyés via le routage privé."; @@ -4534,6 +5590,9 @@ chat item action */ /* alert title */ "SimpleX address settings" = "Paramètres de réception automatique"; +/* simplex link type */ +"SimpleX channel link" = "Lien de canal SimpleX"; + /* No comment provided by engineer. */ "SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat et Flux ont conclu un accord pour inclure les serveurs exploités par Flux dans l'application."; @@ -4576,6 +5635,9 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocoles SimpleX audité par Trail of Bits."; +/* simplex link type */ +"SimpleX relay address" = "Adresse relais SimpleX"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "Mode incognito simplifié"; @@ -4618,6 +5680,10 @@ chat item action */ /* notification title */ "Somebody" = "Quelqu'un"; +/* blocking reason +report reason */ +"Spam" = "Spam"; + /* No comment provided by engineer. */ "Square, circle, or anything in between." = "Carré, circulaire, ou toute autre forme intermédiaire."; @@ -4625,13 +5691,13 @@ chat item action */ "standard end-to-end encryption" = "chiffrement de bout en bout standard"; /* No comment provided by engineer. */ -"Star on GitHub" = "Star sur GitHub"; +"Star on GitHub" = "Donnez une étoile sur GitHub"; /* No comment provided by engineer. */ -"Start chat" = "Démarrer le chat"; +"Start chat" = "Démarrer la discussion"; /* No comment provided by engineer. */ -"Start chat?" = "Lancer le chat ?"; +"Start chat?" = "Démarrer la discussion ?"; /* No comment provided by engineer. */ "Start migration" = "Démarrer la migration"; @@ -4645,17 +5711,20 @@ chat item action */ /* No comment provided by engineer. */ "Statistics" = "Statistiques"; +/* No comment provided by engineer. */ +"Status" = "État"; + /* No comment provided by engineer. */ "Stop" = "Arrêter"; /* No comment provided by engineer. */ -"Stop chat" = "Arrêter le chat"; +"Stop chat" = "Arrêter la discussion"; /* No comment provided by engineer. */ -"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté."; +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Arrêtez la discussion pour exporter, importer ou supprimer la base de données de la discussion. Vous ne pourrez pas recevoir et envoyer de messages pendant que la discussion est arrêtée."; /* No comment provided by engineer. */ -"Stop chat?" = "Arrêter le chat ?"; +"Stop chat?" = "Arrêter la discussion ?"; /* cancel file action */ "Stop file" = "Arrêter le fichier"; @@ -4678,6 +5747,9 @@ chat item action */ /* No comment provided by engineer. */ "Stopping chat" = "Arrêt du chat"; +/* No comment provided by engineer. */ +"Storage" = "Stockage"; + /* No comment provided by engineer. */ "strike" = "barré"; @@ -4690,15 +5762,15 @@ chat item action */ /* No comment provided by engineer. */ "Subscribed" = "Inscriptions"; +/* No comment provided by engineer. */ +"Subscriber" = "Abonné·e"; + /* No comment provided by engineer. */ "Subscription errors" = "Erreurs d'inscription"; /* No comment provided by engineer. */ "Subscriptions ignored" = "Inscriptions ignorées"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Supporter SimpleX Chat"; - /* No comment provided by engineer. */ "Switch audio and video during the call." = "Passer de l'audio à la vidéo pendant l'appel."; @@ -4717,6 +5789,9 @@ chat item action */ /* No comment provided by engineer. */ "Take picture" = "Prendre une photo"; +/* No comment provided by engineer. */ +"Talk to someone" = "Parlez à quelqu'un"; + /* No comment provided by engineer. */ "Tap button " = "Appuyez sur le bouton "; @@ -4838,6 +5913,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Le deuxième coche que nous avons manqué ! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "L'expéditeur a peut-être supprimé la demande de connexion."; + /* alert message */ "The sender will NOT be notified" = "L'expéditeur N'en sera PAS informé"; @@ -4845,7 +5923,7 @@ server test failure */ "The servers for new connections of your current chat profile **%@**." = "Les serveurs pour les nouvelles connexions de votre profil de chat actuel **%@**."; /* No comment provided by engineer. */ -"The servers for new files of your current chat profile **%@**." = "Les serveurs pour les nouveaux fichiers de votre profil de chat actuel **%@**."; +"The servers for new files of your current chat profile **%@**." = "Les serveurs pour les nouveaux fichiers de votre profil de discussion actuel **%@**."; /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; @@ -4856,6 +5934,9 @@ server test failure */ /* No comment provided by engineer. */ "Themes" = "Thèmes"; +/* No comment provided by engineer. */ +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Puis nous avons déménagé en ligne, et chaque plateforme a demandé un morceau de vous - votre nom, votre numéro, vos amis. Nous avons accepté que le prix à payer pour parler aux autres est de faire savoir à quelqu'un à qui nous parlons. À chaque génération, les gens et la technique le faisaient de cette manière : téléphone, courriels, messagers, médias sociaux. C'était le seul moyen possible."; + /* No comment provided by engineer. */ "These conditions will also apply for: **%@**." = "Ces conditions s'appliquent également aux : **%@**."; @@ -4901,6 +5982,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**."; +/* No comment provided by engineer. */ +"Time to disappear is set only for new contacts." = "Le délai de disparition est défini seulement pour les nouveaux contacts."; + /* No comment provided by engineer. */ "Title" = "Titre"; @@ -4944,7 +6028,7 @@ server test failure */ "To record voice message please grant permission to use Microphone." = "Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de chat**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page **Vos profils de discussion**."; /* No comment provided by engineer. */ "To send" = "Pour envoyer"; @@ -4964,6 +6048,9 @@ server test failure */ /* No comment provided by engineer. */ "Toolbar opacity" = "Opacité de la barre d'outils"; +/* No comment provided by engineer. */ +"Top bar" = "Barre supérieure"; + /* No comment provided by engineer. */ "Total" = "Total"; @@ -5048,9 +6135,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "À moins que vous utilisiez l'interface d'appel d'iOS, activez le mode \"Ne pas déranger\" pour éviter les interruptions."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s'agir d'un bug - veuillez le signaler.\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d'une connexion réseau stable."; - /* No comment provided by engineer. */ "Unlink" = "Délier"; @@ -5087,6 +6171,9 @@ server test failure */ /* No comment provided by engineer. */ "Update settings?" = "Mettre à jour les paramètres ?"; +/* rcv group event chat item */ +"updated channel profile" = "profil du canal mis à jour"; + /* rcv group event chat item */ "updated group profile" = "mise à jour du profil de groupe"; @@ -5171,6 +6258,9 @@ server test failure */ /* No comment provided by engineer. */ "Use the app with one hand." = "Utiliser l'application d'une main."; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "Utilisez cette adresse dans votre profil, votre site Web ou votre signature de courriel."; + /* No comment provided by engineer. */ "User selection" = "Sélection de l'utilisateur"; @@ -5183,8 +6273,8 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "Vérifier"; /* No comment provided by engineer. */ "Verify code with desktop" = "Vérifier le code avec le bureau"; @@ -5207,6 +6297,9 @@ server test failure */ /* No comment provided by engineer. */ "Verify security code" = "Vérifier le code de sécurité"; +/* relay hostname */ +"via %@" = "via %@"; + /* No comment provided by engineer. */ "Via browser" = "Via navigateur"; @@ -5240,6 +6333,9 @@ server test failure */ /* No comment provided by engineer. */ "Video will be received when your contact is online, please wait or check later!" = "La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !"; +/* No comment provided by engineer. */ +"Videos" = "Vidéos"; + /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vidéos et fichiers jusqu'à 1Go"; @@ -5273,9 +6369,18 @@ server test failure */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "Messages vocaux interdits !"; +/* alert action */ +"Wait" = "Attendez"; + +/* relay test step */ +"Wait response" = "Attendre la réponse"; + /* No comment provided by engineer. */ "waiting for answer…" = "en attente de réponse…"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "En attente que le propriétaire du canal ajoute des relais."; + /* No comment provided by engineer. */ "waiting for confirmation…" = "en attente de confirmation…"; @@ -5301,7 +6406,7 @@ server test failure */ "wants to connect to you!" = "veut établir une connexion !"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attention : démarrer une session de discussion sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages"; /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Attention : vous risquez de perdre des données !"; @@ -5321,6 +6426,9 @@ server test failure */ /* No comment provided by engineer. */ "Welcome message is too long" = "Le message de bienvenue est trop long"; +/* No comment provided by engineer. */ +"Welcome your contacts 👋" = "Accueillez vos contacts 👋"; + /* No comment provided by engineer. */ "What's new" = "Quoi de neuf ?"; @@ -5390,6 +6498,9 @@ server test failure */ /* No comment provided by engineer. */ "You accepted connection" = "Vous avez accepté la connexion"; +/* snd group event chat item */ +"you accepted this member" = "vous avez accepté ce membre"; + /* No comment provided by engineer. */ "You allow" = "Vous autorisez"; @@ -5429,6 +6540,9 @@ server test failure */ /* No comment provided by engineer. */ "you are observer" = "vous êtes observateur"; +/* No comment provided by engineer. */ +"you are subscriber" = "vous êtes abonné·e"; + /* snd group event chat item */ "you blocked %@" = "vous avez bloqué %@"; @@ -5447,9 +6561,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Vous pouvez l'activer ultérieurement via Paramètres"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application."; - /* No comment provided by engineer. */ "You can give another try." = "Vous pouvez faire un nouvel essai."; @@ -5514,7 +6625,7 @@ server test failure */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; /* No comment provided by engineer. */ -"You have to enter passphrase every time the app starts - it is not stored on the device." = "Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil."; +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Vous devez saisir la phrase secrète à chaque fois que l'application démarre ; elle n'est pas stockée sur l'appareil."; /* No comment provided by engineer. */ "You invited a contact" = "Vous avez invité votre contact"; @@ -5598,10 +6709,10 @@ server test failure */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n'est pas possible"; /* No comment provided by engineer. */ -"Your calls" = "Vos appels"; +"Your business contact" = "Votre contact professionnel"; /* No comment provided by engineer. */ -"Your chat database" = "Votre base de données de chat"; +"Your calls" = "Vos appels"; /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Votre base de données de chat n'est pas chiffrée - définisez une phrase secrète."; @@ -5610,11 +6721,17 @@ server test failure */ "Your chat preferences" = "Vos préférences de discussion"; /* No comment provided by engineer. */ -"Your chat profiles" = "Vos profils de chat"; +"Your chat profiles" = "Vos profils de discussion"; /* No comment provided by engineer. */ "Your connection was moved to %@ but an error happened when switching profile." = "Votre connexion a été déplacée vers %@ mais une erreur inattendue s'est produite lors de la redirection vers le profil."; +/* No comment provided by engineer. */ +"Your contact" = "Votre contact"; + +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s'agir d'un bug - veuillez le signaler.\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d'une connexion réseau stable."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@)."; @@ -5633,9 +6750,15 @@ server test failure */ /* No comment provided by engineer. */ "Your current profile" = "Votre profil actuel"; +/* No comment provided by engineer. */ +"Your group" = "Votre groupe"; + /* No comment provided by engineer. */ "Your ICE servers" = "Vos serveurs ICE"; +/* No comment provided by engineer. */ +"Your network" = "Votre réseau"; + /* No comment provided by engineer. */ "Your preferences" = "Vos préférences"; @@ -5657,9 +6780,18 @@ server test failure */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts."; +/* No comment provided by engineer. */ +"Your public address" = "Votre adresse publique"; + /* No comment provided by engineer. */ "Your random profile" = "Votre profil aléatoire"; +/* No comment provided by engineer. */ +"Your relay address" = "L'adresse de votre relais"; + +/* No comment provided by engineer. */ +"Your relay name" = "Le nom de votre relais"; + /* No comment provided by engineer. */ "Your server address" = "Votre adresse de serveur"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 029fb9edd5..330688163e 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -5,7 +5,7 @@ "_italic_" = "\\_dőlt_"; /* No comment provided by engineer. */ -"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- kapcsolódás a [könyvtárszolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- kézbesítési jelentések (legfeljebb 20 tagig).\n- gyorsabb és stabilabb."; +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- kapcsolódás a [könyvtárszolgáltatáshoz](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (béta)!\n- kézbesítési jelentések (legfeljebb 20 tagig).\n- gyorsabb és stabilabb."; /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabilabb üzenetkézbesítés.\n- picit továbbfejlesztett csoportok.\n- és még sok más!"; @@ -28,9 +28,6 @@ /* No comment provided by engineer. */ "(new)" = "(új)"; -/* chat link info line */ -"(signed)" = "(aláírva)"; - /* No comment provided by engineer. */ "(this device v%@)" = "(ez az eszköz: v%@)"; @@ -121,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ letöltve"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ befektetett a SimpleX Chat közösségi finanszírozásába."; + /* notification title */ "%@ is connected!" = "%@ kapcsolódott!"; @@ -136,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ kiszolgáló"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ támogatja a SimpleX Chatet."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ feltöltve"; @@ -154,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ támogatta a SimpleX Chatet. A kitűző lejárt ekkor: %2$@."; + /* time interval */ "%d days" = "%d nap"; @@ -181,6 +187,15 @@ /* time interval */ "%d months" = "%d hónap"; +/* channel owners count */ +"%d owner" = "%d tulajdonos"; + +/* channel owners count */ +"%d owners" = "%d tulajdonos"; + +/* channel members count */ +"%d owners & contributors" = "%d tulajdonos és közreműködő"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d átjátszóhoz nem sikerült kapcsolódni"; @@ -451,6 +466,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "Visszaigazolva"; +/* No comment provided by engineer. */ +"acknowledged roster" = "visszaigazolt névsor"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "Visszaigazolási hibák"; @@ -463,9 +481,18 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Aktív kapcsolatok száma"; +/* No comment provided by engineer. */ +"Add" = "Hozzáadás"; + /* No comment provided by engineer. */ "Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Cím hozzáadása a profilhoz, hogy a SimpleX partnerei megoszthassák másokkal. A profilfrissítés el lesz küldve a SimpleX partnerei számára."; +/* No comment provided by engineer. */ +"Add contributors." = "Közreműködők hozzáadása."; + +/* No comment provided by engineer. */ +"Add description" = "Leírás hozzáadása"; + /* No comment provided by engineer. */ "Add friends" = "Barátok hozzáadása"; @@ -478,6 +505,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Profil hozzáadása"; +/* No comment provided by engineer. */ +"Add relay" = "Átjátszó hozzáadása"; + +/* No comment provided by engineer. */ +"Add relays" = "Átjátszók hozzáadása"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Átjátszók hozzáadása az üzenetküldés helyreállításához."; + /* No comment provided by engineer. */ "Add server" = "Kiszolgáló hozzáadása"; @@ -487,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Munkatársak hozzáadása"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Adja hozzá ezt a kódot a weboldalához. Meg fogja jeleníteni a csatornája / csoportja előnézetét."; + /* No comment provided by engineer. */ "Add to another device" = "Hozzáadás egy másik eszközhöz"; @@ -541,6 +580,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Speciális hálózati beállítások"; +/* No comment provided by engineer. */ +"Advanced options" = "Speciális beállítások"; + /* No comment provided by engineer. */ "Advanced settings" = "Speciális beállítások"; @@ -619,6 +661,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Engedélyezés"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Beágyazás engedélyezése bárki számára"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "A hívások kezdeményezése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi."; @@ -730,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Hívás fogadása"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Bármelyik weboldal megjelenítheti az előnézetet."; + /* No comment provided by engineer. */ "App build: %@" = "Alkalmazás összeállítási száma: %@"; @@ -754,6 +802,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "Alkalmazás munkamenete"; +/* alert title */ +"App update required" = "Alkalmazásfrissítés szükséges"; + /* No comment provided by engineer. */ "App version" = "Alkalmazás verziója"; @@ -871,6 +922,9 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Hibás az üzenet azonosítója"; +/* badge alert title */ +"Badge cannot be verified" = "Nem lehetett ellenőrizni a kitűzőt"; + /* No comment provided by engineer. */ "Be free\nin your network" = "Váljon szabaddá\na saját hálózatában"; @@ -883,6 +937,9 @@ swipe action */ /* No comment provided by engineer. */ "Better calls" = "Továbbfejlesztett hívásélmény"; +/* No comment provided by engineer. */ +"Better channels 📢" = "Továbbfejlesztett csatornák 📢"; + /* No comment provided by engineer. */ "Better groups" = "Továbbfejlesztett csoportok"; @@ -1005,7 +1062,7 @@ marked deleted chat item preview text */ "Businesses" = "Üzleti"; /* No comment provided by engineer. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA)."; +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "A csevegési profillal (alapértelmezett), vagy a [kapcsolattal] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (béta)."; /* No comment provided by engineer. */ "call" = "hívás"; @@ -1022,9 +1079,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "hívás…"; -/* No comment provided by engineer. */ -"Calls" = "Hívások"; - /* No comment provided by engineer. */ "Calls prohibited!" = "A hívások le vannak tiltva!"; @@ -1060,6 +1114,12 @@ alert button new chat action */ "Cancel" = "Mégse"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Visszavonás és a csatorna törlése"; + +/* alert title */ +"Cancel creating channel?" = "Visszavonja a csatorna létrehozását?"; + /* No comment provided by engineer. */ "Cancel migration" = "Átköltöztetés visszavonása"; @@ -1096,9 +1156,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Zárolási mód módosítása"; -/* No comment provided by engineer. */ -"Change member role?" = "Módosítja a tag szerepkörét?"; - /* authentication reason */ "Change passcode" = "Jelkód módosítása"; @@ -1111,6 +1168,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "Szerepkör módosítása"; +/* No comment provided by engineer. */ +"Change role?" = "Módosítja a tag szerepkörét?"; + /* authentication reason */ "Change self-destruct mode" = "Önmegsemmisítő-mód módosítása"; @@ -1170,15 +1230,24 @@ alert subtitle */ /* alert message */ "Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Csatornaprofil módosítva. Ha menti, akkor a frissített profil el lesz küldve a csatorna feliratkozóinak."; +/* No comment provided by engineer. */ +"Channel SimpleX name" = "Csatorna SimpleX-neve"; + /* alert title */ "Channel temporarily unavailable" = "A csatorna ideiglenesen nem érhető el"; +/* No comment provided by engineer. */ +"Channel webpage" = "Csatorna weboldala"; + /* No comment provided by engineer. */ "Channel will be deleted for all subscribers - this cannot be undone!" = "A csatorna az összes feliratkozó számára törölve lesz – ez a művelet nem vonható vissza!"; /* No comment provided by engineer. */ "Channel will be deleted for you - this cannot be undone!" = "A csatorna törölve lesz az Ön számára – ez a művelet nem vonható vissza!"; +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "A csatorna %2$d átjátszóból %1$d használatával kezd el működni. Folytatja?"; + /* No comment provided by engineer. */ "Channels" = "Csatornák"; @@ -1197,6 +1266,9 @@ alert subtitle */ /* No comment provided by engineer. */ "Chat console" = "Csevegési konzol"; +/* No comment provided by engineer. */ +"Chat data" = "Csevegési adatok"; + /* No comment provided by engineer. */ "Chat database" = "Csevegési adatbázis"; @@ -1430,6 +1502,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "Gyorsabb kapcsolódás! 🚀"; +/* new chat action */ +"Connect to %@" = "Kapcsolódás hozzá: %@"; + /* No comment provided by engineer. */ "Connect to desktop" = "Társítás számítógéppel"; @@ -1520,12 +1595,12 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "A kapcsolat le van tiltva"; +/* conn error description */ +"Connection blocked: %@" = "A kapcsolat le van tiltva: %@"; + /* alert title */ "Connection error" = "Kapcsolódási hiba"; -/* conn error description */ -"Connection error (AUTH)" = "Kapcsolódási hiba (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "kapcsolat létrehozva"; @@ -1535,6 +1610,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "A kiszolgáló üzemeltetője letiltotta a kapcsolatot:\n%@"; +/* conn error description */ +"Connection link removed" = "Kapcsolódási hiba"; + /* No comment provided by engineer. */ "Connection not ready." = "A kapcsolat nem áll készen."; @@ -1565,6 +1643,9 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Kapcsolatok"; +/* No comment provided by engineer. */ +"Contact" = "Kapcsolat"; + /* profile update event chat item */ "contact %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@"; @@ -1634,12 +1715,18 @@ server test step */ /* No comment provided by engineer. */ "Contribute" = "Közreműködés"; +/* member role */ +"contributor" = "közreműködő"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Beszélgetés törölve!"; /* No comment provided by engineer. */ "Copy" = "Másolás"; +/* No comment provided by engineer. */ +"Copy code" = "Kód másolása"; + /* No comment provided by engineer. */ "Copy error" = "Hiba másolása"; @@ -1658,6 +1745,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "Csoport létrehozása véletlenszerű profillal."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "Hozzon létre egy weboldalt a csatorna előnézetének megjelenítéséhez a látogatók számára, mielőtt feliratkoznának. Üzemeltesse saját maga, vagy használjon tetszőleges statikus tárhelyet."; + /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1682,15 +1772,15 @@ server test step */ /* No comment provided by engineer. */ "Create public channel" = "Nyilvános csatorna létrehozása"; -/* No comment provided by engineer. */ -"Create public channel (BETA)" = "Nyilvános csatorna létrehozása (BÉTA)"; - /* server test step */ "Create queue" = "Várólista létrehozása"; /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX-cím létrehozása"; +/* No comment provided by engineer. */ +"Create web preview." = "Előnézet készítése a weboldalakhoz."; + /* No comment provided by engineer. */ "Create your address" = "Saját cím létrehozása"; @@ -1918,6 +2008,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Csak nálam"; +/* No comment provided by engineer. */ +"Delete from history" = "Törlés az előzményekből"; + /* No comment provided by engineer. */ "Delete group" = "Csoport törlése"; @@ -2043,13 +2136,13 @@ alert button */ "Desktop devices" = "Számítógépek"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "A(z) %1$@ célkiszolgáló címe nem kompatibilis a(z) %2$@ továbbító kiszolgáló beállításaival."; /* snd error text */ "Destination server error: %@" = "Célkiszolgáló-hiba: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "A(z) %1$@ célkiszolgáló verziója nem kompatibilis a(z) %2$@ továbbító kiszolgálóval."; /* No comment provided by engineer. */ "Detailed statistics" = "Részletes statisztikák"; @@ -2058,14 +2151,11 @@ alert button */ "Details" = "További részletek"; /* No comment provided by engineer. */ -"Develop" = "Fejlesztés"; +"Developer" = "Fejlesztői eszközök"; /* No comment provided by engineer. */ "Developer options" = "Fejlesztői beállítások"; -/* No comment provided by engineer. */ -"Developer tools" = "Fejlesztői eszközök"; - /* No comment provided by engineer. */ "Device" = "Eszköz"; @@ -2153,6 +2243,9 @@ alert button */ /* No comment provided by engineer. */ "Do it later" = "Befejezés később"; +/* No comment provided by engineer. */ +"Do not require signing messages." = "Üzenetek aláírásának mellőzése."; + /* No comment provided by engineer. */ "Do not send history to new members." = "Az előzmények ne legyenek elküldve az új tagok számára."; @@ -2183,6 +2276,9 @@ alert button */ /* No comment provided by engineer. */ "Don't miss important messages." = "Ne maradjon le a fontos üzenetekről."; +/* alert action */ +"Don't save" = "Folytatás mentés nélkül"; + /* alert action */ "Don't show again" = "Ne jelenjen meg újra"; @@ -2241,12 +2337,18 @@ chat item action */ /* No comment provided by engineer. */ "Easier to invite your friends 👋" = "Könnyebben hívhatja meg a barátait 👋"; +/* No comment provided by engineer. */ +"Easier to read." = "Könnyebb olvashatóság."; + /* chat item action */ "Edit" = "Szerkesztés"; /* No comment provided by engineer. */ "Edit channel profile" = "Csatornaprofil szerkesztése"; +/* No comment provided by engineer. */ +"Edit description" = "Leírás szerkesztése"; + /* No comment provided by engineer. */ "Edit group profile" = "Csoportprofil szerkesztése"; @@ -2281,7 +2383,7 @@ chat item action */ "Enable for all" = "Engedélyezés az összes tag számára"; /* No comment provided by engineer. */ -"Enable in direct chats (BETA)!" = "Engedélyezés a közvetlen csevegésekben (BÉTA)!"; +"Enable in direct chats (BETA)!" = "Engedélyezés a közvetlen csevegésekben (béta)!"; /* No comment provided by engineer. */ "Enable instant notifications?" = "Engedélyezi az azonnali értesítéseket?"; @@ -2403,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter correct passphrase." = "Adja meg a helyes jelmondatot."; +/* placeholder */ +"Enter description (optional)" = "Adja meg a leírást (nem kötelező)"; + /* No comment provided by engineer. */ "Enter group name…" = "Adja meg a csoport nevét…"; @@ -2430,6 +2535,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter this device name…" = "Adja meg ennek az eszköznek a nevét…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "Adja meg az oldal webcímét"; + /* placeholder */ "Enter welcome message…" = "Adja meg az üdvözlőüzenetet…"; @@ -2442,7 +2550,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "hiba"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Hiba"; /* No comment provided by engineer. */ @@ -2463,6 +2571,9 @@ chat item action */ /* alert title */ "Error adding relay" = "Hiba történt az átjátszó hozzáadásakor"; +/* alert title */ +"Error adding relays" = "Hiba történt az átjátszók hozzáadásakor"; + /* alert title */ "Error adding server" = "Hiba történt a kiszolgáló hozzáadásakor"; @@ -2541,6 +2652,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Hiba történt az adatbázis törlésekor"; +/* alert title */ +"Error deleting message" = "Hiba történt az üzenet törlésekor"; + /* alert title */ "Error deleting old database" = "Hiba történt a régi adatbázis törlésekor"; @@ -2619,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error saving ICE servers" = "Hiba történt az ICE-kiszolgálók mentésekor"; +/* alert title */ +"Error saving name" = "Hiba történt a név mentésekor"; + /* No comment provided by engineer. */ "Error saving passcode" = "Hiba történt a jelkód mentésekor"; @@ -2652,6 +2769,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; +/* alert title */ +"Error sharing address" = "Hiba történt a cím megosztásakor"; + /* alert title */ "Error sharing channel" = "Hiba történt a csatorna megosztásakor"; @@ -2701,6 +2821,7 @@ chat item action */ "error: %@" = "hiba: %@"; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Hiba: %@"; @@ -2728,7 +2849,7 @@ server test error */ "Exit without saving" = "Kilépés mentés nélkül"; /* chat item action */ -"Expand" = "Kibontás"; +"Expand" = "Felfedés"; /* No comment provided by engineer. */ "expired" = "lejárt"; @@ -2793,6 +2914,12 @@ server test error */ /* file error text */ "File server error: %@" = "Fájlkiszolgáló-hiba: %@"; +/* No comment provided by engineer. */ +"File servers" = "Fájlkiszolgálók"; + +/* copied message info */ +"File servers: %@" = "Fájlkiszolgálók: %@"; + /* No comment provided by engineer. */ "File status" = "Fájl állapota"; @@ -2901,7 +3028,7 @@ servers warning */ "For me" = "Csak magamnak"; /* No comment provided by engineer. */ -"For private routing" = "A privát útválasztáshoz"; +"For private routing" = "Privát útválasztáshoz"; /* No comment provided by engineer. */ "For social media" = "A közösségi médiához"; @@ -2978,6 +3105,9 @@ servers warning */ /* No comment provided by engineer. */ "Get notified when mentioned." = "Kapjon értesítést, ha megemlítik."; +/* No comment provided by engineer. */ +"Get SimpleX name (BETA)" = "SimpleX-név beszerzése (béta)"; + /* No comment provided by engineer. */ "Get started" = "Vágjunk bele"; @@ -3053,6 +3183,9 @@ servers warning */ /* alert message */ "Group profile was changed. If you save it, the updated profile will be sent to group members." = "Csoportprofil módosítva. Ha menti, akkor a frissített profil el lesz küldve a csoport tagjainak."; +/* No comment provided by engineer. */ +"Group webpage" = "Csoport weboldala"; + /* No comment provided by engineer. */ "Group welcome message" = "A csoport üdvözlőüzenete"; @@ -3068,6 +3201,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "Súgó"; +/* No comment provided by engineer. */ +"Help & support" = "Súgó és támogatás"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "Segítsen az adminisztrátoroknak a csoportjaik moderálásában."; @@ -3081,7 +3217,7 @@ servers warning */ "Hidden profile password" = "Rejtett profiljelszó"; /* chat item action */ -"Hide" = "Összecsukás"; +"Hide" = "Elrejtés"; /* No comment provided by engineer. */ "Hide app screen in the recent apps." = "Alkalmazás képernyőjének elrejtése a gyakran használt alkalmazások között."; @@ -3119,12 +3255,18 @@ servers warning */ /* No comment provided by engineer. */ "How to" = "Útmutató"; +/* No comment provided by engineer. */ +"How to register a test name" = "Egy név regisztrálása tesztelési céllal"; + /* No comment provided by engineer. */ "How to use it" = "Használati útmutató"; /* No comment provided by engineer. */ "How to use your servers" = "Útmutató a saját kiszolgálók használatához"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "Magyar kezelőfelület"; @@ -3404,6 +3546,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Meg fog jelenni a feliratkozóknak, és az előnézet betöltésének engedélyezésére szolgál."; + /* No comment provided by engineer. */ "Italian interface" = "Olasz kezelőfelület"; @@ -3422,6 +3567,9 @@ servers warning */ /* No comment provided by engineer. */ "Join channel" = "Csatlakozás a csatornához"; +/* new chat action */ +"Join channel %@" = "Csatlakozás a(z) %@ nevű csatornához"; + /* new chat sheet title */ "Join group" = "Csatlakozás a csoporthoz"; @@ -3464,7 +3612,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Nagy fájl!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Tudjon meg többet"; /* swipe action */ @@ -3494,6 +3642,12 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Kevesebb adatforgalom a mobilhálózatokon."; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "Tegye lehetővé mások számára a kapcsolódást a saját SimpleX-címével regisztrált néven keresztül."; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "Tegye lehetővé mások számára a csatlakozást az ezzel a csatornahivatkozással regisztrált néven keresztül."; + /* No comment provided by engineer. */ "Let someone connect to you" = "Legyen elérhető mások számára"; @@ -3566,6 +3720,9 @@ servers warning */ /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a megadott WebRTC ICE-kiszolgálók címei megfelelő formátumúak, soronként elkülönítettek, és nincsenek duplikálva."; +/* No comment provided by engineer. */ +"Manage your relays." = "Saját átjátszók kezelése."; + /* No comment provided by engineer. */ "Mark deleted for everyone" = "Jelölje meg az összes tag számára töröltként"; @@ -3623,15 +3780,6 @@ servers warning */ /* chat feature */ "Member reports" = "Tagok jelentései"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "A tag el lesz távolítva a csevegésből – ez a művelet nem vonható vissza!"; @@ -3702,7 +3850,7 @@ servers warning */ "Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; /* No comment provided by engineer. */ -"Message queue info" = "Üzenet várólista információi"; +"Message queue info" = "Üzenet várólista-információi"; /* chat feature */ "Message reactions" = "Üzenetreakciók"; @@ -3725,6 +3873,12 @@ servers warning */ /* No comment provided by engineer. */ "Message shape" = "Üzenetbuborék alakja"; +/* No comment provided by engineer. */ +"Message signing is not required." = "Nem kötelező aláírni az üzeneteket."; + +/* No comment provided by engineer. */ +"Message signing is required." = "Kötelező aláírni az üzeneteket."; + /* No comment provided by engineer. */ "Message source remains private." = "Az üzenet forrása titokban marad."; @@ -3845,6 +3999,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Hamarosan további fejlesztések érkeznek!"; +/* No comment provided by engineer. */ +"More privacy" = "További adatvédelem"; + /* No comment provided by engineer. */ "More reliable network connection." = "Megbízhatóbb hálózati kapcsolat."; @@ -3869,6 +4026,9 @@ servers warning */ /* swipe action */ "Name" = "Név"; +/* No comment provided by engineer. */ +"Name not found" = "Nem található a név"; + /* No comment provided by engineer. */ "Network & servers" = "Hálózat és kiszolgálók"; @@ -3989,6 +4149,9 @@ servers warning */ /* Authentication unavailable */ "No app password" = "Nincs alkalmazás jelszó"; +/* No comment provided by engineer. */ +"No available relays" = "Nincsenek elérhető átjátszók"; + /* No comment provided by engineer. */ "No chat relays" = "Nincsenek csevegési átjátszók"; @@ -4067,6 +4230,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "Nincsenek fogadott vagy küldött fájlok"; +/* No comment provided by engineer. */ +"No relays" = "Nincsenek átjátszók"; + /* servers error */ "No servers for private message routing." = "Nincsenek kiszolgálók a privát üzenet-útválasztáshoz."; @@ -4076,6 +4242,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "Nincsenek üzenetfogadási kiszolgálók."; +/* servers warning */ +"No servers to resolve names." = "Nincsenek kiszolgálók a nevek feloldásához."; + /* servers error */ "No servers to send files." = "Nincsenek fájlküldési kiszolgálók."; @@ -4091,12 +4260,18 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "Nincsenek olvasatlan csevegések"; +/* No comment provided by engineer. */ +"No valid link" = "Nincs érvényes hivatkozás"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Senki sem követte nyomon a beszélgetéseinket. Senki sem készített térképet arról, hogy merre jártunk. A magánéletünk nem csak egy funkció volt, hanem az életmódunk."; /* No comment provided by engineer. */ "Non-profit governance" = "Nonprofit irányítás"; +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "Egyik saját kiszolgálója sincs beállítva a SimpleX-nevek feloldásához. Állítsa be a kiszolgálókat, vagy használjon egy kapcsolattartási hivatkozást."; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Nem egy jobb zár mások ajtaján. Nem egy kedvesebb házmester, aki tiszteletben tartja az Ön magánéletét, de mégis nyilvántartást vezet minden látogatójáról. Ön itt nem csak egy vendég. Ön itt otthon van. Nincs az a hatalom, amely beléphetne ide - Ön itt szuverén."; @@ -4249,6 +4424,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Csak a partnere küldhet hangüzeneteket."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Csak az Ön fenti oldala jelenítheti meg az előnézetet."; + /* alert action alert button */ "Open" = "Megnyitás"; @@ -4371,7 +4549,7 @@ alert button */ "owners" = "tulajdonosok"; /* No comment provided by engineer. */ -"Owners" = "Tulajdonosok"; +"Owners & contributors" = "Tulajdonosok és közreműködők"; /* No comment provided by engineer. */ "Ownership: you can run your own relays." = "Tulajdonjog: saját átjátszókat üzemeltethet."; @@ -4532,9 +4710,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Korábban kapcsolódott kiszolgálók"; -/* No comment provided by engineer. */ -"Privacy & security" = "Adatvédelem és biztonság"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Saját ügyfeleinek adatvédelme."; @@ -4586,7 +4761,8 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Profiltéma"; -/* alert message */ +/* alert message +alert title */ "Profile update will be sent to your SimpleX contacts." = "A profilfrissítés el lesz küldve a SimpleX partnerei számára."; /* No comment provided by engineer. */ @@ -4658,6 +4834,9 @@ alert button */ /* No comment provided by engineer. */ "Public channels - speak freely 🚀" = "Nyilvános csatornák – mondja el szabadon a véleményét 🚀"; +/* No comment provided by engineer. */ +"Public names for your channel or business." = "Nyilvános nevek a csatornákhoz vagy az üzleti profilokhoz."; + /* No comment provided by engineer. */ "Push notifications" = "Leküldéses értesítések"; @@ -4682,14 +4861,14 @@ alert button */ /* swipe action */ "Read" = "Olvasott"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Tudjon meg többet"; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "További információ a GitHub-tárolónkban."; +"Read more in our GitHub repository." = "További információkat a GitHub-tárolónkban talál."; /* No comment provided by engineer. */ -"Read more in User Guide." = "További információ a Használati útmutatóban."; +"Read more in User Guide." = "További információkat a használati útmutatóban talál."; /* No comment provided by engineer. */ "Receipts are disabled" = "A kézbesítési jelentések le vannak tiltva"; @@ -4825,6 +5004,12 @@ swipe action */ /* No comment provided by engineer. */ "Relay test failed!" = "Nem sikerült tesztelni az átjátszót!"; +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "Az átjátszó el lesz távolítva a csatornából – ez a művelet nem vonható vissza!"; + +/* alert message */ +"Relays added: %@." = "Átjátszók hozzáadva: %@."; + /* No comment provided by engineer. */ "Reliability: many relays per channel." = "Megbízhatóság: több átjátszó is használható csatornánként."; @@ -4849,9 +5034,18 @@ swipe action */ /* alert title */ "Remove member?" = "Eltávolítja a tagot?"; +/* No comment provided by engineer. */ +"Remove name" = "Név eltávolítása"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Eltávolítja a jelmondatot a kulcstartóból?"; +/* No comment provided by engineer. */ +"Remove relay" = "Átjátszó eltávolítása"; + +/* alert title */ +"Remove relay?" = "Eltávolítja az átjátszót?"; + /* alert title */ "Remove subscriber?" = "Eltávolítja a feliratkozót?"; @@ -4951,6 +5145,9 @@ swipe action */ /* chat list item title */ "requested to connect" = "függőben lévő kapcsolat"; +/* No comment provided by engineer. */ +"Require signing messages." = "Üzenetek aláírásának kötelezővé tétele."; + /* No comment provided by engineer. */ "Required" = "Szükséges"; @@ -4978,6 +5175,9 @@ swipe action */ /* No comment provided by engineer. */ "Reset to user theme" = "Felhasználó által létrehozott téma visszaállítása"; +/* No comment provided by engineer. */ +"Resolver error: %@" = "Feloldási hiba: %@"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Új csevegési profil létrehozásához indítsa újra az alkalmazást"; @@ -5032,6 +5232,18 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Szerepkör"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csevegés összes tagja értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre a következőre fog módosulni: „%@”. A csoport az összes tagja értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "A szerepkör a következőre fog módosulni: „%@”. A csatorna összes feliratkozója értesítést fog kapni."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre a következőre fog módosulni: „%@”. A tag új meghívást fog kapni."; + /* No comment provided by engineer. */ "Run chat" = "Csevegési szolgáltatás indítása"; @@ -5044,7 +5256,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Biztonságosabb csoportok"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Mentés"; @@ -5066,6 +5279,9 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Mentés és a csoporttagok értesítése"; +/* No comment provided by engineer. */ +"Save and notify members" = "Mentés és a tagok értesítése"; + /* No comment provided by engineer. */ "Save and notify subscribers" = "Mentés és a feliratkozók értesítése"; @@ -5108,6 +5324,12 @@ chat item action */ /* alert title */ "Save servers?" = "Menti a kiszolgálókat?"; +/* alert title */ +"Save SimpleX name?" = "Menti a SimpleX-nevet?"; + +/* alert title */ +"Save webpage settings?" = "Menti a weboldal beállításait?"; + /* No comment provided by engineer. */ "Save welcome message?" = "Menti az üdvözlőüzenetet?"; @@ -5309,9 +5531,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "A fájl küldője visszavonta az átvitelt."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "A kérés küldője törölhette a kapcsolódási kérést."; - /* alert message */ "Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "A hivatkozáselőnézet küldése felfedheti az Ön IP-címét a weboldal számára. Ezt később módosíthatja az adatvédelmi beállításokban."; @@ -5369,6 +5588,9 @@ chat item action */ /* No comment provided by engineer. */ "Server" = "Kiszolgáló"; +/* No comment provided by engineer. */ +"Server %@ does not support name resolution. Configure servers, or use a connection link." = "A(z) %@ kiszolgáló nem támogatja a névfeloldást. Állítsa be a kiszolgálókat, vagy használjon kapcsolattartási hivatkozást."; + /* alert message */ "Server added to operator %@." = "Kiszolgáló hozzáadva a következő üzemeltetőhöz: %@."; @@ -5391,7 +5613,7 @@ chat item action */ "Server protocol changed." = "A kiszolgálóprotokoll módosult."; /* queue info */ -"server queue info: %@\n\nlast received msg: %@" = "a kiszolgáló várólista információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; +"server queue info: %@\n\nlast received msg: %@" = "kiszolgáló várólista-információi: %1$@\n\nutoljára fogadott üzenet: %2$@"; /* relay test error */ "Server requires authorization to connect to relay, check password." = "A kiszolgáló hitelesítést igényel az átjátszóhoz való kapcsolódáshoz, ellenőrizze a jelszavát."; @@ -5565,6 +5787,9 @@ chat item action */ /* No comment provided by engineer. */ "Show developer options" = "Fejlesztői beállítások megjelenítése"; +/* No comment provided by engineer. */ +"Show encryption" = "Titkosítás megjelenítése"; + /* No comment provided by engineer. */ "Show last messages" = "Legutóbbi üzenetek előnézetének megjelenítése"; @@ -5583,6 +5808,25 @@ chat item action */ /* No comment provided by engineer. */ "Show:" = "Megjelenítve:"; +/* No comment provided by engineer. */ +"Sign message" = "Üzenet aláírása"; + +/* chat feature */ +"Sign messages" = "Üzenetek aláírása"; + +/* alert title +copied message info */ +"Signature missing" = "Hiányzik az aláírás"; + +/* copied message info */ +"Signed" = "Aláírva"; + +/* copied message info */ +"Signed & verified" = "Aláírva és ellenőrizve"; + +/* No comment provided by engineer. */ +"Signing proves you authored this message and can't be denied later." = "Az aláírás igazolja, hogy Ön írta ezt az üzenetet, és azt később már nem lehet letagadni."; + /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -5640,12 +5884,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "SimpleX-zár bekapcsolva"; +/* No comment provided by engineer. */ +"SimpleX name" = "SimpleX-név"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "Hibás SimpleX-név"; + +/* alert title */ +"SimpleX name not verified" = "Nincs ellenőrizve a SimpleX-név"; + /* simplex link type */ "SimpleX one-time invitation" = "Egyszer használható SimpleX meghívó"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "A SimpleX protokollokat a Trail of Bits auditálta."; +/* No comment provided by engineer. */ +"SimpleX public names (BETA)" = "Nyilvános SimpleX-nevek (béta)"; + /* simplex link type */ "SimpleX relay address" = "SimpleX-átjátszó címe"; @@ -5722,6 +5978,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "Statisztikák"; +/* No comment provided by engineer. */ +"Status" = "Állapot"; + /* No comment provided by engineer. */ "Stop" = "Megállítás"; @@ -5770,6 +6029,9 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Feliratkozva"; +/* member role */ +"subscriber" = "feliratkozó"; + /* No comment provided by engineer. */ "Subscriber" = "Feliratkozó"; @@ -5819,7 +6081,7 @@ report reason */ "Subscriptions ignored" = "Mellőzött feliratkozások"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "SimpleX Chat támogatása"; +"Support the project" = "A projekt támogatása"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "Hang/Videó váltása hívás közben."; @@ -5951,6 +6213,12 @@ server test failure */ /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának módosítására tett kísérlet nem fejeződött be."; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "A kitűző egy olyan kulccsal van aláírva, amelyet az alkalmazás ezen verziója nem ismer fel. Frissítse az alkalmazást a kitűző ellenőrzéséhez."; + +/* alert message */ +"The channel required this message to be signed, but the signature is missing." = "A csatorna megköveteli az üzenet aláírását, de az hiányzik."; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "A beolvasott QR-kód nem egy SimpleX-hivatkozás."; @@ -6011,6 +6279,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "A második pipa, ami már nagyon hiányzott! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "A kérés küldője törölhette a kapcsolódási kérést."; + /* alert message */ "The sender will NOT be notified" = "A kérés küldője NEM lesz értesítve"; @@ -6020,6 +6291,18 @@ server test failure */ /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "A jelenlegi **%@** nevű csevegési profiljához tartozó új fájlok kiszolgálói."; +/* alert message */ +"The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." = "A(z) @%@ SimpleX-név SimpleX-cím nélkül lett regisztrálva. A regisztrációs oldalon adja hozzá a névhez a saját SimpleX-címét."; + +/* alert message */ +"The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." = "A(z) #%@ SimpleX-név csatornahivatkozás nélkül lett regisztrálva. A regisztrációs oldalon adjon hozzá a névhez egy csatornahivatkozást."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but it has no valid link." = "A(z) %@ SimpleX-név regisztrálva van, de nem rendelkezik érvényes hivatkozással."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." = "A(z) %@ SimpleX-név regisztrálva van, de nincs hozzáadva a profilhoz. Adja hozzá a címéhez vagy a csatornaprofiljához, amennyiben Ön a tulajdonosa."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX-hivatkozás."; @@ -6056,6 +6339,9 @@ server test failure */ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek."; +/* badge alert */ +"This badge could not be verified and may not be genuine." = "Nem sikerült ellenőrizni ezt a kitűzőt, és lehet, hogy nem eredeti."; + /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Ez a csevegés végpontok közötti titkosítással védett."; @@ -6077,9 +6363,16 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "Ez a csoport már nem létezik."; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "Ehhez a csoporthoz az alkalmazás újabb verziója szükséges. A csatlakozáshoz frissítse az alkalmazást."; + /* alert message */ "This is a chat relay address, it cannot be used to connect." = "Ez egy csevegési átjátszó címe, nem használható kapcsolódásra."; +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "Ez az utolsó aktív átjátszó. Ha eltávolítja, akkor azzal megakadályozza az üzenetek eljuttatását a feliratkozóknak."; + /* new chat action */ "This is your link for channel %@!" = "Ez a saját hivatkozása a(z) %@ nevű csatornához!"; @@ -6098,6 +6391,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting is for your current profile **%@**." = "Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik."; +/* No comment provided by engineer. */ +"This SimpleX name is not registered. Please check the name." = "Ez a SimpleX-név nincs regisztrálva. Ellenőrizze a nevet."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Az üzeneteltűnési idő csak az új partnerekre vonatkozik."; @@ -6135,7 +6431,7 @@ server test failure */ "To protect your privacy, SimpleX uses separate IDs for each of your contacts." = "Adatainak védelme érdekében a SimpleX külön azonosítókat használ minden egyes kapcsolatához."; /* No comment provided by engineer. */ -"To receive" = "A fogadáshoz"; +"To receive" = "Üzenetek fogadásához"; /* No comment provided by engineer. */ "To record speech please grant permission to use Microphone." = "A beszéd rögzítéséhez adjon engedélyt a Mikrofon használatára."; @@ -6146,11 +6442,14 @@ server test failure */ /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz."; +/* No comment provided by engineer. */ +"To resolve names" = "Nevek feloldásához"; + /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a **Csevegési profilok** menüben."; /* No comment provided by engineer. */ -"To send" = "A küldéshez"; +"To send" = "Üzenetek küldéséhez"; /* alert message */ "To send commands you must be connected." = "A parancsok küldéséhez kapcsolódva kell lennie."; @@ -6167,6 +6466,9 @@ server test failure */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) a partnere eszközén lévő kóddal."; +/* No comment provided by engineer. */ +"To verify keys with this subscriber, compare (or scan) the code on your devices." = "A kulcsok ellenőrzéséhez ezzel a feliratkozóval hasonlítsa össze (vagy olvassa be) az eszközökön található kódot."; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognitó profil használata kapcsolódáskor ki/be."; @@ -6224,6 +6526,9 @@ server test failure */ /* rcv group event chat item */ "unblocked %@" = "feloldotta %@ letiltását"; +/* No comment provided by engineer. */ +"Unconfirmed name" = "Megerősítetlen név"; + /* No comment provided by engineer. */ "Undelivered messages" = "Kézbesítetlen üzenetek"; @@ -6269,9 +6574,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakadások elkerülése érdekében."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e."; - /* No comment provided by engineer. */ "Unlink" = "Leválasztás"; @@ -6296,6 +6598,9 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "Nem támogatott kapcsolattartási hivatkozás"; +/* badge alert title */ +"Unverified badge" = "Ellenőrizetlen kitűző"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Legfeljebb az utolsó 100 üzenet lesz elküldve az új tagok számára."; @@ -6335,7 +6640,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Cím frissítése"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Frissíti a címet?"; /* No comment provided by engineer. */ @@ -6443,6 +6749,9 @@ server test failure */ /* No comment provided by engineer. */ "Use web port" = "Webport használata"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "Az Ön által használt csevegési átjátszók nem támogatják a weboldalakat."; + /* No comment provided by engineer. */ "User selection" = "Felhasználó kiválasztása"; @@ -6455,9 +6764,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* relay test step */ "Verify" = "Ellenőrzés"; @@ -6476,12 +6782,18 @@ server test failure */ /* No comment provided by engineer. */ "Verify database passphrase" = "Adatbázis jelmondatának ellenőrzése"; +/* No comment provided by engineer. */ +"Verify name" = "Név ellenőrzése"; + /* No comment provided by engineer. */ "Verify passphrase" = "Jelmondat ellenőrzése"; /* No comment provided by engineer. */ "Verify security code" = "Biztonsági kód ellenőrzése"; +/* No comment provided by engineer. */ +"Verify SimpleX names" = "SimpleX-nevek ellenőrzése"; + /* relay hostname */ "via %@" = "a következőn keresztül: %@"; @@ -6599,6 +6911,12 @@ server test failure */ /* No comment provided by engineer. */ "We made connecting simpler for new users." = "Az új felhasználók számára egyszerűbbé tettük a kapcsolatok létrehozását."; +/* No comment provided by engineer. */ +"Webpage code" = "Weboldalba ágyazható kód"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "A weboldal beállításai módosultak. Ha menti a módosításokat, a frissített beállítások el lesznek küldve a feliratkozóknak."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE-kiszolgálók"; @@ -6759,7 +7077,7 @@ server test failure */ "You can enable later via Settings" = "Később engedélyezheti a beállításokban"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; +"You can enable them later via app Your privacy settings." = "Később is engedélyezheti őket az „Adatvédelem” menüben."; /* No comment provided by engineer. */ "You can give another try." = "Megpróbálhatja még egyszer."; @@ -6797,6 +7115,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű partnerével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában."; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "A SimpleXet az alkalmazás v7-es verziójától kezdve támogathatja."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be."; @@ -6941,9 +7262,6 @@ server test failure */ /* No comment provided by engineer. */ "Your channel" = "Saját csatorna"; -/* No comment provided by engineer. */ -"Your chat database" = "Csevegési adatbázis"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; @@ -6962,6 +7280,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Partner"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "A partnere a jelenleg támogatott legnagyobb (%@) fájlméretnél nagyobbat küldött."; @@ -6992,6 +7313,9 @@ server test failure */ /* No comment provided by engineer. */ "Your network" = "Saját hálózat"; +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "Az új %1$@ nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódott.\nHa visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja."; + /* No comment provided by engineer. */ "Your preferences" = "Beállítások"; @@ -7040,3 +7364,6 @@ server test failure */ /* No comment provided by engineer. */ "Your SimpleX address" = "Profil SimpleX-címe"; +/* No comment provided by engineer. */ +"Your SimpleX name" = "Saját SimpleX-név"; + diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index c882eb662c..3544dc1813 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -28,14 +28,11 @@ /* No comment provided by engineer. */ "(new)" = "(nuovo)"; -/* chat link info line */ -"(signed)" = "(firmato)"; - /* No comment provided by engineer. */ "(this device v%@)" = "(questo dispositivo v%@)"; /* No comment provided by engineer. */ -"[Send us email](mailto:chat@simplex.chat)" = "[Inviaci un'email](mailto:chat@simplex.chat)"; +"[Send us email](mailto:chat@simplex.chat)" = "[Inviaci un'e-mail](mailto:chat@simplex.chat)"; /* No comment provided by engineer. */ "**Create 1-time link**: to create and share a new invitation link." = "**Aggiungi contatto**: per creare un nuovo link di invito."; @@ -121,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ scaricati"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ ha investito nella raccolta fondi di SimpleX Chat."; + /* notification title */ "%@ is connected!" = "%@ è connesso/a!"; @@ -136,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ server"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ sostiene SimpleX Chat."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ caricati"; @@ -154,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ ha sostenuto SimpleX Chat. La targhetta è scaduta il %2$@."; + /* time interval */ "%d days" = "%d giorni"; @@ -181,6 +187,15 @@ /* time interval */ "%d months" = "%d mesi"; +/* channel owners count */ +"%d owner" = "%d proprietario"; + +/* channel owners count */ +"%d owners" = "%d proprietari"; + +/* channel members count */ +"%d owners & contributors" = "%d proprietari e collaboratori"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d relay falliti"; @@ -194,7 +209,7 @@ channel subscriber relay bar */ "%d relays removed" = "%d relay rimossi"; /* time interval */ -"%d sec" = "%d sec"; +"%d sec" = "%d s"; /* delete after time */ "%d seconds(s)" = "%d secondo/i"; @@ -451,6 +466,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "Riconosciuto"; +/* No comment provided by engineer. */ +"acknowledged roster" = "lista riconosciuta"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "Errori di riconoscimento"; @@ -463,9 +481,18 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Connessioni attive"; +/* No comment provided by engineer. */ +"Add" = "Aggiungi"; + /* No comment provided by engineer. */ "Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX."; +/* No comment provided by engineer. */ +"Add contributors." = "Aggiungi collaboratori."; + +/* No comment provided by engineer. */ +"Add description" = "Aggiungi descrizione"; + /* No comment provided by engineer. */ "Add friends" = "Aggiungi amici"; @@ -478,6 +505,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Aggiungi profilo"; +/* No comment provided by engineer. */ +"Add relay" = "Aggiungi relay"; + +/* No comment provided by engineer. */ +"Add relays" = "Aggiungi relay"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Aggiungi relay per ripristinare la consegna dei messaggi."; + /* No comment provided by engineer. */ "Add server" = "Aggiungi server"; @@ -487,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Aggiungi membri del team"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Aggiungi questo codice alla tua pagina web. Mostrerà l'anteprima del tuo canale / gruppo."; + /* No comment provided by engineer. */ "Add to another device" = "Aggiungi ad un altro dispositivo"; @@ -541,6 +580,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Impostazioni di rete avanzate"; +/* No comment provided by engineer. */ +"Advanced options" = "Opzioni avanzate"; + /* No comment provided by engineer. */ "Advanced settings" = "Impostazioni avanzate"; @@ -619,6 +661,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Consenti"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Consenti a chiunque di incorporare"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Consenti le chiamate solo se il tuo contatto le consente."; @@ -730,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Rispondi alla chiamata"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Qualsiasi pagina web può mostrare l'anteprima."; + /* No comment provided by engineer. */ "App build: %@" = "Build dell'app: %@"; @@ -754,6 +802,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "Sessione dell'app"; +/* alert title */ +"App update required" = "Aggiornamento dell'app necessario"; + /* No comment provided by engineer. */ "App version" = "Versione dell'app"; @@ -871,6 +922,9 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "ID del messaggio errato"; +/* badge alert title */ +"Badge cannot be verified" = "La targhetta non può essere verificata"; + /* No comment provided by engineer. */ "Be free\nin your network" = "Vivi libero\nnella tua rete"; @@ -883,6 +937,9 @@ swipe action */ /* No comment provided by engineer. */ "Better calls" = "Chiamate migliorate"; +/* No comment provided by engineer. */ +"Better channels 📢" = "Canali migliorati 📢"; + /* No comment provided by engineer. */ "Better groups" = "Gruppi migliorati"; @@ -1022,9 +1079,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "chiamata…"; -/* No comment provided by engineer. */ -"Calls" = "Chiamate"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Chiamate proibite!"; @@ -1060,6 +1114,12 @@ alert button new chat action */ "Cancel" = "Annulla"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Annulla ed elimina il canale"; + +/* alert title */ +"Cancel creating channel?" = "Annullare la creazione del canale?"; + /* No comment provided by engineer. */ "Cancel migration" = "Annulla migrazione"; @@ -1096,9 +1156,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Cambia modalità di blocco"; -/* No comment provided by engineer. */ -"Change member role?" = "Cambiare ruolo del membro?"; - /* authentication reason */ "Change passcode" = "Cambia codice di accesso"; @@ -1111,6 +1168,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "Cambia ruolo"; +/* No comment provided by engineer. */ +"Change role?" = "Cambiare il ruolo?"; + /* authentication reason */ "Change self-destruct mode" = "Cambia modalità di autodistruzione"; @@ -1170,15 +1230,24 @@ alert subtitle */ /* alert message */ "Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Il profilo del canale è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato agli iscritti di canale."; +/* No comment provided by engineer. */ +"Channel SimpleX name" = "Nome SimpleX per il canale"; + /* alert title */ "Channel temporarily unavailable" = "Canale non disponibile temporaneamente"; +/* No comment provided by engineer. */ +"Channel webpage" = "Pagina web del canale"; + /* No comment provided by engineer. */ "Channel will be deleted for all subscribers - this cannot be undone!" = "Il canale verrà eliminato per tutti gli iscritti, non è reversibile!"; /* No comment provided by engineer. */ "Channel will be deleted for you - this cannot be undone!" = "Il canale verrà eliminato per te, non è reversibile!"; +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "Il canale sarà operativo con %1$d di %2$d relay. Continuare?"; + /* No comment provided by engineer. */ "Channels" = "Canali"; @@ -1197,6 +1266,9 @@ alert subtitle */ /* No comment provided by engineer. */ "Chat console" = "Console della chat"; +/* No comment provided by engineer. */ +"Chat data" = "Dati della chat"; + /* No comment provided by engineer. */ "Chat database" = "Database della chat"; @@ -1430,6 +1502,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "Connettiti più velocemente! 🚀"; +/* new chat action */ +"Connect to %@" = "Connetti a %@"; + /* No comment provided by engineer. */ "Connect to desktop" = "Connetti al desktop"; @@ -1520,12 +1595,12 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "Connessione bloccata"; +/* conn error description */ +"Connection blocked: %@" = "Connessione bloccata: %@"; + /* alert title */ "Connection error" = "Errore di connessione"; -/* conn error description */ -"Connection error (AUTH)" = "Errore di connessione (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; @@ -1535,6 +1610,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "La connessione è bloccata dall'operatore del server:\n%@"; +/* conn error description */ +"Connection link removed" = "Errore di connessione"; + /* No comment provided by engineer. */ "Connection not ready." = "Connessione non pronta."; @@ -1565,6 +1643,9 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Connessioni"; +/* No comment provided by engineer. */ +"Contact" = "Contatto"; + /* profile update event chat item */ "contact %@ changed to %@" = "contatto %1$@ cambiato in %2$@"; @@ -1614,7 +1695,7 @@ server test step */ "Contact requests from groups" = "Richieste di contatto dai gruppi"; /* No comment provided by engineer. */ -"contact should accept…" = "il contatto dovrebbe accettare…"; +"contact should accept…" = "il contatto deve accettare…"; /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "Il contatto verrà eliminato - non è reversibile!"; @@ -1634,12 +1715,18 @@ server test step */ /* No comment provided by engineer. */ "Contribute" = "Contribuisci"; +/* member role */ +"contributor" = "collaboratore"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Conversazione eliminata!"; /* No comment provided by engineer. */ "Copy" = "Copia"; +/* No comment provided by engineer. */ +"Copy code" = "Copia codice"; + /* No comment provided by engineer. */ "Copy error" = "Copia errore"; @@ -1658,6 +1745,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "Crea un gruppo usando un profilo casuale."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "Crea una pagina web per mostrare l'anteprima del tuo canale ai visitatori prima che si iscrivano. Ospitala da solo o usa un qualsiasi hosting statico."; + /* server test step */ "Create file" = "Crea file"; @@ -1682,15 +1772,15 @@ server test step */ /* No comment provided by engineer. */ "Create public channel" = "Crea canale pubblico"; -/* No comment provided by engineer. */ -"Create public channel (BETA)" = "Crea canale pubblico (BETA)"; - /* server test step */ "Create queue" = "Crea coda"; /* No comment provided by engineer. */ "Create SimpleX address" = "Crea indirizzo SimpleX"; +/* No comment provided by engineer. */ +"Create web preview." = "Crea un'anteprima web."; + /* No comment provided by engineer. */ "Create your address" = "Crea il tuo indirizzo"; @@ -1918,6 +2008,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Elimina per me"; +/* No comment provided by engineer. */ +"Delete from history" = "Elimina dalla cronologia"; + /* No comment provided by engineer. */ "Delete group" = "Elimina gruppo"; @@ -2043,13 +2136,13 @@ alert button */ "Desktop devices" = "Dispositivi desktop"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "L'indirizzo del server di destinazione di %1$@ è incompatibile con le impostazioni del server di inoltro %2$@."; /* snd error text */ "Destination server error: %@" = "Errore del server di destinazione: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "La versione del server di destinazione di %1$@ è incompatibile con il server di inoltro %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Statistiche dettagliate"; @@ -2058,14 +2151,11 @@ alert button */ "Details" = "Dettagli"; /* No comment provided by engineer. */ -"Develop" = "Sviluppa"; +"Developer" = "Strumenti di sviluppo"; /* No comment provided by engineer. */ "Developer options" = "Opzioni sviluppatore"; -/* No comment provided by engineer. */ -"Developer tools" = "Strumenti di sviluppo"; - /* No comment provided by engineer. */ "Device" = "Dispositivo"; @@ -2153,6 +2243,9 @@ alert button */ /* No comment provided by engineer. */ "Do it later" = "Fallo dopo"; +/* No comment provided by engineer. */ +"Do not require signing messages." = "Non richiedere la firma dei messaggi."; + /* No comment provided by engineer. */ "Do not send history to new members." = "Non inviare la cronologia ai nuovi membri."; @@ -2183,6 +2276,9 @@ alert button */ /* No comment provided by engineer. */ "Don't miss important messages." = "Non perdere messaggi importanti."; +/* alert action */ +"Don't save" = "Non salvare"; + /* alert action */ "Don't show again" = "Non mostrare più"; @@ -2241,12 +2337,18 @@ chat item action */ /* No comment provided by engineer. */ "Easier to invite your friends 👋" = "È più facile invitare i tuoi amici 👋"; +/* No comment provided by engineer. */ +"Easier to read." = "Lettura più facile."; + /* chat item action */ "Edit" = "Modifica"; /* No comment provided by engineer. */ "Edit channel profile" = "Modifica profilo canale"; +/* No comment provided by engineer. */ +"Edit description" = "Modifica descrizione"; + /* No comment provided by engineer. */ "Edit group profile" = "Modifica il profilo del gruppo"; @@ -2403,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter correct passphrase." = "Inserisci la password giusta."; +/* placeholder */ +"Enter description (optional)" = "Inserisci la descrizione (facoltativa)"; + /* No comment provided by engineer. */ "Enter group name…" = "Inserisci il nome del gruppo…"; @@ -2430,6 +2535,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter this device name…" = "Inserisci il nome di questo dispositivo…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "Inserisci URL della pagina"; + /* placeholder */ "Enter welcome message…" = "Inserisci il messaggio di benvenuto…"; @@ -2442,7 +2550,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "errore"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Errore"; /* No comment provided by engineer. */ @@ -2463,6 +2571,9 @@ chat item action */ /* alert title */ "Error adding relay" = "Errore di aggiunta del relay"; +/* alert title */ +"Error adding relays" = "Errore di aggiunta dei relay"; + /* alert title */ "Error adding server" = "Errore di aggiunta del server"; @@ -2541,6 +2652,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Errore nell'eliminazione del database"; +/* alert title */ +"Error deleting message" = "Errore di eliminazione del messaggio"; + /* alert title */ "Error deleting old database" = "Errore nell'eliminazione del database vecchio"; @@ -2619,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error saving ICE servers" = "Errore nel salvataggio dei server ICE"; +/* alert title */ +"Error saving name" = "Errore di salvataggio del nome"; + /* No comment provided by engineer. */ "Error saving passcode" = "Errore nel salvataggio del codice di accesso"; @@ -2638,7 +2755,7 @@ chat item action */ "Error scanning code: %@" = "Errore di scansione del codice: %@"; /* No comment provided by engineer. */ -"Error sending email" = "Errore nell'invio dell'email"; +"Error sending email" = "Errore nell'invio dell'e-mail"; /* No comment provided by engineer. */ "Error sending member contact invitation" = "Errore di invio dell'invito al contatto"; @@ -2652,6 +2769,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Errore nell'impostazione delle ricevute di consegna!"; +/* alert title */ +"Error sharing address" = "Errore di condivisione dell'indirizzo"; + /* alert title */ "Error sharing channel" = "Errore nella condivisione del canale"; @@ -2701,6 +2821,7 @@ chat item action */ "error: %@" = "errore: %@"; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Errore: %@"; @@ -2793,6 +2914,12 @@ server test error */ /* file error text */ "File server error: %@" = "Errore del server dei file: %@"; +/* No comment provided by engineer. */ +"File servers" = "Server di file"; + +/* copied message info */ +"File servers: %@" = "Server di file: %@"; + /* No comment provided by engineer. */ "File status" = "Stato del file"; @@ -2978,6 +3105,9 @@ servers warning */ /* No comment provided by engineer. */ "Get notified when mentioned." = "Ricevi una notifica quando menzionato."; +/* No comment provided by engineer. */ +"Get SimpleX name (BETA)" = "Ottieni nome SimpleX (BETA)"; + /* No comment provided by engineer. */ "Get started" = "Cominciamo"; @@ -3053,6 +3183,9 @@ servers warning */ /* alert message */ "Group profile was changed. If you save it, the updated profile will be sent to group members." = "Il profilo del gruppo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato ai membri del gruppo."; +/* No comment provided by engineer. */ +"Group webpage" = "Pagina web del gruppo"; + /* No comment provided by engineer. */ "Group welcome message" = "Messaggio di benvenuto del gruppo"; @@ -3068,6 +3201,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "Aiuto"; +/* No comment provided by engineer. */ +"Help & support" = "Aiuto e supporto"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "Aiuta gli amministratori a moderare i loro gruppi."; @@ -3119,12 +3255,18 @@ servers warning */ /* No comment provided by engineer. */ "How to" = "Come si fa"; +/* No comment provided by engineer. */ +"How to register a test name" = "Registra un nome di prova"; + /* No comment provided by engineer. */ "How to use it" = "Come si usa"; /* No comment provided by engineer. */ "How to use your servers" = "Come usare i tuoi server"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "Interfaccia in ungherese"; @@ -3404,6 +3546,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Verrà mostrato agli iscritti e usato per permettere il caricamento dell'anteprima."; + /* No comment provided by engineer. */ "Italian interface" = "Interfaccia italiana"; @@ -3422,6 +3567,9 @@ servers warning */ /* No comment provided by engineer. */ "Join channel" = "Iscriviti al canale"; +/* new chat action */ +"Join channel %@" = "Entra nel canale %@"; + /* new chat sheet title */ "Join group" = "Entra nel gruppo"; @@ -3464,7 +3612,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "File grande!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Maggiori informazioni"; /* swipe action */ @@ -3494,6 +3642,12 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Meno traffico sulle reti mobili."; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "Consenti alle persone di collegarsi tramite il nome registrato con il tuo indirizzo SimpleX."; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "Consenti alle persone di entrare attraverso il nome registrato con questo link del canale."; + /* No comment provided by engineer. */ "Let someone connect to you" = "Lascia che qualcuno si connetta a te"; @@ -3566,6 +3720,9 @@ servers warning */ /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi."; +/* No comment provided by engineer. */ +"Manage your relays." = "Gestisci i tuoi relay."; + /* No comment provided by engineer. */ "Mark deleted for everyone" = "Contrassegna eliminato per tutti"; @@ -3623,15 +3780,6 @@ servers warning */ /* chat feature */ "Member reports" = "Segnalazioni dei membri"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno notificati tutti i membri della chat."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Il ruolo del membro verrà cambiato in \"%@\". Il membro riceverà un invito nuovo."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Il membro verrà rimosso dalla chat, non è reversibile!"; @@ -3725,6 +3873,12 @@ servers warning */ /* No comment provided by engineer. */ "Message shape" = "Forma del messaggio"; +/* No comment provided by engineer. */ +"Message signing is not required." = "La firma dei messaggi non è richiesta."; + +/* No comment provided by engineer. */ +"Message signing is required." = "La firma dei messaggi è richiesta."; + /* No comment provided by engineer. */ "Message source remains private." = "La fonte del messaggio resta privata."; @@ -3845,6 +3999,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Altri miglioramenti sono in arrivo!"; +/* No comment provided by engineer. */ +"More privacy" = "Più privacy"; + /* No comment provided by engineer. */ "More reliable network connection." = "Connessione di rete più affidabile."; @@ -3869,6 +4026,9 @@ servers warning */ /* swipe action */ "Name" = "Nome"; +/* No comment provided by engineer. */ +"Name not found" = "Nome non trovato"; + /* No comment provided by engineer. */ "Network & servers" = "Rete e server"; @@ -3989,6 +4149,9 @@ servers warning */ /* Authentication unavailable */ "No app password" = "Nessuna password dell'app"; +/* No comment provided by engineer. */ +"No available relays" = "Nessun relay disponibile"; + /* No comment provided by engineer. */ "No chat relays" = "Nessun relay di chat"; @@ -4067,6 +4230,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "Nessun file ricevuto o inviato"; +/* No comment provided by engineer. */ +"No relays" = "Nessun relay"; + /* servers error */ "No servers for private message routing." = "Nessun server per l'instradamento dei messaggi privati."; @@ -4076,6 +4242,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "Nessun server per ricevere messaggi."; +/* servers warning */ +"No servers to resolve names." = "Nessun server per risolvere i nomi."; + /* servers error */ "No servers to send files." = "Nessun server per inviare file."; @@ -4091,12 +4260,18 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "Nessuna chat non letta"; +/* No comment provided by engineer. */ +"No valid link" = "Nessun link valido"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Nessuno monitorava le tue conversazioni. Nessuno disegnava una mappa delle tue posizioni. La privacy non era mai stata una caratteristica, era uno stile di vita."; /* No comment provided by engineer. */ "Non-profit governance" = "Organizzazione non a scopo di lucro"; +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "Nessuno dei tuoi server è impostato per risolvere i nomi SimpleX. Configura i server o usa un link di connessione."; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Non una serratura migliore sulla porta di qualcun altro. Non un padrone di casa più gentile che rispetta la tua privacy, ma che continua a tenere traccia di tutti i visitatori. Non sei un ospite. Sei a casa tua. Nessun re può entrarvi: sei tu il sovrano."; @@ -4143,10 +4318,10 @@ servers warning */ group pref value member criteria value time to disappear */ -"off" = "off"; +"off" = "disattivato"; /* blur media */ -"Off" = "Off"; +"Off" = "Disattivato"; /* feature offered item */ "offered %@" = "offerto %@"; @@ -4163,10 +4338,10 @@ new chat action */ "OK" = "OK"; /* No comment provided by engineer. */ -"Old database" = "Database vecchio"; +"Old database" = "Base di dati vecchia"; /* group pref value */ -"on" = "on"; +"on" = "attivato"; /* No comment provided by engineer. */ "On your phone, not on servers." = "Sul tuo telefono, non sui server."; @@ -4249,6 +4424,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Solo il tuo contatto può inviare messaggi vocali."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Solo la tua pagina soprastante può mostrare l'anteprima."; + /* alert action alert button */ "Open" = "Apri"; @@ -4371,7 +4549,7 @@ alert button */ "owners" = "proprietari"; /* No comment provided by engineer. */ -"Owners" = "Proprietari"; +"Owners & contributors" = "Proprietari e collaboratori"; /* No comment provided by engineer. */ "Ownership: you can run your own relays." = "Proprietà: puoi gestire i tuoi relay personali."; @@ -4532,9 +4710,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Server precedentemente connessi"; -/* No comment provided by engineer. */ -"Privacy & security" = "Privacy e sicurezza"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Privacy per i tuoi clienti."; @@ -4586,7 +4761,8 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Tema del profilo"; -/* alert message */ +/* alert message +alert title */ "Profile update will be sent to your SimpleX contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX."; /* No comment provided by engineer. */ @@ -4658,6 +4834,9 @@ alert button */ /* No comment provided by engineer. */ "Public channels - speak freely 🚀" = "Canali pubblici - parla liberamente 🚀"; +/* No comment provided by engineer. */ +"Public names for your channel or business." = "Nomi pubblici per il tuo canale o per il lavoro."; + /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; @@ -4682,7 +4861,7 @@ alert button */ /* swipe action */ "Read" = "Leggi"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Leggi tutto"; /* No comment provided by engineer. */ @@ -4825,6 +5004,12 @@ swipe action */ /* No comment provided by engineer. */ "Relay test failed!" = "Prova del relay fallita!"; +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "Il relay verrà rimosso dal canale, non è reversibile!"; + +/* alert message */ +"Relays added: %@." = "Relay aggiunti: %@."; + /* No comment provided by engineer. */ "Reliability: many relays per channel." = "Affidabilità: relay multipli per canale."; @@ -4849,9 +5034,18 @@ swipe action */ /* alert title */ "Remove member?" = "Rimuovere il membro?"; +/* No comment provided by engineer. */ +"Remove name" = "Rimuovi nome"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Rimuovere la password dal portachiavi?"; +/* No comment provided by engineer. */ +"Remove relay" = "Rimuovi relay"; + +/* alert title */ +"Remove relay?" = "Rimuovere il relay?"; + /* alert title */ "Remove subscriber?" = "Rimuovere l'iscritto?"; @@ -4951,6 +5145,9 @@ swipe action */ /* chat list item title */ "requested to connect" = "richiesto di connettersi"; +/* No comment provided by engineer. */ +"Require signing messages." = "Richiedi la firma dei messaggi."; + /* No comment provided by engineer. */ "Required" = "Obbligatorio"; @@ -4978,6 +5175,9 @@ swipe action */ /* No comment provided by engineer. */ "Reset to user theme" = "Ripristina al tema dell'utente"; +/* No comment provided by engineer. */ +"Resolver error: %@" = "Errore del risolutore: %@"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Riavvia l'app per creare un nuovo profilo di chat"; @@ -5032,6 +5232,18 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Ruolo"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno avvisati tutti i membri della chat."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Verranno avvisati tutti i membri del gruppo."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "Il ruolo verrà cambiato in \"%@\". Verranno avvisati tutti gli iscritti."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Il ruolo del membro verrà cambiato in \"%@\". Il membro riceverà un invito nuovo."; + /* No comment provided by engineer. */ "Run chat" = "Avvia chat"; @@ -5044,7 +5256,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Salva"; @@ -5066,6 +5279,9 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Salva e avvisa i membri del gruppo"; +/* No comment provided by engineer. */ +"Save and notify members" = "Salva e avvisa i membri"; + /* No comment provided by engineer. */ "Save and notify subscribers" = "Salva e avvisa gli iscritti"; @@ -5108,6 +5324,12 @@ chat item action */ /* alert title */ "Save servers?" = "Salvare i server?"; +/* alert title */ +"Save SimpleX name?" = "Salvare il nome SimpleX?"; + +/* alert title */ +"Save webpage settings?" = "Salvare le impostazioni della pagina web?"; + /* No comment provided by engineer. */ "Save welcome message?" = "Salvare il messaggio di benvenuto?"; @@ -5309,9 +5531,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Il mittente ha annullato il trasferimento del file."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Il mittente potrebbe aver eliminato la richiesta di connessione."; - /* alert message */ "Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "L'invio di un'anteprima del link può rivelare il tuo indirizzo IP al sito. Puoi modificarlo nelle impostazioni di Privacy più tardi."; @@ -5369,6 +5588,9 @@ chat item action */ /* No comment provided by engineer. */ "Server" = "Server"; +/* No comment provided by engineer. */ +"Server %@ does not support name resolution. Configure servers, or use a connection link." = "Il server %@ non supporta la risoluzione dei nomi. Configura i server o usa un link di connessione."; + /* alert message */ "Server added to operator %@." = "Server aggiunto all'operatore %@."; @@ -5565,6 +5787,9 @@ chat item action */ /* No comment provided by engineer. */ "Show developer options" = "Mostra opzioni sviluppatore"; +/* No comment provided by engineer. */ +"Show encryption" = "Mostra la crittografia"; + /* No comment provided by engineer. */ "Show last messages" = "Mostra ultimi messaggi"; @@ -5583,6 +5808,25 @@ chat item action */ /* No comment provided by engineer. */ "Show:" = "Mostra:"; +/* No comment provided by engineer. */ +"Sign message" = "Firma il messaggio"; + +/* chat feature */ +"Sign messages" = "Firma i messaggi"; + +/* alert title +copied message info */ +"Signature missing" = "Firma mancante"; + +/* copied message info */ +"Signed" = "Firmato"; + +/* copied message info */ +"Signed & verified" = "Firmato e verificato"; + +/* No comment provided by engineer. */ +"Signing proves you authored this message and can't be denied later." = "La firma dimostra che hai scritto questo messaggio e non può essere negato più tardi."; + /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -5640,12 +5884,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "SimpleX Lock attivato"; +/* No comment provided by engineer. */ +"SimpleX name" = "Nome SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "Errore del nome SimpleX"; + +/* alert title */ +"SimpleX name not verified" = "Nome SimpleX non verificato"; + /* simplex link type */ "SimpleX one-time invitation" = "Invito SimpleX una tantum"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Protocolli di SimpleX esaminati da Trail of Bits."; +/* No comment provided by engineer. */ +"SimpleX public names (BETA)" = "Nomi pubblici SimpleX (BETA)"; + /* simplex link type */ "SimpleX relay address" = "Indirizzo del relay SimpleX"; @@ -5722,6 +5978,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "Statistiche"; +/* No comment provided by engineer. */ +"Status" = "Stato"; + /* No comment provided by engineer. */ "Stop" = "Ferma"; @@ -5770,6 +6029,9 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Iscritto/a"; +/* member role */ +"subscriber" = "iscritto"; + /* No comment provided by engineer. */ "Subscriber" = "Iscritto"; @@ -5819,7 +6081,7 @@ report reason */ "Subscriptions ignored" = "Iscrizioni ignorate"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "Supporta SimpleX Chat"; +"Support the project" = "Sostieni il progetto"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "Cambia tra audio e video durante la chiamata."; @@ -5951,6 +6213,12 @@ server test failure */ /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Il tentativo di cambiare la password del database non è stato completato."; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "La targhetta è firmata con una chiave che questa versione dell'app non riconosce. Aggiorna l'app per verificare questa targhetta."; + +/* alert message */ +"The channel required this message to be signed, but the signature is missing." = "Il canale ha richiesto di firmare questo messaggio, ma la firma non è presente."; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Il codice che hai scansionato non è un codice QR di link SimpleX."; @@ -6011,6 +6279,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Il secondo segno di spunta che ci mancava! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Il mittente potrebbe aver eliminato la richiesta di connessione."; + /* alert message */ "The sender will NOT be notified" = "Il mittente NON verrà avvisato"; @@ -6020,6 +6291,18 @@ server test failure */ /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "I server per nuovi file del tuo profilo di chat attuale **%@**."; +/* alert message */ +"The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." = "Il nome SimpleX @%@ è registrato senza indirizzo SimpleX. Aggiungi il tuo indirizzo SimpleX al nome tramite la pagina di registrazione."; + +/* alert message */ +"The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." = "Il nome SimpleX #%@ è registrato senza link del canale. Aggiungi il link del canale al nome tramite la pagina di registrazione."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but it has no valid link." = "Il nome SimpleX %@ è registrato, ma non ha alcun link valido."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." = "Il nome SimpleX %@ è registrato, ma non aggiunto al profilo. Aggiungilo al profilo del tuo indirizzo o canale, se sei il proprietario."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; @@ -6030,7 +6313,7 @@ server test failure */ "Themes" = "Temi"; /* No comment provided by engineer. */ -"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, email, messenger, social media. Sembrava l'unico modo possibile."; +"Then we moved online, and every platform asked for a piece of you - your name, your number, your friends. We accepted that the price of talking to others is letting someone know who we talk to. Every generation, people and tech, had it this way - telephone, email, messengers, social media. It seemed the only way possible." = "Poi ci siamo trasferiti online e ogni piattaforma ha chiesto un pezzo di noi: il nome, il numero, gli amici. Abbiamo accettato che il prezzo da pagare per comunicare con gli altri fosse quello di far sapere a qualcuno con chi parliamo. Ogni generazione, sia di persone che di tecnologia, ha funzionato così: telefono, e-mail, messenger, social media. Sembrava l'unico modo possibile."; /* No comment provided by engineer. */ "There is another way. A network with no phone numbers. No usernames. No accounts. No user identities of any kind. A network that connects people and carries encrypted messages without knowing who is connected." = "C'è un'altra via. Una rete senza numeri di telefono. Senza nomi utente. Senza account. Senza identificatori utente di alcun tipo. Una rete che connette le persone e trasferisce messaggi crittografati senza sapere chi è connesso."; @@ -6056,6 +6339,9 @@ server test failure */ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile."; +/* badge alert */ +"This badge could not be verified and may not be genuine." = "Non è stato possibile verificare questa targhetta e potrebbe non essere autentica."; + /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Questa chat è protetta da crittografia end-to-end."; @@ -6077,9 +6363,16 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "Questo gruppo non esiste più."; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "Questo gruppo richiede una versione dell'app più recente. Aggiorna l'app per entrare."; + /* alert message */ "This is a chat relay address, it cannot be used to connect." = "Questo è un indirizzo di relay di chat, non può essere usato per connettersi."; +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "Questo è l'ultimo relay attivo. La sua rimozione impedirà la consegna dei messaggi agli iscritti."; + /* new chat action */ "This is your link for channel %@!" = "Questo è il tuo link per il canale %@!"; @@ -6098,6 +6391,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting is for your current profile **%@**." = "Questa impostazione è per il tuo profilo attuale **%@**."; +/* No comment provided by engineer. */ +"This SimpleX name is not registered. Please check the name." = "Questo nome SimpleX non è registrato. Controlla il nome."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Il tempo di scomparsa è impostato solo per i contatti nuovi."; @@ -6146,6 +6442,9 @@ server test failure */ /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono."; +/* No comment provided by engineer. */ +"To resolve names" = "Per risolvere nomi"; + /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina **I tuoi profili di chat**."; @@ -6167,6 +6466,9 @@ server test failure */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi."; +/* No comment provided by engineer. */ +"To verify keys with this subscriber, compare (or scan) the code on your devices." = "Per verificare le chiavi con questo iscritto, confrontate (o scansionate) il codice sui vostri dispositivi."; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; @@ -6224,6 +6526,9 @@ server test failure */ /* rcv group event chat item */ "unblocked %@" = "ha sbloccato %@"; +/* No comment provided by engineer. */ +"Unconfirmed name" = "Nome non confermato"; + /* No comment provided by engineer. */ "Undelivered messages" = "Messaggi non consegnati"; @@ -6269,9 +6574,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "A meno che non utilizzi l'interfaccia di chiamata iOS, attiva la modalità Non disturbare per evitare interruzioni."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile."; - /* No comment provided by engineer. */ "Unlink" = "Scollega"; @@ -6296,6 +6598,9 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "Link di connessione non supportato"; +/* badge alert title */ +"Unverified badge" = "Targhetta non verificata"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "Vengono inviati ai nuovi membri fino a 100 ultimi messaggi."; @@ -6335,7 +6640,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Aggiorna l'indirizzo"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Aggiornare l'indirizzo?"; /* No comment provided by engineer. */ @@ -6438,11 +6744,14 @@ server test failure */ "Use the app with one hand." = "Usa l'app con una mano sola."; /* No comment provided by engineer. */ -"Use this address in your social media profile, website, or email signature." = "Usa questo indirizzo nel tuo profilo di social media, sito web o firma email."; +"Use this address in your social media profile, website, or email signature." = "Usa questo indirizzo nel tuo profilo di social media, sito web o firma e-mail."; /* No comment provided by engineer. */ "Use web port" = "Usa porta web"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "I relay di chat usati non supportano le pagine web."; + /* No comment provided by engineer. */ "User selection" = "Selezione utente"; @@ -6455,9 +6764,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* relay test step */ "Verify" = "Verifica"; @@ -6476,12 +6782,18 @@ server test failure */ /* No comment provided by engineer. */ "Verify database passphrase" = "Verifica password del database"; +/* No comment provided by engineer. */ +"Verify name" = "Verifica nome"; + /* No comment provided by engineer. */ "Verify passphrase" = "Verifica password"; /* No comment provided by engineer. */ "Verify security code" = "Verifica codice di sicurezza"; +/* No comment provided by engineer. */ +"Verify SimpleX names" = "Verifica nomi SimpleX"; + /* relay hostname */ "via %@" = "via %@"; @@ -6599,6 +6911,12 @@ server test failure */ /* No comment provided by engineer. */ "We made connecting simpler for new users." = "Abbiamo semplificato la connessione per i nuovi utenti."; +/* No comment provided by engineer. */ +"Webpage code" = "Codice pagina web"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "Le impostazioni della pagina web sono state cambiate. Se salvi, le impostazioni aggiornate verranno inviate agli iscritti."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "Server WebRTC ICE"; @@ -6759,7 +7077,7 @@ server test failure */ "You can enable later via Settings" = "Puoi attivarle più tardi nelle impostazioni"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app."; +"You can enable them later via app Your privacy settings." = "Puoi attivarle più tardi nelle impostazioni dell'app \"La tua privacy\"."; /* No comment provided by engineer. */ "You can give another try." = "Puoi fare un altro tentativo."; @@ -6797,6 +7115,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "Puoi ancora vedere la conversazione con %@ nell'elenco delle chat."; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "Puoi sostenere SimpleX dalla versione 7 dell'app."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puoi attivare SimpleX Lock tramite le impostazioni."; @@ -6941,9 +7262,6 @@ server test failure */ /* No comment provided by engineer. */ "Your channel" = "Il tuo canale"; -/* No comment provided by engineer. */ -"Your chat database" = "Il tuo database della chat"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Il tuo database della chat non è crittografato: imposta la password per crittografarlo."; @@ -6962,6 +7280,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Il tuo contatto"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@)."; @@ -6992,6 +7313,9 @@ server test failure */ /* No comment provided by engineer. */ "Your network" = "La tua rete"; +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "Il tuo nuovo canale %1$@ è connesso a %2$d di %3$d relay.\nSe annulli, il canale verrà eliminato. Potrai crearlo di nuovo."; + /* No comment provided by engineer. */ "Your preferences" = "Le tue preferenze"; @@ -7040,3 +7364,6 @@ server test failure */ /* No comment provided by engineer. */ "Your SimpleX address" = "Il tuo indirizzo SimpleX"; +/* No comment provided by engineer. */ +"Your SimpleX name" = "Il tuo nome SimpleX"; + diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 35d8732e3f..60ef7e2d36 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -169,6 +169,10 @@ /* time interval */ "%d months" = "%d 月"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d リレーが失敗"; + /* time interval */ "%d sec" = "%d 秒"; @@ -181,6 +185,35 @@ /* time interval */ "%d weeks" = "%d 週"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%2$d 個中 %1$d 個のリレーがアクティブ"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個がエラー"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が失敗"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が削除済み"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%2$d 個中 %1$d 個のリレーが接続済み"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個がエラー"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が失敗"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が削除済み"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -447,7 +480,7 @@ swipe action */ "All group members will remain connected." = "グループ全員の接続が継続します。"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "すべてのメッセージが削除されます。この操作は元に戻せません!"; +"All messages will be deleted - this cannot be undone!" = "全てのメッセージが削除されます - これは元に戻せません!"; /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ。"; @@ -692,9 +725,6 @@ swipe action */ /* call status */ "calling…" = "発信中…"; -/* No comment provided by engineer. */ -"Calls" = "通話"; - /* alert title */ "Can't change profile" = "プロフィールを変更できません"; @@ -733,9 +763,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "ロックモードを変更"; -/* No comment provided by engineer. */ -"Change member role?" = "メンバーの役割を変更しますか?"; - /* authentication reason */ "Change passcode" = "パスコードを変更"; @@ -957,12 +984,12 @@ server test step */ /* alert title */ "Connection error" = "接続エラー"; -/* conn error description */ -"Connection error (AUTH)" = "接続エラー (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "接続済み"; +/* conn error description */ +"Connection link removed" = "接続エラー"; + /* No comment provided by engineer. */ "Connection request sent!" = "接続リクエストを送信しました!"; @@ -1264,14 +1291,11 @@ alert button */ "Desktop devices" = "デスクトップ機器"; /* No comment provided by engineer. */ -"Develop" = "開発"; +"Developer" = "開発ツール"; /* No comment provided by engineer. */ "Developer options" = "開発者向けの設定"; -/* No comment provided by engineer. */ -"Developer tools" = "開発ツール"; - /* No comment provided by engineer. */ "Device" = "端末"; @@ -1350,15 +1374,33 @@ alert button */ /* No comment provided by engineer. */ "Downgrade and open chat" = "ダウングレードしてチャットを開く"; +/* No comment provided by engineer. */ +"Download errors" = "ダウンロードエラー"; + +/* No comment provided by engineer. */ +"Download failed" = "ダウンロード失敗"; + /* server test step */ "Download file" = "ファイルをダウンロード"; +/* No comment provided by engineer. */ +"Downloaded" = "ダウンロード済"; + +/* No comment provided by engineer. */ +"Downloaded files" = "ダウンロード済ファイル"; + +/* No comment provided by engineer. */ +"Downloading archive" = "アーカイブをダウンロード中"; + /* No comment provided by engineer. */ "Duplicate display name!" = "表示の名前が重複してます!"; /* integrity error chat item */ "duplicate message" = "重複メッセージ"; +/* No comment provided by engineer. */ +"duplicates" = "重複"; + /* No comment provided by engineer. */ "Duration" = "間隔"; @@ -1371,6 +1413,9 @@ alert button */ /* No comment provided by engineer. */ "Edit group profile" = "グループのプロフィールを編集"; +/* No comment provided by engineer. */ +"Empty message!" = "メッセージが空です!"; + /* alert button */ "Enable" = "有効"; @@ -1500,7 +1545,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "エラー"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "エラー"; /* No comment provided by engineer. */ @@ -1633,6 +1678,7 @@ alert button */ "Error: " = "エラー : "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "エラー : %@"; @@ -2055,7 +2101,7 @@ server test error */ /* No comment provided by engineer. */ "Large file!" = "大きなファイル!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "さらに詳しく"; /* swipe action */ @@ -2136,12 +2182,6 @@ server test error */ /* rcv group event chat item */ "member connected" = "接続中"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "メンバーの役割が \"%@\" に変更されます。 グループメンバー全員に通知されます。"; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "メンバーの役割が \"%@\" に変更されます。 メンバーは新たな招待を受け取ります。"; - /* alert message */ "Member will be removed from group - this cannot be undone!" = "メンバーをグループから除名する (※元に戻せません※)!"; @@ -2532,9 +2572,6 @@ alert button */ /* No comment provided by engineer. */ "Preview" = "プレビュー"; -/* No comment provided by engineer. */ -"Privacy & security" = "プライバシーとセキュリティ"; - /* No comment provided by engineer. */ "Private filenames" = "プライベートなファイル名"; @@ -2598,7 +2635,7 @@ alert button */ /* swipe action */ "Read" = "読む"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "続きを読む"; /* No comment provided by engineer. */ @@ -2747,10 +2784,17 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "役割"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "メンバーの役割が \"%@\" に変更されます。 グループメンバー全員に通知されます。"; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "メンバーの役割が \"%@\" に変更されます。 メンバーは新たな招待を受け取ります。"; + /* No comment provided by engineer. */ "Run chat" = "チャット起動"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "保存"; @@ -2874,9 +2918,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "送信者がファイル転送をキャンセルしました。"; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "送信元が繋がりリクエストを削除したかもしれません。"; - /* No comment provided by engineer. */ "Sending file will be stopped." = "ファイルの送信を停止します。"; @@ -2896,7 +2937,7 @@ chat item action */ "Sent messages will be deleted after set time." = "一定時間が経ったら送信されたメッセージが削除されます。"; /* server test error */ -"Server requires authorization to create queues, check password." = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください"; +"Server requires authorization to create queues, check password." = "キューを作成するにはサーバーの認証が必要です。パスワードを確認してください。"; /* server test error */ "Server requires authorization to upload, check password." = "アップロードにはサーバーの認証が必要です。パスワードを確認してください"; @@ -3061,9 +3102,6 @@ chat item action */ /* No comment provided by engineer. */ "Submit" = "送信"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Simplex Chatを支援"; - /* No comment provided by engineer. */ "System" = "システム"; @@ -3155,6 +3193,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "長らくお待たせしました! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "送信元が繋がりリクエストを削除したかもしれません。"; + /* alert message */ "The sender will NOT be notified" = "送信者には通知されません"; @@ -3260,9 +3301,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "iOS 通話インターフェイスを使用しない場合は、中断を避けるために「おやすみモード」を有効にしてください。"; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "連絡先が接続を削除したか、このリンクがすでに使用されている場合を除き、バグである可能性がありますので、報告してください。\n接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。"; - /* No comment provided by engineer. */ "Unlock" = "ロック解除"; @@ -3320,9 +3358,6 @@ server test failure */ /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX チャット サーバーを使用する。"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify connection security" = "接続のセキュリティを確認"; @@ -3458,9 +3493,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "あとで設定から有効にできます"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。"; - /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; @@ -3575,15 +3607,15 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "あなたの通話"; -/* No comment provided by engineer. */ -"Your chat database" = "あなたのチャットデータベース"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "チャット データベースは暗号化されていません - 暗号化するにはパスフレーズを設定してください。"; /* No comment provided by engineer. */ "Your chat profiles" = "あなたのチャットプロフィール"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "連絡先が接続を削除したか、このリンクがすでに使用されている場合を除き、バグである可能性がありますので、報告してください。\n接続するには、連絡先に別の接続リンクを作成するよう依頼し、ネットワーク接続が安定していることを確認してください。"; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 407665bbec..6715a09b80 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -882,9 +882,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "bellen…"; -/* No comment provided by engineer. */ -"Calls" = "Oproepen"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Bellen niet toegestaan!"; @@ -950,9 +947,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Wijzig de vergrendelings modus"; -/* No comment provided by engineer. */ -"Change member role?" = "Rol van lid wijzigen?"; - /* authentication reason */ "Change passcode" = "Toegangscode wijzigen"; @@ -1289,15 +1283,15 @@ server test step */ /* alert title */ "Connection error" = "Verbindingsfout"; -/* conn error description */ -"Connection error (AUTH)" = "Verbindingsfout (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "verbinding gemaakt"; /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Verbinding is geblokkeerd door serveroperator:\n%@"; +/* conn error description */ +"Connection link removed" = "Verbindingsfout"; + /* No comment provided by engineer. */ "Connection not ready." = "Verbinding nog niet klaar."; @@ -1752,13 +1746,13 @@ alert button */ "Desktop devices" = "Desktop apparaten"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Het bestemmingsserveradres van %1$@ is niet compatibel met de doorstuurserverinstellingen %2$@."; /* snd error text */ "Destination server error: %@" = "Bestemmingsserverfout: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "De versie van de bestemmingsserver %1$@ is niet compatibel met de doorstuurserver %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Gedetailleerde statistieken"; @@ -1767,14 +1761,11 @@ alert button */ "Details" = "Details"; /* No comment provided by engineer. */ -"Develop" = "Ontwikkelen"; +"Developer" = "Ontwikkelaar"; /* No comment provided by engineer. */ "Developer options" = "Ontwikkelaars opties"; -/* No comment provided by engineer. */ -"Developer tools" = "Ontwikkel gereedschap"; - /* No comment provided by engineer. */ "Device" = "Apparaat"; @@ -2112,7 +2103,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "fout"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Fout"; /* No comment provided by engineer. */ @@ -2341,6 +2332,7 @@ chat item action */ "Error: " = "Fout: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Fout: %@"; @@ -3040,7 +3032,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Groot bestand!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Kom meer te weten"; /* swipe action */ @@ -3166,15 +3158,6 @@ servers warning */ /* chat feature */ "Member reports" = "Ledenrapporten"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "De rol van het lid wordt gewijzigd naar \"%@\". Alle chatleden worden op de hoogte gebracht."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "De rol van lid wordt gewijzigd in \"%@\". Het lid ontvangt een nieuwe uitnodiging."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Lid wordt verwijderd uit de chat - dit kan niet ongedaan worden gemaakt!"; @@ -3928,9 +3911,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Eerder verbonden servers"; -/* No comment provided by engineer. */ -"Privacy & security" = "Privacy en beveiliging"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Privacy voor uw klanten."; @@ -4054,7 +4034,7 @@ alert button */ /* swipe action */ "Read" = "Lees"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Lees meer"; /* No comment provided by engineer. */ @@ -4350,6 +4330,15 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rol"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "De rol van het lid wordt gewijzigd naar \"%@\". Alle chatleden worden op de hoogte gebracht."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "De rol van lid wordt gewijzigd in \"%@\". Het lid ontvangt een nieuwe uitnodiging."; + /* No comment provided by engineer. */ "Run chat" = "Chat uitvoeren"; @@ -4359,7 +4348,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Opslaan"; @@ -4570,9 +4560,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Afzender heeft bestandsoverdracht geannuleerd."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "De afzender heeft mogelijk het verbindingsverzoek verwijderd."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen."; @@ -4989,9 +4976,6 @@ report reason */ /* No comment provided by engineer. */ "Subscriptions ignored" = "Subscriptions genegeerd"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Ondersteuning van SimpleX Chat"; - /* No comment provided by engineer. */ "Switch audio and video during the call." = "Wisselen tussen audio en video tijdens het gesprek."; @@ -5137,6 +5121,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "De tweede vink die we gemist hebben! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "De afzender heeft mogelijk het verbindingsverzoek verwijderd."; + /* alert message */ "The sender will NOT be notified" = "De afzender wordt NIET op de hoogte gebracht"; @@ -5359,9 +5346,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft."; - /* No comment provided by engineer. */ "Unlink" = "Ontkoppelen"; @@ -5509,9 +5493,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify code with desktop" = "Code verifiëren met desktop"; @@ -5776,9 +5757,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "U kunt later inschakelen via Instellingen"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app."; - /* No comment provided by engineer. */ "You can give another try." = "Je kunt het nog een keer proberen."; @@ -5935,9 +5913,6 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Uw oproepen"; -/* No comment provided by engineer. */ -"Your chat database" = "Uw chat database"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Uw chat database is niet versleuteld, stel een wachtwoord in om deze te versleutelen."; @@ -5950,6 +5925,9 @@ server test failure */ /* No comment provided by engineer. */ "Your connection was moved to %@ but an error happened when switching profile." = "Uw verbinding is verplaatst naar %@, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel."; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@)."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 8905300160..07c407416a 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -921,9 +921,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "dzwonie…"; -/* No comment provided by engineer. */ -"Calls" = "Połączenia"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Połączenia zakazane!"; @@ -992,9 +989,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Zmień tryb blokady"; -/* No comment provided by engineer. */ -"Change member role?" = "Zmienić rolę członka?"; - /* authentication reason */ "Change passcode" = "Zmień pin"; @@ -1337,9 +1331,6 @@ server test step */ /* alert title */ "Connection error" = "Błąd połączenia"; -/* conn error description */ -"Connection error (AUTH)" = "Błąd połączenia (UWIERZYTELNIANIE)"; - /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; @@ -1349,6 +1340,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Połączenie zostało zablokowane przez operatora serwera:\n%@"; +/* conn error description */ +"Connection link removed" = "Błąd połączenia"; + /* No comment provided by engineer. */ "Connection not ready." = "Połączenie nie jest gotowe."; @@ -1824,13 +1818,13 @@ alert button */ "Desktop devices" = "Urządzenia komputerowe"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Adres serwera docelowego %1$@ jest niekompatybilny z ustawieniami serwera przekazującego %2$@."; /* snd error text */ "Destination server error: %@" = "Błąd docelowego serwera: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "Wersja serwera docelowego %1$@ jest niekompatybilna z serwerem przekierowującym %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Szczegółowe statystyki"; @@ -1839,14 +1833,11 @@ alert button */ "Details" = "Szczegóły"; /* No comment provided by engineer. */ -"Develop" = "Deweloperskie"; +"Developer" = "Narzędzia deweloperskie"; /* No comment provided by engineer. */ "Developer options" = "Opcje deweloperskie"; -/* No comment provided by engineer. */ -"Developer tools" = "Narzędzia deweloperskie"; - /* No comment provided by engineer. */ "Device" = "Urządzenie"; @@ -2190,7 +2181,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "błąd"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Błąd"; /* No comment provided by engineer. */ @@ -2434,6 +2425,7 @@ chat item action */ "Error: " = "Błąd: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Błąd: %@"; @@ -3173,7 +3165,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Duży plik!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Dowiedz się więcej"; /* swipe action */ @@ -3317,15 +3309,6 @@ servers warning */ /* chat feature */ "Member reports" = "Raporty członków"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Rola członka zostanie zmieniona na \"%@\". Wszyscy członkowie czatu zostaną o tym poinformowani."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Rola członka grupy zostanie zmieniona na \"%@\". Wszyscy członkowie grupy zostaną powiadomieni."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Rola członka zostanie zmieniona na \"%@\". Członek otrzyma nowe zaproszenie."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Członek zostanie usunięty z czatu – nie można tego cofnąć!"; @@ -4130,9 +4113,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Wcześniej połączone serwery"; -/* No comment provided by engineer. */ -"Privacy & security" = "Prywatność i bezpieczeństwo"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Prywatność dla Twoich klientów."; @@ -4262,7 +4242,7 @@ alert button */ /* swipe action */ "Read" = "Czytaj"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Przeczytaj więcej"; /* No comment provided by engineer. */ @@ -4579,6 +4559,15 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rola"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Rola członka zostanie zmieniona na \"%@\". Wszyscy członkowie czatu zostaną o tym poinformowani."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Rola członka grupy zostanie zmieniona na \"%@\". Wszyscy członkowie grupy zostaną powiadomieni."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Rola członka zostanie zmieniona na \"%@\". Członek otrzyma nowe zaproszenie."; + /* No comment provided by engineer. */ "Run chat" = "Uruchom czat"; @@ -4588,7 +4577,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Bezpieczniejsze grupy"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Zapisz"; @@ -4832,9 +4822,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Nadawca anulował transfer pliku."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Nadawca mógł usunąć prośbę o połączenie."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Wysyłanie potwierdzeń dostawy zostanie włączone dla wszystkich kontaktów we wszystkich widocznych profilach czatu."; @@ -5269,9 +5256,6 @@ report reason */ /* No comment provided by engineer. */ "Subscriptions ignored" = "Subskrypcje zignorowane"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Wspieraj SimpleX Chat"; - /* No comment provided by engineer. */ "Switch audio and video during the call." = "Przełączanie audio i wideo podczas połączenia."; @@ -5441,6 +5425,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Nadawca mógł usunąć prośbę o połączenie."; + /* alert message */ "The sender will NOT be notified" = "Nadawca NIE zostanie powiadomiony"; @@ -5684,9 +5671,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go.\nAby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią."; - /* No comment provided by engineer. */ "Unlink" = "Odłącz"; @@ -5744,7 +5728,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Uaktualnij adres"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Uaktualnić adres?"; /* No comment provided by engineer. */ @@ -5855,9 +5840,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify code with desktop" = "Zweryfikuj kod z komputera"; @@ -6134,9 +6116,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Możesz włączyć później w Ustawieniach"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji."; - /* No comment provided by engineer. */ "You can give another try." = "Możesz spróbować ponownie."; @@ -6302,9 +6281,6 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Twoje połączenia"; -/* No comment provided by engineer. */ -"Your chat database" = "Twoja baza danych czatu"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Baza danych czatu nie jest szyfrowana - ustaw hasło, aby ją zaszyfrować."; @@ -6323,6 +6299,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Twój kontakt"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go.\nAby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@)."; diff --git a/apps/ios/product/README.md b/apps/ios/product/README.md index 107c0e6569..fd25b09d01 100644 --- a/apps/ios/product/README.md +++ b/apps/ios/product/README.md @@ -101,7 +101,7 @@ End-to-end encrypted audio and video communication. | Call history | Call events displayed as chat items | `Shared/Views/Chat/ChatItem/CICallItemView.swift` | | Incoming call view | Dedicated UI for incoming call notifications | `Shared/Views/Call/IncomingCallView.swift` | -### 5. Privacy & Security +### 5. Your privacy Encryption, authentication, and privacy controls. diff --git a/apps/ios/product/flows/connection.md b/apps/ios/product/flows/connection.md index c621dc5124..115e420f7c 100644 --- a/apps/ios/product/flows/connection.md +++ b/apps/ios/product/flows/connection.md @@ -147,7 +147,7 @@ Establishing contact between two SimpleX Chat users. SimpleX uses no user identi |-------|-------|----------| | `ChatError.invalidConnReq` | Malformed or expired link | Alert: "Invalid connection link" | | `ChatError.unsupportedConnReq` | Link requires newer app version | Alert: "Unsupported connection link" | -| `ChatError.errorAgent(.SMP(_, .AUTH))` | Link already used or deleted | Alert: "Connection error (AUTH)" | +| `ChatError.errorAgent(.SMP(_, .AUTH))` | Link already used or deleted | Alert: "Connection link removed" | | `ChatError.errorAgent(.SMP(_, .BLOCKED(info)))` | Server operator blocked connection | Alert: "Connection blocked" with reason | | `ChatError.errorAgent(.SMP(_, .QUOTA))` | Too many undelivered messages | Alert: "Undelivered messages" | | `ChatError.errorAgent(.INTERNAL("SEUniqueID"))` | Duplicate connection attempt | Alert: "Already connected?" | diff --git a/apps/ios/product/flows/messaging.md b/apps/ios/product/flows/messaging.md index d37fefdd7d..28eddff929 100644 --- a/apps/ios/product/flows/messaging.md +++ b/apps/ios/product/flows/messaging.md @@ -147,7 +147,7 @@ Complete message lifecycle in SimpleX Chat iOS: composing, sending, receiving, e | Error | Cause | Handling | |-------|-------|----------| -| `ChatError.errorAgent(.SMP(_, .AUTH))` | Recipient queue issue | Show "Connection error (AUTH)" alert | +| `ChatError.errorAgent(.SMP(_, .AUTH))` | Recipient queue issue | Show "Connection link removed" alert | | `ChatError.errorAgent(.BROKER(_, .TIMEOUT))` | Server timeout | Retryable: show retry dialog via `chatApiSendCmdWithRetry` | | `ChatError.errorAgent(.BROKER(_, .NETWORK))` | Network failure | Retryable: show retry dialog | | Send message error | Core processing failure | `sendMessageErrorAlert` shown to user | diff --git a/apps/ios/product/views/new-chat.md b/apps/ios/product/views/new-chat.md index 2ab5f9ba8f..1ab84c098a 100644 --- a/apps/ios/product/views/new-chat.md +++ b/apps/ios/product/views/new-chat.md @@ -81,7 +81,7 @@ Accessed via `NewChatMenuButton` dropdown: ## Create Channel (`AddChannelView`) -Accessed via `NewChatMenuButton` dropdown: "Create channel (BETA)" with antenna icon (`antenna.radiowaves.left.and.right.circle.fill`). +Accessed via `NewChatMenuButton` dropdown: "Create public channel" with antenna icon (`antenna.radiowaves.left.and.right.circle.fill`). ### Three-Step Channel Creation Wizard diff --git a/apps/ios/product/views/settings.md b/apps/ios/product/views/settings.md index 3cc4da5d2b..7e7f653910 100644 --- a/apps/ios/product/views/settings.md +++ b/apps/ios/product/views/settings.md @@ -4,7 +4,7 @@ ## Purpose -Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker sheet on the chat list. +Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and Developer. Accessed from the UserPicker sheet on the chat list. ## Route / Navigation @@ -22,7 +22,7 @@ Configure all aspects of app behavior including notifications, network/servers, | Notifications | `bolt` (color varies by token status) | `NotificationsView` | Push notification mode and preview settings | | Network & servers | `externaldrive.connected.to.line.below` | `NetworkAndServers` | SMP/XFTP servers, proxy, .onion hosts, advanced network | | Audio & video calls | `video` | `CallSettings` | WebRTC relay policy, ICE servers, CallKit options | -| Privacy & security | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept | +| Your privacy | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept | | Appearance | `sun.max` | `AppearanceSettings` | Theme, language, wallpapers, chat bubbles, toolbar opacity | All rows disabled when `chatModel.chatRunning != true`. Appearance row only shown when `UIApplication.shared.supportsAlternateIcons`. @@ -77,7 +77,7 @@ Adding a relay: `NewChatRelayView` form with name, address, test, and enable tog Server validation (`validateServers_`) now returns both errors and warnings. -#### Privacy & Security (`PrivacySettings`) +#### Your privacy (`PrivacySettings`) | Setting | Description | |---|---| @@ -152,7 +152,7 @@ Database row shows exclamation octagon icon in red when `chatRunning == false`. | Row | Icon | Destination | Description | |---|---|---|---| -| Developer tools | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades | +| Developer | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades | | App version | (none) | `VersionView` | Shows "v{version} ({build})" | ## Loading / Error States diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 9f79b5dea0..ce7e446211 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -28,9 +28,6 @@ /* No comment provided by engineer. */ "(new)" = "(новое)"; -/* chat link info line */ -"(signed)" = "(с подписью)"; - /* No comment provided by engineer. */ "(this device v%@)" = "(это устройство v%@)"; @@ -121,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ загружено"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ инвестировал(а) в краудфандинг SimpleX Chat."; + /* notification title */ "%@ is connected!" = "Установлено соединение с %@!"; @@ -136,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ серверы"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ поддерживает SimpleX Chat."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ загружено"; @@ -154,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ поддерживал(а) SimpleX Chat. Срок действия значка истёк %2$@."; + /* time interval */ "%d days" = "%d дней"; @@ -181,6 +187,15 @@ /* time interval */ "%d months" = "%d мес"; +/* channel owners count */ +"%d owner" = "%d владелец"; + +/* channel owners count */ +"%d owners" = "%d владельцев"; + +/* channel members count */ +"%d owners & contributors" = "%d владельцев и авторов"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d релеев с ошибками"; @@ -451,6 +466,9 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "Подтверждено"; +/* No comment provided by engineer. */ +"acknowledged roster" = "подтверждённый список"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "Ошибки подтверждения"; @@ -463,9 +481,18 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Активные соединения"; +/* No comment provided by engineer. */ +"Add" = "Добавить"; + /* No comment provided by engineer. */ "Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Добавьте адрес в свой профиль, чтобы Ваши SimpleX контакты могли поделиться им. Профиль будет отправлен Вашим SimpleX контактам."; +/* No comment provided by engineer. */ +"Add contributors." = "Добавить соавторов."; + +/* No comment provided by engineer. */ +"Add description" = "Добавить описание"; + /* No comment provided by engineer. */ "Add friends" = "Добавить друзей"; @@ -478,6 +505,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Добавить профиль"; +/* No comment provided by engineer. */ +"Add relay" = "Добавить релей"; + +/* No comment provided by engineer. */ +"Add relays" = "Добавить релеи"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Добавить релеи для восстановления доставки сообщений."; + /* No comment provided by engineer. */ "Add server" = "Добавить сервер"; @@ -487,6 +523,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Добавить сотрудников"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Добавьте этот код на свой веб-сайт. Он отобразит предпросмотр вашего канала или группы."; + /* No comment provided by engineer. */ "Add to another device" = "Добавить на другое устройство"; @@ -541,6 +580,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Настройки сети"; +/* No comment provided by engineer. */ +"Advanced options" = "Продвинутые настройки"; + /* No comment provided by engineer. */ "Advanced settings" = "Дополнительные настройки"; @@ -619,6 +661,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "Разрешить"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "Разрешить всем встраивать"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Разрешить звонки, только если их разрешает Ваш контакт."; @@ -730,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Принять звонок"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Предпросмотр можно отобразить на любой веб-странице."; + /* No comment provided by engineer. */ "App build: %@" = "Сборка приложения: %@"; @@ -754,6 +802,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "Сессия приложения"; +/* alert title */ +"App update required" = "Необходимо обновление приложения"; + /* No comment provided by engineer. */ "App version" = "Версия приложения"; @@ -871,6 +922,9 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "Ошибка ID сообщения"; +/* badge alert title */ +"Badge cannot be verified" = "Не удалось проверить подлинность значка"; + /* No comment provided by engineer. */ "Be free\nin your network" = "Будь свободен\nв своей сети"; @@ -883,6 +937,9 @@ swipe action */ /* No comment provided by engineer. */ "Better calls" = "Улучшенные звонки"; +/* No comment provided by engineer. */ +"Better channels 📢" = "Улучшенные каналы 📢"; + /* No comment provided by engineer. */ "Better groups" = "Улучшенные группы"; @@ -1022,9 +1079,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "входящий звонок…"; -/* No comment provided by engineer. */ -"Calls" = "Звонки"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Звонки запрещены!"; @@ -1060,6 +1114,12 @@ alert button new chat action */ "Cancel" = "Отменить"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "Отменить и удалить канал"; + +/* alert title */ +"Cancel creating channel?" = "Отменить создание канала?"; + /* No comment provided by engineer. */ "Cancel migration" = "Отменить миграцию"; @@ -1096,9 +1156,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Изменить режим блокировки"; -/* No comment provided by engineer. */ -"Change member role?" = "Поменять роль члена группы?"; - /* authentication reason */ "Change passcode" = "Изменить код доступа"; @@ -1111,6 +1168,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "Поменять роль"; +/* No comment provided by engineer. */ +"Change role?" = "Изменить роль?"; + /* authentication reason */ "Change self-destruct mode" = "Изменить режим самоуничтожения"; @@ -1170,15 +1230,24 @@ alert subtitle */ /* alert message */ "Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "Профиль канала был изменен. Если Вы сохраните его, обновлённый профиль будет отправлен подписчикам канала."; +/* No comment provided by engineer. */ +"Channel SimpleX name" = "SimpleX имя канала"; + /* alert title */ "Channel temporarily unavailable" = "Канал временно недоступен"; +/* No comment provided by engineer. */ +"Channel webpage" = "Веб-страница канала"; + /* No comment provided by engineer. */ "Channel will be deleted for all subscribers - this cannot be undone!" = "Канал будет удалён для всех подписчиков - это нельзя отменить!"; /* No comment provided by engineer. */ "Channel will be deleted for you - this cannot be undone!" = "Канал будет удалён для Вас - это нельзя отменить!"; +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "Канал начнёт работу с %1$d из %2$d релеев. Продолжить?"; + /* No comment provided by engineer. */ "Channels" = "Каналы"; @@ -1197,6 +1266,9 @@ alert subtitle */ /* No comment provided by engineer. */ "Chat console" = "Консоль"; +/* No comment provided by engineer. */ +"Chat data" = "Данные чата"; + /* No comment provided by engineer. */ "Chat database" = "Архив чата"; @@ -1430,6 +1502,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "Соединяйтесь быстрее! 🚀"; +/* new chat action */ +"Connect to %@" = "Соединиться с %@"; + /* No comment provided by engineer. */ "Connect to desktop" = "Подключиться к компьютеру"; @@ -1520,12 +1595,12 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "Соединение заблокировано"; +/* conn error description */ +"Connection blocked: %@" = "Соединение заблокировано: %@"; + /* alert title */ "Connection error" = "Ошибка соединения"; -/* conn error description */ -"Connection error (AUTH)" = "Ошибка соединения (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "соединение установлено"; @@ -1535,6 +1610,9 @@ server test step */ /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Соединение заблокировано сервером оператора:\n%@"; +/* conn error description */ +"Connection link removed" = "Ошибка соединения"; + /* No comment provided by engineer. */ "Connection not ready." = "Соединение не готово."; @@ -1565,6 +1643,9 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "Соединения"; +/* No comment provided by engineer. */ +"Contact" = "Контакт"; + /* profile update event chat item */ "contact %@ changed to %@" = "контакт %1$@ изменён на %2$@"; @@ -1634,12 +1715,18 @@ server test step */ /* No comment provided by engineer. */ "Contribute" = "Внести свой вклад"; +/* member role */ +"contributor" = "соавтор"; + /* No comment provided by engineer. */ "Conversation deleted!" = "Разговор удалён!"; /* No comment provided by engineer. */ "Copy" = "Копировать"; +/* No comment provided by engineer. */ +"Copy code" = "Скопировать код"; + /* No comment provided by engineer. */ "Copy error" = "Скопировать ошибку"; @@ -1658,6 +1745,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "Создайте группу, используя случайный профиль."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "Создайте веб-страницу, чтобы показывать предпросмотр Вашего канала посетителям до подписки. Хостите её сами или используйте любой статический хостинг."; + /* server test step */ "Create file" = "Создание файла"; @@ -1682,15 +1772,15 @@ server test step */ /* No comment provided by engineer. */ "Create public channel" = "Создать публичный канал"; -/* No comment provided by engineer. */ -"Create public channel (BETA)" = "Создать публичный канал (БЕТА)"; - /* server test step */ "Create queue" = "Создание очереди"; /* No comment provided by engineer. */ "Create SimpleX address" = "Создать адрес SimpleX"; +/* No comment provided by engineer. */ +"Create web preview." = "Создать веб-предпросмотр."; + /* No comment provided by engineer. */ "Create your address" = "Создайте Ваш адрес"; @@ -1918,6 +2008,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "Удалить для меня"; +/* No comment provided by engineer. */ +"Delete from history" = "Удалить из истории"; + /* No comment provided by engineer. */ "Delete group" = "Удалить группу"; @@ -2043,13 +2136,13 @@ alert button */ "Desktop devices" = "Компьютеры"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Адрес сервера назначения %1$@ несовместим с настройками пересылающего сервера %2$@."; /* snd error text */ "Destination server error: %@" = "Ошибка сервера получателя: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "Версия сервера назначения %@ несовместима с пересылающим сервером %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "Версия сервера назначения %1$@ несовместима с пересылающим сервером %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Подробная статистика"; @@ -2058,14 +2151,11 @@ alert button */ "Details" = "Подробности"; /* No comment provided by engineer. */ -"Develop" = "Для разработчиков"; +"Developer" = "Инструменты разработчика"; /* No comment provided by engineer. */ "Developer options" = "Опции разработчика"; -/* No comment provided by engineer. */ -"Developer tools" = "Инструменты разработчика"; - /* No comment provided by engineer. */ "Device" = "Устройство"; @@ -2153,6 +2243,9 @@ alert button */ /* No comment provided by engineer. */ "Do it later" = "Отложить"; +/* No comment provided by engineer. */ +"Do not require signing messages." = "Не требовать подпись сообщений."; + /* No comment provided by engineer. */ "Do not send history to new members." = "Не отправлять историю новым членам."; @@ -2183,6 +2276,9 @@ alert button */ /* No comment provided by engineer. */ "Don't miss important messages." = "Не пропустите важные сообщения."; +/* alert action */ +"Don't save" = "Не сохранять"; + /* alert action */ "Don't show again" = "Не показывать"; @@ -2241,12 +2337,18 @@ chat item action */ /* No comment provided by engineer. */ "Easier to invite your friends 👋" = "Проще пригласить друзей 👋"; +/* No comment provided by engineer. */ +"Easier to read." = "Легче для чтения."; + /* chat item action */ "Edit" = "Редактировать"; /* No comment provided by engineer. */ "Edit channel profile" = "Редактировать профиль канала"; +/* No comment provided by engineer. */ +"Edit description" = "Редактировать описание"; + /* No comment provided by engineer. */ "Edit group profile" = "Редактировать профиль группы"; @@ -2403,6 +2505,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter correct passphrase." = "Введите правильный пароль."; +/* placeholder */ +"Enter description (optional)" = "Введите описание (необязательно)"; + /* No comment provided by engineer. */ "Enter group name…" = "Введите имя группы…"; @@ -2430,6 +2535,9 @@ chat item action */ /* No comment provided by engineer. */ "Enter this device name…" = "Введите имя этого устройства…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "Введите адрес страницы"; + /* placeholder */ "Enter welcome message…" = "Введите приветственное сообщение…"; @@ -2442,7 +2550,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "ошибка"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Ошибка"; /* No comment provided by engineer. */ @@ -2463,6 +2571,9 @@ chat item action */ /* alert title */ "Error adding relay" = "Ошибка добавления релея"; +/* alert title */ +"Error adding relays" = "Ошибка добавления релеев"; + /* alert title */ "Error adding server" = "Ошибка добавления сервера"; @@ -2541,6 +2652,9 @@ chat item action */ /* alert title */ "Error deleting database" = "Ошибка при удалении данных чата"; +/* alert title */ +"Error deleting message" = "Ошибка удаления сообщения"; + /* alert title */ "Error deleting old database" = "Ошибка при удалении предыдущей версии данных чата"; @@ -2619,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error saving ICE servers" = "Ошибка при сохранении ICE-серверов"; +/* alert title */ +"Error saving name" = "Ошибка сохранения имени"; + /* No comment provided by engineer. */ "Error saving passcode" = "Ошибка сохранения кода"; @@ -2652,6 +2769,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Ошибка настроек отчётов о доставке!"; +/* alert title */ +"Error sharing address" = "Ошибка отправки адреса"; + /* alert title */ "Error sharing channel" = "Ошибка при публикации канала"; @@ -2701,6 +2821,7 @@ chat item action */ "error: %@" = "ошибка: %@"; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Ошибка: %@"; @@ -2793,6 +2914,12 @@ server test error */ /* file error text */ "File server error: %@" = "Ошибка сервера файлов: %@"; +/* No comment provided by engineer. */ +"File servers" = "Серверы файлов"; + +/* copied message info */ +"File servers: %@" = "Серверы файлов: %@"; + /* No comment provided by engineer. */ "File status" = "Статус файла"; @@ -2978,6 +3105,9 @@ servers warning */ /* No comment provided by engineer. */ "Get notified when mentioned." = "Уведомления, когда Вас упомянули."; +/* No comment provided by engineer. */ +"Get SimpleX name (BETA)" = "Зарегистрировать SimpleX имя (BETA)"; + /* No comment provided by engineer. */ "Get started" = "Начать"; @@ -3053,6 +3183,9 @@ servers warning */ /* 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 webpage" = "Веб-страница группы"; + /* No comment provided by engineer. */ "Group welcome message" = "Приветственное сообщение группы"; @@ -3068,6 +3201,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "Помощь"; +/* No comment provided by engineer. */ +"Help & support" = "Помощь и поддержка"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "Помогайте админам модерировать их группы."; @@ -3119,12 +3255,18 @@ servers warning */ /* No comment provided by engineer. */ "How to" = "Инфо"; +/* No comment provided by engineer. */ +"How to register a test name" = "Как зарегистрировать тестовое имя"; + /* No comment provided by engineer. */ "How to use it" = "Как использовать"; /* No comment provided by engineer. */ "How to use your servers" = "Как использовать серверы"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "Венгерский интерфейс"; @@ -3404,6 +3546,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "Адрес будет показан подписчикам и разрешит загрузку предпросмотра."; + /* No comment provided by engineer. */ "Italian interface" = "Итальянский интерфейс"; @@ -3417,11 +3562,14 @@ servers warning */ "Join" = "Вступить"; /* No comment provided by engineer. */ -"Join as %@" = "Вступить как %s"; +"Join as %@" = "Вступить как %@"; /* No comment provided by engineer. */ "Join channel" = "Вступить в канал"; +/* new chat action */ +"Join channel %@" = "Вступить в канал %@"; + /* new chat sheet title */ "Join group" = "Вступить в группу"; @@ -3464,7 +3612,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Большой файл!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Узнать больше"; /* swipe action */ @@ -3494,6 +3642,12 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "Меньше трафик в мобильных сетях."; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "Позвольте людям соединяться с Вами через имя, зарегистрированное для Вашего SimpleX адреса."; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "Позвольте людям вступать через имя, зарегистрированное для ссылки этого канала."; + /* No comment provided by engineer. */ "Let someone connect to you" = "Дайте собеседнику Вашу ссылку"; @@ -3566,6 +3720,9 @@ servers warning */ /* No comment provided by engineer. */ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Пожалуйста, проверьте, что адреса WebRTC ICE-серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется."; +/* No comment provided by engineer. */ +"Manage your relays." = "Управлять своими релеями."; + /* No comment provided by engineer. */ "Mark deleted for everyone" = "Пометить как удалённое для всех"; @@ -3623,15 +3780,6 @@ servers warning */ /* chat feature */ "Member reports" = "Сообщения о нарушениях"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Роль члена будет изменена на \"%@\". Все члены группы получат уведомление."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Член будет удалён из разговора - это действие нельзя отменить!"; @@ -3725,6 +3873,12 @@ servers warning */ /* No comment provided by engineer. */ "Message shape" = "Форма сообщений"; +/* No comment provided by engineer. */ +"Message signing is not required." = "Подпись сообщений не обязательна."; + +/* No comment provided by engineer. */ +"Message signing is required." = "Подпись сообщений обязательна."; + /* No comment provided by engineer. */ "Message source remains private." = "Источник сообщения остаётся конфиденциальным."; @@ -3845,6 +3999,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Дополнительные улучшения скоро!"; +/* No comment provided by engineer. */ +"More privacy" = "Больше конфиденциальности"; + /* No comment provided by engineer. */ "More reliable network connection." = "Более надёжное соединение с сетью."; @@ -3869,6 +4026,9 @@ servers warning */ /* swipe action */ "Name" = "Имя"; +/* No comment provided by engineer. */ +"Name not found" = "Имя не найдено"; + /* No comment provided by engineer. */ "Network & servers" = "Сеть и серверы"; @@ -3989,6 +4149,9 @@ servers warning */ /* Authentication unavailable */ "No app password" = "Нет кода доступа"; +/* No comment provided by engineer. */ +"No available relays" = "Нет доступных релеев"; + /* No comment provided by engineer. */ "No chat relays" = "Нет чат-релеев"; @@ -4067,6 +4230,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "Нет полученных или отправленных файлов"; +/* No comment provided by engineer. */ +"No relays" = "Релеи отсутствуют"; + /* servers error */ "No servers for private message routing." = "Нет серверов для доставки сообщений."; @@ -4076,6 +4242,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "Нет серверов для приёма сообщений."; +/* servers warning */ +"No servers to resolve names." = "Нет серверов для разрешения имён."; + /* servers error */ "No servers to send files." = "Нет серверов для отправки файлов."; @@ -4091,12 +4260,18 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "Нет непрочитанных чатов"; +/* No comment provided by engineer. */ +"No valid link" = "Нет действительной ссылки"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "Никто не отслеживал ваши разговоры. Никто не составлял карту ваших перемещений. Конфиденциальность не была функцией - это был образ жизни."; /* No comment provided by engineer. */ "Non-profit governance" = "Некоммерческое управление"; +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "Ни один из Ваших серверов не настроен для разрешения SimpleX имён. Настройте серверы или используйте ссылку для соединения."; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "Не более надёжный замок на чужой двери. Не более вежливый хозяин, который уважает вашу частную жизнь, но всё равно ведёт учёт всех посетителей. Вы не гость. Вы у себя дома. Ни один король не войдёт в ваш дом - вы суверенны."; @@ -4249,6 +4424,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Только Ваш контакт может отправлять голосовые сообщения."; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "Предпросмотр можно отобразить только на Вашей странице, указанной выше."; + /* alert action alert button */ "Open" = "Открыть"; @@ -4371,7 +4549,7 @@ alert button */ "owners" = "владельцы"; /* No comment provided by engineer. */ -"Owners" = "Владельцы"; +"Owners & contributors" = "Владельцы и соавторы"; /* No comment provided by engineer. */ "Ownership: you can run your own relays." = "Владение: Вы можете запустить свои собственные релеи."; @@ -4532,9 +4710,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Ранее подключенные серверы"; -/* No comment provided by engineer. */ -"Privacy & security" = "Конфиденциальность"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Конфиденциальность для ваших покупателей."; @@ -4586,7 +4761,8 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "Тема профиля"; -/* alert message */ +/* alert message +alert title */ "Profile update will be sent to your SimpleX contacts." = "Обновление профиля будет отправлено Вашим SimpleX контактам."; /* No comment provided by engineer. */ @@ -4658,6 +4834,9 @@ alert button */ /* No comment provided by engineer. */ "Public channels - speak freely 🚀" = "Публичные каналы - говорите свободно 🚀"; +/* No comment provided by engineer. */ +"Public names for your channel or business." = "Публичные имена для Вашего канала или бизнеса."; + /* No comment provided by engineer. */ "Push notifications" = "Доставка уведомлений"; @@ -4682,7 +4861,7 @@ alert button */ /* swipe action */ "Read" = "Прочитано"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Узнать больше"; /* No comment provided by engineer. */ @@ -4825,6 +5004,12 @@ swipe action */ /* No comment provided by engineer. */ "Relay test failed!" = "Тест релея не пройден!"; +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "Релей будет удалён из канала - это нельзя отменить!"; + +/* alert message */ +"Relays added: %@." = "Добавлены релеи: %@."; + /* No comment provided by engineer. */ "Reliability: many relays per channel." = "Надёжность: несколько релеев на каждый канал."; @@ -4849,9 +5034,18 @@ swipe action */ /* alert title */ "Remove member?" = "Удалить члена группы?"; +/* No comment provided by engineer. */ +"Remove name" = "Удалить имя"; + /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "Удалить пароль из Keychain?"; +/* No comment provided by engineer. */ +"Remove relay" = "Удалить релей"; + +/* alert title */ +"Remove relay?" = "Удалить релей?"; + /* alert title */ "Remove subscriber?" = "Удалить подписчика?"; @@ -4951,6 +5145,9 @@ swipe action */ /* chat list item title */ "requested to connect" = "запрошено соединение"; +/* No comment provided by engineer. */ +"Require signing messages." = "Требовать подпись сообщений."; + /* No comment provided by engineer. */ "Required" = "Обязательно"; @@ -4978,6 +5175,9 @@ swipe action */ /* No comment provided by engineer. */ "Reset to user theme" = "Сбросить на тему пользователя"; +/* No comment provided by engineer. */ +"Resolver error: %@" = "Ошибка разрешения имени: %@"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль"; @@ -5032,6 +5232,18 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Роль"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Роль члена будет изменена на \"%@\". Все члены группы получат уведомление."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "Роль будет изменена на \"%@\". Все подписчики получат сообщение."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение."; + /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; @@ -5044,7 +5256,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Сохранить"; @@ -5066,6 +5279,9 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "Сохранить и уведомить членов группы"; +/* No comment provided by engineer. */ +"Save and notify members" = "Сохранить и уведомить членов группы"; + /* No comment provided by engineer. */ "Save and notify subscribers" = "Сохранить и уведомить подписчиков"; @@ -5108,6 +5324,12 @@ chat item action */ /* alert title */ "Save servers?" = "Сохранить серверы?"; +/* alert title */ +"Save SimpleX name?" = "Сохранить SimpleX имя?"; + +/* alert title */ +"Save webpage settings?" = "Сохранить настройки веб-страницы?"; + /* No comment provided by engineer. */ "Save welcome message?" = "Сохранить приветственное сообщение?"; @@ -5309,9 +5531,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Отправитель отменил передачу файла."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Отправитель мог удалить запрос на соединение."; - /* alert message */ "Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "Отправка картинки ссылки может раскрыть Ваш IP-адрес веб-сайту. Вы можете изменить это в настройках безопасности позже."; @@ -5369,6 +5588,9 @@ chat item action */ /* No comment provided by engineer. */ "Server" = "Сервер"; +/* No comment provided by engineer. */ +"Server %@ does not support name resolution. Configure servers, or use a connection link." = "Сервер %@ не поддерживает разрешение имён. Настройте серверы или используйте ссылку для соединения."; + /* alert message */ "Server added to operator %@." = "Сервер добавлен к оператору %@."; @@ -5565,6 +5787,9 @@ chat item action */ /* No comment provided by engineer. */ "Show developer options" = "Показать опции для разработчиков"; +/* No comment provided by engineer. */ +"Show encryption" = "Показывать шифрование"; + /* No comment provided by engineer. */ "Show last messages" = "Показывать последние сообщения"; @@ -5583,6 +5808,25 @@ chat item action */ /* No comment provided by engineer. */ "Show:" = "Показать:"; +/* No comment provided by engineer. */ +"Sign message" = "Подписать сообщение"; + +/* chat feature */ +"Sign messages" = "Подпись сообщений"; + +/* alert title +copied message info */ +"Signature missing" = "Подпись отсутствует"; + +/* copied message info */ +"Signed" = "Подписано"; + +/* copied message info */ +"Signed & verified" = "Подписано и проверено"; + +/* No comment provided by engineer. */ +"Signing proves you authored this message and can't be denied later." = "Подпись доказывает, что Вы — автор этого сообщения, и это нельзя будет отрицать."; + /* No comment provided by engineer. */ "SimpleX" = "SimpleX"; @@ -5640,12 +5884,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "Блокировка SimpleX включена"; +/* No comment provided by engineer. */ +"SimpleX name" = "SimpleX имя"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "Ошибка SimpleX имени"; + +/* alert title */ +"SimpleX name not verified" = "SimpleX имя не проверено"; + /* simplex link type */ "SimpleX one-time invitation" = "SimpleX одноразовая ссылка"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "Аудит SimpleX протоколов от Trail of Bits."; +/* No comment provided by engineer. */ +"SimpleX public names (BETA)" = "Публичные SimpleX имена (BETA)"; + /* simplex link type */ "SimpleX relay address" = "Адрес релея SimpleX"; @@ -5722,6 +5978,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "Статистика"; +/* No comment provided by engineer. */ +"Status" = "Статус"; + /* No comment provided by engineer. */ "Stop" = "Остановить"; @@ -5770,6 +6029,9 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "Подписано"; +/* member role */ +"subscriber" = "подписчик"; + /* No comment provided by engineer. */ "Subscriber" = "Подписчик"; @@ -5819,7 +6081,7 @@ report reason */ "Subscriptions ignored" = "Подписок игнорировано"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "Поддержать SimpleX Chat"; +"Support the project" = "Поддержать проект"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "Переключайте звук и видео во время звонка."; @@ -5951,6 +6213,12 @@ server test failure */ /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Попытка поменять пароль базы данных не была завершена."; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "Этот значок подписан ключом, который неизвестен текущей версии приложения. Обновите приложение, чтобы проверить его подлинность."; + +/* alert message */ +"The channel required this message to be signed, but the signature is missing." = "Канал требует подпись сообщений, но у этого сообщения подпись отсутствует."; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "Этот QR-код не является SimpleX-ccылкой."; @@ -6003,7 +6271,7 @@ server test failure */ "The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "Древнейшая человеческая свобода - говорить с другим человеком без слежки - построенная на инфраструктуре, которая не может её предать."; /* No comment provided by engineer. */ -"The same conditions will apply to operator **%@**." = "Те же условия будут действовать для оператора **%s**."; +"The same conditions will apply to operator **%@**." = "Те же условия будут действовать для оператора **%@**."; /* No comment provided by engineer. */ "The second preset operator in the app!" = "Второй оператор серверов в приложении!"; @@ -6011,6 +6279,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Вторая галочка - знать, что доставлено! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Отправитель мог удалить запрос на соединение."; + /* alert message */ "The sender will NOT be notified" = "Отправитель не будет уведомлён"; @@ -6020,6 +6291,18 @@ server test failure */ /* No comment provided by engineer. */ "The servers for new files of your current chat profile **%@**." = "Серверы для новых файлов Вашего текущего профиля **%@**."; +/* alert message */ +"The SimpleX name @%@ is registered without SimpleX address. Add your SimpleX address to the name via the registration page." = "SimpleX имя @%@ зарегистрировано без SimpleX адреса. Добавьте Ваш SimpleX адрес к имени на странице регистрации."; + +/* alert message */ +"The SimpleX name #%@ is registered without channel link. Add channel link to the name via the registration page." = "SimpleX имя #%@ зарегистрировано без ссылки канала. Добавьте ссылку канала к имени на странице регистрации."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but it has no valid link." = "SimpleX имя %@ зарегистрировано, но не имеет действительной ссылки."; + +/* No comment provided by engineer. */ +"The SimpleX name %@ is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner." = "SimpleX имя %@ зарегистрировано, но не добавлено в профиль. Пожалуйста, добавьте его в профиль Вашего адреса или канала, если Вы владелец."; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; @@ -6056,6 +6339,9 @@ server test failure */ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Это действие нельзя отменить - Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны."; +/* badge alert */ +"This badge could not be verified and may not be genuine." = "Не удалось проверить подлинность этого значка. Возможно, он не является подлинным."; + /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "Чат защищён сквозным шифрованием."; @@ -6077,9 +6363,16 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "Эта группа требует более новой версии приложения. Пожалуйста, обновите приложение, чтобы вступить."; + /* alert message */ "This is a chat relay address, it cannot be used to connect." = "Это адрес чат-релея, с ним нельзя соединиться."; +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "Это последний активный релей. После его удаления доставка сообщений подписчикам будет невозможна."; + /* new chat action */ "This is your link for channel %@!" = "Это ваша ссылка на канал %@!"; @@ -6098,6 +6391,9 @@ server test failure */ /* No comment provided by engineer. */ "This setting is for your current profile **%@**." = "Эта настройка применяется к Вашему текущему профилю чата **%@**."; +/* No comment provided by engineer. */ +"This SimpleX name is not registered. Please check the name." = "Это SimpleX имя не зарегистрировано. Пожалуйста, проверьте имя."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Время удаления устанавливается только для новых контактов."; @@ -6146,6 +6442,9 @@ server test failure */ /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону."; +/* No comment provided by engineer. */ +"To resolve names" = "Для разрешения имён"; + /* No comment provided by engineer. */ "To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Чтобы показать Ваш скрытый профиль, введите его пароль в поле поиска на странице **Ваши профили чата**."; @@ -6167,6 +6466,9 @@ server test failure */ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Чтобы подтвердить безопасность сквозного шифрования с Вашим контактом сравните (или сканируйте) код на ваших устройствах."; +/* No comment provided by engineer. */ +"To verify keys with this subscriber, compare (or scan) the code on your devices." = "Чтобы подтвердить ключи с этим подписчиком, сравните (или сканируйте) код на ваших устройствах."; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; @@ -6224,6 +6526,9 @@ server test failure */ /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; +/* No comment provided by engineer. */ +"Unconfirmed name" = "Неподтверждённое имя"; + /* No comment provided by engineer. */ "Undelivered messages" = "Недоставленные сообщения"; @@ -6269,9 +6574,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью."; - /* No comment provided by engineer. */ "Unlink" = "Забыть"; @@ -6296,6 +6598,9 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "Ссылка не поддерживается"; +/* badge alert title */ +"Unverified badge" = "Неподтверждённый значок"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам."; @@ -6335,7 +6640,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Обновить адрес"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Обновить адрес?"; /* No comment provided by engineer. */ @@ -6443,6 +6749,9 @@ server test failure */ /* No comment provided by engineer. */ "Use web port" = "Использовать веб-порт"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "Используемые чат-релеи не поддерживают веб-страницы."; + /* No comment provided by engineer. */ "User selection" = "Выбор пользователя"; @@ -6455,9 +6764,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* relay test step */ "Verify" = "Проверить"; @@ -6476,12 +6782,18 @@ server test failure */ /* No comment provided by engineer. */ "Verify database passphrase" = "Проверка пароля базы данных"; +/* No comment provided by engineer. */ +"Verify name" = "Проверить имя"; + /* No comment provided by engineer. */ "Verify passphrase" = "Проверить пароль"; /* No comment provided by engineer. */ "Verify security code" = "Подтвердить код безопасности"; +/* No comment provided by engineer. */ +"Verify SimpleX names" = "Проверять SimpleX имена"; + /* relay hostname */ "via %@" = "через %@"; @@ -6599,6 +6911,12 @@ server test failure */ /* No comment provided by engineer. */ "We made connecting simpler for new users." = "Мы упростили подключение для новых пользователей."; +/* No comment provided by engineer. */ +"Webpage code" = "Код веб-страницы"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "Настройки веб-страницы были изменены. Если Вы сохраните их, обновлённые настройки будут отправлены подписчикам."; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE-серверы"; @@ -6759,7 +7077,7 @@ server test failure */ "You can enable later via Settings" = "Вы можете включить их позже в Настройках"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Вы можете включить их позже в настройках Конфиденциальности."; +"You can enable them later via app Your privacy settings." = "Вы можете включить их позже в настройках Конфиденциальности."; /* No comment provided by engineer. */ "You can give another try." = "Вы можете попробовать ещё раз."; @@ -6797,6 +7115,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "Вы по-прежнему можете просмотреть разговор с %@ в списке чатов."; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "Вы можете поддержать SimpleX начиная с версии приложения v7."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Вы можете включить Блокировку SimpleX через Настройки."; @@ -6941,9 +7262,6 @@ server test failure */ /* No comment provided by engineer. */ "Your channel" = "Ваш канал"; -/* No comment provided by engineer. */ -"Your chat database" = "База данных"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "База данных НЕ зашифрована. Установите пароль, чтобы защитить Ваши данные."; @@ -6962,6 +7280,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Ваш контакт"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; @@ -6992,6 +7313,9 @@ server test failure */ /* No comment provided by engineer. */ "Your network" = "Ваша сеть"; +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "Ваш новый канал %1$@ подключен к %2$d из %3$d релеев.\nЕсли Вы отмените, канал будет удалён - Вы сможете создать его снова."; + /* No comment provided by engineer. */ "Your preferences" = "Ваши предпочтения"; @@ -7040,3 +7364,6 @@ server test failure */ /* No comment provided by engineer. */ "Your SimpleX address" = "Ваш адрес SimpleX"; +/* No comment provided by engineer. */ +"Your SimpleX name" = "Ваше SimpleX имя"; + diff --git a/apps/ios/spec/client/navigation.md b/apps/ios/spec/client/navigation.md index 22985c6fe1..920780cc0f 100644 --- a/apps/ios/spec/client/navigation.md +++ b/apps/ios/spec/client/navigation.md @@ -299,7 +299,7 @@ Migration state (`ChatModel.migrationState != nil`) takes precedence over onboar ### Entry Point -`NewChatMenuButton` includes a NavigationLink "Create channel (BETA)" with antenna icon, navigating to `AddChannelView`. +`NewChatMenuButton` includes a NavigationLink "Create public channel" with antenna icon, navigating to `AddChannelView`. ### Three-Step Wizard diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index cc3abea189..8114685292 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -464,9 +464,6 @@ swipe action */ /* call status */ "calling…" = "กำลังโทร…"; -/* No comment provided by engineer. */ -"Calls" = "โทร"; - /* No comment provided by engineer. */ "Can't invite contact!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; @@ -496,9 +493,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "เปลี่ยนโหมดล็อค"; -/* No comment provided by engineer. */ -"Change member role?" = "เปลี่ยนบทบาทของสมาชิก?"; - /* authentication reason */ "Change passcode" = "เปลี่ยนรหัสผ่าน"; @@ -660,12 +654,12 @@ server test step */ /* alert title */ "Connection error" = "การเชื่อมต่อผิดพลาด"; -/* conn error description */ -"Connection error (AUTH)" = "การเชื่อมต่อผิดพลาด (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "สร้างการเชื่อมต่อแล้ว"; +/* conn error description */ +"Connection link removed" = "การเชื่อมต่อผิดพลาด"; + /* No comment provided by engineer. */ "Connection request sent!" = "ส่งคําขอเชื่อมต่อแล้ว!"; @@ -943,10 +937,7 @@ alert button */ "Description" = "คำอธิบาย"; /* No comment provided by engineer. */ -"Develop" = "พัฒนา"; - -/* No comment provided by engineer. */ -"Developer tools" = "เครื่องมือสำหรับนักพัฒนา"; +"Developer" = "เครื่องมือสำหรับนักพัฒนา"; /* No comment provided by engineer. */ "Device" = "อุปกรณ์"; @@ -1164,7 +1155,7 @@ alert button */ /* No comment provided by engineer. */ "error" = "ผิดพลาด"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "ผิดพลาด"; /* No comment provided by engineer. */ @@ -1294,6 +1285,7 @@ alert button */ "Error: " = "ผิดพลาด: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "ข้อผิดพลาด: % @"; @@ -1710,7 +1702,7 @@ server test error */ /* No comment provided by engineer. */ "Large file!" = "ไฟล์ขนาดใหญ่!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "ศึกษาเพิ่มเติม"; /* swipe action */ @@ -1791,12 +1783,6 @@ server test error */ /* rcv group event chat item */ "member connected" = "เชื่อมต่อสำเร็จ"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "บทบาทของสมาชิกจะถูกเปลี่ยนเป็น \"%@\" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง"; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "บทบาทของสมาชิกจะถูกเปลี่ยนเป็น \"%@\" สมาชิกจะได้รับคำเชิญใหม่"; - /* alert message */ "Member will be removed from group - this cannot be undone!" = "สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!"; @@ -2171,9 +2157,6 @@ new chat action */ /* No comment provided by engineer. */ "Preview" = "ดูตัวอย่าง"; -/* No comment provided by engineer. */ -"Privacy & security" = "ความเป็นส่วนตัวและความปลอดภัย"; - /* No comment provided by engineer. */ "Private filenames" = "ชื่อไฟล์ส่วนตัว"; @@ -2234,7 +2217,7 @@ new chat action */ /* swipe action */ "Read" = "อ่าน"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "อ่านเพิ่มเติม"; /* No comment provided by engineer. */ @@ -2383,7 +2366,8 @@ swipe action */ /* No comment provided by engineer. */ "Run chat" = "เรียกใช้แชท"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "บันทึก"; @@ -2510,9 +2494,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "ผู้ส่งยกเลิกการโอนไฟล์"; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว"; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิดในโปรไฟล์แชทที่มองเห็นได้ทั้งหมด"; @@ -2700,9 +2681,6 @@ chat item action */ /* No comment provided by engineer. */ "Submit" = "ส่ง"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "สนับสนุน SimpleX แชท"; - /* No comment provided by engineer. */ "System" = "ระบบ"; @@ -2794,6 +2772,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว"; + /* alert message */ "The sender will NOT be notified" = "ผู้ส่งจะไม่ได้รับแจ้ง"; @@ -2896,9 +2877,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน"; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน\nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร"; - /* No comment provided by engineer. */ "Unlock" = "ปลดล็อค"; @@ -2950,9 +2928,6 @@ server test failure */ /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify connection security" = "ตรวจสอบความปลอดภัยในการเชื่อมต่อ"; @@ -3088,9 +3063,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป"; - /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; @@ -3199,15 +3171,15 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "การโทรของคุณ"; -/* No comment provided by engineer. */ -"Your chat database" = "ฐานข้อมูลการแชทของคุณ"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt"; /* No comment provided by engineer. */ "Your chat profiles" = "โปรไฟล์แชทของคุณ"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน\nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร"; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e06989afee..b0dd32c383 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -109,6 +109,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ indirildi"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@, SimpleX Chat kitle fonlamasına yatırım yaptı."; + /* notification title */ "%@ is connected!" = "%@ bağlandı!"; @@ -124,6 +127,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ sunucular"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@, SimpleX Chat'i destekliyor."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ yüklendi"; @@ -142,6 +148,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@, SimpleX Chat'i destekledi. Rozetin süresi %2$@ tarihinde doldu."; + /* time interval */ "%d days" = "%d gün"; @@ -169,6 +178,15 @@ /* time interval */ "%d months" = "%d ay"; +/* channel owners count */ +"%d owner" = "%d sahibi"; + +/* channel owners count */ +"%d owners" = "%d sahibi"; + +/* channel members count */ +"%d owners & contributors" = "%d sahibi & katkıda bulunanlar"; + /* channel relay bar channel subscriber relay bar */ "%d relays failed" = "%d aktarıcı başarısız oldu"; @@ -944,9 +962,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "aranıyor…"; -/* No comment provided by engineer. */ -"Calls" = "Aramalar"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Aramalara izin verilmiyor!"; @@ -1015,9 +1030,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Kilit modunu değiştir"; -/* No comment provided by engineer. */ -"Change member role?" = "Üye rolünü değiştir?"; - /* authentication reason */ "Change passcode" = "Şifreyi değiştir"; @@ -1360,15 +1372,15 @@ server test step */ /* alert title */ "Connection error" = "Bağlantı hatası"; -/* conn error description */ -"Connection error (AUTH)" = "Bağlantı hatası (DOĞRULAMA)"; - /* chat list item title (it should not be shown */ "connection established" = "bağlantı kuruldu"; /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Bağlantı sunucu operatörü tarafından engellendi:\n%@"; +/* conn error description */ +"Connection link removed" = "Bağlantı hatası"; + /* No comment provided by engineer. */ "Connection not ready." = "Bağlantı hazır değil."; @@ -1838,13 +1850,13 @@ alert button */ "Desktop devices" = "Bilgisayar cihazları"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Hedef sunucu adresi %@, yönlendirme sunucusu %@ ayarlarıyla uyumlu değil."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Hedef sunucu adresi %1$@, yönlendirme sunucusu %2$@ ayarlarıyla uyumlu değil."; /* snd error text */ "Destination server error: %@" = "Hedef sunucu hatası: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "Hedef sunucu %@ sürümü, yönlendirme sunucusu %@ ile uyumlu değil."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "Hedef sunucu %1$@ sürümü, yönlendirme sunucusu %2$@ ile uyumlu değil."; /* No comment provided by engineer. */ "Detailed statistics" = "Detaylı istatistikler"; @@ -1853,14 +1865,11 @@ alert button */ "Details" = "Detaylar"; /* No comment provided by engineer. */ -"Develop" = "Geliştir"; +"Developer" = "Geliştirici araçları"; /* No comment provided by engineer. */ "Developer options" = "Geliştirici seçenekleri"; -/* No comment provided by engineer. */ -"Developer tools" = "Geliştirici araçları"; - /* No comment provided by engineer. */ "Device" = "Cihaz"; @@ -2204,7 +2213,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "hata"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Hata"; /* No comment provided by engineer. */ @@ -2445,6 +2454,7 @@ chat item action */ "Error: " = "Hata: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Hata: %@"; @@ -3156,7 +3166,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Büyük dosya!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Daha fazlası"; /* swipe action */ @@ -3294,15 +3304,6 @@ servers warning */ /* chat feature */ "Member reports" = "Üye raporları"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Üye sohbetten kaldırılacak - bu geri alınamaz!"; @@ -4098,9 +4099,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Önceden bağlanılmış sunucular"; -/* No comment provided by engineer. */ -"Privacy & security" = "Gizlilik & güvenlik"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Müşterileriniz için gizlilik."; @@ -4230,7 +4228,7 @@ alert button */ /* swipe action */ "Read" = "Oku"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Dahasını oku"; /* No comment provided by engineer. */ @@ -4544,6 +4542,15 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Rol"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Tüm sohbet üyeleri bilgilendirilecektir."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Üye rolü \"%@\" olarak değiştirilecektir. Ve üye yeni bir davetiye alacaktır."; + /* No comment provided by engineer. */ "Run chat" = "Sohbeti çalıştır"; @@ -4553,7 +4560,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Daha güvenli gruplar"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Kaydet"; @@ -4782,9 +4790,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Gönderici dosya gönderimini iptal etti."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Gönderici bağlantı isteğini silmiş olabilir."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Görüldü bilgisi, tüm görünür sohbet profillerindeki tüm kişiler için etkinleştirilecektir."; @@ -5219,9 +5224,6 @@ report reason */ /* No comment provided by engineer. */ "Subscriptions ignored" = "Abonelikler göz ardı edildi"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "SimpleX Chat'e destek ol"; - /* No comment provided by engineer. */ "Switch audio and video during the call." = "Görüşme sırasında ses ve görüntüyü değiştirin."; @@ -5388,6 +5390,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Gönderici bağlantı isteğini silmiş olabilir."; + /* alert message */ "The sender will NOT be notified" = "Gönderene BİLDİRİLMEYECEKTİR"; @@ -5622,9 +5627,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "iOS arama arayüzünü kullanmadığınız sürece, kesintileri önlemek için Rahatsız Etmeyin modunu etkinleştirin."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin.\nBağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin."; - /* No comment provided by engineer. */ "Unlink" = "Bağlantıyı Kaldır"; @@ -5682,7 +5684,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Adres güncelleme"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Adres güncellensin mi?"; /* No comment provided by engineer. */ @@ -5793,9 +5796,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify code with desktop" = "Bilgisayarla kodu doğrula"; @@ -6063,9 +6063,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Daha sonra Ayarlardan etkinleştirebilirsin"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz."; - /* No comment provided by engineer. */ "You can give another try." = "Bir kez daha deneyebilirsiniz."; @@ -6228,9 +6225,6 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Aramaların"; -/* No comment provided by engineer. */ -"Your chat database" = "Sohbet veritabanınız"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Sohbet veritabanınız şifrelenmemiş - şifrelemek için parola ayarlayın."; @@ -6249,6 +6243,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "İrtibat kişiniz"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Kişiniz bağlantıyı silmediyse veya bu bağlantı kullanılmadıysa, bu bir hata olabilir - lütfen bildirin.\nBağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını isteyin ve sabit bir ağ bağlantınız olduğunu kontrol edin."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 4a21eb4ae8..55cce558f9 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- стабільніша доставка повідомлень.\n- трохи кращі групи.\n- і багато іншого!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- увімкнути надсилання попереднього перегляду посилань.\n- запобігти фішингу за допомогою гіперпосилань.\n- вимкнути відстеження посилань."; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- опція сповіщати про видалені контакти.\n- імена профілів з пробілами.\n- та багато іншого!"; @@ -19,6 +22,9 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 кольоровий!"; +/* chat link info line */ +"(from owner)" = "(від власника)"; + /* No comment provided by engineer. */ "(new)" = "(новий)"; @@ -58,6 +64,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**Відсканувати / Вставити посилання**: підключитися за отриманим посиланням."; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**Тестування перемикача** щоб дізнатися його назву."; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; @@ -109,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ встановлено"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ інвестував в SimpleX Chat краудфандінг."; + /* notification title */ "%@ is connected!" = "%@ підключено!"; @@ -124,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "%@ сервери"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ підтримує SimpleX Chat."; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ завантажено"; @@ -142,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ підтримував SimpleX Chat. Термін дії значка вичерпався %2$@."; + /* time interval */ "%d days" = "%d днів"; @@ -169,6 +187,18 @@ /* time interval */ "%d months" = "%d місяців"; +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d перемикач вийшов з ладу"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d перемикач не працює"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d перемикач видалений"; + /* time interval */ "%d sec" = "%d сек"; @@ -178,15 +208,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "%d пропущено повідомлення(ь)"; +/* channel subscriber count */ +"%d subscriber" = "%d підписник"; + +/* channel subscriber count */ +"%d subscribers" = "%d підписники"; + /* time interval */ "%d weeks" = "%d тижнів"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d перемикач активний"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d перемикач активний, %3$d помилки"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d перемикач активний, %3$d невдачно"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d перемикач активний, %3$d видалено"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d перемикачі зʼєднані"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d перемикачі зʼєднані, %3$d помилки"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d перемикачі зʼєднані, %3$d невдачно"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d перемикачі зʼєднані, %3$d видалено"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld події каналу"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld контакт(и) вибрані"; @@ -251,7 +316,7 @@ "`a + b`" = "\\`a + b`"; /* email text */ -"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Привіт!</p>\n<p><a href=\"%@\"> Зв'яжіться зі мною через SimpleX Chat</a></p>"; +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Привіт!</p>\n<p><a href=\"%@\">Зв'яжіться зі мною через SimpleX Chat</a></p>"; /* No comment provided by engineer. */ "~strike~" = "\\~закреслити~"; @@ -301,6 +366,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "Ще кілька речей"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "Посилання для підключення однієї особи"; + /* notification title */ "A new contact" = "Новий контакт"; @@ -392,6 +460,12 @@ swipe action */ /* No comment provided by engineer. */ "Active connections" = "Активні з'єднання"; +/* No comment provided by engineer. */ +"Add" = "Додати"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "Додайте адресу до свого профілю, щоб ваші контакти в SimpleX могли поділитися нею з іншими людьми. Інформація про оновлення профілю буде надіслана вашим контактам у SimpleX."; + /* No comment provided by engineer. */ "Add friends" = "Додайте друзів"; @@ -404,6 +478,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "Додати профіль"; +/* No comment provided by engineer. */ +"Add relay" = "Додати перемикач"; + +/* No comment provided by engineer. */ +"Add relays" = "Додати перемикачі"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "Додати перемикачі для відновлення доставки повідомлень."; + /* No comment provided by engineer. */ "Add server" = "Додати сервер"; @@ -413,6 +496,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "Додайте учасників команди"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "Додай цей код на твою сторінку. Це буде відображено для передперегляду твого каналу / групи."; + /* No comment provided by engineer. */ "Add to another device" = "Додати до іншого пристрою"; @@ -467,6 +553,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "Розширені налаштування мережі"; +/* No comment provided by engineer. */ +"Advanced options" = "Розширені параметри"; + /* No comment provided by engineer. */ "Advanced settings" = "Додаткові налаштування"; @@ -503,6 +592,9 @@ swipe action */ /* feature role */ "all members" = "всі учасники"; +/* No comment provided by engineer. */ +"All messages" = "Усі повідомлення"; + /* No comment provided by engineer. */ "All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Всі повідомлення та файли надсилаються **наскрізним шифруванням**, з пост-квантовим захистом у прямих повідомленнях."; @@ -518,6 +610,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "Всі профілі"; +/* No comment provided by engineer. */ +"All relays failed" = "Усі перемикачі провалилися"; + +/* No comment provided by engineer. */ +"All relays removed" = "Усі перемикачі видалені"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "Всі скарги будуть заархівовані для вас."; @@ -537,7 +635,10 @@ swipe action */ "Allow" = "Дозволити"; /* No comment provided by engineer. */ -"Allow calls only if your contact allows them." = "Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх."; +"Allow anyone to embed" = "Дозволити будь-кому вбудовувати"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Дозволити дзвінки, тільки якщо ваш контакт дозволяє їх."; /* No comment provided by engineer. */ "Allow calls?" = "Дозволити дзвінки?"; @@ -548,9 +649,15 @@ swipe action */ /* No comment provided by engineer. */ "Allow downgrade" = "Дозволити пониження версії"; +/* No comment provided by engineer. */ +"Allow files and media only if your contact allows them." = "Дозволяйте доступ до файлів та мультимедіа лише в тому випадку, якщо ваш контакт на це дав згоду."; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години)"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "Дозволити учасникам спілкуватися в чаті з адміністраторами."; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх."; @@ -560,12 +667,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Дозволяє надсилати прямі повідомлення користувачам."; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "Дозволити надсилання прямих повідомлень підписникам."; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; /* No comment provided by engineer. */ "Allow sharing" = "Дозволити спільний доступ"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "Дозволяє абонентам спілкуватися з адміністраторами."; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)"; @@ -599,6 +712,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow your contacts to send disappearing messages." = "Дозвольте своїм контактам надсилати зникаючі повідомлення."; +/* No comment provided by engineer. */ +"Allow your contacts to send files and media." = "Дозволяє вашим контактам надсилати файли та медіа."; + /* No comment provided by engineer. */ "Allow your contacts to send voice messages." = "Дозвольте своїм контактам надсилати голосові повідомлення."; @@ -632,6 +748,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "Відповісти на дзвінок"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "Будь-яка веб-сторінка може відображати попередній огляд."; + /* No comment provided by engineer. */ "App build: %@" = "Збірка програми: %@"; @@ -656,6 +775,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "Сесія програми"; +/* alert title */ +"App update required" = "Потрібно оновити додаток"; + /* No comment provided by engineer. */ "App version" = "Версія програми"; @@ -716,6 +838,9 @@ swipe action */ /* No comment provided by engineer. */ "Audio and video calls" = "Аудіо та відеодзвінки"; +/* No comment provided by engineer. */ +"Audio call" = "Аудіодзвінок"; + /* No comment provided by engineer. */ "audio call (not e2e encrypted)" = "аудіовиклик (без шифрування e2e)"; @@ -897,9 +1022,6 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "дзвоніть…"; -/* No comment provided by engineer. */ -"Calls" = "Дзвінки"; - /* No comment provided by engineer. */ "Calls prohibited!" = "Дзвінки заборонені!"; @@ -968,9 +1090,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "Зміна режиму блокування"; -/* No comment provided by engineer. */ -"Change member role?" = "Змінити роль учасника?"; - /* authentication reason */ "Change passcode" = "Змінити код доступу"; @@ -1296,7 +1415,7 @@ server test step */ "Connecting to contact, please wait or check later!" = "З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше!"; /* No comment provided by engineer. */ -"Connecting to desktop" = "Підключення до ПК"; +"Connecting to desktop" = "Підключення до компʼютера"; /* No comment provided by engineer. */ "connecting…" = "з'єднання…"; @@ -1313,15 +1432,15 @@ server test step */ /* alert title */ "Connection error" = "Помилка підключення"; -/* conn error description */ -"Connection error (AUTH)" = "Помилка підключення (AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "з'єднання встановлене"; /* No comment provided by engineer. */ "Connection is blocked by server operator:\n%@" = "Підключення заблоковано оператором сервера:\n%@"; +/* conn error description */ +"Connection link removed" = "Помилка підключення"; + /* No comment provided by engineer. */ "Connection not ready." = "Підключення не готове."; @@ -1785,13 +1904,13 @@ alert button */ "Desktop devices" = "Настільні пристрої"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@."; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "Адреса сервера призначення %1$@ несумісна з налаштуваннями сервера пересилання %2$@."; /* snd error text */ "Destination server error: %@" = "Помилка сервера призначення: %@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "Версія сервера призначення %@ несумісна з версією сервера переадресації %@."; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "Версія сервера призначення %1$@ несумісна з версією сервера переадресації %2$@."; /* No comment provided by engineer. */ "Detailed statistics" = "Детальна статистика"; @@ -1800,14 +1919,11 @@ alert button */ "Details" = "Деталі"; /* No comment provided by engineer. */ -"Develop" = "Розробник"; +"Developer" = "Інструменти для розробників"; /* No comment provided by engineer. */ "Developer options" = "Можливості для розробників"; -/* No comment provided by engineer. */ -"Developer tools" = "Інструменти для розробників"; - /* No comment provided by engineer. */ "Device" = "Пристрій"; @@ -2151,7 +2267,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "помилка"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "Помилка"; /* No comment provided by engineer. */ @@ -2389,6 +2505,7 @@ chat item action */ "Error: " = "Помилка: "; /* alert message +conn error description file error text snd error text */ "Error: %@" = "Помилка: %@"; @@ -3097,7 +3214,7 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "Великий файл!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "Дізнайтеся більше"; /* swipe action */ @@ -3130,6 +3247,9 @@ servers warning */ /* No comment provided by engineer. */ "Limitations" = "Обмеження"; +/* No comment provided by engineer. */ +"link" = "посилання"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "Зв'яжіть мобільні та десктопні додатки! 🔗"; @@ -3229,15 +3349,6 @@ servers warning */ /* chat feature */ "Member reports" = "Повідомлення учасників"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "Роль учасника буде змінено на \"%@\". Усі учасники чату отримають сповіщення."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "Учасника буде видалено з чату – це неможливо скасувати!"; @@ -4018,9 +4129,6 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "Раніше підключені сервери"; -/* No comment provided by engineer. */ -"Privacy & security" = "Конфіденційність і безпека"; - /* No comment provided by engineer. */ "Privacy for your customers." = "Конфіденційність для ваших клієнтів."; @@ -4150,7 +4258,7 @@ alert button */ /* swipe action */ "Read" = "Читати"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "Читати далі"; /* No comment provided by engineer. */ @@ -4455,6 +4563,15 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "Роль"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "Роль учасника буде змінено на \"%@\". Усі учасники чату отримають сповіщення."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; + /* No comment provided by engineer. */ "Run chat" = "Запустити чат"; @@ -4464,7 +4581,8 @@ swipe action */ /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "Зберегти"; @@ -4693,9 +4811,6 @@ chat item action */ /* alert message */ "Sender cancelled file transfer." = "Відправник скасував передачу файлу."; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "Можливо, відправник видалив запит на підключення."; - /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Надсилання підтверджень доставки буде ввімкнено для всіх контактів у всіх видимих профілях чату."; @@ -5130,9 +5245,6 @@ report reason */ /* No comment provided by engineer. */ "Subscriptions ignored" = "Підписки ігноруються"; -/* No comment provided by engineer. */ -"Support SimpleX Chat" = "Підтримка чату SimpleX"; - /* No comment provided by engineer. */ "Switch audio and video during the call." = "Перемикайте аудіо та відео під час дзвінка."; @@ -5296,6 +5408,9 @@ server test failure */ /* No comment provided by engineer. */ "The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "Можливо, відправник видалив запит на підключення."; + /* alert message */ "The sender will NOT be notified" = "Відправник НЕ буде повідомлений"; @@ -5524,9 +5639,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим \"Не турбувати\", щоб уникнути переривань."; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею."; - /* No comment provided by engineer. */ "Unlink" = "Роз'єднати зв'язок"; @@ -5584,7 +5696,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "Адреса оновлення"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "Змінити адресу?"; /* No comment provided by engineer. */ @@ -5695,9 +5808,6 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; - /* No comment provided by engineer. */ "Verify code with desktop" = "Перевірте код на робочому столі"; @@ -5965,9 +6075,6 @@ server test failure */ /* No comment provided by engineer. */ "You can enable later via Settings" = "Ви можете увімкнути пізніше в Налаштуваннях"; -/* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; - /* No comment provided by engineer. */ "You can give another try." = "Ви можете спробувати ще раз."; @@ -6130,9 +6237,6 @@ server test failure */ /* No comment provided by engineer. */ "Your calls" = "Твої дзвінки"; -/* No comment provided by engineer. */ -"Your chat database" = "Ваша база даних чату"; - /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її."; @@ -6151,6 +6255,9 @@ server test failure */ /* No comment provided by engineer. */ "Your contact" = "Ваш контакт"; +/* No comment provided by engineer. */ +"Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею."; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 13be5125ea..e5d35b7426 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -10,6 +10,9 @@ /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- 更稳定的传输!\n- 更好的社群!\n- 以及更多!"; +/* No comment provided by engineer. */ +"- opt-in to send link previews.\n- prevent hyperlink phishing.\n- remove link tracking." = "- 选择是否发送链接预览。\n- 防止超链接钓鱼。\n- 移除链接跟踪。"; + /* No comment provided by engineer. */ "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!" = "- 可选择通知已删除的联系人。\n- 带空格的个人资料名称。\n- 以及更多!"; @@ -19,6 +22,9 @@ /* No comment provided by engineer. */ "!1 colored!" = "!1 种彩色!"; +/* chat link info line */ +"(from owner)" = "(来自所有者)"; + /* No comment provided by engineer. */ "(new)" = "(新)"; @@ -58,6 +64,9 @@ /* No comment provided by engineer. */ "**Scan / Paste link**: to connect via a link you received." = "**扫描/粘贴链接**:用您收到的链接连接。"; +/* No comment provided by engineer. */ +"**Test relay** to retrieve its name." = "**测试中继**,获取其名称。"; + /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**警告**:及时推送通知需要保存在钥匙串的密码。"; @@ -98,10 +107,10 @@ "%@ and %@" = "%@ 和 %@"; /* No comment provided by engineer. */ -"%@ and %@ connected" = "%@ 和%@ 以建立连接"; +"%@ and %@ connected" = "%@ 和%@ 已建立连接"; /* copied message info, <sender> at <time> */ -"%@ at %@:" = "@ %2$@:"; +"%@ at %@:" = "%1$@ 于 %2$@:"; /* No comment provided by engineer. */ "%@ connected" = "%@ 已连接"; @@ -109,6 +118,9 @@ /* No comment provided by engineer. */ "%@ downloaded" = "%@ 已下载"; +/* badge alert */ +"%@ invested in SimpleX Chat crowdfunding." = "%@ 出资支持了 SimpleX Chat 的众筹。"; + /* notification title */ "%@ is connected!" = "%@ 已连接!"; @@ -124,6 +136,9 @@ /* No comment provided by engineer. */ "%@ servers" = "服务器"; +/* badge alert */ +"%@ supports SimpleX Chat." = "%@ 是 SimpleX Chat 支持者。"; + /* No comment provided by engineer. */ "%@ uploaded" = "%@ 已上传"; @@ -142,6 +157,9 @@ /* copied message info */ "%@:" = "%@:"; +/* badge alert */ +"%1$@ supported SimpleX Chat. The badge expired on %2$@." = "%1$@ 曾是 SimpleX Chat 支持者。徽章已于 %2$@ 过期。"; + /* time interval */ "%d days" = "%d 天"; @@ -169,6 +187,27 @@ /* time interval */ "%d months" = "%d 月"; +/* channel owners count */ +"%d owner" = "%d位所有者"; + +/* channel owners count */ +"%d owners" = "%d位所有者"; + +/* channel members count */ +"%d owners & contributors" = "%d位所有者和贡献者"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays failed" = "%d 个中继失败"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays not active" = "%d 个中继未启用"; + +/* channel relay bar +channel subscriber relay bar */ +"%d relays removed" = "%d 个中继已移除"; + /* time interval */ "%d sec" = "%d 秒"; @@ -178,15 +217,50 @@ /* integrity error chat item */ "%d skipped message(s)" = "跳过的 %d 条消息"; +/* channel subscriber count */ +"%d subscriber" = "%d 位订阅者"; + +/* channel subscriber count */ +"%d subscribers" = "%d 位订阅者"; + /* time interval */ "%d weeks" = "%d 星期"; +/* channel creation progress +channel relay bar progress */ +"%d/%d relays active" = "%1$d/%2$d 个中继已启用"; + +/* channel relay bar */ +"%d/%d relays active, %d errors" = "%1$d/%2$d 个中继已启用,%3$d 个错误"; + +/* channel creation progress with errors +channel relay bar */ +"%d/%d relays active, %d failed" = "%1$d/%2$d 个中继已启用,%3$d 个失败"; + +/* channel relay bar */ +"%d/%d relays active, %d removed" = "%1$d/%2$d 个中继已启用,%3$d 个已移除"; + +/* channel subscriber relay bar progress */ +"%d/%d relays connected" = "%1$d/%2$d 个中继已连接"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d errors" = "%1$d/%2$d 个中继已连接,%3$d 个错误"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d failed" = "%1$d/%2$d 个中继已连接,%3$d 个失败"; + +/* channel subscriber relay bar */ +"%d/%d relays connected, %d removed" = "%1$d/%2$d 个中继已连接,%3$d 个已移除"; + /* No comment provided by engineer. */ "%lld" = "%lld"; /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; +/* No comment provided by engineer. */ +"%lld channel events" = "%lld 个频道事件"; + /* No comment provided by engineer. */ "%lld contact(s) selected" = "%lld 联系人已选择"; @@ -251,11 +325,14 @@ "`a + b`" = "\\`a + b`"; /* email text */ -"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>你好!</p>\n<p><a href=\"%@\">通过 SimpleX Chat </a></p>与我联系"; +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>你好!</p>\n<p><a href=\"%@\">通过 SimpleX Chat 联系我</a></p>"; /* No comment provided by engineer. */ "~strike~" = "\\~删去~"; +/* owner verification */ +"⚠️ Signature verification failed: %@." = "⚠️ 签名验证失败:%@。"; + /* time to disappear */ "0 sec" = "0 秒"; @@ -301,6 +378,9 @@ time interval */ /* No comment provided by engineer. */ "A few more things" = "一些杂项"; +/* No comment provided by engineer. */ +"A link for one person to connect" = "供一人连接的链接"; + /* notification title */ "A new contact" = "新联系人"; @@ -365,6 +445,12 @@ swipe action */ /* alert title */ "Accept member" = "接受成员"; +/* No comment provided by engineer. */ +"accepted" = "已接受"; + +/* rcv group event chat item */ +"accepted %@" = "已接受 %@"; + /* call status */ "accepted call" = "已接受通话"; @@ -380,15 +466,27 @@ swipe action */ /* No comment provided by engineer. */ "Acknowledged" = "确认"; +/* No comment provided by engineer. */ +"acknowledged roster" = "已确认名单"; + /* No comment provided by engineer. */ "Acknowledgement errors" = "确认错误"; +/* No comment provided by engineer. */ +"active" = "活跃"; + /* token status text */ "Active" = "活跃"; /* No comment provided by engineer. */ "Active connections" = "活动连接"; +/* No comment provided by engineer. */ +"Add" = "添加"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your SimpleX contacts can share it with other people. Profile update will be sent to your SimpleX contacts." = "将地址添加到你的个人资料,让你的 SimpleX 联系人可以与其他人分享。个人资料更新将发送给你的 SimpleX 联系人。"; + /* No comment provided by engineer. */ "Add friends" = "添加好友"; @@ -401,6 +499,15 @@ swipe action */ /* No comment provided by engineer. */ "Add profile" = "添加个人资料"; +/* No comment provided by engineer. */ +"Add relay" = "添加中继"; + +/* No comment provided by engineer. */ +"Add relays" = "添加中继"; + +/* No comment provided by engineer. */ +"Add relays to restore message delivery." = "添加中继来恢复消息传送。"; + /* No comment provided by engineer. */ "Add server" = "添加服务器"; @@ -410,6 +517,9 @@ swipe action */ /* No comment provided by engineer. */ "Add team members" = "添加团队成员"; +/* No comment provided by engineer. */ +"Add this code to your webpage. It will display the preview of your channel / group." = "将此代码添加到你的网页。它会显示你的频道 / 群组预览。"; + /* No comment provided by engineer. */ "Add to another device" = "添加另一设备"; @@ -464,6 +574,9 @@ swipe action */ /* No comment provided by engineer. */ "Advanced network settings" = "高级网络设置"; +/* No comment provided by engineer. */ +"Advanced options" = "高级选项"; + /* No comment provided by engineer. */ "Advanced settings" = "高级设置"; @@ -518,6 +631,12 @@ swipe action */ /* profile dropdown */ "All profiles" = "所有配置文件"; +/* No comment provided by engineer. */ +"All relays failed" = "所有中继均失败"; + +/* No comment provided by engineer. */ +"All relays removed" = "所有中继均已移除"; + /* No comment provided by engineer. */ "All reports will be archived for you." = "将为你存档所有举报。"; @@ -536,6 +655,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow" = "允许"; +/* No comment provided by engineer. */ +"Allow anyone to embed" = "允许任何人嵌入"; + /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "仅当您的联系人允许时才允许呼叫。"; @@ -554,6 +676,9 @@ swipe action */ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "仅有您的联系人许可后才允许不可撤回消息移除"; +/* No comment provided by engineer. */ +"Allow members to chat with admins." = "允许成员与管理员聊天。"; + /* No comment provided by engineer. */ "Allow message reactions only if your contact allows them." = "只有您的联系人允许时才允许消息回应。"; @@ -563,12 +688,18 @@ swipe action */ /* No comment provided by engineer. */ "Allow sending direct messages to members." = "允许向成员发送私信。"; +/* No comment provided by engineer. */ +"Allow sending direct messages to subscribers." = "允许向订阅者发送直接消息。"; + /* No comment provided by engineer. */ "Allow sending disappearing messages." = "允许发送限时消息。"; /* No comment provided by engineer. */ "Allow sharing" = "允许共享"; +/* No comment provided by engineer. */ +"Allow subscribers to chat with admins." = "允许订阅者与管理员聊天。"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "允许不可撤回地删除已发送消息"; @@ -638,6 +769,9 @@ swipe action */ /* No comment provided by engineer. */ "Answer call" = "接听来电"; +/* No comment provided by engineer. */ +"Any webpage can show the preview." = "任何网页都可以显示预览。"; + /* No comment provided by engineer. */ "App build: %@" = "应用程序构建:%@"; @@ -662,6 +796,9 @@ swipe action */ /* No comment provided by engineer. */ "App session" = "应用会话"; +/* alert title */ +"App update required" = "需要更新应用程序"; + /* No comment provided by engineer. */ "App version" = "应用程序版本"; @@ -779,6 +916,12 @@ swipe action */ /* No comment provided by engineer. */ "Bad message ID" = "错误消息 ID"; +/* badge alert title */ +"Badge cannot be verified" = "无法验证徽章"; + +/* No comment provided by engineer. */ +"Be free\nin your network" = "在你的网络中\n保持自由"; + /* No comment provided by engineer. */ "Be free in your network." = "在你的网络中自由畅行。"; @@ -842,6 +985,9 @@ swipe action */ /* No comment provided by engineer. */ "Block member?" = "封禁成员吗?"; +/* No comment provided by engineer. */ +"Block subscriber for all?" = "要为所有人封锁订阅者吗?"; + /* marked deleted chat item preview text */ "blocked" = "已封禁"; @@ -885,6 +1031,12 @@ marked deleted chat item preview text */ /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "您和您的联系人都可以发送语音消息。"; +/* No comment provided by engineer. */ +"Bottom bar" = "底部栏"; + +/* compose placeholder for channel owner */ +"Broadcast" = "广播"; + /* No comment provided by engineer. */ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "保加利亚语、芬兰语、泰语和乌克兰语——感谢用户和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; @@ -918,15 +1070,15 @@ marked deleted chat item preview text */ /* call status */ "calling…" = "呼叫中……"; -/* No comment provided by engineer. */ -"Calls" = "通话"; - /* No comment provided by engineer. */ "Calls prohibited!" = "禁止来电!"; /* No comment provided by engineer. */ "Camera not available" = "相机不可用"; +/* No comment provided by engineer. */ +"can't broadcast" = "无法广播"; + /* No comment provided by engineer. */ "Can't call contact" = "无法呼叫联系人"; @@ -953,6 +1105,12 @@ alert button new chat action */ "Cancel" = "取消"; +/* No comment provided by engineer. */ +"Cancel and delete channel" = "取消并删除频道"; + +/* alert title */ +"Cancel creating channel?" = "要取消创建频道吗?"; + /* No comment provided by engineer. */ "Cancel migration" = "取消迁移"; @@ -989,9 +1147,6 @@ new chat action */ /* authentication reason */ "Change lock mode" = "更改锁定模式"; -/* No comment provided by engineer. */ -"Change member role?" = "更改成员角色?"; - /* authentication reason */ "Change passcode" = "更改密码"; @@ -1004,6 +1159,9 @@ new chat action */ /* No comment provided by engineer. */ "Change role" = "改变角色"; +/* No comment provided by engineer. */ +"Change role?" = "改变角色?"; + /* authentication reason */ "Change self-destruct mode" = "更改自毁模式"; @@ -1026,6 +1184,61 @@ set passcode view */ /* chat item text */ "changing address…" = "更改地址…"; +/* shown as sender role for channel messages */ +"channel" = "频道"; + +/* No comment provided by engineer. */ +"Channel" = "频道"; + +/* No comment provided by engineer. */ +"Channel display name" = "频道显示名称"; + +/* No comment provided by engineer. */ +"Channel full name (optional)" = "频道全名(可选)"; + +/* alert message +alert subtitle */ +"Channel has no active relays. Please try to join later." = "频道没有已启用的中继。请稍后再尝试加入。"; + +/* No comment provided by engineer. */ +"Channel image" = "频道图片"; + +/* chat link info line */ +"Channel link" = "频道链接"; + +/* No comment provided by engineer. */ +"Channel preferences" = "频道偏好设置"; + +/* No comment provided by engineer. */ +"Channel profile" = "频道资料"; + +/* No comment provided by engineer. */ +"Channel profile is stored on subscribers' devices and on the chat relays." = "频道资料会存储在订阅者的设备和聊天中继上。"; + +/* snd group event chat item */ +"channel profile updated" = "频道资料已更新"; + +/* alert message */ +"Channel profile was changed. If you save it, the updated profile will be sent to channel subscribers." = "频道资料已更改。如果保存,更新后的资料将发送给频道订阅者。"; + +/* alert title */ +"Channel temporarily unavailable" = "频道暂时不可用"; + +/* No comment provided by engineer. */ +"Channel webpage" = "频道网页"; + +/* No comment provided by engineer. */ +"Channel will be deleted for all subscribers - this cannot be undone!" = "将为所有订阅者删除频道-此操作无法撤销!"; + +/* No comment provided by engineer. */ +"Channel will be deleted for you - this cannot be undone!" = "频道将为你删除,且无法撤消!"; + +/* alert message */ +"Channel will start working with %d of %d relays. Continue?" = "频道将以 %1$d/%2$d 个中继开始运行。要继续吗?"; + +/* No comment provided by engineer. */ +"Channels" = "频道"; + /* No comment provided by engineer. */ "Chat" = "聊天"; @@ -1041,6 +1254,9 @@ set passcode view */ /* No comment provided by engineer. */ "Chat console" = "聊天控制台"; +/* No comment provided by engineer. */ +"Chat data" = "聊天数据"; + /* No comment provided by engineer. */ "Chat database" = "聊天数据库"; @@ -1077,6 +1293,18 @@ set passcode view */ /* No comment provided by engineer. */ "Chat profile" = "用户资料"; +/* No comment provided by engineer. */ +"Chat relay" = "聊天中继"; + +/* No comment provided by engineer. */ +"Chat relays" = "聊天中继"; + +/* No comment provided by engineer. */ +"Chat relays forward messages in channels you create." = "聊天中继会转发你创建的频道中的消息。"; + +/* No comment provided by engineer. */ +"Chat relays forward messages to channel subscribers." = "聊天中继会将消息转发给频道订阅者。"; + /* No comment provided by engineer. */ "Chat theme" = "聊天主题"; @@ -1094,20 +1322,35 @@ chat toolbar */ "Chat with member" = "和成员聊天"; /* No comment provided by engineer. */ -"Chat with members before they join." = "在成员加入前和这些人聊天"; +"Chat with members before they join." = "在成员加入前与其聊天。"; /* No comment provided by engineer. */ "Chats" = "聊天"; +/* No comment provided by engineer. */ +"Chats with admins are prohibited." = "禁止与管理员聊天。"; + +/* alert message */ +"Chats with admins in public channels have no E2E encryption - use only with trusted chat relays." = "与管理员在公开频道中聊天没有端到端加密 — 请只在受信任聊天中继中使用。"; + /* No comment provided by engineer. */ "Chats with members" = "和成员聊天"; +/* No comment provided by engineer. */ +"Chats with members are disabled" = "禁止与成员聊天"; + /* No comment provided by engineer. */ "Check messages every 20 min." = "每 20 分钟检查消息。"; /* No comment provided by engineer. */ "Check messages when allowed." = "在被允许时检查消息。"; +/* alert message */ +"Check relay address and try again." = "请检查中继地址并重试。"; + +/* alert message */ +"Check relay name and try again." = "请检查中继名称并重试。"; + /* alert title */ "Check server address and try again." = "检查服务器地址并再试一次。"; @@ -1201,6 +1444,9 @@ chat toolbar */ /* No comment provided by engineer. */ "Configure ICE servers" = "配置 ICE 服务器"; +/* No comment provided by engineer. */ +"Configure relays" = "配置中继"; + /* No comment provided by engineer. */ "Confirm" = "确认"; @@ -1244,6 +1490,9 @@ server test step */ /* No comment provided by engineer. */ "Connect faster! 🚀" = "更快地连接!🚀"; +/* new chat action */ +"Connect to %@" = "连接到%@"; + /* No comment provided by engineer. */ "Connect to desktop" = "连接到桌面"; @@ -1265,6 +1514,9 @@ server test step */ /* new chat sheet title */ "Connect via link" = "通过链接连接"; +/* No comment provided by engineer. */ +"Connect via link or QR code" = "通过链接或二维码连接"; + /* new chat sheet title */ "Connect via one-time link" = "通过一次性链接连接"; @@ -1331,17 +1583,23 @@ server test step */ /* No comment provided by engineer. */ "Connection blocked" = "连接被阻止"; +/* conn error description */ +"Connection blocked: %@" = "连接被阻止:%@"; + /* alert title */ "Connection error" = "连接错误"; -/* conn error description */ -"Connection error (AUTH)" = "连接错误(AUTH)"; - /* chat list item title (it should not be shown */ "connection established" = "连接已建立"; /* No comment provided by engineer. */ -"Connection is blocked by server operator:\n%@" = "连接被运营方 %@ 阻止"; +"Connection failed" = "连接失败"; + +/* No comment provided by engineer. */ +"Connection is blocked by server operator:\n%@" = "连接已被运营方阻止:\n%@"; + +/* conn error description */ +"Connection link removed" = "连接错误"; /* No comment provided by engineer. */ "Connection not ready." = "连接未就绪。"; @@ -1373,9 +1631,15 @@ server test step */ /* No comment provided by engineer. */ "Connections" = "连接"; +/* No comment provided by engineer. */ +"Contact" = "联系人"; + /* profile update event chat item */ "contact %@ changed to %@" = "联系人 %1$@ 已更改为 %2$@"; +/* chat link info line */ +"Contact address" = "联系地址"; + /* No comment provided by engineer. */ "Contact allows" = "联系人允许"; @@ -1445,6 +1709,9 @@ server test step */ /* No comment provided by engineer. */ "Copy" = "复制"; +/* No comment provided by engineer. */ +"Copy code" = "复制代码"; + /* No comment provided by engineer. */ "Copy error" = "复制错误"; @@ -1463,6 +1730,9 @@ server test step */ /* No comment provided by engineer. */ "Create a group using a random profile." = "使用随机身份创建群组."; +/* No comment provided by engineer. */ +"Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting." = "创建网页,在访客订阅前向他们显示你的频道预览。你可以自行托管,或使用任何静态托管服务。"; + /* server test step */ "Create file" = "创建文件"; @@ -1484,6 +1754,9 @@ server test step */ /* No comment provided by engineer. */ "Create profile" = "创建个人资料"; +/* No comment provided by engineer. */ +"Create public channel" = "创建公开频道"; + /* server test step */ "Create queue" = "创建队列"; @@ -1493,9 +1766,15 @@ server test step */ /* No comment provided by engineer. */ "Create your address" = "创建地址"; +/* No comment provided by engineer. */ +"Create your link" = "创建你的链接"; + /* No comment provided by engineer. */ "Create your profile" = "创建您的资料"; +/* No comment provided by engineer. */ +"Create your public address" = "创建你的公开地址"; + /* No comment provided by engineer. */ "Created" = "已创建"; @@ -1508,6 +1787,9 @@ server test step */ /* No comment provided by engineer. */ "Creating archive link" = "正在创建存档链接"; +/* No comment provided by engineer. */ +"Creating channel" = "正在创建频道"; + /* No comment provided by engineer. */ "Creating link…" = "创建链接中…"; @@ -1610,6 +1892,9 @@ server test step */ /* No comment provided by engineer. */ "Debug delivery" = "调试交付"; +/* relay test step */ +"Decode link" = "解码链接"; + /* message decrypt error item */ "Decryption error" = "解密错误"; @@ -1651,6 +1936,12 @@ swipe action */ /* No comment provided by engineer. */ "Delete and notify contact" = "删除并通知联系人"; +/* No comment provided by engineer. */ +"Delete channel" = "删除频道"; + +/* No comment provided by engineer. */ +"Delete channel?" = "要删除频道吗?"; + /* No comment provided by engineer. */ "Delete chat" = "删除聊天"; @@ -1699,6 +1990,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete for me" = "为我删除"; +/* No comment provided by engineer. */ +"Delete from history" = "从历史记录中删除"; + /* No comment provided by engineer. */ "Delete group" = "删除群组"; @@ -1723,6 +2017,9 @@ swipe action */ /* No comment provided by engineer. */ "Delete member messages" = "删除成员消息"; +/* alert title */ +"Delete member messages?" = "要删除成员消息吗?"; + /* No comment provided by engineer. */ "Delete message?" = "删除消息吗?"; @@ -1751,6 +2048,9 @@ alert button */ /* server test step */ "Delete queue" = "删除队列"; +/* No comment provided by engineer. */ +"Delete relay" = "删除中继"; + /* No comment provided by engineer. */ "Delete report" = "删除举报"; @@ -1775,6 +2075,9 @@ alert button */ /* copied message info */ "Deleted at: %@" = "已删除于:%@"; +/* rcv group event chat item */ +"deleted channel" = "已删除频道"; + /* rcv direct event chat item */ "deleted contact" = "已删除联系人"; @@ -1815,13 +2118,13 @@ alert button */ "Desktop devices" = "桌面设备"; /* No comment provided by engineer. */ -"Destination server address of %@ is incompatible with forwarding server %@ settings." = "目标服务器地址 %@ 与转发服务器 %@ 设置不兼容。"; +"Destination server address of %1$@ is incompatible with forwarding server %2$@ settings." = "目标服务器地址 %1$@ 与转发服务器 %2$@ 设置不兼容。"; /* snd error text */ "Destination server error: %@" = "目标服务器错误:%@"; /* No comment provided by engineer. */ -"Destination server version of %@ is incompatible with forwarding server %@." = "目标服务器版本 %@ 与转发服务器 %@ 不兼容。"; +"Destination server version of %1$@ is incompatible with forwarding server %2$@." = "目标服务器版本 %1$@ 与转发服务器 %2$@ 不兼容。"; /* No comment provided by engineer. */ "Detailed statistics" = "详细的统计数据"; @@ -1830,14 +2133,11 @@ alert button */ "Details" = "详细信息"; /* No comment provided by engineer. */ -"Develop" = "开发"; +"Developer" = "开发者工具"; /* No comment provided by engineer. */ "Developer options" = "开发者选项"; -/* No comment provided by engineer. */ -"Developer tools" = "开发者工具"; - /* No comment provided by engineer. */ "Device" = "设备"; @@ -1865,6 +2165,12 @@ alert button */ /* No comment provided by engineer. */ "Direct messages between members are prohibited." = "此群禁止成员间私信。"; +/* No comment provided by engineer. */ +"Direct messages between subscribers are prohibited." = "禁止订阅者之间发送直接消息。"; + +/* alert button */ +"Disable" = "停用"; + /* No comment provided by engineer. */ "Disable (keep overrides)" = "禁用(保留覆盖)"; @@ -1922,6 +2228,9 @@ alert button */ /* No comment provided by engineer. */ "Do not send history to new members." = "不给新成员发送历史消息。"; +/* No comment provided by engineer. */ +"Do not send history to new subscribers." = "不要将历史记录发送给新订阅者。"; + /* No comment provided by engineer. */ "Do NOT send messages directly, even if your or destination server does not support private routing." = "请勿直接发送消息,即使您的服务器或目标服务器不支持私有路由。"; @@ -2001,9 +2310,15 @@ chat item action */ /* No comment provided by engineer. */ "E2E encrypted notifications." = "端到端加密的通知。"; +/* No comment provided by engineer. */ +"Easier to invite your friends 👋" = "邀请好友更简单 👋"; + /* chat item action */ "Edit" = "编辑"; +/* No comment provided by engineer. */ +"Edit channel profile" = "编辑频道简介"; + /* No comment provided by engineer. */ "Edit group profile" = "编辑群组资料"; @@ -2016,12 +2331,18 @@ chat item action */ /* No comment provided by engineer. */ "Enable (keep overrides)" = "启用(保持覆盖)"; +/* channel creation warning */ +"Enable at least one chat relay in Network & Servers." = "请在「网络与服务器」中启用至少一个聊天中继。"; + /* alert title */ "Enable automatic message deletion?" = "启用自动删除消息?"; /* No comment provided by engineer. */ "Enable camera access" = "启用相机访问"; +/* alert title */ +"Enable chats with admins?" = "要启用与管理员聊天吗?"; + /* No comment provided by engineer. */ "Enable disappearing messages by default." = "默认启用定时消失消息。"; @@ -2037,6 +2358,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable instant notifications?" = "启用即时通知?"; +/* alert title */ +"Enable link previews?" = "要启用链接预览吗?"; + /* No comment provided by engineer. */ "Enable lock" = "启用锁定"; @@ -2145,6 +2469,9 @@ chat item action */ /* call status */ "ended call %@" = "结束通话 %@"; +/* No comment provided by engineer. */ +"Enter channel name…" = "输入频道名称…"; + /* No comment provided by engineer. */ "Enter correct passphrase." = "输入正确密码。"; @@ -2163,12 +2490,21 @@ chat item action */ /* No comment provided by engineer. */ "Enter password above to show!" = "在上面输入密码以显示!"; +/* No comment provided by engineer. */ +"Enter profile name..." = "输入个人资料名称..."; + +/* No comment provided by engineer. */ +"Enter relay name…" = "输入中继名称…"; + /* No comment provided by engineer. */ "Enter server manually" = "手动输入服务器"; /* No comment provided by engineer. */ "Enter this device name…" = "输入此设备名…"; +/* No comment provided by engineer. */ +"Enter webpage URL" = "请输入网址"; + /* placeholder */ "Enter welcome message…" = "输入欢迎消息……"; @@ -2181,7 +2517,7 @@ chat item action */ /* No comment provided by engineer. */ "error" = "错误"; -/* conn error description */ +/* No comment provided by engineer. */ "Error" = "错误"; /* No comment provided by engineer. */ @@ -2199,6 +2535,12 @@ chat item action */ /* No comment provided by engineer. */ "Error adding member(s)" = "添加成员错误"; +/* alert title */ +"Error adding relay" = "添加中继时出错"; + +/* alert title */ +"Error adding relays" = "添加中继时出错"; + /* alert title */ "Error adding server" = "添加服务器出错"; @@ -2229,9 +2571,15 @@ chat item action */ /* alert message */ "Error connecting to forwarding server %@. Please try later." = "连接到转发服务器 %@ 时出错。请稍后尝试。"; +/* subscription status explanation */ +"Error connecting to the server used to receive messages from this connection: %@" = "连接用于接收此连接消息的服务器时出错:%@"; + /* No comment provided by engineer. */ "Error creating address" = "创建地址错误"; +/* alert title */ +"Error creating channel" = "创建频道时出错"; + /* No comment provided by engineer. */ "Error creating group" = "创建群组错误"; @@ -2271,6 +2619,9 @@ chat item action */ /* alert title */ "Error deleting database" = "删除数据库错误"; +/* alert title */ +"Error deleting message" = "删除消息时出错"; + /* alert title */ "Error deleting old database" = "删除旧数据库错误"; @@ -2337,6 +2688,9 @@ chat item action */ /* No comment provided by engineer. */ "Error resetting statistics" = "重置统计信息时出错"; +/* No comment provided by engineer. */ +"Error saving channel profile" = "保存频道资料时出错"; + /* alert title */ "Error saving chat list" = "保存聊天列表出错"; @@ -2379,6 +2733,9 @@ chat item action */ /* No comment provided by engineer. */ "Error setting delivery receipts!" = "设置送达回执出错!"; +/* alert title */ +"Error sharing channel" = "分享频道时出错"; + /* No comment provided by engineer. */ "Error starting chat" = "启动聊天错误"; @@ -2421,7 +2778,11 @@ chat item action */ /* No comment provided by engineer. */ "Error: " = "错误: "; +/* receive error chat item */ +"error: %@" = "错误:%@"; + /* alert message +conn error description file error text snd error text */ "Error: %@" = "错误: %@"; @@ -2475,6 +2836,9 @@ server test error */ /* No comment provided by engineer. */ "Exporting database archive…" = "导出数据库档案中…"; +/* No comment provided by engineer. */ +"failed" = "失败"; + /* No comment provided by engineer. */ "Failed to remove passphrase" = "移除密码失败"; @@ -2579,7 +2943,7 @@ server test error */ /* relay test error server test error */ -"Fingerprint in server address does not match certificate." = "服务器地址中的证书指纹可能不正确"; +"Fingerprint in server address does not match certificate." = "服务器地址中的指纹与证书不符。"; /* No comment provided by engineer. */ "Fix" = "修复"; @@ -2602,6 +2966,9 @@ server test error */ /* No comment provided by engineer. */ "For all moderators" = "所有 moderators"; +/* No comment provided by engineer. */ +"For anyone to reach you" = "让任何人都能联系你"; + /* servers error servers warning */ "For chat profile %@:" = "为聊天资料 %@:"; @@ -2687,9 +3054,15 @@ servers warning */ /* No comment provided by engineer. */ "Further reduced battery usage" = "进一步减少电池使用"; +/* relay test step */ +"Get link" = "获取链接"; + /* No comment provided by engineer. */ "Get notified when mentioned." = "被提及时收到通知。"; +/* No comment provided by engineer. */ +"Get started" = "开始使用"; + /* No comment provided by engineer. */ "GIFs and stickers" = "GIF 和贴纸"; @@ -2762,6 +3135,9 @@ servers warning */ /* 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 webpage" = "群组网页"; + /* No comment provided by engineer. */ "Group welcome message" = "群欢迎词"; @@ -2777,6 +3153,9 @@ servers warning */ /* No comment provided by engineer. */ "Help" = "帮助"; +/* No comment provided by engineer. */ +"Help & support" = "帮助与支持"; + /* No comment provided by engineer. */ "Help admins moderating their groups." = "帮助管理员管理群组。"; @@ -2807,6 +3186,9 @@ servers warning */ /* No comment provided by engineer. */ "History is not sent to new members." = "未发送历史消息给新成员。"; +/* No comment provided by engineer. */ +"History is not sent to new subscribers." = "历史记录不会发送给新订阅者。"; + /* time unit */ "hours" = "小时"; @@ -2831,6 +3213,9 @@ servers warning */ /* No comment provided by engineer. */ "How to use your servers" = "如何使用您的服务器"; +/* No comment provided by engineer. */ +"https://" = "https://"; + /* No comment provided by engineer. */ "Hungarian interface" = "匈牙利语界面"; @@ -2846,6 +3231,9 @@ servers warning */ /* No comment provided by engineer. */ "If you enter your self-destruct passcode while opening the app:" = "如果您在打开应用程序时输入自毁密码:"; +/* down migration warning */ +"If you joined or created channels, they will stop working permanently." = "如果你加入或创建了频道,它们将永久停止工作。"; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "如果您现在需要使用聊天,请点击下面的**稍后再做**(当您重新启动应用程序时,系统会提示您迁移数据库)。"; @@ -3020,6 +3408,12 @@ servers warning */ /* No comment provided by engineer. */ "Invalid QR code" = "无效的二维码"; +/* alert title */ +"Invalid relay address!" = "中继地址无效!"; + +/* alert title */ +"Invalid relay name!" = "中继名称无效!"; + /* No comment provided by engineer. */ "Invalid response" = "无效的响应"; @@ -3047,6 +3441,9 @@ servers warning */ /* No comment provided by engineer. */ "Invite members" = "邀请成员"; +/* No comment provided by engineer. */ +"Invite someone privately" = "私下邀请某人"; + /* No comment provided by engineer. */ "Invite to chat" = "邀请加入聊天"; @@ -3098,6 +3495,9 @@ servers warning */ /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。"; +/* No comment provided by engineer. */ +"It will be shown to subscribers and used to allow loading the preview." = "它会显示给订阅者,并用于允许加载预览。"; + /* No comment provided by engineer. */ "Italian interface" = "意大利语界面"; @@ -3113,6 +3513,12 @@ servers warning */ /* No comment provided by engineer. */ "Join as %@" = "以 %@ 身份加入"; +/* No comment provided by engineer. */ +"Join channel" = "加入频道"; + +/* new chat action */ +"Join channel %@" = "加入频道%@"; + /* new chat sheet title */ "Join group" = "加入群组"; @@ -3155,12 +3561,18 @@ servers warning */ /* No comment provided by engineer. */ "Large file!" = "大文件!"; -/* No comment provided by engineer. */ +/* badge alert button */ "Learn more" = "了解更多"; /* swipe action */ "Leave" = "离开"; +/* No comment provided by engineer. */ +"Leave channel" = "离开频道"; + +/* No comment provided by engineer. */ +"Leave channel?" = "要离开频道吗?"; + /* No comment provided by engineer. */ "Leave chat" = "离开聊天"; @@ -3179,6 +3591,15 @@ servers warning */ /* No comment provided by engineer. */ "Less traffic on mobile networks." = "消耗更少的移动网络数据。"; +/* No comment provided by engineer. */ +"Let people connect to you via name registered with your SimpleX address." = "让别人通过用你的 SimpleX 地址注册的名称和你建立联系。"; + +/* No comment provided by engineer. */ +"Let people join via name registered with this channel link." = "让别人通过用这个频道链接注册的名称加入。"; + +/* No comment provided by engineer. */ +"Let someone connect to you" = "让某人连接到你"; + /* email subject */ "Let's talk in SimpleX Chat" = "让我们一起在 SimpleX Chat 里聊天"; @@ -3188,9 +3609,15 @@ servers warning */ /* No comment provided by engineer. */ "Limitations" = "限制"; +/* No comment provided by engineer. */ +"link" = "链接"; + /* No comment provided by engineer. */ "Link mobile and desktop apps! 🔗" = "连接移动端和桌面端应用程序!🔗"; +/* owner verification */ +"Link signature verified." = "链接签名已验证。"; + /* No comment provided by engineer. */ "Linked desktop options" = "已链接桌面选项"; @@ -3293,18 +3720,12 @@ servers warning */ /* No comment provided by engineer. */ "Member is deleted - can't accept request" = "成员被删除——无法接受请求"; +/* alert message */ +"Member messages will be deleted - this cannot be undone!" = "成员消息将被删除,且无法撤消!"; + /* chat feature */ "Member reports" = "成员举报"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All chat members will be notified." = "将变更成员角色为“%@”。所有成员都会收到通知。"; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "成员角色将更改为 \"%@\"。所有群成员将收到通知。"; - -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "成员角色将更改为 \"%@\"。该成员将收到一份新的邀请。"; - /* alert message */ "Member will be removed from chat - this cannot be undone!" = "将从聊天中删除成员 - 此操作无法撤销!"; @@ -3317,6 +3738,9 @@ servers warning */ /* No comment provided by engineer. */ "Members can add message reactions." = "群组成员可以添加信息回应。"; +/* No comment provided by engineer. */ +"Members can chat with admins." = "成员可以与管理员聊天。"; + /* No comment provided by engineer. */ "Members can irreversibly delete sent messages. (24 hours)" = "群组成员可以不可撤回地删除已发送的消息"; @@ -3359,6 +3783,9 @@ servers warning */ /* No comment provided by engineer. */ "Message draft" = "消息草稿"; +/* No comment provided by engineer. */ +"Message error" = "消息错误"; + /* item status text */ "Message forwarded" = "消息已转发"; @@ -3419,6 +3846,12 @@ servers warning */ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "将显示来自 %@ 的消息!"; +/* No comment provided by engineer. */ +"Messages in this channel are **not end-to-end encrypted**. Chat relays can see these messages." = "此频道中的消息**并非端到端加密**。聊天中继可以看到这些消息。"; + +/* E2EE info chat item */ +"Messages in this channel are not end-to-end encrypted. Chat relays can see these messages." = "此频道中的消息并非端到端加密。聊天中继可以看到这些消息。"; + /* alert message */ "Messages in this chat will never be deleted." = "此聊天中的消息永远不会被删除。"; @@ -3437,6 +3870,9 @@ servers warning */ /* No comment provided by engineer. */ "Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "消息、文件和通话受到 **抗量子 e2e 加密** 的保护,具有完全正向保密、否认和闯入恢复。"; +/* No comment provided by engineer. */ +"Migrate" = "迁移"; + /* No comment provided by engineer. */ "Migrate device" = "迁移设备"; @@ -3468,7 +3904,7 @@ servers warning */ "Migration is completed" = "迁移完成"; /* No comment provided by engineer. */ -"Migrations:" = "迁移"; +"Migrations:" = "迁移:"; /* time unit */ "minutes" = "分钟"; @@ -3503,6 +3939,9 @@ servers warning */ /* No comment provided by engineer. */ "More improvements are coming soon!" = "更多改进即将推出!"; +/* No comment provided by engineer. */ +"More privacy" = "更多隐私"; + /* No comment provided by engineer. */ "More reliable network connection." = "更可靠的网络连接。"; @@ -3527,15 +3966,24 @@ servers warning */ /* swipe action */ "Name" = "名称"; +/* No comment provided by engineer. */ +"Name not found" = "未找到名称"; + /* No comment provided by engineer. */ "Network & servers" = "网络和服务器"; +/* No comment provided by engineer. */ +"Network commitments" = "网络承诺"; + /* No comment provided by engineer. */ "Network connection" = "网络连接"; /* No comment provided by engineer. */ "Network decentralization" = "网络去中心化"; +/* conn error description */ +"Network error" = "网络错误"; + /* snd error text */ "Network issues - message expired after many attempts to send it." = "网络问题 - 消息在多次尝试发送后过期。"; @@ -3545,6 +3993,9 @@ servers warning */ /* No comment provided by engineer. */ "Network operator" = "网络运营方"; +/* No comment provided by engineer. */ +"Network routers cannot know\nwho talks to whom" = "网络路由器无法知道\n谁在与谁通信"; + /* No comment provided by engineer. */ "Network settings" = "网络设置"; @@ -3554,15 +4005,24 @@ servers warning */ /* delete after time */ "never" = "从不"; +/* No comment provided by engineer. */ +"new" = "新"; + /* token status text */ "New" = "新"; +/* No comment provided by engineer. */ +"New 1-time link" = "新的一次性链接"; + /* No comment provided by engineer. */ "New chat" = "新聊天"; /* No comment provided by engineer. */ "New chat experience 🎉" = "新的聊天体验 🎉"; +/* No comment provided by engineer. */ +"New chat relay" = "新的聊天中继"; + /* notification */ "New contact request" = "新联系人请求"; @@ -3620,9 +4080,24 @@ servers warning */ /* No comment provided by engineer. */ "No" = "否"; +/* No comment provided by engineer. */ +"No account. No phone. No email. No ID.\nThe most secure encryption." = "无需账号。无需电话号码。无需电子邮件。无需 ID。\n最安全的加密。"; + +/* No comment provided by engineer. */ +"No active relays" = "没有已启用的中继"; + /* Authentication unavailable */ "No app password" = "没有应用程序密码"; +/* No comment provided by engineer. */ +"No available relays" = "没有可用的中继"; + +/* No comment provided by engineer. */ +"No chat relays" = "没有聊天中继"; + +/* servers warning */ +"No chat relays enabled." = "未启用聊天中继。"; + /* No comment provided by engineer. */ "No chats" = "无聊天"; @@ -3695,6 +4170,9 @@ servers warning */ /* No comment provided by engineer. */ "No received or sent files" = "未收到或发送文件"; +/* No comment provided by engineer. */ +"No relays" = "没有中继"; + /* servers error */ "No servers for private message routing." = "无私密消息路由服务器。"; @@ -3704,6 +4182,9 @@ servers warning */ /* servers error */ "No servers to receive messages." = "无消息接收服务器。"; +/* servers warning */ +"No servers to resolve names." = "没有解析名称的服务器。"; + /* servers error */ "No servers to send files." = "无文件发送服务器。"; @@ -3719,12 +4200,24 @@ servers warning */ /* No comment provided by engineer. */ "No unread chats" = "没有未读聊天"; +/* No comment provided by engineer. */ +"No valid link" = "无有效链接"; + /* No comment provided by engineer. */ "Nobody tracked your conversations. No one drew a map of where you'd been. Privacy was never a feature - it was the way of life." = "没有人追踪你的谈话内容。没有人绘制你去过的地方的地图。隐私从来都不是一项功能--而是一种生活方式。"; +/* No comment provided by engineer. */ +"Non-profit governance" = "非营利治理"; + +/* No comment provided by engineer. */ +"None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link." = "你的服务器没有一台被设定为解析 SimpleX 名称。配置服务器或使用连接链接。"; + /* No comment provided by engineer. */ "Not a better lock on someone else's door. Not a nicer landlord that respects your privacy, but still keeps the record of all visitors. You are not a guest. You are home. No king can enter it - you are sovereign." = "别人家的门锁再好也比不上这里。房东再好也比不上这里,他既尊重你的隐私,又保留着所有访客的记录。你不是客人,你是家。没有国王能闯入--你是主人。"; +/* alert title */ +"Not all relays connected" = "并非所有中继都已连接"; + /* No comment provided by engineer. */ "Not compatible!" = "不兼容!"; @@ -3790,9 +4283,15 @@ new chat action */ /* group pref value */ "on" = "开启"; +/* No comment provided by engineer. */ +"On your phone, not on servers." = "在你的手机上,而不是在服务器上。"; + /* No comment provided by engineer. */ "One-time invitation link" = "一次性邀请链接"; +/* chat link info line */ +"One-time link" = "一次性链接"; + /* No comment provided by engineer. */ "Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion 主机将是连接所必需的。\n需要兼容的 VPN。"; @@ -3802,6 +4301,9 @@ new chat action */ /* No comment provided by engineer. */ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; +/* No comment provided by engineer. */ +"Only channel owners can change channel preferences." = "只有频道所有者才能更改频道偏好设置。"; + /* No comment provided by engineer. */ "Only chat owners can change preferences." = "仅聊天所有人可更改首选项。"; @@ -3862,6 +4364,9 @@ new chat action */ /* No comment provided by engineer. */ "Only your contact can send voice messages." = "只有您的联系人可以发送语音消息。"; +/* No comment provided by engineer. */ +"Only your page above can show the preview." = "只有你在上方设置的页面可以显示预览。"; + /* alert action alert button */ "Open" = "打开"; @@ -3869,6 +4374,9 @@ alert button */ /* No comment provided by engineer. */ "Open changes" = "打开更改"; +/* new chat action */ +"Open channel" = "打开频道"; + /* new chat action */ "Open chat" = "打开聊天"; @@ -3881,6 +4389,9 @@ alert button */ /* No comment provided by engineer. */ "Open conditions" = "打开条款"; +/* alert title */ +"Open external link?" = "打开外部链接?"; + /* alert action */ "Open full link" = "打开完整链接"; @@ -3893,6 +4404,9 @@ alert button */ /* authentication reason */ "Open migration to another device" = "打开迁移到另一台设备"; +/* new chat action */ +"Open new channel" = "打开新频道"; + /* new chat action */ "Open new chat" = "打开新聊天"; @@ -3906,7 +4420,7 @@ alert button */ "Open to accept" = "打开以接受"; /* No comment provided by engineer. */ -"Open to connect" = "打开以连接"; +"Open to connect" = "打开并连接"; /* No comment provided by engineer. */ "Open to join" = "打开以加入"; @@ -3923,6 +4437,9 @@ alert button */ /* alert title */ "Operator server" = "运营方服务器"; +/* No comment provided by engineer. */ +"Operators commit to:\n- Be independent\n- Minimize metadata usage\n- Run verified open-source code" = "运营商承诺:\n- 保持独立\n- 尽量减少元数据使用\n- 运行经验证的开源代码"; + /* No comment provided by engineer. */ "Or import archive file" = "或者导入或者导入压缩文件"; @@ -3935,12 +4452,18 @@ alert button */ /* No comment provided by engineer. */ "Or securely share this file link" = "或安全地分享此文件链接"; +/* No comment provided by engineer. */ +"Or show QR in person or via video call." = "或当面或通过视频通话显示二维码。"; + /* No comment provided by engineer. */ "Or show this code" = "或者显示此码"; /* No comment provided by engineer. */ "Or to share privately" = "或者私下分享"; +/* No comment provided by engineer. */ +"Or use this QR - print or show online." = "或使用此二维码,可以打印或在线显示。"; + /* No comment provided by engineer. */ "Organize chats into lists" = "将聊天组织到列表"; @@ -3959,9 +4482,18 @@ alert button */ /* member role */ "owner" = "群主"; +/* No comment provided by engineer. */ +"Owner" = "所有者"; + /* feature role */ "owners" = "所有者"; +/* No comment provided by engineer. */ +"Owners & contributors" = "所有者和贡献者"; + +/* No comment provided by engineer. */ +"Ownership: you can run your own relays." = "所有权:你可以运营自己的中继。"; + /* No comment provided by engineer. */ "Passcode" = "密码"; @@ -3989,6 +4521,9 @@ alert button */ /* No comment provided by engineer. */ "Paste image" = "粘贴图片"; +/* No comment provided by engineer. */ +"Paste link / Scan" = "粘贴链接 / 扫描"; + /* No comment provided by engineer. */ "Paste link to connect!" = "粘贴链接以连接!"; @@ -3998,6 +4533,9 @@ alert button */ /* No comment provided by engineer. */ "peer-to-peer" = "点对点"; +/* No comment provided by engineer. */ +"pending" = "待处理"; + /* No comment provided by engineer. */ "Pending" = "待定"; @@ -4094,6 +4632,12 @@ alert button */ /* No comment provided by engineer. */ "Preserve the last message draft, with attachments." = "保留最后的消息草稿及其附件。"; +/* No comment provided by engineer. */ +"Preset relay address" = "预设中继地址"; + +/* No comment provided by engineer. */ +"Preset relay name" = "预设中继名称"; + /* No comment provided by engineer. */ "Preset server address" = "预设服务器地址"; @@ -4106,15 +4650,18 @@ alert button */ /* No comment provided by engineer. */ "Previously connected servers" = "以前连接的服务器"; -/* No comment provided by engineer. */ -"Privacy & security" = "隐私和安全"; - /* 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: for owners and subscribers." = "隐私:保护所有者和订阅者。"; + +/* No comment provided by engineer. */ +"Private and secure messaging." = "私密且安全的消息传递。"; + /* No comment provided by engineer. */ "Private filenames" = "私密文件名"; @@ -4154,9 +4701,16 @@ alert button */ /* No comment provided by engineer. */ "Profile theme" = "个人资料主题"; +/* alert message +alert title */ +"Profile update will be sent to your SimpleX contacts." = "个人资料更新将发送给你的 SimpleX 联系人。"; + /* No comment provided by engineer. */ "Prohibit audio/video calls." = "禁止音频/视频通话。"; +/* No comment provided by engineer. */ +"Prohibit chats with admins." = "禁止与管理员聊天。"; + /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "禁止不可撤回消息删除。"; @@ -4172,6 +4726,9 @@ alert button */ /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "禁止向成员发送私信。"; +/* No comment provided by engineer. */ +"Prohibit sending direct messages to subscribers." = "禁止向订阅者发送直接消息。"; + /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "禁止发送限时消息。"; @@ -4214,6 +4771,9 @@ alert button */ /* No comment provided by engineer. */ "Proxy requires password" = "代理需要密码"; +/* No comment provided by engineer. */ +"Public channels - speak freely 🚀" = "公开频道 - 自由发言 🚀"; + /* No comment provided by engineer. */ "Push notifications" = "推送通知"; @@ -4238,7 +4798,7 @@ alert button */ /* swipe action */ "Read" = "已读"; -/* No comment provided by engineer. */ +/* profile description teaser */ "Read more" = "阅读更多"; /* No comment provided by engineer. */ @@ -4328,6 +4888,9 @@ alert button */ /* No comment provided by engineer. */ "Register" = "注册"; +/* token info */ +"Register notification token?" = "注册通知令牌?"; + /* token status text */ "Registered" = "已注册"; @@ -4351,12 +4914,42 @@ swipe action */ /* call status */ "rejected call" = "拒接来电"; +/* member role */ +"relay" = "中继"; + +/* No comment provided by engineer. */ +"Relay" = "中继"; + +/* alert title */ +"Relay address" = "中继地址"; + +/* alert title */ +"Relay connection failed" = "中继连接失败"; + +/* No comment provided by engineer. */ +"Relay link" = "中继链接"; + +/* alert message */ +"Relay results:" = "中继结果:"; + /* No comment provided by engineer. */ "Relay server is only used if necessary. Another party can observe your IP address." = "中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。"; /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。"; +/* No comment provided by engineer. */ +"Relay test failed!" = "中继测试失败!"; + +/* alert message */ +"Relay will be removed from channel - this cannot be undone!" = "中继将从频道移除,且无法撤消!"; + +/* alert message */ +"Relays added: %@." = "已添加中继:%@。"; + +/* No comment provided by engineer. */ +"Reliability: many relays per channel." = "可靠性:每个频道使用多个中继。"; + /* alert action */ "Remove" = "移除"; @@ -4381,12 +4974,27 @@ swipe action */ /* No comment provided by engineer. */ "Remove passphrase from keychain?" = "从钥匙串中删除密码?"; +/* No comment provided by engineer. */ +"Remove relay" = "移除中继"; + +/* alert title */ +"Remove relay?" = "要移除中继吗?"; + +/* alert title */ +"Remove subscriber?" = "要移除订阅者吗?"; + /* No comment provided by engineer. */ "removed" = "已删除"; +/* receive error chat item */ +"removed (%d attempts)" = "已移除(%d 次尝试)"; + /* rcv group event chat item */ "removed %@" = "已删除 %@"; +/* No comment provided by engineer. */ +"removed by operator" = "已被运营商移除"; + /* profile update event chat item */ "removed contact address" = "删除了联系地址"; @@ -4534,6 +5142,9 @@ swipe action */ /* admission stage */ "Review members" = "审核成员"; +/* admission stage description */ +"Review members before admitting (\"knocking\")." = "在批准加入前审核成员(“敲门”)。"; + /* No comment provided by engineer. */ "reviewed by admins" = "由管理员审核"; @@ -4549,16 +5160,32 @@ swipe action */ /* No comment provided by engineer. */ "Role" = "角色"; +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All chat members will be notified." = "将变更成员角色为“%@”。所有成员都会收到通知。"; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All group members will be notified." = "成员角色将更改为 \"%@\"。所有群成员将收到通知。"; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". All subscribers will be notified." = "角色将被变更为“%@”。所有订阅者将会收到通知。"; + +/* No comment provided by engineer. */ +"Role will be changed to \"%@\". The member will receive a new invitation." = "成员角色将更改为 \"%@\"。该成员将收到一份新的邀请。"; + /* No comment provided by engineer. */ "Run chat" = "运行聊天"; +/* No comment provided by engineer. */ +"Safe web links" = "安全的网页链接"; + /* No comment provided by engineer. */ "Safely receive files" = "安全接收文件"; /* No comment provided by engineer. */ "Safer groups" = "更安全的群组"; -/* alert button +/* alert action +alert button chat item action */ "Save" = "保存"; @@ -4568,6 +5195,9 @@ chat item action */ /* alert button */ "Save (and notify members)" = "保存(并通知成员)"; +/* alert button */ +"Save (and notify subscribers)" = "保存(并通知订阅者)"; + /* alert title */ "Save admission settings?" = "保存入群设置?"; @@ -4577,12 +5207,24 @@ chat item action */ /* No comment provided by engineer. */ "Save and notify group members" = "保存并通知群组成员"; +/* No comment provided by engineer. */ +"Save and notify members" = "保存并通知成员"; + +/* No comment provided by engineer. */ +"Save and notify subscribers" = "保存并通知订阅者"; + /* No comment provided by engineer. */ "Save and reconnect" = "保存并重新连接"; /* No comment provided by engineer. */ "Save and update group profile" = "保存和更新组配置文件"; +/* No comment provided by engineer. */ +"Save channel profile" = "保存频道资料"; + +/* alert title */ +"Save channel profile?" = "要保存频道资料吗?"; + /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; @@ -4610,6 +5252,9 @@ chat item action */ /* alert title */ "Save servers?" = "保存服务器?"; +/* alert title */ +"Save webpage settings?" = "要保存网页设置吗?"; + /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; @@ -4712,6 +5357,9 @@ chat item action */ /* chat item text */ "security code changed" = "安全密码已更改"; +/* No comment provided by engineer. */ +"Security: owners hold channel keys." = "安全性:所有者持有频道密钥。"; + /* chat item action */ "Select" = "选择"; @@ -4790,20 +5438,26 @@ chat item action */ /* No comment provided by engineer. */ "Send request without message" = "发送无消息请求"; +/* No comment provided by engineer. */ +"Send the link via any messenger - it's secure. Ask to paste into SimpleX." = "通过任何通讯应用发送链接,这是安全的。请对方粘贴到 SimpleX 中。"; + /* 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 up to 100 last messages to new subscribers." = "最多将最近 100 条消息发送给新订阅者。"; + /* No comment provided by engineer. */ "Send your private feedback to groups." = "向群发送私密反馈。"; /* alert message */ "Sender cancelled file transfer." = "发送人已取消文件传输。"; -/* No comment provided by engineer. */ -"Sender may have deleted the connection request." = "发送人可能已删除连接请求。"; +/* alert message */ +"Sending a link preview may reveal your IP address to the website. You can change this in Privacy settings later." = "发送链接预览可能会向该网站透露你的 IP 地址。你稍后可以在隐私设置中更改此设置。"; /* No comment provided by engineer. */ "Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "将对所有可见聊天配置文件中的所有联系人启用送达回执功能。"; @@ -4883,11 +5537,14 @@ chat item action */ /* queue info */ "server queue info: %@\n\nlast received msg: %@" = "服务器队列信息: %1$@\n\n上次收到的消息: %2$@"; -/* server test error */ -"Server requires authorization to create queues, check password." = "服务器需要授权才能创建队列,检查密码"; +/* relay test error */ +"Server requires authorization to connect to relay, check password." = "服务器需要授权才能连接到中继,请检查密码。"; /* server test error */ -"Server requires authorization to upload, check password." = "服务器需要授权来上传,检查密码"; +"Server requires authorization to create queues, check password." = "服务器需要授权才能创建队列,请检查密码。"; + +/* server test error */ +"Server requires authorization to upload, check password." = "服务器需要授权才能上传,请检查密码。"; /* No comment provided by engineer. */ "Server test failed!" = "服务器测试失败!"; @@ -4967,6 +5624,12 @@ chat item action */ /* alert message */ "Settings were changed." = "设置已修改。"; +/* No comment provided by engineer. */ +"Setup notifications" = "设置通知"; + +/* No comment provided by engineer. */ +"Setup routers" = "设置路由器"; + /* No comment provided by engineer. */ "Shape profile images" = "改变个人资料图形状"; @@ -4986,6 +5649,12 @@ chat item action */ /* No comment provided by engineer. */ "Share address publicly" = "公开分享地址"; +/* alert title */ +"Share address with SimpleX contacts?" = "要与 SimpleX 联系人分享地址吗?"; + +/* No comment provided by engineer. */ +"Share channel" = "分享频道"; + /* No comment provided by engineer. */ "Share from other apps." = "从其他应用程序共享。"; @@ -5001,6 +5670,9 @@ chat item action */ /* No comment provided by engineer. */ "Share profile" = "分享资料"; +/* No comment provided by engineer. */ +"Share relay address" = "分享中继地址"; + /* No comment provided by engineer. */ "Share SimpleX address on social media." = "在社媒上分享 SimpleX 地址。"; @@ -5010,6 +5682,12 @@ chat item action */ /* No comment provided by engineer. */ "Share to SimpleX" = "分享到 SimpleX"; +/* No comment provided by engineer. */ +"Share via chat" = "通过聊天分享"; + +/* No comment provided by engineer. */ +"Share with SimpleX contacts" = "与 SimpleX 联系人分享"; + /* No comment provided by engineer. */ "Share your address" = "分享地址"; @@ -5106,12 +5784,24 @@ chat item action */ /* No comment provided by engineer. */ "SimpleX Lock turned on" = "已开启 SimpleX 锁定"; +/* No comment provided by engineer. */ +"SimpleX name" = "SimpleX 名称"; + +/* No comment provided by engineer. */ +"SimpleX name error" = "SimpleX 名称错误"; + +/* alert title */ +"SimpleX name not verified" = "SimpleX 名称未验证"; + /* simplex link type */ "SimpleX one-time invitation" = "SimpleX 一次性邀请"; /* No comment provided by engineer. */ "SimpleX protocols reviewed by Trail of Bits." = "SimpleX 协议由 Trail of Bits 审阅。"; +/* simplex link type */ +"SimpleX relay address" = "SimpleX 中继地址"; + /* No comment provided by engineer. */ "Simplified incognito mode" = "简化的隐身模式"; @@ -5185,6 +5875,9 @@ report reason */ /* No comment provided by engineer. */ "Statistics" = "统计"; +/* No comment provided by engineer. */ +"Status" = "状态"; + /* No comment provided by engineer. */ "Stop" = "停止"; @@ -5218,6 +5911,9 @@ report reason */ /* No comment provided by engineer. */ "Stopping chat" = "正在停止聊天"; +/* No comment provided by engineer. */ +"Storage" = "存储"; + /* No comment provided by engineer. */ "strike" = "删去"; @@ -5230,6 +5926,48 @@ report reason */ /* No comment provided by engineer. */ "Subscribed" = "已订阅"; +/* No comment provided by engineer. */ +"Subscriber" = "订阅者"; + +/* chat feature */ +"Subscriber reports" = "订阅者举报报告"; + +/* alert message */ +"Subscriber will be removed from channel - this cannot be undone!" = "订阅者将从频道移除,且无法撤消!"; + +/* No comment provided by engineer. */ +"Subscribers" = "订阅者"; + +/* No comment provided by engineer. */ +"Subscribers can add message reactions." = "订阅者可以添加消息回应。"; + +/* No comment provided by engineer. */ +"Subscribers can chat with admins." = "订阅者可以与管理员聊天。"; + +/* No comment provided by engineer. */ +"Subscribers can irreversibly delete sent messages. (24 hours)" = "订阅者可以删除已发送的消息,且无法撤消。(24 小时)"; + +/* No comment provided by engineer. */ +"Subscribers can report messsages to moderators." = "订阅者可以向审核员举报消息。"; + +/* No comment provided by engineer. */ +"Subscribers can send direct messages." = "订阅者可以发送直接消息。"; + +/* No comment provided by engineer. */ +"Subscribers can send disappearing messages." = "订阅者可以发送自动销毁消息。"; + +/* No comment provided by engineer. */ +"Subscribers can send files and media." = "订阅者可以发送文件和媒体。"; + +/* No comment provided by engineer. */ +"Subscribers can send SimpleX links." = "订阅者可以发送 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"Subscribers can send voice messages." = "订阅者可以发送语音消息。"; + +/* No comment provided by engineer. */ +"Subscribers use relay link to connect to the channel.\nRelay address was used to set up this relay for the channel." = "订阅者使用中继链接连接到频道。\n中继地址曾用于为此频道设置此中继。"; + /* No comment provided by engineer. */ "Subscription errors" = "订阅错误"; @@ -5237,7 +5975,7 @@ report reason */ "Subscriptions ignored" = "忽略订阅"; /* No comment provided by engineer. */ -"Support SimpleX Chat" = "支持 SimpleX Chat"; +"Support the project" = "支持项目"; /* No comment provided by engineer. */ "Switch audio and video during the call." = "通话期间切换音频和视频。"; @@ -5257,6 +5995,9 @@ report reason */ /* No comment provided by engineer. */ "Take picture" = "拍照"; +/* No comment provided by engineer. */ +"Talk to someone" = "与某人聊天"; + /* No comment provided by engineer. */ "Tap button " = "点击按钮 "; @@ -5269,6 +6010,9 @@ report reason */ /* No comment provided by engineer. */ "Tap Connect to use bot" = "轻按“连接”使用机器人"; +/* No comment provided by engineer. */ +"Tap Join channel" = "点按“加入频道”"; + /* No comment provided by engineer. */ "Tap Join group" = "轻按加入群"; @@ -5284,6 +6028,9 @@ report reason */ /* No comment provided by engineer. */ "Tap to join incognito" = "点击以加入隐身聊天"; +/* No comment provided by engineer. */ +"Tap to open" = "点按即可打开"; + /* No comment provided by engineer. */ "Tap to paste link" = "轻按粘贴链接"; @@ -5321,6 +6068,9 @@ server test failure */ /* No comment provided by engineer. */ "Test notifications" = "测试通知"; +/* No comment provided by engineer. */ +"Test relay" = "测试中继"; + /* No comment provided by engineer. */ "Test server" = "测试服务器"; @@ -5348,15 +6098,24 @@ server test failure */ /* No comment provided by engineer. */ "The app protects your privacy by using different operators in each conversation." = "应用通过在每个对话中使用不同运营方保护你的隐私。"; +/* No comment provided by engineer. */ +"The app removed this message after %lld attempts to receive it." = "应用程序在尝试接收此消息 %lld 次后已将其移除。"; + /* No comment provided by engineer. */ "The app will ask to confirm downloads from unknown file servers (except .onion)." = "该应用程序将要求确认从未知文件服务器(.onion 除外)下载。"; /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "更改数据库密码的尝试未完成。"; +/* badge alert */ +"The badge is signed with a key that this version of the app does not recognize. Update the app to verify this badge." = "此徽章使用此版本应用程序无法识别的密钥签署。请更新应用程序来验证此徽章。"; + /* No comment provided by engineer. */ "The code you scanned is not a SimpleX link QR code." = "您扫描的码不是 SimpleX 链接的二维码。"; +/* conn error description */ +"The connection reached the limit of undelivered messages" = "连接已达到未送达消息数量上限"; + /* No comment provided by engineer. */ "The connection reached the limit of undelivered messages, your contact may be offline." = "连接达到了未送达消息上限,你的联系人可能处于离线状态。"; @@ -5372,6 +6131,9 @@ server test failure */ /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "加密正在运行,不需要新的加密协议。这可能会导致连接错误!"; +/* No comment provided by engineer. */ +"The first network where you own\nyour contacts and groups." = "第一个让你拥有\n自己的联系人和群组的网络。"; + /* No comment provided by engineer. */ "The hash of the previous message is different." = "上一条消息的散列不同。"; @@ -5399,18 +6161,27 @@ server test failure */ /* No comment provided by engineer. */ "The oldest human freedom - to speak to another person without being watched - built on infrastructure that cannot betray it." = "人类最古老的自由--与他人交谈而不被监视--建立在不会背叛它的基础设施之上。"; +/* No comment provided by engineer. */ +"The same conditions will apply to operator **%@**." = "相同条件将适用于运营商 **%@**。"; + /* No comment provided by engineer. */ "The second preset operator in the app!" = "应用中的第二个预设运营方!"; /* No comment provided by engineer. */ "The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; +/* No comment provided by engineer. */ +"The sender deleted the connection request." = "发送人可能已删除连接请求。"; + /* alert message */ "The sender will NOT be notified" = "发送者将不会收到通知"; /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "您当前聊天资料 **%@** 的新连接服务器。"; +/* No comment provided by engineer. */ +"The servers for new files of your current chat profile **%@**." = "当前聊天个人资料 **%@** 的新文件服务器。"; + /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; @@ -5447,6 +6218,9 @@ server test failure */ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。"; +/* badge alert */ +"This badge could not be verified and may not be genuine." = "无法验证此徽章,可能并非真品。"; + /* E2EE info chat item */ "This chat is protected by end-to-end encryption." = "此聊天受端到端加密保护。"; @@ -5468,6 +6242,19 @@ server test failure */ /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; +/* alert message +alert subtitle */ +"This group requires a newer version of the app. Please update the app to join." = "此群组需要较新的应用程序版本。请更新应用程序才能加入。"; + +/* alert message */ +"This is a chat relay address, it cannot be used to connect." = "这是聊天中继地址,无法用于连接。"; + +/* alert message */ +"This is the last active relay. Removing it will prevent message delivery to subscribers." = "这是最后一个已启用的中继。移除它会导致无法向订阅者发送消息。"; + +/* new chat action */ +"This is your link for channel %@!" = "这是你的频道 %@ 链接!"; + /* 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." = "此链接需要更新的应用版本。请升级应用或请求你的联系人发送相容的链接。"; @@ -5501,6 +6288,9 @@ server test failure */ /* No comment provided by engineer. */ "To make a new connection" = "建立新连接"; +/* No comment provided by engineer. */ +"To make SimpleX Network last." = "让 SimpleX Network 长久延续。"; + /* No comment provided by engineer. */ "To protect against your link being replaced, you can compare contact security codes." = "为了防止链接被替换,你可以比较联系人安全代码。"; @@ -5552,9 +6342,15 @@ server test failure */ /* No comment provided by engineer. */ "Toggle incognito when connecting." = "在连接时切换隐身模式。"; +/* token status */ +"Token status: %@." = "令牌状态:%@。"; + /* No comment provided by engineer. */ "Toolbar opacity" = "工具栏不透明度"; +/* No comment provided by engineer. */ +"Top bar" = "顶部栏"; + /* No comment provided by engineer. */ "Total" = "共计"; @@ -5594,6 +6390,9 @@ server test failure */ /* No comment provided by engineer. */ "Unblock member?" = "解封成员吗?"; +/* No comment provided by engineer. */ +"Unblock subscriber for all?" = "要为所有人解除封锁订阅者吗?"; + /* rcv group event chat item */ "unblocked %@" = "未阻止 %@"; @@ -5642,9 +6441,6 @@ server test failure */ /* No comment provided by engineer. */ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。"; -/* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。"; - /* No comment provided by engineer. */ "Unlink" = "取消链接"; @@ -5669,9 +6465,15 @@ server test failure */ /* conn error description */ "Unsupported connection link" = "不支持的连接链接"; +/* badge alert title */ +"Unverified badge" = "未验证的徽章"; + /* No comment provided by engineer. */ "Up to 100 last messages are sent to new members." = "给新成员发送了最多 100 条历史消息。"; +/* No comment provided by engineer. */ +"Up to 100 last messages are sent to new subscribers." = "最多会将最近 100 条消息发送给新订阅者。"; + /* No comment provided by engineer. */ "Update" = "更新"; @@ -5684,6 +6486,9 @@ server test failure */ /* No comment provided by engineer. */ "Update settings?" = "更新设置?"; +/* rcv group event chat item */ +"updated channel profile" = "频道更新了频道资料"; + /* No comment provided by engineer. */ "Updated conditions" = "条款已更新"; @@ -5702,7 +6507,8 @@ server test failure */ /* No comment provided by engineer. */ "Upgrade address" = "升级地址"; -/* alert message */ +/* alert message +alert title */ "Upgrade address?" = "升级地址?"; /* No comment provided by engineer. */ @@ -5750,6 +6556,9 @@ server test failure */ /* No comment provided by engineer. */ "Use for messages" = "用于消息"; +/* No comment provided by engineer. */ +"Use for new channels" = "用于新频道"; + /* No comment provided by engineer. */ "Use for new connections" = "用于新连接"; @@ -5774,6 +6583,9 @@ server test failure */ /* No comment provided by engineer. */ "Use private routing with unknown servers." = "对未知服务器使用私有路由。"; +/* No comment provided by engineer. */ +"Use relay" = "使用中继"; + /* No comment provided by engineer. */ "Use server" = "使用服务器"; @@ -5798,9 +6610,15 @@ server test failure */ /* No comment provided by engineer. */ "Use the app with one hand." = "用一只手使用应用程序。"; +/* No comment provided by engineer. */ +"Use this address in your social media profile, website, or email signature." = "在社交媒体资料、网站或电子邮件签名中使用该地址。"; + /* No comment provided by engineer. */ "Use web port" = "使用 web 端口"; +/* No comment provided by engineer. */ +"Used chat relays do not support webpages." = "使用中的聊天中继不支持网页。"; + /* No comment provided by engineer. */ "User selection" = "用户选择"; @@ -5813,8 +6631,8 @@ server test failure */ /* No comment provided by engineer. */ "v%@" = "v%@"; -/* No comment provided by engineer. */ -"v%@ (%@)" = "v%@ (%@)"; +/* relay test step */ +"Verify" = "验证"; /* No comment provided by engineer. */ "Verify code with desktop" = "用桌面端验证代码"; @@ -5837,6 +6655,9 @@ server test failure */ /* No comment provided by engineer. */ "Verify security code" = "验证安全码"; +/* relay hostname */ +"via %@" = "通过 %@"; + /* No comment provided by engineer. */ "Via browser" = "通过浏览器"; @@ -5906,9 +6727,18 @@ server test failure */ /* No comment provided by engineer. */ "Voice messages prohibited!" = "语音消息禁止发送!"; +/* alert action */ +"Wait" = "等待"; + +/* relay test step */ +"Wait response" = "等待响应"; + /* No comment provided by engineer. */ "waiting for answer…" = "等待答复中……"; +/* No comment provided by engineer. */ +"Waiting for channel owner to add relays." = "正在等待频道所有者添加中继。"; + /* No comment provided by engineer. */ "waiting for confirmation…" = "等待确认中……"; @@ -5939,6 +6769,15 @@ server test failure */ /* No comment provided by engineer. */ "Warning: you may lose some data!" = "警告:您可能会丢失部分数据!"; +/* No comment provided by engineer. */ +"We made connecting simpler for new users." = "我们让连接对新用户更简单。"; + +/* No comment provided by engineer. */ +"Webpage code" = "网页代码"; + +/* alert message */ +"Webpage settings were changed. If you save, the updated settings will be sent to subscribers." = "网页设置已更改。如果保存,更新后的设置将发送给订阅者。"; + /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE 服务器"; @@ -5975,6 +6814,9 @@ server test failure */ /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; +/* No comment provided by engineer. */ +"Why SimpleX is built." = "为何打造 SimpleX。"; + /* No comment provided by engineer. */ "WiFi" = "WiFi"; @@ -6074,6 +6916,9 @@ server test failure */ /* No comment provided by engineer. */ "you are observer" = "您是观察者"; +/* No comment provided by engineer. */ +"you are subscriber" = "你是订阅者"; + /* snd group event chat item */ "you blocked %@" = "你阻止了%@"; @@ -6093,7 +6938,7 @@ server test failure */ "You can enable later via Settings" = "您可以稍后在设置中启用它"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; +"You can enable them later via app Your privacy settings." = "你可以稍后通过应用程序的你的隐私设置启用它们。"; /* No comment provided by engineer. */ "You can give another try." = "你可以再试一次。"; @@ -6116,6 +6961,9 @@ server test failure */ /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the channel." = "你可以分享链接或二维码,任何人都可以加入频道。"; + /* No comment provided by engineer. */ "You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。"; @@ -6128,6 +6976,9 @@ server test failure */ /* No comment provided by engineer. */ "You can still view conversation with %@ in the list of chats." = "您仍然可以在聊天列表中查看与 %@的对话。"; +/* badge alert */ +"You can support SimpleX starting from v7 of the app." = "从 v7 版本起您可以支持 SimpleX。"; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "您可以通过设置开启 SimpleX 锁定。"; @@ -6155,6 +7006,12 @@ server test failure */ /* snd group event chat item */ "you changed role of %@ to %@" = "您已将 %1$@ 的角色更改为 %2$@"; +/* No comment provided by engineer. */ +"You commit to:\n- Only legal content in public groups\n- Respect other users - no spam" = "你承诺:\n- 只在公开群组中发布合法内容\n- 尊重其他用户 - 不发垃圾消息"; + +/* No comment provided by engineer. */ +"You connected to the channel via this relay link." = "你已通过此中继链接连接到频道。"; + /* No comment provided by engineer. */ "You could not be verified; please try again." = "您的身份无法验证,请再试一次。"; @@ -6206,11 +7063,14 @@ server test failure */ /* 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 were born without an account" = "你生来就没有账户。"; +"You were born without an account" = "你生来就没有账户"; /* No comment provided by engineer. */ "You will be able to send messages **only after your request is accepted**." = "**只有在你的请求被接受后**你才能发送消息。"; @@ -6233,6 +7093,9 @@ server test failure */ /* No comment provided by engineer. */ "You will still receive calls and notifications from muted profiles when they are active." = "当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。"; +/* No comment provided by engineer. */ +"You will stop receiving messages from this channel. Chat history will be preserved." = "你将停止收到来自该频道的消息。聊天记录将被保留。"; + /* No comment provided by engineer. */ "You will stop receiving messages from this chat. Chat history will be preserved." = "你将停止从这个聊天收到消息。聊天历史将被保留。"; @@ -6258,7 +7121,7 @@ server test failure */ "Your calls" = "您的通话"; /* No comment provided by engineer. */ -"Your chat database" = "您的聊天数据库"; +"Your channel" = "你的频道"; /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "您的聊天数据库未加密——设置密码来加密。"; @@ -6269,9 +7132,18 @@ server test failure */ /* 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 removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link." = "除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。"; + /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。"; @@ -6299,6 +7171,12 @@ server test failure */ /* No comment provided by engineer. */ "Your ICE servers" = "您的 ICE 服务器"; +/* No comment provided by engineer. */ +"Your network" = "您的网络"; + +/* alert message */ +"Your new channel %@ is connected to %d of %d relays.\nIf you cancel, the channel will be deleted - you can create it again." = "你的新频道 %1$@ 已连接到 %2$d/%3$d 个中继。\n如果取消,频道将被删除;你可以再次创建。"; + /* No comment provided by engineer. */ "Your preferences" = "您的偏好设置"; @@ -6308,6 +7186,9 @@ server test failure */ /* No comment provided by engineer. */ "Your profile" = "您的个人资料"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared with channel relays and subscribers.\nRelays can access channel messages." = "你的个人资料 **%@** 将与频道中继和订阅者分享。\n中继可以访问频道消息。"; + /* No comment provided by engineer. */ "Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。"; @@ -6320,9 +7201,18 @@ server test failure */ /* alert message */ "Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "您的个人资料已修改。如果进行保存,更新后的个人资料将发送到所有联系人。"; +/* No comment provided by engineer. */ +"Your public address" = "你的公开地址"; + /* No comment provided by engineer. */ "Your random profile" = "您的随机资料"; +/* No comment provided by engineer. */ +"Your relay address" = "你的中继地址"; + +/* No comment provided by engineer. */ +"Your relay name" = "你的中继名称"; + /* No comment provided by engineer. */ "Your server address" = "您的服务器地址"; diff --git a/apps/multiplatform/README.md b/apps/multiplatform/README.md index eef1048ada..54e17d4c4e 100644 --- a/apps/multiplatform/README.md +++ b/apps/multiplatform/README.md @@ -9,11 +9,19 @@ This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX ## Build Commands ```bash -# Android debug APK -./gradlew assembleDebug +# Android debug APK, assembleGoogleDebug builds the flavor with the Play Billing dependency +./gradlew assembleFossDebug -# Android release APK -./gradlew assembleRelease +# Android release APK, distributed via F-Droid and GitHub +./gradlew assembleFossRelease + +# Android app bundle, distributed via Google Play, includes Play Billing +./gradlew bundleGoogleRelease + +# Always name the flavor for releases. The aggregate tasks (build, assemble, assembleRelease, +# bundle, bundleRelease) fail on purpose: they would package a release APK with Play Billing, +# or an app bundle without it. +# The fdroiddata recipe defaults to assembleRelease and must be changed to assembleFossRelease. # Desktop distribution (current OS) ./gradlew :desktop:packageDistributionForCurrentOS @@ -22,7 +30,7 @@ This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX ./gradlew desktopTest # Run Android instrumented tests (requires connected device/emulator) -./gradlew connectedAndroidTest +./gradlew connectedFossDebugAndroidTest # Build native libraries for all platforms ./gradlew common:cmakeBuild -PcrossCompile diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 5255319194..419fef90b9 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -35,6 +35,21 @@ android { manifestPlaceholders["extract_native_libs"] = rootProject.extra["compression.level"] as Int != 0 } + // `google` is distributed via Google Play as an app bundle and includes Play Billing. + // `foss` is distributed via F-Droid and as APKs on GitHub, without Play dependencies. + flavorDimensions += "store" + productFlavors { + create("google") { + dimension = "store" + buildConfigField("boolean", "PLAY_STORE", "true") + } + create("foss") { + dimension = "store" + isDefault = true + buildConfigField("boolean", "PLAY_STORE", "false") + } + } + buildTypes { debug { applicationIdSuffix = rootProject.extra["application_id.suffix"] as String @@ -128,8 +143,28 @@ android { } } +// The graph is checked rather than the requested task, because every aggregate task +// (assemble, assembleRelease, build, bundle, ...) packages these variants too. +val projectPath = project.path +val apkTasks = setOf("packageFossDebug", "packageGoogleDebug", "packageFossRelease", "packageGoogleRelease") +val apkTaskPaths = apkTasks.map { "$projectPath:$it" }.toSet() +val bundleTaskPaths = apkTaskPaths.map { it + "Bundle" }.toSet() +gradle.taskGraph.whenReady { + if (hasTask("$projectPath:packageGoogleRelease")) { + throw GradleException("A release apk must not include Play Billing, use assembleFossRelease or bundleGoogleRelease") + } + if (hasTask("$projectPath:packageFossReleaseBundle")) { + throw GradleException("An app bundle must include Play Billing, use bundleGoogleRelease or assembleFossRelease") + } + // `isBundle` above is derived from the whole invocation, so a bundle in it disables abi splits + if (apkTaskPaths.any { hasTask(it) } && bundleTaskPaths.any { hasTask(it) }) { + throw GradleException("Build the apks and the bundle in separate invocations, the bundle disables abi splits") + } +} + dependencies { implementation(project(":common")) + "googleImplementation"("com.android.billingclient:billing:9.1.0") implementation("androidx.core:core-ktx:1.13.1") //implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}") //implementation("androidx.compose.material:material:$compose_version") @@ -160,58 +195,61 @@ dependencies { tasks { val compressApk by creating { doLast { - val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null - val buildType: String = if (isRelease) "release" else "debug" val javaHome = System.getProperties()["java.home"] ?: org.gradle.internal.jvm.Jvm.current().javaHome val sdkDir = android.sdkDirectory.absolutePath - val keyAlias: String - val keyPassword: String - val storeFile: String - val storePassword: String - if (project.properties["android.injected.signing.key.alias"] != null) { - keyAlias = project.properties["android.injected.signing.key.alias"] as String - keyPassword = project.properties["android.injected.signing.key.password"] as String - storeFile = project.properties["android.injected.signing.store.file"] as String - storePassword = project.properties["android.injected.signing.store.password"] as String - } else { - try { - val gradleConfig = android.signingConfigs.getByName(buildType) - keyAlias = gradleConfig.keyAlias!! - keyPassword = gradleConfig.keyPassword!! - storeFile = gradleConfig.storeFile!!.absolutePath - storePassword = gradleConfig.storePassword!! - } catch (e: UnknownDomainObjectException) { - // There is no signing config for current build type, can"t sign the apk - println("No signing configs for this build type: $buildType") - return@doLast + // A single invocation can package more than one variant, for example assembleDebug + gradle.taskGraph.allTasks.filter { it.path in apkTaskPaths }.forEach { packageTask -> + val variant = packageTask.name.removePrefix("package") + val buildType: String = if (variant.endsWith("Release")) "release" else "debug" + val keyAlias: String + val keyPassword: String + val storeFile: String + val storePassword: String + if (project.properties["android.injected.signing.key.alias"] != null) { + keyAlias = project.properties["android.injected.signing.key.alias"] as String + keyPassword = project.properties["android.injected.signing.key.password"] as String + storeFile = project.properties["android.injected.signing.store.file"] as String + storePassword = project.properties["android.injected.signing.store.password"] as String + } else { + try { + val gradleConfig = android.signingConfigs.getByName(buildType) + keyAlias = gradleConfig.keyAlias!! + keyPassword = gradleConfig.keyPassword!! + storeFile = gradleConfig.storeFile!!.absolutePath + storePassword = gradleConfig.storePassword!! + } catch (e: UnknownDomainObjectException) { + // There is no signing config for current build type, can"t sign the apk + println("No signing configs for this build type: $buildType") + return@forEach + } + } + val outputDir = packageTask.outputs.files.files.last() + exec { + workingDir("../../scripts/android") + environment = mapOf( + "JAVA_HOME" to "$javaHome", + "PATH" to "${System.getenv("PATH")}:$javaHome/bin" + ) + commandLine = listOf( + "./compress-and-sign-apk.sh", + "${rootProject.extra["compression.level"]}", + "$outputDir", + sdkDir, + storeFile, + storePassword, + keyAlias, + keyPassword + ) } - } - lateinit var outputDir: File - named(if (isRelease) "packageRelease" else "packageDebug") { - outputDir = outputs.files.files.last() - } - exec { - workingDir("../../scripts/android") - environment = mapOf( - "JAVA_HOME" to "$javaHome", - "PATH" to "${System.getenv("PATH")}:$javaHome/bin" - ) - commandLine = listOf( - "./compress-and-sign-apk.sh", - "${rootProject.extra["compression.level"]}", - "$outputDir", - sdkDir, - storeFile, - storePassword, - keyAlias, - keyPassword - ) - } - if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") { - File(outputDir, "android-release.apk").renameTo(File(outputDir, "simplex.apk")) - File(outputDir, "android-armeabi-v7a-release.apk").renameTo(File(outputDir, "simplex-armv7a.apk")) - File(outputDir, "android-arm64-v8a-release.apk").renameTo(File(outputDir, "simplex.apk")) + if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") { + val flavor = variant.removeSuffix("Release").lowercase() + mapOf("arm64-v8a" to "simplex.apk", "armeabi-v7a" to "simplex-armv7a.apk").forEach { (abi, name) -> + if (!File(outputDir, "android-$flavor-$abi-release.apk").renameTo(File(outputDir, name))) { + logger.warn("No $abi apk to rename to $name") + } + } + } } // View all gradle properties set // project.properties.each { k, v -> println "$k -> $v" } @@ -221,9 +259,7 @@ tasks { // Don"t do anything if no compression is needed if (rootProject.extra["compression.level"] as Int != 0) { whenTaskAdded { - if (name == "packageDebug") { - finalizedBy(compressApk) - } else if (name == "packageRelease") { + if (name in apkTasks) { finalizedBy(compressApk) } } diff --git a/apps/multiplatform/android/src/foss/java/chat/simplex/app/PlayStore.kt b/apps/multiplatform/android/src/foss/java/chat/simplex/app/PlayStore.kt new file mode 100644 index 0000000000..181fe42389 --- /dev/null +++ b/apps/multiplatform/android/src/foss/java/chat/simplex/app/PlayStore.kt @@ -0,0 +1,4 @@ +package chat.simplex.app + +// Play Billing is only in the google flavor, so the Play country stays unknown here +fun loadPlayStoreCountry() {} diff --git a/apps/multiplatform/android/src/google/java/chat/simplex/app/PlayStore.kt b/apps/multiplatform/android/src/google/java/chat/simplex/app/PlayStore.kt new file mode 100644 index 0000000000..a0e7734ff0 --- /dev/null +++ b/apps/multiplatform/android/src/google/java/chat/simplex/app/PlayStore.kt @@ -0,0 +1,31 @@ +package chat.simplex.app + +import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.platform.androidPlayStoreCountry +import com.android.billingclient.api.* + +// Requests the country of the Google Play account into [androidPlayStoreCountry]. +// It stays null when Play is unavailable or the user is not signed in. +fun loadPlayStoreCountry() { + val client = BillingClient.newBuilder(androidAppContext) + .setListener { _, _ -> } + .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) + .build() + client.startConnection(object : BillingClientStateListener { + override fun onBillingSetupFinished(result: BillingResult) { + if (result.responseCode != BillingClient.BillingResponseCode.OK) { + client.endConnection() + return + } + client.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build()) { configResult, config -> + if (configResult.responseCode == BillingClient.BillingResponseCode.OK) { + androidPlayStoreCountry.value = config?.countryCode + } + client.endConnection() + } + } + + // The connection is only used for this one request, it is not retried + override fun onBillingServiceDisconnected() = client.endConnection() + }) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index bacdfe70af..7415b6015d 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,6 +1,5 @@ package chat.simplex.app -import android.content.Context import android.content.Intent import android.net.Uri import android.os.* @@ -8,7 +7,6 @@ import android.view.View import android.view.WindowManager import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.ui.platform.ClipboardManager import androidx.fragment.app.FragmentActivity import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.getUserIdFromIntent @@ -21,7 +19,6 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.* import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { @@ -73,17 +70,6 @@ class MainActivity: FragmentActivity() { override fun onResume() { super.onResume() AppLock.recheckAuthState() - withApi { - delay(1000) - if (!isAppOnForeground) return@withApi - /** - * When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies - * access to clipboard because the app considered in background. - * This will ensure that the app will get the event on resume - * */ - val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - chatModel.clipboardHasText.value = service.hasPrimaryClip() - } } override fun onPause() { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 83767f90d7..ce47d2c5de 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -341,6 +341,8 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidIsXiaomiDevice(): Boolean = setOf("xiaomi", "redmi", "poco").contains(Build.BRAND.lowercase()) + override fun androidLoadPlayStoreCountry() = loadPlayStoreCountry() + @SuppressLint("SourceLockedOrientationActivity") @Composable override fun androidLockPortraitOrientation() { @@ -370,6 +372,8 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidCreateActiveCallState(): Closeable = ActiveCallState() override val androidApiLevel: Int get() = Build.VERSION.SDK_INT + + override val androidIsPlayStoreBuild: Boolean get() = BuildConfig.PLAY_STORE } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 98845365fc..1f55b9c660 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -189,7 +189,6 @@ buildConfig { buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"") buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}") buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"") - buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}") buildConfigField("Boolean", "SIMPLEX_ASSETS", "$hasSimplexAssets") } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt index 83677f3318..1826b2114e 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt @@ -42,10 +42,9 @@ actual fun SavePassphraseSetting( } Text( stringResource(MR.strings.save_passphrase_in_keychain), - Modifier.padding(end = 24.dp), + Modifier.weight(1f).padding(end = 24.dp), color = Color.Unspecified ) - Spacer(Modifier.fillMaxWidth().weight(1f)) DefaultSwitch( checked = useKeychain, onCheckedChange = onCheckedChange, diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index a5021ae54c..141d2d2665 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -1,7 +1,5 @@ package chat.simplex.common.views.helpers -import android.content.ClipboardManager -import android.content.Context import android.content.res.Resources import android.graphics.* import android.graphics.Typeface @@ -14,7 +12,6 @@ import android.text.SpannedString import android.text.style.* import android.util.Base64 import android.view.WindowManager -import androidx.compose.runtime.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* @@ -58,19 +55,6 @@ fun keepScreenOn(on: Boolean) { } } -@Composable -actual fun SetupClipboardListener() { - DisposableEffect(Unit) { - val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val listener = { chatModel.clipboardHasText.value = service.hasPrimaryClip() } - chatModel.clipboardHasText.value = service.hasPrimaryClip() - service.addPrimaryClipChangedListener(listener) - onDispose { - service.removePrimaryClipChangedListener(listener) - } - } -} - actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) } @@ -214,13 +198,14 @@ private fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, req // First decode with inJustDecodeBounds=true to check dimensions return BitmapFactory.Options().run { inJustDecodeBounds = true - BitmapFactory.decodeByteArray(data, 0, data.size) + BitmapFactory.decodeByteArray(data, 0, data.size, this) // Calculate inSampleSize inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) // Decode bitmap with inSampleSize set inJustDecodeBounds = false - BitmapFactory.decodeByteArray(data, 0, data.size) + BitmapFactory.decodeByteArray(data, 0, data.size, this) + ?: throw IOException("Unable to decode image") } } @@ -356,6 +341,19 @@ actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolea VideoPlayerInterface.PreviewAndDuration(null, 0, 0) } +actual suspend fun hasVideoTrack(uri: URI): Boolean { + val mmr = MediaMetadataRetriever() + return try { + mmr.setDataSource(androidAppContext, uri.toUri()) + mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) == "yes" + } catch (e: Exception) { + Log.e(TAG, "Utils.android hasVideoTrack error: ${e.message}") + false + } finally { + mmr.release() + } +} + actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encodeToString(this, Base64.DEFAULT) actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this, Base64.DEFAULT) 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 47506d9532..c16d1ea90d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced -import SectionSpacer import SectionView import android.app.Activity import android.content.ComponentName @@ -126,9 +125,9 @@ fun AppearanceScope.AppearanceLayout( SectionDividerSpaced() ProfileImageSection() - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { + SectionView(stringResource(MR.strings.settings_section_title_icon), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF)) { LazyRow { items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> val item = AppIcon.values()[index] @@ -152,7 +151,7 @@ fun AppearanceScope.AppearanceLayout( } } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() FontScaleSection() SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 04b59732dd..5e9706d713 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -1,7 +1,15 @@ package chat.simplex.common.views.usersettings +import SectionItemView import SectionView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* @@ -11,19 +19,19 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -actual fun SettingsSectionApp( +actual fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) - SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) + SectionView { SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) - AppVersionItem(showVersion) } } +@Composable +actual fun AppShutdownItem() { + SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), ::shutdownAppAlert) +} fun restartApp() { ProcessPhoenix.triggerRebirth(androidAppContext) @@ -36,11 +44,28 @@ private fun shutdownApp() { Runtime.getRuntime().exit(0) } -private fun shutdownAppAlert(onConfirm: () -> Unit) { - AlertManager.shared.showAlertDialog( +private fun shutdownAppAlert() { + AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.shutdown_alert_question), text = generalGetString(MR.strings.shutdown_alert_desc), - destructive = true, - onConfirm = onConfirm + buttons = { + Column { + SectionItemView({ AlertManager.shared.hideAlert() }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + } + SectionItemView({ + AlertManager.shared.hideAlert() + restartApp() + }) { + Text(stringResource(MR.strings.settings_restart_app), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + shutdownApp() + }) { + Text(stringResource(MR.strings.settings_shutdown), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + } + } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 7542a0b8c6..cb91c386ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -163,7 +163,6 @@ fun MainScreen() { userPickerState.value = AnimatedViewState.VISIBLE } } - SetupClipboardListener() if (appPlatform.isAndroid) { AndroidWrapInCallLayout { AndroidScreen(userPickerState) 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 19b36067ed..c9c30950ea 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 @@ -202,6 +202,7 @@ object ChatModel { val migrationState: MutableState<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) } var draft = mutableStateOf(null as ComposeState?) + // chat id with chat scope, see draftChatId() - group chat and its support chats have the same chat id var draftChatId = mutableStateOf(null as String?) // working with external intents or internal forwarding of chat items @@ -210,7 +211,6 @@ object ChatModel { val filesToDelete = mutableSetOf<File>() val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) } - val clipboardHasText = mutableStateOf(false) val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) val conditions = mutableStateOf(ServerOperatorConditionsDetail.empty) @@ -697,15 +697,15 @@ object ChatModel { } suspend fun removeMemberItems(rhId: Long?, removedMember: GroupMember, byMember: GroupMember, groupInfo: GroupInfo) { - fun removedUpdatedItem(item: ChatItem): ChatItem? { - val newContent = when { - item.chatDir is CIDirection.GroupSnd && removedMember.groupMemberId == groupInfo.membership.groupMemberId -> CIContent.SndModerated - item.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember.groupMemberId == removedMember.groupMemberId -> CIContent.RcvModerated - else -> return null - } + fun isRemovedMemberItem(item: ChatItem): Boolean = when { + item.chatDir is CIDirection.GroupSnd -> removedMember.groupMemberId == groupInfo.membership.groupMemberId + item.chatDir is CIDirection.GroupRcv -> item.chatDir.groupMember.groupMemberId == removedMember.groupMemberId + else -> false + } + fun markedUpdatedItem(item: ChatItem): ChatItem? { + if (!isRemovedMemberItem(item)) return null val updatedItem = item.copy( - meta = item.meta.copy(itemDeleted = CIDeleted.Moderated(Clock.System.now(), byGroupMember = byMember)), - content = if (groupInfo.fullGroupPreferences.fullDelete.on) newContent else item.content + meta = item.meta.copy(itemDeleted = CIDeleted.Moderated(Clock.System.now(), byGroupMember = byMember)) ) if (item.isActiveReport) { decreaseGroupReportsCounter(rhId, groupInfo.id) @@ -713,21 +713,52 @@ object ChatModel { return updatedItem } + // Mirrors backend groupFeatureMemberAllowed: fullDelete may be role-gated in business groups. + val fullDeletePref = groupInfo.fullGroupPreferences.fullDelete + val fullDelete = fullDeletePref.on && + byMember.memberRole >= (fullDeletePref.role ?: GroupMemberRole.Observer) val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review if (chatId.value == groupInfo.id) { - for (i in 0 until chatItems.value.size) { - val updatedItem = removedUpdatedItem(chatItems.value[i]) - if (updatedItem != null) { - updateChatItem(cInfo, updatedItem, atIndex = i) + if (fullDelete) { + for (item in chatItems.value) { + if (isRemovedMemberItem(item)) { + if (item.isRcvNew) { + decreaseCounterInPrimaryContext(rhId, groupInfo.id) + } + if (item.isActiveReport) { + decreaseGroupReportsCounter(rhId, groupInfo.id) + } + } + } + chatItems.removeAllAndNotify { item -> + val remove = isRemovedMemberItem(item) + if (remove) AudioPlayer.stop(item) + remove + } + } else { + for (i in 0 until chatItems.value.size) { + val updatedItem = markedUpdatedItem(chatItems.value[i]) + if (updatedItem != null) { + updateChatItem(cInfo, updatedItem, atIndex = i) + } } } } else { val i = getChatIndex(rhId, groupInfo.id) - val chat = chats[i] - if (chat.chatItems.isNotEmpty()) { - val updatedItem = removedUpdatedItem(chat.chatItems[0]) - if (updatedItem != null) { - chats.value[i] = chat.copy(chatItems = listOf(updatedItem)) + if (i >= 0) { + val chat = chats[i] + if (chat.chatItems.isNotEmpty()) { + val preview = chat.chatItems[0] + if (isRemovedMemberItem(preview)) { + if (fullDelete) { + chats.value[i] = chat.copy(chatItems = listOf(ChatItem.deletedItemDummy)) + } else { + val updatedItem = markedUpdatedItem(preview) + if (updatedItem != null) { + chats.value[i] = chat.copy(chatItems = listOf(updatedItem)) + } + } + } } } } @@ -1229,6 +1260,12 @@ fun sameChatScope(scope1: GroupChatScope, scope2: GroupChatScope) = && scope2 is GroupChatScope.MemberSupport && scope1.groupMemberId_ == scope2.groupMemberId_ +fun draftChatId(chatId: String?, scope: GroupChatScope?): String? = + if (chatId == null || scope == null) chatId + else when (scope) { + is GroupChatScope.MemberSupport -> "$chatId support:${scope.groupMemberId_ ?: ""}" + } + @Serializable sealed class GroupChatScopeInfo { @Serializable @SerialName("memberSupport") data class MemberSupport(val groupMember_: GroupMember?) : GroupChatScopeInfo() @@ -1263,6 +1300,7 @@ data class User( override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName override val shortDescr: String? get() = profile.shortDescr + override val profileDescription: String? get() = profile.description override val image: String? get() = profile.image override val localAlias: String = "" @@ -1335,6 +1373,7 @@ interface NamedChat { val displayName: String val fullName: String val shortDescr: String? + val profileDescription: String? get() = null val image: String? val localAlias: String val chatViewName: String @@ -1460,6 +1499,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = contact.displayName override val fullName get() = contact.fullName override val shortDescr get() = contact.profile.shortDescr + override val profileDescription get() = contact.profile.description override val image get() = contact.image override val localAlias: String get() = contact.localAlias override fun anyNameContains(searchAnyCase: String): Boolean = contact.anyNameContains(searchAnyCase) @@ -1488,6 +1528,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = groupInfo.displayName override val fullName get() = groupInfo.fullName override val shortDescr get() = groupInfo.groupProfile.shortDescr + override val profileDescription get() = groupInfo.profileDescription override val image get() = groupInfo.image override val localAlias get() = groupInfo.localAlias @@ -1542,6 +1583,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val displayName get() = contactRequest.displayName override val fullName get() = contactRequest.fullName override val shortDescr get() = contactRequest.profile.shortDescr + override val profileDescription get() = contactRequest.profile.description override val image get() = contactRequest.image override val localAlias get() = contactRequest.localAlias @@ -1629,9 +1671,6 @@ sealed class ChatInfo: SomeChat, NamedChat { if (groupInfo.membership.memberActive) { when (groupChatScope) { null -> { - if (allRelaysBroken && groupInfo.useRelays) { - return generalGetString(MR.strings.cant_broadcast_message) to null - } if (groupInfo.membership.memberPending) { return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc) } @@ -1642,6 +1681,9 @@ sealed class ChatInfo: SomeChat, NamedChat { generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc) } } + if (allRelaysBroken && groupInfo.useRelays) { + return generalGetString(MR.strings.cant_broadcast_message) to null + } return null } is GroupChatScopeInfo.MemberSupport -> @@ -1832,6 +1874,7 @@ data class Contact( override val displayName get() = localAlias.ifEmpty { profile.displayName } override val fullName get() = profile.fullName override val shortDescr get() = profile.shortDescr + override val profileDescription get() = profile.description override val image get() = profile.image val contactLink: String? = profile.contactLink override val localAlias get() = profile.localAlias @@ -2001,6 +2044,7 @@ data class Profile( override val displayName: String, override val fullName: String, override val shortDescr: String?, + val description: String? = null, override val image: String? = null, override val localAlias : String = "", val contactLink: String? = null, @@ -2008,14 +2052,17 @@ data class Profile( val peerType: ChatPeerType? = null, // the badge proof from the wire profile: not interpreted by the UI (display uses crypto-free LocalBadge), // but preserved so passing a link profile back to the core (apiPrepareContact) keeps the proof - val badge: BadgeProof? = null + val badge: BadgeProof? = null, + val contactDomain: SimplexDomainClaim? = null ): NamedChat { + override val profileDescription: String? get() = description + val profileViewName: String get() { return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, description, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = Profile( @@ -2032,16 +2079,21 @@ data class LocalProfile( override val displayName: String, override val fullName: String, override val shortDescr: String?, + val description: String? = null, override val image: String? = null, override val localAlias: String, val contactLink: String? = null, val preferences: ChatPreferences? = null, val peerType: ChatPeerType? = null, - val localBadge: LocalBadge? = null + val localBadge: LocalBadge? = null, + val contactDomain: SimplexDomainClaim? = null, + val contactDomainVerified: Boolean? = null ): NamedChat { + override val profileDescription: String? get() = description + val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, description, image, localAlias, contactLink, preferences, peerType, contactDomain = contactDomain) companion object { val sampleData = LocalProfile( @@ -2167,6 +2219,7 @@ data class GroupInfo ( val chatTags: List<Long>, val chatItemTTL: Long?, override val localAlias: String, + val groupDomainVerified: Boolean? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -2190,6 +2243,7 @@ data class GroupInfo ( override val displayName get() = localAlias.ifEmpty { groupProfile.displayName } override val fullName get() = groupProfile.fullName override val shortDescr get() = groupProfile.shortDescr + override val profileDescription get() = if (businessChat != null) groupProfile.description else null override val image get() = groupProfile.image val isOwner: Boolean @@ -2226,6 +2280,7 @@ data class GroupInfo ( GroupFeature.Reports -> p.reports.on GroupFeature.History -> p.history.on GroupFeature.Support -> p.support.on + GroupFeature.SignMessages -> p.signMessages.on } } @@ -2288,10 +2343,18 @@ object GroupTypeSerializer : KSerializer<GroupType> { } } +@Serializable +data class SimplexDomainClaim( + val domain: String, + val proof: SimplexDomainProof? = null +) { + val shortName: String get() = domain.removeSuffix(".simplex") +} + @Serializable data class PublicGroupAccess( val groupWebPage: String? = null, - val groupDomain: String? = null, + val groupDomainClaim: SimplexDomainClaim? = null, val domainWebPage: Boolean = false, val allowEmbedding: Boolean = false ) @@ -2379,11 +2442,31 @@ data class GroupShortLinkData ( val publicGroupData: PublicGroupData? = null ) +@Serializable +enum class MsgSigStatus { + @SerialName("verified") Verified, + @SerialName("signedNoKey") SignedNoKey; +} + +@Serializable +sealed class MsgVerified { + @Serializable @SerialName("signed") data class Signed(val sigStatus: MsgSigStatus): MsgVerified() + @Serializable @SerialName("sigMissing") object SigMissing: MsgVerified() + + val verified: Boolean get() = this is Signed && sigStatus == MsgSigStatus.Verified + + val sigMissingInfo: Pair<String, String>? get() = when (this) { + is SigMissing -> generalGetString(MR.strings.signature_missing_alert_title) to generalGetString(MR.strings.signature_missing_alert_desc) + else -> null + } +} + @Serializable enum class RelayStatus { @SerialName("new") New, @SerialName("invited") Invited, @SerialName("accepted") Accepted, + @SerialName("acknowledgedRoster") AcknowledgedRoster, @SerialName("active") Active, @SerialName("inactive") Inactive, @SerialName("rejected") Rejected; @@ -2392,6 +2475,7 @@ enum class RelayStatus { New -> generalGetString(MR.strings.relay_status_new) Invited -> generalGetString(MR.strings.relay_status_invited) Accepted -> generalGetString(MR.strings.relay_status_accepted) + AcknowledgedRoster -> generalGetString(MR.strings.relay_status_acknowledged_roster) Active -> generalGetString(MR.strings.relay_status_active) Inactive -> generalGetString(MR.strings.relay_status_inactive) Rejected -> generalGetString(MR.strings.relay_status_rejected) @@ -2443,6 +2527,7 @@ data class BusinessChatInfo ( val chatType: BusinessChatType, val businessId: String, val customerId: String, + val businessDomain: SimplexDomainClaim? = null, ) @Serializable @@ -2469,7 +2554,8 @@ data class GroupMember ( var activeConn: Connection? = null, val supportChat: GroupSupportChat? = null, val memberChatVRange: VersionRange, - val relayLink: String? = null + val relayLink: String? = null, + val memberVerifiedCode: SecurityCode? = null ): NamedChat { val id: String get() = "#$groupId @$groupMemberId" val ready get() = activeConn?.connStatus == ConnStatus.Ready @@ -2487,9 +2573,10 @@ data class GroupMember ( } override val fullName: String get() = memberProfile.fullName override val shortDescr: String? get() = memberProfile.shortDescr + override val profileDescription: String? get() = memberProfile.description override val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink - val verified get() = activeConn?.connectionCode != null + val verified get() = memberVerifiedCode != null || activeConn?.connectionCode != null // the badge shown for a member's name; a badge that expired over a month ago (ExpiredOld) is not shown val nameBadge: LocalBadge? get() { val badge = memberProfile.localBadge @@ -2584,8 +2671,15 @@ data class GroupMember ( fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? = if (memberRole == GroupMemberRole.Relay || !canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null + else if (groupInfo.useRelays && !groupInfo.isOwner) null else groupInfo.membership.memberRole.let { userRole -> - GroupMemberRole.selectableRoles.filter { it <= userRole } + if (groupInfo.useRelays) + // TODO [relays]: for now owners can only set observer/member in channels. + // Restore the full Owner-excluded picker when moderator/admin promotion is supported: + // GroupMemberRole.selectableRoles.filter { it <= userRole && it != GroupMemberRole.Owner } + listOf(GroupMemberRole.Observer, GroupMemberRole.Member) + else + GroupMemberRole.selectableRoles.filter { it <= userRole } } fun canBlockForAll(groupInfo: GroupInfo): Boolean { @@ -2663,11 +2757,11 @@ enum class GroupMemberRole(val memberRole: String) { val selectableRoles: List<GroupMemberRole> = listOf(Observer, Member, Moderator, Admin, Owner) } - val text: String get() = when (this) { + fun text(isChannel: Boolean): String = when (this) { Relay -> generalGetString(MR.strings.group_member_role_relay) - Observer -> generalGetString(MR.strings.group_member_role_observer) + Observer -> generalGetString(if (isChannel) MR.strings.group_member_role_observer_channel else MR.strings.group_member_role_observer) Author -> generalGetString(MR.strings.group_member_role_author) - Member -> generalGetString(MR.strings.group_member_role_member) + Member -> generalGetString(if (isChannel) MR.strings.group_member_role_member_channel else MR.strings.group_member_role_member) Moderator -> generalGetString(MR.strings.group_member_role_moderator) Admin -> generalGetString(MR.strings.group_member_role_admin) Owner -> generalGetString(MR.strings.group_member_role_owner) @@ -3502,7 +3596,8 @@ data class CIMeta ( val userMention: Boolean, val deletable: Boolean, val editable: Boolean, - val showGroupAsSender: Boolean + val showGroupAsSender: Boolean, + val msgVerified: MsgVerified? = null ) { val timestampText: String get() = getTimestampText(itemTs, true) @@ -4737,7 +4832,7 @@ sealed class MsgChatLink { is Invitation -> generalGetString(MR.strings.chat_link_one_time) } if (signed) { - s += " " + if (isPublicGroup) generalGetString(MR.strings.chat_link_from_owner) else generalGetString(MR.strings.chat_link_signed) + s += " " + generalGetString(MR.strings.chat_link_from_owner) } return s } @@ -4784,6 +4879,11 @@ sealed class Format { @Serializable @SerialName("simplexName") class SimplexName(val nameInfo: SimplexNameInfo): Format() @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() + @Serializable @SerialName("modal") class Modal(val modalName: String, val text: String): Format() { + companion object { + const val Description = "description" + } + } @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @Serializable @SerialName("unknown") class Unknown: Format() @@ -4804,6 +4904,7 @@ sealed class Format { is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle + is Modal -> linkStyle is Unknown -> SpanStyle() } @@ -4834,15 +4935,33 @@ enum class SimplexLinkType(val linkType: String) { @Serializable data class SimplexNameInfo( val nameType: SimplexNameType, - val nameDomain: SimplexNameDomain -) + val nameDomain: SimplexDomain +) { + // mirrors backend shortNameInfoStr: "#name" for a simplex public group, else prefix + full domain + val shortStr: String get() = when { + nameType == SimplexNameType.publicGroup && nameDomain.nameTLD == SimplexTLD.simplex && nameDomain.subDomain.isEmpty() -> "#" + nameDomain.domain + else -> (if (nameType == SimplexNameType.publicGroup) "#" else "@") + nameDomain.fullDomainName + } +} @Serializable -data class SimplexNameDomain( +data class SimplexDomain( val nameTLD: SimplexTLD, val domain: String, val subDomain: List<String> -) +) { + // mirrors backend fullDomainName: reverse(subDomain) + [domain] + tld + val fullDomainName: String get() { + val tld = when (nameTLD) { + SimplexTLD.simplex -> listOf("simplex") + SimplexTLD.testing -> listOf("testing") + SimplexTLD.web -> emptyList() + } + return (subDomain.reversed() + domain + tld).joinToString(".") + } + + val cmdString: String get() = "domain=$fullDomainName" +} @Serializable enum class SimplexTLD { @@ -4857,6 +4976,14 @@ enum class SimplexNameType { @SerialName("contact") contact } +// peer's signed name claim; UI only checks presence +@Serializable +data class SimplexDomainProof( + val linkOwnerId: String? = null, + val presHeader: String, + val signature: String +) + @Serializable enum class FormatColor(val color: String) { red("red"), @@ -5061,13 +5188,13 @@ sealed class RcvGroupEvent() { is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName) is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted) is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) - is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) + is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel)) is MemberBlocked -> if (blocked) { String.format(generalGetString(MR.strings.rcv_group_event_member_blocked), profile.profileViewName) } else { String.format(generalGetString(MR.strings.rcv_group_event_member_unblocked), profile.profileViewName) } - is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text) + is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text(isChannel = isChannel)) is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted) is GroupDeleted -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_channel_deleted else MR.strings.rcv_group_event_group_deleted) @@ -5106,8 +5233,8 @@ sealed class SndGroupEvent() { val text: String get() = text(isChannel = false) fun text(isChannel: Boolean): String = when (this) { - is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) - is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text) + is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text(isChannel = isChannel)) + is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text(isChannel = isChannel)) is MemberBlocked -> if (blocked) { String.format(generalGetString(MR.strings.snd_group_event_member_blocked), profile.profileViewName) } else { @@ -5262,7 +5389,8 @@ data class ChatTag( class ChatItemInfo( val itemVersions: List<ChatItemVersion>, val memberDeliveryStatuses: List<MemberDeliveryStatus>?, - val forwardedFromChatItem: AChatItem? + val forwardedFromChatItem: AChatItem?, + val fileXftpServers: List<String> = emptyList() ) @Serializable 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 8f7cce21c4..f9438fca32 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 @@ -122,12 +122,15 @@ class AppPreferences { val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true) val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true) val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true) + val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, false) val privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true) val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, false) // TODO remove val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } } val simplexLinkMode: SharedPreference<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default) val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) + val privacyShowSignature = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_SIGNATURE, true) + val privacyShowEncryption = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_FILE_ENCRYPTION, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) @@ -185,6 +188,7 @@ class AppPreferences { val networkTCPKeepCnt = mkIntPreference(SHARED_PREFS_NETWORK_TCP_KEEP_CNT, KeepAliveOpts.defaults.keepCnt) val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false) + val signMessageAlertShown = mkBoolPreference(SHARED_PREFS_SIGN_MESSAGE_ALERT_SHOWN, false) val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true) val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) @@ -270,6 +274,7 @@ class AppPreferences { hintPref(oneHandUICardShown, false), hintPref(addressCreationCardShown, false), hintPref(liveMessageAlertShown, false), + hintPref(signMessageAlertShown, false), hintPref(showHiddenProfilesNotice, true), hintPref(showMuteProfileAlert, true), hintPref(showReportsInSupportChatAlert, true), @@ -397,11 +402,14 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages" private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" + private const val SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES = "PrivacyVerifySimplexNames" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "PrivacyLinkPreviewsShowAlert" private const val SHARED_PREFS_PRIVACY_SANITIZE_LINKS = "PrivacySanitizeLinks" private const val SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" // TODO remove private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" + private const val SHARED_PREFS_PRIVACY_SHOW_SIGNATURE = "PrivacyShowSignature" + private const val SHARED_PREFS_PRIVACY_SHOW_FILE_ENCRYPTION = "PrivacyShowEncryption" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" @@ -452,6 +460,7 @@ class AppPreferences { private const val SHARED_PREFS_NETWORK_TCP_KEEP_CNT = "NetworkTCPKeepCnt" private const val SHARED_PREFS_INCOGNITO = "Incognito" private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown" + private const val SHARED_PREFS_SIGN_MESSAGE_ALERT_SHOWN = "SignMessageAlertShown" private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice" private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" @@ -1094,8 +1103,8 @@ object ChatController { suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds)) - suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, sendAsGroup: Boolean = false, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? { - val cmd = CC.ApiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages) + suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, sendAsGroup: Boolean = false, live: Boolean = false, ttl: Int? = null, sign: Boolean = false, composedMessages: List<ComposedMessage>): List<AChatItem>? { + val cmd = CC.ApiSendMessages(type, id, scope, sendAsGroup, live, ttl, sign, composedMessages) return processSendMessageCmd(rh, cmd) } @@ -1165,6 +1174,13 @@ object ChatController { return null } + suspend fun apiShareMyAddress(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, sendAsGroup: Boolean): MsgContent? { + val r = sendCmd(rh, CC.ApiShareMyAddress(toChatType, toChatId, toScope, sendAsGroup)) + if (r is API.Result && r.res is CR.ChatMsgContent) return r.res.msgContent + apiErrorAlert("apiShareMyAddress", generalGetString(MR.strings.error_sharing_address), r) + return null + } + suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List<Long>): CR.ForwardPlan? { val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds)) if (r is API.Result && r.res is CR.ForwardPlan) return r.res @@ -1515,10 +1531,12 @@ object ChatController { return null } - suspend fun apiConnectPlan(rh: Long?, connLink: String, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState<Boolean>): Pair<CreatedConnLink, ConnectionPlan>? { + suspend fun apiConnectPlan(rh: Long?, connLink: String, resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, linkOwnerSig: LinkOwnerSig? = null, inProgress: MutableState<Boolean>): ConnectionPlanResult? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, linkOwnerSig), inProgress = inProgress) - if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink, resolveMode, linkOwnerSig), inProgress = inProgress) + if (r is API.Result && r.res is CR.CRConnectionPlan) return ConnectionPlanResult(r.res.connLink, r.res.planSimplexName, r.res.otherSimplexName, r.res.connectionPlan) + // a PRMNever (typing) search that matches nothing locally is not an error to surface + if (r is API.Error && r.err is ChatError.ChatErrorChat && r.err.errorType is ChatErrorType.NotResolvedLocally) return null if (inProgress.value && r != null) apiConnectResponseAlert(r) return null } @@ -1555,6 +1573,46 @@ object ChatController { generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade) ) } + r is API.Error && r.err is ChatError.ChatErrorChat + && r.err.errorType is ChatErrorType.SimplexDomainNotReady -> { + val domain = r.err.errorType.simplexDomain.fullDomainName + if (r.err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_no_valid_link), + generalGetString(MR.strings.simplex_name_no_valid_link_desc).format(domain) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_unconfirmed), + generalGetString(MR.strings.simplex_name_unconfirmed_desc).format(domain) + ) + } + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.NO_NAME_SERVERS -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_no_servers_desc) + ) + } + r is API.Error && r.err is ChatError.ChatErrorAgent + && r.err.agentError is AgentErrorType.SMP + && r.err.agentError.smpErr is SMPErrorType.NAME -> { + when (val nameErr = r.err.agentError.smpErr.nameErr) { + is NameErrorType.NOT_FOUND -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_not_found), + generalGetString(MR.strings.simplex_name_not_found_desc) + ) + is NameErrorType.NO_RESOLVER -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_server_no_resolver_desc).format(r.err.agentError.serverAddress) + ) + is NameErrorType.RESOLVER -> AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.simplex_name_error), + generalGetString(MR.strings.simplex_name_resolver_error_desc).format(nameErr.resolverErr) + ) + } + } r is API.Error && r.err is ChatError.ChatErrorAgent && r.err.agentError is AgentErrorType.SMP && r.err.agentError.smpErr is SMPErrorType.AUTH -> { @@ -1587,11 +1645,29 @@ object ChatController { } } + // owner-specific wording for setting one's own/channel name; null for other errors (handled by apiConnectResponseAlert) + fun simplexNameOwnerError(err: ChatError, isChannel: Boolean): String? = + if (err is ChatError.ChatErrorChat && err.errorType is ChatErrorType.SimplexDomainNotReady && err.errorType.simplexDomainError is SimplexDomainError.NoValidLink) { + val domain = err.errorType.simplexDomain.fullDomainName + if (isChannel) generalGetString(MR.strings.simplex_name_owner_no_channel_link).format(domain) + else generalGetString(MR.strings.simplex_name_owner_no_address).format(domain) + } else null + fun connErrorText(e: ChatError): String = when { e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.InvalidConnReq -> generalGetString(MR.strings.invalid_connection_link) e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.UnsupportedConnReq -> generalGetString(MR.strings.unsupported_connection_link) + e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.SimplexDomainNotReady -> + if (e.errorType.simplexDomainError is SimplexDomainError.NoValidLink) + generalGetString(MR.strings.simplex_name_no_valid_link) + else generalGetString(MR.strings.simplex_name_unconfirmed) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.NO_NAME_SERVERS -> + generalGetString(MR.strings.simplex_name_error) + e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.NAME -> + if (e.agentError.smpErr.nameErr is NameErrorType.NOT_FOUND) + generalGetString(MR.strings.simplex_name_not_found) + else generalGetString(MR.strings.simplex_name_error) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH -> generalGetString(MR.strings.connection_error_auth) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.BLOCKED -> @@ -1604,19 +1680,19 @@ object ChatController { "${generalGetString(MR.strings.error_prefix)}: ${e.string}" } - suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? { + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? { val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) - if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData, verifiedDomain)) + if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}") return null } - suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? { + suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData, verifiedDomain: SimplexDomain? = null): Chat? { val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData)) - if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat + val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData, verifiedDomain)) + if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh) Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}") AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}") return null @@ -1762,6 +1838,38 @@ object ChatController { } } + // name is the encoded SimplexName (e.g. "@alice.simplex"); null clears it. Throws on rejection. + suspend fun apiSetUserDomain(rh: Long?, simplexDomain: String?): User { + val userId = currentUserId("apiSetUserDomain") + val r = sendCmd(rh, CC.ApiSetUserDomain(userId, simplexDomain)) + return when { + r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh) + r is API.Result && r.res is CR.UserProfileNoChange -> r.res.user.updateRemoteHostId(rh) + else -> { + if (r is API.Error) { + val ownerMsg = simplexNameOwnerError(r.err, isChannel = false) + if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg) + else apiConnectResponseAlert(r) + } + throw Exception("failed to set SimpleX name: ${r.responseType} ${r.details}") + } + } + } + + suspend fun apiVerifyContactDomain(rh: Long?, contactId: Long): Pair<Contact, String?>? { + val r = sendCmd(rh, CC.ApiVerifyContactDomain(contactId)) + if (r is API.Result && r.res is CR.ContactDomainVerified) return r.res.contact to r.res.verificationFailure + Log.e(TAG, "apiVerifyContactDomain bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiVerifyGroupDomain(rh: Long?, groupId: Long): Pair<GroupInfo, String?>? { + val r = sendCmd(rh, CC.ApiVerifyGroupDomain(groupId)) + if (r is API.Result && r.res is CR.GroupDomainVerified) return r.res.groupInfo to r.res.verificationFailure + Log.e(TAG, "apiVerifyGroupDomain bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiSetContactPrefs(rh: Long?, contactId: Long, prefs: ChatPreferences): Contact? { val r = sendCmd(rh, CC.ApiSetContactPrefs(contactId, prefs)) if (r is API.Result && r.res is CR.ContactPrefsUpdated) return r.res.toContact @@ -2172,8 +2280,8 @@ object ChatController { return null } - suspend fun apiGetGroupRelays(groupId: Long): List<GroupRelay> { - val r = sendCmd(null, CC.ApiGetGroupRelays(groupId)) + suspend fun apiGetGroupRelays(rh: Long?, groupId: Long): List<GroupRelay> { + val r = sendCmd(rh, CC.ApiGetGroupRelays(groupId)) if (r is API.Result && r.res is CR.GroupRelays) return r.res.groupRelays return emptyList() } @@ -2183,8 +2291,8 @@ object ChatController { data class AddFailed(val addRelayResults: List<AddRelayResult>): AddGroupRelaysResult() } - suspend fun apiAddGroupRelays(groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? { - val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds)) + suspend fun apiAddGroupRelays(rh: Long?, groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? { + val r = sendCmdWithRetry(rh, CC.ApiAddGroupRelays(groupId, relayIds)) if (r is API.Result && r.res is CR.GroupRelaysAdded) return AddGroupRelaysResult.Added(r.res.groupInfo, r.res.groupLink, r.res.groupRelays) if (r is API.Result && r.res is CR.GroupRelaysAddFailed) return AddGroupRelaysResult.AddFailed(r.res.addRelayResults) if (r != null) throw Exception("${r.responseType}: ${r.details}") @@ -2289,7 +2397,7 @@ object ChatController { return when { r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup r is API.Error -> { - AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "$r.err") + AlertManager.shared.showAlertMsg(generalGetString(errorTitle), r.err.string) null } else -> { @@ -2303,6 +2411,23 @@ object ChatController { } } + suspend fun apiSetPublicGroupAccess(rh: Long?, groupId: Long, access: PublicGroupAccess): GroupInfo? { + val r = sendCmd(rh, CC.ApiSetPublicGroupAccess(groupId, access)) + return when { + r is API.Result && r.res is CR.GroupUpdated -> r.res.toGroup + r is API.Error -> { + val ownerMsg = simplexNameOwnerError(r.err, isChannel = true) + if (ownerMsg != null) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), ownerMsg) + else apiConnectResponseAlert(r) + null + } + else -> { + Log.e(TAG, "apiSetPublicGroupAccess bad response: ${r.responseType} ${r.details}") + null + } + } + } + suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink @@ -3673,7 +3798,7 @@ sealed class CC { class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatContentTypes(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC() - class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val sendAsGroup: Boolean, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC() + class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val sendAsGroup: Boolean, val live: Boolean, val ttl: Int?, val sign: Boolean, val composedMessages: List<ComposedMessage>): CC() class ApiCreateChatTag(val tag: ChatTagData): CC() class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC() class ApiDeleteChatTag(val tagId: Long): CC() @@ -3691,6 +3816,7 @@ sealed class CC { class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List<Long>): CC() class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC() class ApiShareChatMsgContent(val shareChatType: ChatType, val shareChatId: Long, val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() + class ApiShareMyAddress(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val sendAsGroup: Boolean): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewPublicGroup(val userId: Long, val incognito: Boolean, val relayIds: List<Long>, val groupProfile: GroupProfile): CC() class ApiGetGroupRelays(val groupId: Long): CC() @@ -3705,6 +3831,7 @@ sealed class CC { class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() + class ApiSetPublicGroupAccess(val groupId: Long, val access: PublicGroupAccess): CC() class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() @@ -3751,9 +3878,9 @@ sealed class CC { class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() - class APIConnectPlan(val userId: Long, val connLink: String, val linkOwnerSig: LinkOwnerSig? = null): CC() - class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() - class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData): CC() + class APIConnectPlan(val userId: Long, val connLink: String, val resolveMode: PlanResolveMode = PlanResolveMode.PRMUnknown, val linkOwnerSig: LinkOwnerSig? = null): CC() + class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() + class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val directLink: Boolean, val groupShortLinkData: GroupShortLinkData, val verifiedDomain: SimplexDomain? = null): CC() class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC() class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC() @@ -3775,6 +3902,9 @@ sealed class CC { class ApiShowMyAddress(val userId: Long): CC() class ApiAddMyAddressShortLink(val userId: Long): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() + class ApiSetUserDomain(val userId: Long, val simplexDomain: String?): CC() + class ApiVerifyContactDomain(val contactId: Long): CC() + class ApiVerifyGroupDomain(val groupId: Long): CC() class ApiSetAddressSettings(val userId: Long, val addressSettings: AddressSettings): CC() class ApiGetCallInvitations: CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() @@ -3867,7 +3997,7 @@ sealed class CC { is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) val ttlStr = if (ttl != null) "$ttl" else "default" - "/_send ${chatRef(type, id, scope)}${if (sendAsGroup) "(as_group=on)" else ""} live=${onOff(live)} ttl=${ttlStr} json $msgs" + "/_send ${chatRef(type, id, scope)}${if (sendAsGroup) "(as_group=on)" else ""} live=${onOff(live)} ttl=${ttlStr} sign=${onOff(sign)} json $msgs" } is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}" is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}" @@ -3893,6 +4023,7 @@ sealed class CC { is ApiShareChatMsgContent -> { "/_share chat content ${chatRef(shareChatType, shareChatId, null)} ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" } + is ApiShareMyAddress -> "/_share address ${chatRef(toChatType, toChatId, toScope)}${if (sendAsGroup) "(as_group=on)" else ""}" is ApiPlanForwardChatItems -> { "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}" } @@ -3957,11 +4088,12 @@ sealed class CC { is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" is APIConnectPlan -> { + val resolveStr = if (resolveMode != PlanResolveMode.PRMUnknown) " resolve=${resolveMode.cmdString}" else "" val sigStr = if (linkOwnerSig != null) " sig=${json.encodeToString(linkOwnerSig)}" else "" - "/_connect plan $userId $connLink$sigStr" + "/_connect plan $userId $connLink$resolveStr$sigStr" } - is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" - is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} direct=${onOff(directLink)} ${json.encodeToString(groupShortLinkData)}" + is APIPrepareContact -> "/_prepare contact $userId ${connLink.cmdString}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(contactShortLinkData)}" + is APIPrepareGroup -> "/_prepare group $userId ${connLink.cmdString} direct=${onOff(directLink)}${verifiedDomain?.let { " ${it.cmdString}" } ?: ""} ${json.encodeToString(groupShortLinkData)}" is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId" is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}" @@ -3983,6 +4115,10 @@ sealed class CC { is ApiShowMyAddress -> "/_show_address $userId" is ApiAddMyAddressShortLink -> "/_short_link_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" + is ApiSetUserDomain -> "/_set domain $userId" + (if (simplexDomain != null) " $simplexDomain" else "") + is ApiSetPublicGroupAccess -> "/_public group access #$groupId ${json.encodeToString(access)}" + is ApiVerifyContactDomain -> "/_verify domain @$contactId" + is ApiVerifyGroupDomain -> "/_verify domain #$groupId" is ApiSetAddressSettings -> "/_address_settings $userId ${json.encodeToString(addressSettings)}" is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" @@ -4079,6 +4215,7 @@ sealed class CC { is ApiGetReactionMembers -> "apiGetReactionMembers" is ApiForwardChatItems -> "apiForwardChatItems" is ApiShareChatMsgContent -> "apiShareChatMsgContent" + is ApiShareMyAddress -> "apiShareMyAddress" is ApiPlanForwardChatItems -> "apiPlanForwardChatItems" is ApiNewGroup -> "apiNewGroup" is ApiNewPublicGroup -> "apiNewPublicGroup" @@ -4164,6 +4301,10 @@ sealed class CC { is ApiShowMyAddress -> "apiShowMyAddress" is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink" is ApiSetProfileAddress -> "apiSetProfileAddress" + is ApiSetUserDomain -> "apiSetUserDomain" + is ApiSetPublicGroupAccess -> "apiSetPublicGroupAccess" + is ApiVerifyContactDomain -> "apiVerifyContactDomain" + is ApiVerifyGroupDomain -> "apiVerifyGroupDomain" is ApiSetAddressSettings -> "apiSetAddressSettings" is ApiAcceptContact -> "apiAcceptContact" is ApiRejectContact -> "apiRejectContact" @@ -4443,8 +4584,8 @@ data class ServerOperator( serverDomains = listOf("simplex.im"), conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false), enabled = true, - smpRoles = ServerRoles(storage = true, proxy = true), - xftpRoles = ServerRoles(storage = true, proxy = true) + smpRoles = ServerRoles(storage = true, proxy = true, names = true), + xftpRoles = ServerRoles(storage = true, proxy = true, names = false) ) } @@ -4504,7 +4645,20 @@ data class ServerOperator( @Serializable data class ServerRoles( val storage: Boolean, - val proxy: Boolean + val proxy: Boolean, + val names: Boolean +) { + companion object { + // roles applied when a server matches no operator, mirrors core resolveServerRoles (Operators.hs) + val noOperatorDefault = ServerRoles(storage = true, proxy = true, names = false) + } +} + +@Serializable +data class ServerRolesOverride( + val storage: Boolean? = null, + val proxy: Boolean? = null, + val names: Boolean? = null ) @Serializable @@ -4526,8 +4680,8 @@ data class UserOperatorServers( serverDomains = emptyList(), conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false), enabled = false, - smpRoles = ServerRoles(storage = true, proxy = true), - xftpRoles = ServerRoles(storage = true, proxy = true) + smpRoles = ServerRoles.noOperatorDefault, + xftpRoles = ServerRoles.noOperatorDefault ) companion object { @@ -4613,6 +4767,7 @@ sealed class UserServersError { @Serializable sealed class UserServersWarning { @Serializable @SerialName("noChatRelays") data class NoChatRelays(val user: UserRef? = null): UserServersWarning() + @Serializable @SerialName("noNamesServers") data class NoNamesServers(val user: UserRef? = null): UserServersWarning() val globalWarning: String? get() = when (this) { @@ -4622,6 +4777,12 @@ sealed class UserServersWarning { String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text } else text } + is NoNamesServers -> { + val text = generalGetString(MR.strings.no_names_servers_enabled) + if (user != null) { + String.format(generalGetString(MR.strings.for_chat_profile), user.localDisplayName) + " " + text + } else text + } } } @@ -4652,7 +4813,8 @@ data class UserServer( val preset: Boolean, val tested: Boolean? = null, val enabled: Boolean, - val deleted: Boolean + val deleted: Boolean, + val roles: ServerRolesOverride = ServerRolesOverride(), ) { @Transient private val createdAt: Date = Date() @@ -5725,7 +5887,8 @@ enum class GroupFeature: Feature { @SerialName("simplexLinks") SimplexLinks, @SerialName("reports") Reports, @SerialName("history") History, - @SerialName("support") Support; + @SerialName("support") Support, + @SerialName("signMessages") SignMessages; override val hasParam: Boolean get() = when(this) { TimedMessages -> true @@ -5744,6 +5907,7 @@ enum class GroupFeature: Feature { Reports -> false History -> false Support -> false + SignMessages -> false } override val text: String get() = text(isChannel = false) @@ -5759,6 +5923,7 @@ enum class GroupFeature: Feature { Reports -> generalGetString(if (isChannel) MR.strings.group_reports_subscriber_reports else MR.strings.group_reports_member_reports) History -> generalGetString(MR.strings.recent_history) Support -> generalGetString(MR.strings.chat_with_admins) + SignMessages -> generalGetString(MR.strings.sign_messages) } val icon: Painter @@ -5773,6 +5938,7 @@ enum class GroupFeature: Feature { Reports -> painterResource(MR.images.ic_flag) History -> painterResource(MR.images.ic_schedule) Support -> painterResource(MR.images.ic_help) + SignMessages -> painterResource(MR.images.ic_verified) } @Composable @@ -5787,6 +5953,7 @@ enum class GroupFeature: Feature { Reports -> painterResource(MR.images.ic_flag_filled) History -> painterResource(MR.images.ic_schedule_filled) Support -> painterResource(MR.images.ic_help_filled) + SignMessages -> painterResource(MR.images.ic_verified_filled) } fun enableDescription(enabled: GroupFeatureEnabled, canEdit: Boolean, isChannel: Boolean = false): String = @@ -5832,6 +5999,10 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.allow_chat_with_admins_channel else MR.strings.allow_chat_with_admins) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_chat_with_admins) } + SignMessages -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(MR.strings.require_message_signatures) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.do_not_require_message_signatures) + } } } else { when(this) { @@ -5875,6 +6046,10 @@ enum class GroupFeature: Feature { GroupFeatureEnabled.ON -> generalGetString(if (isChannel) MR.strings.members_can_chat_with_admins_channel else MR.strings.members_can_chat_with_admins) GroupFeatureEnabled.OFF -> generalGetString(MR.strings.chat_with_admins_is_prohibited) } + SignMessages -> when(enabled) { + GroupFeatureEnabled.ON -> generalGetString(MR.strings.message_signatures_are_required) + GroupFeatureEnabled.OFF -> generalGetString(MR.strings.message_signatures_are_not_required) + } } } } @@ -6001,6 +6176,7 @@ data class FullGroupPreferences( val reports: GroupPreference, val history: GroupPreference, val support: GroupPreference, + val signMessages: GroupPreference, val commands: List<ChatBotCommand>, ) { fun toGroupPreferences(): GroupPreferences = @@ -6015,6 +6191,7 @@ data class FullGroupPreferences( reports = reports, history = history, support = support, + signMessages = signMessages, commands = commands, ) @@ -6030,6 +6207,7 @@ data class FullGroupPreferences( reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), support = GroupPreference(GroupFeatureEnabled.ON), + signMessages = GroupPreference(GroupFeatureEnabled.OFF), commands = listOf() ) } @@ -6047,6 +6225,7 @@ data class GroupPreferences( val reports: GroupPreference? = null, val history: GroupPreference? = null, val support: GroupPreference? = null, + val signMessages: GroupPreference? = null, val commands: List<ChatBotCommand>? = null ) { companion object { @@ -6067,7 +6246,8 @@ data class GroupPreferences( @Serializable data class GroupPreference( - val enable: GroupFeatureEnabled + val enable: GroupFeatureEnabled, + val role: GroupMemberRole? = null, ) { val on: Boolean get() = enable == GroupFeatureEnabled.ON @@ -6383,7 +6563,7 @@ sealed class CR { @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connLinkInvitation: CreatedConnLink, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() - @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val planSimplexName: SimplexNameInfo? = null, val otherSimplexName: SimplexNameInfo? = null, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("newPreparedChat") class NewPreparedChat(val user: UserRef, val chat: Chat): CR() @Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR() @Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): CR() @@ -6461,6 +6641,8 @@ sealed class CR { @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() + @Serializable @SerialName("contactDomainVerified") class ContactDomainVerified(val user: UserRef, val contact: Contact, val verificationFailure: String? = null): CR() + @Serializable @SerialName("groupDomainVerified") class GroupDomainVerified(val user: UserRef, val groupInfo: GroupInfo, val verificationFailure: String? = null): CR() @Serializable @SerialName("groupLinkDataUpdated") class GroupLinkDataUpdated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink, val groupRelays: List<GroupRelay>, val relaysChanged: Boolean): CR() @Serializable @SerialName("groupRelayUpdated") class GroupRelayUpdated(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val groupRelay: GroupRelay): CR() @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR() @@ -6652,6 +6834,8 @@ sealed class CR { is JoinedGroupMember -> "joinedGroupMember" is ConnectedToGroupMember -> "connectedToGroupMember" is GroupUpdated -> "groupUpdated" + is ContactDomainVerified -> "contactDomainVerified" + is GroupDomainVerified -> "groupDomainVerified" is GroupLinkDataUpdated -> "groupLinkDataUpdated" is GroupRelayUpdated -> "groupRelayUpdated" is GroupLinkCreated -> "groupLinkCreated" @@ -6759,7 +6943,7 @@ sealed class CR { is Invitation -> withUser(user, "connLinkInvitation: ${json.encodeToString(connLinkInvitation)}\nconnection: $connection") is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) - is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}") + is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nplanSimplexName: $planSimplexName\notherSimplexName: $otherSimplexName\nconnectionPlan: ${json.encodeToString(connectionPlan)}") is NewPreparedChat -> withUser(user, json.encodeToString(chat)) is ContactUserChanged -> withUser(user, "fromContact: ${json.encodeToString(fromContact)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoContact: ${json.encodeToString(toContact)}") is GroupUserChanged -> withUser(user, "fromGroup: ${json.encodeToString(fromGroup)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoGroup: ${json.encodeToString(toGroup)}") @@ -6836,6 +7020,8 @@ sealed class CR { is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) + is ContactDomainVerified -> withUser(user, "contact: ${json.encodeToString(contact)}\nverificationFailure: $verificationFailure") + is GroupDomainVerified -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nverificationFailure: $verificationFailure") is GroupLinkDataUpdated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink\ngroupRelays: $groupRelays\nrelaysChanged: $relaysChanged") is GroupRelayUpdated -> withUser(user, "groupInfo: $groupInfo\nmember: $member\ngroupRelay: $groupRelay") is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink") @@ -6947,6 +7133,8 @@ data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) fun simplexChatUri(short: Boolean): String = if (short) connShortLink ?: simplexChatLink(connFullLink) else simplexChatLink(connFullLink) + + val cmdString: String get() = connFullLink + (if (connShortLink == null) "" else " $connShortLink") } fun simplexChatLink(uri: String): String = @@ -6959,6 +7147,29 @@ sealed class OwnerVerification { @Serializable @SerialName("failed") class Failed(val reason: String) : OwnerVerification() } +@Serializable +sealed class SimplexDomainError { + @Serializable @SerialName("noValidLink") object NoValidLink : SimplexDomainError() + @Serializable @SerialName("unknownDomain") object UnknownDomain : SimplexDomainError() +} + +data class ConnectionPlanResult( + val connLink: CreatedConnLink, + val planSimplexName: SimplexNameInfo?, + val otherSimplexName: SimplexNameInfo?, + val connectionPlan: ConnectionPlan, +) + +// APIConnectPlan resolution scope; PRMNever is local-store-only (no network), used for per-keystroke name search +enum class PlanResolveMode { + PRMAllGroups, PRMUnknown, PRMNever; + val cmdString: String get() = when (this) { + PRMAllGroups -> "allGroups" + PRMUnknown -> "unknown" + PRMNever -> "never" + } +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -7296,6 +7507,8 @@ sealed class ChatErrorType { is ChatStoreChanged -> "chatStoreChanged" is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" + is SimplexDomainNotReady -> "simplexDomainNotReady" + is NotResolvedLocally -> "notResolvedLocally" is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" is ConnReqMessageProhibited -> "connReqMessageProhibited" @@ -7378,6 +7591,8 @@ sealed class ChatErrorType { @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("simplexDomainNotReady") class SimplexDomainNotReady(val simplexDomain: SimplexDomain, val simplexDomainError: SimplexDomainError): ChatErrorType() + @Serializable @SerialName("notResolvedLocally") object NotResolvedLocally: ChatErrorType() @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() @@ -7646,6 +7861,7 @@ sealed class AgentErrorType { is INTERNAL -> "INTERNAL $internalErr" is CRITICAL -> "CRITICAL $offerRestart $criticalErr" is INACTIVE -> "INACTIVE" + is NO_NAME_SERVERS -> "NO_NAME_SERVERS" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType() @@ -7660,6 +7876,19 @@ sealed class AgentErrorType { @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() @Serializable @SerialName("CRITICAL") data class CRITICAL(val offerRestart: Boolean, val criticalErr: String): AgentErrorType() @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() + @Serializable @SerialName("NO_NAME_SERVERS") object NO_NAME_SERVERS: AgentErrorType() +} + +@Serializable +sealed class NameErrorType { + val string: String get() = when (this) { + is NO_RESOLVER -> "NO_RESOLVER" + is NOT_FOUND -> "NOT_FOUND" + is RESOLVER -> "RESOLVER $resolverErr" + } + @Serializable @SerialName("NO_RESOLVER") object NO_RESOLVER: NameErrorType() + @Serializable @SerialName("NOT_FOUND") object NOT_FOUND: NameErrorType() + @Serializable @SerialName("RESOLVER") class RESOLVER(val resolverErr: String): NameErrorType() } @Serializable @@ -7729,6 +7958,7 @@ sealed class SMPErrorType { is LARGE_MSG -> "LARGE_MSG" is EXPIRED -> "EXPIRED" is INTERNAL -> "INTERNAL" + is NAME -> "NAME ${nameErr.string}" } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() @@ -7743,6 +7973,7 @@ sealed class SMPErrorType { @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() @Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType() + @Serializable @SerialName("NAME") class NAME(val nameErr: NameErrorType): SMPErrorType() } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index 7a96bd99d2..140c1951ee 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -1,5 +1,7 @@ package chat.simplex.common.platform +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.ui.theme.DefaultTheme @@ -30,6 +32,9 @@ else val databaseBackend: String = if (appPlatform == AppPlatform.ANDROID) "sqlite" else BuildConfigCommon.DATABASE_BACKEND +// Country of the Google Play account, only set in the google flavor of the Android app +val androidPlayStoreCountry: MutableState<String?> = mutableStateOf(null) + class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() { override fun add(element: E): Boolean { if (size > capacity) removeFirstOrNull() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 385120f18b..89f23f3326 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -49,11 +49,21 @@ abstract class NtfManager { } fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { - val isCurrentUser = ChatModel.currentUser.value?.userId == userId val apiId = chatId.replace("<@", "").toLongOrNull() ?: return - // TODO include remote host in notification - acceptContactRequest(null, incognito, apiId, isCurrentUser, ChatModel) - cancelNotificationsForChat(chatId) + withLongRunningApi { + awaitChatStartedIfNeeded(chatModel) + // switching to the user the request was sent to, so that accepted contact is shown + if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { + chatModel.controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(null, userId, null) + } + chatModel.clearOverlays.value = true + } + val isCurrentUser = chatModel.currentUser.value?.userId == userId + // TODO include remote host in notification + acceptContactRequest(null, incognito, apiId, isCurrentUser, chatModel) + cancelNotificationsForChat(chatId) + } } fun openChatAction(userId: Long?, chatId: ChatId) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index 448100bc17..b46123c9cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -29,7 +29,11 @@ interface PlatformInterface { fun androidRestartNetworkObserver() {} fun androidCreateActiveCallState(): Closeable = Closeable { } fun androidIsXiaomiDevice(): Boolean = false + // Requests the Google Play account country into [androidPlayStoreCountry] + fun androidLoadPlayStoreCountry() {} val androidApiLevel: Int? get() = null + // The build distributed via Google Play, which has to follow its policies + val androidIsPlayStoreBuild: Boolean get() = false @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true @Composable fun desktopShowAppUpdateNotice() {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 1de47df7ce..20e0280acd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -568,7 +568,7 @@ data class ThemeModeOverride ( background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null, surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null, title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null, - primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null, + primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primaryVariant2 else null, sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null, sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null, receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null, @@ -596,10 +596,38 @@ data class ThemeModeOverride ( } } -fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?/*, shape: Shape = RectangleShape*/): Modifier { +// Canvas color for settings/info screens (drawn behind cards by themedBackground) +// and for the 2dp item divider inside section cards (matches canvas so dividers +// read as gaps showing the screen behind). +// LIGHT: formula derives off-white from palette bg + onBackground — lifts white +// cards above. DARK/BLACK: palette bg (cards already raised via founder's +// formula in Section.kt). SIMPLEX: gradient bottom stop (darker), since the +// canvas itself is a gradient drawn by themedBackgroundBrush. +fun canvasColorForCurrentTheme(): Color { + val theme = CurrentColors.value + val c = theme.colors + return when (theme.base) { + DefaultTheme.LIGHT -> c.background.mixWith(c.onBackground, 0.94f) + DefaultTheme.SIMPLEX -> c.background.darker(0.4f) + else -> c.background + } +} + +// Card background color for SectionView. LIGHT: pure white (raised above the +// off-white canvas). DARK/BLACK/SIMPLEX: founder's mixWith formula (lifts cards +// above palette bg using onBackground tint). +fun sectionCardColor(): Color { + val theme = CurrentColors.value + return if (theme.base == DefaultTheme.LIGHT) Color.White + else theme.colors.background.mixWith(theme.colors.onBackground, 0.95f) +} + +fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, bgLayerSize: MutableState<IntSize>?, bgLayer: GraphicsLayer?, overrideColor: Color? = null): Modifier { return drawBehind { copyBackgroundToAppBar(bgLayerSize, bgLayer) { - if (baseTheme == DefaultTheme.SIMPLEX) { + if (overrideColor != null) { + drawRect(overrideColor) + } else if (baseTheme == DefaultTheme.SIMPLEX) { drawRect(brush = themedBackgroundBrush()) } else { drawRect(CurrentColors.value.colors.background) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 3e4b8ce1db..843bacecec 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -370,7 +370,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, shortDesc withBGApi { val rhId = chatModel.remoteHostId() val user = chatModel.controller.apiCreateActiveUser( - rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image) + rhId, Profile(displayName.trim(), "", shortDescr.trim().ifEmpty { null }, image = image) ) ?: return@withBGApi chatModel.currentUser.value = user if (chatModel.users.isEmpty()) { @@ -392,6 +392,8 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () null, Profile(displayName.trim(), "", null, null) ) ?: return@withBGApi chatModel.localUserCreated.value = true + // new users don't need the local file encryption indicator (all files are encrypted); existing users keep it on + chatModel.controller.appPrefs.privacyShowEncryption.set(false) val onboardingStage = chatModel.controller.appPrefs.onboardingStage // No users or no visible users if (chatModel.users.none { u -> !u.user.hidden }) { 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 97101f253e..91b3270dea 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 @@ -3,10 +3,9 @@ package chat.simplex.common.views.chat import InfoRow import InfoRowEllipsis import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween -import SectionSpacer +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview @@ -249,6 +248,8 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), + text = contact.displayName, + parseHtml = false, buttons = { Column { // Only delete conversation @@ -307,7 +308,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { Column { // Keep conversation toggle @@ -362,7 +364,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.confirm_delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { Column { // Delete and notify contact @@ -418,7 +421,8 @@ private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, c private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.confirm_delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { // Confirm SectionItemView({ @@ -493,7 +497,8 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.clear_chat_question), - text = generalGetString(MR.strings.clear_chat_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}", + parseHtml = false, confirmText = generalGetString(MR.strings.clear_verb), onConfirm = { controller.clearChat(chat, close) }, destructive = true, @@ -553,7 +558,7 @@ fun ChatInfoLayout( LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) - SectionSpacer() + SectionDividerSpaced() Box( Modifier.fillMaxWidth(), @@ -573,10 +578,10 @@ fun ChatInfoLayout( } } - SectionSpacer() + SectionDividerSpaced() if (customUserProfile != null) { - SectionView(generalGetString(MR.strings.incognito).uppercase()) { + SectionView(generalGetString(MR.strings.incognito)) { SectionItemViewSpaceBetween { Text(generalGetString(MR.strings.incognito_random_profile)) Text(customUserProfile.chatViewName, color = Indigo) @@ -601,7 +606,7 @@ fun ChatInfoLayout( } WallpaperButton { - ModalManager.end.showModal { + ModalManager.end.showModal(cardScreen = true) { val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { @@ -610,30 +615,30 @@ fun ChatInfoLayout( } } } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) - SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + SectionDividerSpaced() val conn = contact.activeConn if (conn != null) { SectionView { InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard") - SectionDividerSpaced() } + SectionDividerSpaced() } if (contact.contactLink != null) { - SectionView(stringResource(MR.strings.address_section_title).uppercase()) { + SectionView(stringResource(MR.strings.address_section_title)) { SimpleXLinkQRCode(contact.contactLink) val clipboard = LocalClipboardManager.current ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) } - SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } - SectionDividerSpaced(maxTopPadding = true) + SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) + SectionDividerSpaced() } if (contact.ready && contact.active) { @@ -670,7 +675,7 @@ fun ChatInfoLayout( } } } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() } SectionView { @@ -752,6 +757,7 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + ContactSimplexNameView(contact) } } @@ -769,19 +775,46 @@ fun ChatInfoDescription(c: NamedChat, displayName: String, copyNameToClipboard: modifier = Modifier.padding(top = DEFAULT_PADDING_HALF).combinedClickable(onClick = copyFullName, onLongClick = copyFullName).onRightClick(copyFullName) ) } - val descr = c.shortDescr?.trim() - if (descr != null && descr != "") { - MarkdownText( - descr, - parseToMarkdown(descr), - toggleSecrets = true, - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), - maxLines = 4, - overflow = TextOverflow.Ellipsis, - uriHandler = LocalUriHandler.current, - modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), - linkMode = chatModel.simplexLinkMode.value - ) + ProfileDescriptionText( + shortDescr = c.shortDescr, + description = c.profileDescription, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) +} + +@Composable +fun ProfileDescriptionText(shortDescr: String?, description: String?, style: TextStyle, modifier: Modifier = Modifier) { + val short = shortDescr?.trim()?.ifEmpty { null } + val descr = description?.trim()?.ifEmpty { null } + val uriHandler = LocalUriHandler.current + val linkMode = chatModel.simplexLinkMode.value + if (descr == null) { + if (short != null) { + MarkdownText( + short, parseToMarkdown(short), toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } + } else { + val firstLine = descr.lineSequence().first() + val truncated = firstLine.length > 100 + val multiline = descr.length > firstLine.length + if (short == null && !truncated && !multiline) { + MarkdownText( + descr, parseToMarkdown(descr), toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } else { + val teaser = short ?: (if (truncated) firstLine.take(100).trimEnd() + "…" else "$firstLine…") + val readMore = stringResource(MR.strings.whats_new_read_more) + val formatted = (parseToMarkdown(teaser) ?: FormattedText.plain(teaser)) + + FormattedText(" ") + FormattedText(readMore, Format.Modal(Format.Modal.Description, descr)) + MarkdownText( + "$teaser $readMore", formatted, toggleSecrets = true, style = style, maxLines = 4, + overflow = TextOverflow.Ellipsis, uriHandler = uriHandler, modifier = modifier, linkMode = linkMode + ) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index affe5ce326..64c6160665 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -30,6 +30,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.MemberProfileImage import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.usersettings.networkAndServers.serverHostname import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import kotlinx.serialization.encodeToString @@ -272,7 +273,16 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (deleteAt != null) { InfoRow(stringResource(MR.strings.info_row_disappears_at), localTimestamp(deleteAt)) } - if (devTools) { + if (ci.meta.msgVerified?.verified == true) { + val signedRes = if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified + InfoRow(stringResource(signedRes), "", icon = painterResource(MR.images.ic_verified)) + } else if (ci.meta.msgVerified is MsgVerified.SigMissing) { + InfoRow(stringResource(MR.strings.signature_missing_alert_title), "", icon = painterResource(MR.images.ic_verified_missing), iconTint = Color.Red) + } + } + if (devTools) { + SectionDividerSpaced() + SectionView { InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString()) InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus)) @@ -281,6 +291,16 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } } + if (ci.file != null && ciInfo.fileXftpServers.isNotEmpty()) { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView(stringResource(MR.strings.info_row_file_servers)) { + ciInfo.fileXftpServers.forEach { server -> + SectionItemView { + Text(serverHostname(server), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth()) + } + } + } + } } @Composable @@ -559,6 +579,11 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem if (deleteAt != null) { shareText.add(String.format(generalGetString(MR.strings.share_text_disappears_at), localTimestamp(deleteAt))) } + if (ci.meta.msgVerified?.verified == true) { + shareText.add(generalGetString(if (sent) MR.strings.info_row_signed else MR.strings.info_row_signed_verified)) + } else if (ci.meta.msgVerified is MsgVerified.SigMissing) { + shareText.add(generalGetString(MR.strings.signature_missing_alert_title)) + } if (devTools) { shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) @@ -567,6 +592,9 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus))) } } + if (ci.file != null && chatItemInfo.fileXftpServers.isNotEmpty()) { + shareText.add(String.format(generalGetString(MR.strings.share_text_file_servers), chatItemInfo.fileXftpServers.joinToString(", ") { serverHostname(it) })) + } val qi = ci.quotedItem if (qi != null) { shareText.add("") 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 68e5ee3394..12f13a426f 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 @@ -135,7 +135,7 @@ fun ChatView( val draft = chatModel.draft.value val sharedContent = chatModel.sharedContent.value mutableStateOf( - if (chatModel.draftChatId.value == staleChatId.value && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) { + if (chatModel.draftChatId.value == draftChatId(staleChatId.value, chatsCtx.groupScopeInfo?.toChatScope()) && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) { draft } else { ComposeState(useLinkPreviews = useLinkPreviews) @@ -181,19 +181,23 @@ fun ChatView( availableContent.value = ContentFilter.initialList selectedChatItems.value = null selectionManager?.clearSelection() - val cInfo = activeChat.value?.chatInfo + // The outer chat/chatInfo/chatRh are captured once by LaunchedEffect(Unit) and go stale on desktop, where + // ChatView is reused across chat switches; read the opened chat fresh to avoid regressing the previous one. + val openedChat = chatModel.getChat(chatId) + val cInfo = openedChat?.chatInfo + val openedChatRh = openedChat?.remoteHostId if (chatsCtx.secondaryContextFilter == null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group || cInfo is ChatInfo.Local)) { - updateAvailableContent(chatRh, activeChat, availableContent) + updateAvailableContent(openedChatRh, activeChat, availableContent) } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.activeConn != null) { + if (cInfo is ChatInfo.Direct && cInfo.contact.activeConn != null) { withBGApi { - val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + val r = chatModel.controller.apiContactInfo(openedChatRh, cInfo.apiId) if (r != null) { val contactStats = r.first if (contactStats != null) withContext(Dispatchers.Main) { - chatModel.chatsContext.updateContactConnectionStats(chatRh, chat.chatInfo.contact, contactStats) - chatModel.chatAgentConnId.value = chat.chatInfo.contact.activeConn.agentConnId + chatModel.chatsContext.updateContactConnectionStats(openedChatRh, cInfo.contact, contactStats) + chatModel.chatAgentConnId.value = cInfo.contact.activeConn.agentConnId chatModel.chatSubStatus.value = contactStats.subStatus } } @@ -206,17 +210,17 @@ fun ChatView( } if (cInfo is ChatInfo.Group && cInfo.groupInfo.useRelays) { withBGApi { - setGroupMembers(chatRh, cInfo.groupInfo, chatModel) + setGroupMembers(openedChatRh, cInfo.groupInfo, chatModel) if (cInfo.groupInfo.membership.memberRole == GroupMemberRole.Owner) { - val relays = chatModel.controller.apiGetGroupRelays(cInfo.groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(openedChatRh, cInfo.groupInfo.groupId) withContext(Dispatchers.Main) { ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays) } } else if (cInfo.groupInfo.membership.memberCurrent) { - val gInfo = chatModel.controller.apiGetUpdatedGroupLinkData(chatRh, cInfo.groupInfo.groupId) + val gInfo = chatModel.controller.apiGetUpdatedGroupLinkData(openedChatRh, cInfo.groupInfo.groupId) if (gInfo != null) { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateGroup(chatRh, gInfo) + chatModel.chatsContext.updateGroup(openedChatRh, gInfo) } } } @@ -407,7 +411,7 @@ fun ChatView( val selectedItems: MutableState<Set<Long>?> = mutableStateOf(null) ModalManager.end.showCustomModal { close -> val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) } - ModalView(close, appBar = appBar.value) { + ModalView(close, cardScreen = true, appBar = appBar.value) { val chatInfo = remember { activeChat }.value?.chatInfo if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) } @@ -510,7 +514,7 @@ fun ChatView( if (chatsCtx.secondaryContextFilter == null) { ModalManager.end.closeModals() } - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close = close, closeAll = close) } @@ -802,7 +806,7 @@ fun ChatView( } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, content = { + ModalView(close, showClose = appPlatform.isAndroid, cardScreen = true, content = { ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, false, close) }) LaunchedEffect(chatInfo.id) { @@ -1547,6 +1551,11 @@ fun subscriberCountStr(count: Long): String = if (count == 1L) String.format(generalGetString(MR.strings.channel_subscriber_count_singular), count) else String.format(generalGetString(MR.strings.channel_subscriber_count_plural), count) +fun ownersContributorsCountStr(count: Int, withContributors: Boolean): String = + if (withContributors) String.format(generalGetString(MR.strings.channel_owners_contributors_count), count) + else if (count == 1) String.format(generalGetString(MR.strings.channel_owner_count_singular), count) + else String.format(generalGetString(MR.strings.channel_owner_count_plural), count) + @Composable fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { Row( @@ -1904,10 +1913,13 @@ fun BoxScope.ChatItemsList( reveal: (Boolean) -> Unit ) { val itemScope = rememberCoroutineScope() + val viewConfiguration = LocalViewConfiguration.current CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. - // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view - LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() + // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view. + // remember: pointerInput handlers observe ViewConfiguration and reset on any change, so a new + // instance per recomposition kills in-flight presses/hover whenever a message is inserted + LocalViewConfiguration provides remember(viewConfiguration) { viewConfiguration.bigTouchSlop() } ) { val provider = { providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> @@ -1955,7 +1967,7 @@ fun BoxScope.ChatItemsList( } false } - val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled) Modifier else SwipeToDismissModifier( + val swipeableModifier = if (appPlatform.isDesktop || !chatInfo.sendMsgEnabled || cItem.meta.itemDeleted != null) Modifier else SwipeToDismissModifier( state = dismissState, directions = setOf(DismissDirection.EndToStart), swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, @@ -2000,7 +2012,7 @@ fun BoxScope.ChatItemsList( Column( Modifier .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .fillMaxWidth() .then(swipeableModifier), verticalArrangement = Arrangement.spacedBy(4.dp), @@ -2035,7 +2047,7 @@ fun BoxScope.ChatItemsList( val tailRendered = style is ShapeStyle.Bubble && style.tailVisible Text( - member.memberRole.text, + member.memberRole.text(isChannel = chatInfo.isChannel), Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp), fontSize = 13.5.sp, fontWeight = FontWeight.Medium, @@ -2079,7 +2091,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { @@ -2092,7 +2104,7 @@ fun BoxScope.ChatItemsList( Column( Modifier .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = 8.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .fillMaxWidth() .then(swipeableModifier), verticalArrangement = Arrangement.spacedBy(4.dp), @@ -2162,7 +2174,7 @@ fun BoxScope.ChatItemsList( } Row( Modifier - .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) + .padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false)) .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value) .then(swipeableOrSelectionModifier) ) { @@ -2323,19 +2335,17 @@ fun BoxScope.ChatItemsList( ) } - val descr = chatInfo.shortDescr?.trim() - if (descr != null && descr != "") { - MarkdownText( - descr, - parseToMarkdown(descr), - toggleSecrets = true, - style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), - maxLines = 4, - overflow = TextOverflow.Ellipsis, - uriHandler = LocalUriHandler.current, - modifier = Modifier.padding(top = DEFAULT_PADDING_HALF), - linkMode = linkMode - ) + ProfileDescriptionText( + shortDescr = chatInfo.shortDescr, + description = chatInfo.profileDescription, + style = MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.onBackground, lineHeight = 21.sp, textAlign = TextAlign.Center), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) + + when (chatInfo) { + is ChatInfo.Direct -> ContactSimplexNameView(chatInfo.contact, verifiable = false) + is ChatInfo.Group -> GroupSimplexNameView(chatInfo.groupInfo, verifiable = false) + else -> {} } val contextStr = chatContext() @@ -3206,7 +3216,7 @@ fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: withBGApi { setGroupMembers(rhId, groupInfo, chatModel) close?.invoke() - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true) { close -> AddGroupMembersView(rhId, groupInfo, false, chatModel, close) } } @@ -3217,7 +3227,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( withBGApi { val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) close?.invoke() - ModalManager.end.showModalCloseable(true) { + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt index 7ab7963547..26e069c739 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt @@ -33,8 +33,7 @@ fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boo .size(36.dp), tint = if (isInDarkTheme()) FileDark else FileLight ) - Text(fileName) - Spacer(Modifier.weight(1f)) + Text(fileName, maxLines = 1, modifier = Modifier.weight(1f)) if (cancelEnabled) { IconButton(onClick = cancelFile, modifier = Modifier.padding(0.dp)) { Icon( 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 6d598a166b..6bfbad52ef 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 @@ -133,15 +133,12 @@ data class ComposeState( ) val memberMentions: Map<String, Long> - get() = this.mentions.mapNotNull { - val memberRef = it.value.memberRef - - if (memberRef != null) { - it.key to memberRef.groupMemberId - } else { - null - } - }.toMap() + get() = parsedMessage + .mapNotNull { (it.format as? Format.Mention)?.memberName } + .distinct() + .mapNotNull { name -> mentions[name]?.memberRef?.groupMemberId?.let { name to it } } + .take(MAX_NUMBER_OF_MENTIONS) + .toMap() val editing: Boolean get() = @@ -288,16 +285,40 @@ expect fun AttachmentSelection( ) fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) { - val groups = uris.groupBy { isImage(it) } - val images = groups[true] ?: emptyList() + // The extension is enough to classify every format except .webm, which is just as commonly an + // audio-only container as a video one. An audio-only file has no frame to embed and is sent as a file, + // but that can only be told from the content, so reading it is deferred to a background thread. + // Only done here, where files arrive without the user saying how to send them (drag & drop, paste) - + // an explicitly picked video is still sent as one. + if (uris.none { isWebmUri(it) }) { + attachFiles(uris, emptySet()) + } else { + CoroutineScope(Dispatchers.IO).launch { + attachFiles(uris, uris.filter { isWebmUri(it) && hasVideoTrack(it) }.toSet()) + } + } +} + +private fun MutableState<ComposeState>.attachFiles(uris: List<URI>, webmVideos: Set<URI>) { + val groups = uris.groupBy { isImage(it) || (isVideoUri(it) && (!isWebmUri(it) || it in webmVideos)) } + val media = groups[true] ?: emptyList() val files = groups[false] ?: emptyList() - if (images.isNotEmpty()) { - CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) } + if (media.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) } } else if (files.isNotEmpty()) { processPickedFile(uris.first(), null) } } +private fun isVideoUri(uri: URI): Boolean { + val name = getFileName(uri)?.lowercase() ?: return false + return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") || + name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") || + name.endsWith(".webm") +} + +private fun isWebmUri(uri: URI): Boolean = getFileName(uri)?.lowercase()?.endsWith(".webm") == true + fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) { if (uri != null) { val maxFileSize = value.maxFileSize @@ -324,7 +345,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: val imagesPreview = ArrayList<String>() uris.forEach { uri -> var bitmap: ImageBitmap? - when { + val uploadContent: UploadContent? = when { isImage(uri) -> { // Image val drawable = getDrawableFromUri(uri) @@ -334,16 +355,19 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: // It's a gif or webp val fileSize = getFileSize(uri) if (fileSize != null && fileSize <= maxFileSize) { - content.add(UploadContent.AnimatedImage(uri)) + UploadContent.AnimatedImage(uri) } else { bitmap = null AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) ) + null } } else if (bitmap != null) { - content.add(UploadContent.SimpleImage(uri)) + UploadContent.SimpleImage(uri) + } else { + null } } else -> { @@ -351,11 +375,22 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: val res = getBitmapFromVideo(uri, withAlertOnException = true) bitmap = res.preview val durationMs = res.duration - content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) + UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0) } } - if (bitmap != null) { + // content and imagesPreview must stay index-aligned and equal-length: both consumers + // (ComposeImageView and sendMessageAsync) cross-index one list by the other's index. + // Only pair them when a preview bitmap exists; otherwise skip the media entirely. + if (bitmap != null && uploadContent != null) { + content.add(uploadContent) imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) + } else if (uploadContent is UploadContent.Video && !AlertManager.shared.hasAlertsShown()) { + // A corrupted/undecodable video can yield a null preview frame without throwing, so + // getBitmapFromVideo shows no alert. Skip it (other picked media still send) and tell + // the user instead of dropping it silently. hasAlertsShown guards against stacking the + // alert across multiple bad items and against duplicating the one already shown on the + // exception path. Image decode failures are already surfaced by getBitmapFromUri above. + showVideoDecodingException() } } if (imagesPreview.isNotEmpty()) { @@ -377,6 +412,8 @@ fun ComposeView( focusRequester: FocusRequester?, ) { val cancelledLinks = rememberSaveable { mutableSetOf<String>() } + val chatScope = remember(chatsCtx) { chatsCtx.groupScopeInfo?.toChatScope() } + val scopeChatId = remember { chat.id } fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) @@ -472,14 +509,14 @@ fun ComposeView( } fun clearPrevDraft(prevChatId: String?) { - if (chatModel.draftChatId.value == prevChatId) { + if (chatModel.draftChatId.value == draftChatId(prevChatId, chatScope)) { chatModel.draft.value = null chatModel.draftChatId.value = null } } - fun clearCurrentDraft() { - if (chatModel.draftChatId.value == chat.id) { + fun clearCurrentDraft(forChat: Chat = chat) { + if (chatModel.draftChatId.value == draftChatId(forChat.id, chatScope)) { chatModel.draft.value = null chatModel.draftChatId.value = null } @@ -509,6 +546,7 @@ fun ComposeView( is SharedContent.Text -> emptyList() is SharedContent.Forward -> emptyList() is SharedContent.ChatLink -> emptyList() + is SharedContent.MyAddress -> emptyList() } // When sharing a file and pasting it in SimpleX itself, the file shouldn't be deleted before sending or before leaving the chat after sharing chatModel.filesToDelete.removeAll { file -> @@ -522,7 +560,7 @@ fun ComposeView( } } - suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map<String, Long>): ChatItem? { + suspend fun send(chat: Chat, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?, mentions: Map<String, Long>, sign: Boolean = false): ChatItem? { val cInfo = chat.chatInfo val chatItems = if (chat.chatInfo.chatType == ChatType.Local) chatModel.controller.apiCreateChatItems( @@ -539,6 +577,7 @@ fun ComposeView( sendAsGroup = cInfo.sendAsGroup, live = live, ttl = ttl, + sign = sign, composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions)) ) if (!chatItems.isNullOrEmpty()) { @@ -554,9 +593,10 @@ fun ComposeView( } // TODO [short links] connectCheckLinkPreview - fun checkLinkPreview(): MsgContent { - val msgText = composeState.value.message.text - return when (val composePreview = composeState.value.preview) { + // the state is passed in by a send that must not read the current one - see sendMessageAsync + fun checkLinkPreview(cs: ComposeState = composeState.value): MsgContent { + val msgText = cs.message.text + return when (val composePreview = cs.preview) { is ComposePreview.CLinkPreview -> { val parsedMsg = parseToMarkdown(msgText) val url = getMessageLinks(parsedMsg).first @@ -576,6 +616,10 @@ fun ComposeView( composeState.value = composeState.value.copy(inProgress = true) } + // composeState and its inProgress flag are shared between the chats opened in this view, and sending is not cancelled + // when the chat is switched - a send may only clear or reset the state while it still holds the message that was sent + fun composeHasSentMessage(): Boolean = chatModel.chatId.value == chat.id && composeState.value.inProgress + suspend fun sendMemberContactInvitation() { val mc = checkLinkPreview() sending() @@ -583,10 +627,10 @@ fun ComposeView( if (contact != null) { withContext(Dispatchers.Main) { chatsCtx.updateContact(chat.remoteHostId, contact) - clearState() + if (composeHasSentMessage()) clearState() } - } else { - composeState.value = composeState.value.copy(inProgress = false) + } else withContext(Dispatchers.Main) { + if (composeHasSentMessage()) composeState.value = composeState.value.copy(inProgress = false) } } @@ -603,10 +647,10 @@ fun ComposeView( if (contact != null) { withContext(Dispatchers.Main) { chatsCtx.updateContact(chat.remoteHostId, contact) - clearState() + if (composeHasSentMessage()) clearState() } - } else { - composeState.value = composeState.value.copy(inProgress = false) + } else withContext(Dispatchers.Main) { + if (composeHasSentMessage()) composeState.value = composeState.value.copy(inProgress = false) } } @@ -648,15 +692,19 @@ fun ComposeView( chatModel.channelRelayHostnames.remove(groupInfo.groupId) chatModel.groupMembers.value = relayResults.map { it.relayMember } chatModel.populateGroupMembersIndexes() - clearState() + if (composeHasSentMessage()) clearState() } - } else { - composeState.value = composeState.value.copy(inProgress = false) + } else withContext(Dispatchers.Main) { + if (composeHasSentMessage()) composeState.value = composeState.value.copy(inProgress = false) } } - suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? { - val cs = composeState.value + // toChat is the chat the message was composed in - it differs from the one this view shows only for the live message + // committed by a chat switch, which has no context item, so the forwarding, editing and reporting branches below + // cannot run with a different chat. cs is that send's state, captured before the switch replaced it. + suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?, sign: Boolean = false, toChat: Chat = chat, cs: ComposeState = composeState.value): List<ChatItem>? { + // a send for another chat may not write to composeState, even after that chat is opened again - it was handed over + fun composeIsForSend(): Boolean = toChat.id == chat.id var sent: List<ChatItem>? var lastMessageFailedToSend: ComposeState? = null val msgText = text ?: cs.message.text @@ -706,8 +754,8 @@ fun ComposeView( fun updateMsgContent(msgContent: MsgContent): MsgContent { return when (msgContent) { - is MsgContent.MCText -> checkLinkPreview() - is MsgContent.MCLink -> checkLinkPreview() + is MsgContent.MCText -> checkLinkPreview(cs) + is MsgContent.MCLink -> checkLinkPreview(cs) is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image) is MsgContent.MCVideo -> MsgContent.MCVideo(msgText, image = msgContent.image, duration = msgContent.duration) is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) @@ -764,12 +812,12 @@ fun ComposeView( } val liveMessage = cs.liveMessage - if (!live) { + if (!live && composeIsForSend()) { if (liveMessage != null) composeState.value = cs.copy(liveMessage = null) sending() } if (!cs.forwarding || chatModel.draft.value?.forwarding == true) { - clearCurrentDraft() + clearCurrentDraft(toChat) } if (cs.contextItem is ComposeContextItem.ForwardingItems) { @@ -780,7 +828,9 @@ fun ComposeView( if (cs.message.text.isNotEmpty()) { sent?.mapIndexed { index, message -> if (index == sent!!.lastIndex) { - send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions) + // the current state, not cs: forwarding is never reached from the chat switch, and keeps what was typed + // while it was in flight + send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl, mentions = cs.memberMentions, sign = sign) } else { message } @@ -793,7 +843,7 @@ fun ComposeView( sent = if (updatedMessage != null) listOf(updatedMessage) else null lastMessageFailedToSend = if (updatedMessage == null) constructFailedMessage(cs) else null } else if (liveMessage != null && liveMessage.sent) { - val updatedMessage = updateMessage(liveMessage.chatItem, chat, live) + val updatedMessage = updateMessage(liveMessage.chatItem, toChat, live) sent = if (updatedMessage != null) listOf(updatedMessage) else null } else if (cs.contextItem is ComposeContextItem.ReportedItem) { sent = sendReport(cs.contextItem.reason, cs.contextItem.chatItem.id) @@ -803,7 +853,7 @@ fun ComposeView( val remoteHost = chatModel.currentRemoteHost.value when (val preview = cs.preview) { ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) - is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) + is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview(cs)) is ComposePreview.ChatLinkPreview -> { val linkStr = preview.chatLink.connLinkStr val text = if (msgText.isEmpty()) linkStr else "$msgText\n$linkStr" @@ -820,7 +870,7 @@ fun ComposeView( if (remoteHost == null) saveAnimImage(it.uri) else CryptoFile.desktopPlain(it.uri) is UploadContent.Video -> - if (remoteHost == null) saveFileFromUri(it.uri, hiddenFileNamePrefix = "video") + if (remoteHost == null) saveFileFromUri(it.uri, cs.maxFileSize, hiddenFileNamePrefix = "video") else CryptoFile.desktopPlain(it.uri) } if (file != null) { @@ -869,7 +919,7 @@ fun ComposeView( } is ComposePreview.FilePreview -> { val file = if (remoteHost == null) { - saveFileFromUri(preview.uri) + saveFileFromUri(preview.uri, cs.maxFileSize) } else { CryptoFile.desktopPlain(preview.uri) } @@ -894,10 +944,11 @@ fun ComposeView( localPath = file.filePath ) } - val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file, + val sendResult = send(toChat, content, if (index == 0) quotedItemId else null, file, live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false, ttl = ttl, - mentions = cs.memberMentions + mentions = cs.memberMentions, + sign = sign ) sent = if (sendResult != null) listOf(sendResult) else null if (sent == null && index == msgs.lastIndex && cs.liveMessage == null) { @@ -910,23 +961,47 @@ fun ComposeView( val wasForwarding = cs.forwarding val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id val lastFailed = lastMessageFailedToSend - if (lastFailed == null) { - clearState(live) - } else { - composeState.value = lastFailed - } - val draft = chatModel.draft.value - if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) { - composeState.value = draft - } else { - clearCurrentDraft() + // composeState is shared between the chats opened in this view, and this runs after the send API call, so the user + // could have switched chats or typed another message in the meantime - only the message that was sent may be + // cleared or restored. On Main, so that these checks and changes are not interleaved with the user switching + // chats or typing. + withContext(Dispatchers.Main) { + val chatIsOpen = composeIsForSend() && chatModel.chatId.value == chat.id + // a live message is held in the compose state of the chat it is sent to, but only while that chat is the one open + val liveSend = live || cs.liveMessage != null + val sentMessageInCompose = chatIsOpen && (liveSend || composeState.value.inProgress) + if (sentMessageInCompose) { + if (lastFailed == null) { + clearState(live) + } else { + composeState.value = lastFailed + } + } + val draft = chatModel.draft.value + if (wasForwarding && chatModel.draftChatId.value == draftChatId(chat.chatInfo.id, chatScope) && forwardingFromChatId != chat.chatInfo.id && draft != null) { + if (sentMessageInCompose) composeState.value = draft + } else { + clearCurrentDraft(toChat) + // liveSend excluded: a failing keystroke send would otherwise write a draft on every attempt + if (!sentMessageInCompose && !liveSend && lastFailed != null) { + // the message was not sent, so it is restored in the chat it was composed in, or kept as its draft if another chat is open + if (chatIsOpen && composeState.value.empty) { + composeState.value = lastFailed + } else if (saveLastDraft) { + chatModel.draft.value = lastFailed + chatModel.draftChatId.value = draftChatId(chat.id, chatScope) + } + } + } } return sent } - fun sendMessage(ttl: Int?) { + // toChat and composed are for the chat switch, which hands the compose state over to the chat it opened; passing + // toChat without doing that leaves the sent message in the input + fun sendMessage(ttl: Int?, sign: Boolean = false, toChat: Chat = chat, composed: ComposeState? = null) { withLongRunningApi(slow = 120_000) { - sendMessageAsync(null, false, ttl) + sendMessageAsync(null, false, ttl, sign, toChat, composed ?: composeState.value) } } @@ -1183,8 +1258,9 @@ fun ComposeView( } val ownerRelayState = ownerRelayState(chat, chatModel) + val subscriberRelayState = subscriberRelayState(chat, chatModel) - val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason(ownerRelayState?.noActiveRelays == true)) + val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason((ownerRelayState?.noActiveRelays ?: subscriberRelayState?.noActiveRelays) == true)) val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @@ -1287,23 +1363,32 @@ fun ComposeView( } } - LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { - if (!chat.chatInfo.sendMsgEnabled) { - clearCurrentDraft() - clearState() - } - } - KeyChangeEffect(chatModel.chatId.value) { prevChatId -> val cs = composeState.value if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) { - sendMessage(null) + // the chat is already switched, so the live message goes to the chat with the id it had before the switch + val liveMessageChat = if (prevChatId == null || prevChatId == chat.id) chat else chatsCtx.getChat(prevChatId) + // if that chat is gone there is nowhere to send it, and it must not be sent to the chat opened instead + // cs is captured on this thread, before the compose state is replaced below + if (liveMessageChat != null) sendMessage(null, toChat = liveMessageChat, composed = cs) else clearState() resetLinkPreview() clearPrevDraft(prevChatId) deleteUnusedFiles() + // the sent message belongs to the chat it was composed in; the chat opened next shows its own draft + val draft = chatModel.draft.value + composeState.value = if (draft != null && chatModel.draftChatId.value == draftChatId(chatModel.chatId.value, chatScope)) draft + else ComposeState(useLinkPreviews = useLinkPreviews) } else if (cs.inProgress) { clearPrevDraft(prevChatId) - composeState.value = cs.copy(inProgress = false, progressByTimeout = false) + // the message being sent must not be kept in the compose state, it is shared with the chat opened next; + // if it fails to send it is restored in this chat or saved as its draft + clearState() + // clearState() does not load the draft of the chat opened next, and without this it is never shown and is + // dropped when that chat is left + val draft = chatModel.draft.value + if (draft != null && chatModel.draftChatId.value == draftChatId(chatModel.chatId.value, chatScope)) { + composeState.value = draft + } } else if (!cs.empty) { if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { recState.value = RecordingState.NotStarted @@ -1312,10 +1397,10 @@ fun ComposeView( } if (saveLastDraft) { chatModel.draft.value = composeState.value - chatModel.draftChatId.value = prevChatId + chatModel.draftChatId.value = draftChatId(prevChatId, chatScope) } composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) - } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { + } else if (chatModel.draftChatId.value == draftChatId(chatModel.chatId.value, chatScope) && chatModel.draft.value != null) { composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) } else { clearPrevDraft(prevChatId) @@ -1324,22 +1409,44 @@ fun ComposeView( chatModel.removeLiveDummy() CIFile.cachedRemoteFileRequests.clear() } + // Must be composed after KeyChangeEffect above (effects run in composition order), + // so that on chat switch the previous chat's draft is saved before it is cleared here. + LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { + if (!chat.chatInfo.sendMsgEnabled) { + clearCurrentDraft() + clearState() + } + } // keep the attach size limit in sync with the chat: the user's active badge raises it, but not in incognito chats where no badge is presented LaunchedEffect(chat.chatInfo) { val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() composeState.value = composeState.value.copy(maxFileSize = getMaxFileSize(FileProtocol.XFTP, if (incognito) null else chatModel.currentUser.value?.profile)) } if (appPlatform.isDesktop) { + // the same ComposeView is reused when switching chats, so `chat` captured by onDispose would be the chat opened first, not the current one + val currentChatId = rememberUpdatedState(chat.id) // Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)` DisposableEffect(Unit) { onDispose { if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) { chatModel.draft.value = composeState.value - chatModel.draftChatId.value = chat.id + chatModel.draftChatId.value = draftChatId(currentChatId.value, chatScope) } } } } + // support chat is closed without changing chat id, and then `KeyChangeEffect(chatModel.chatId.value)` doesn't save the draft + DisposableEffect(Unit) { + onDispose { + val cs = composeState.value + // chat change is handled by KeyChangeEffect, unfinished voice recording should not replace the saved draft + if (chatScope == null || chatModel.chatId.value != scopeChatId || (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished)) return@onDispose + if (saveLastDraft && !cs.empty) { + chatModel.draft.value = cs + chatModel.draftChatId.value = draftChatId(scopeChatId, chatScope) + } else clearPrevDraft(scopeChatId) + } + } @Composable fun SendMsgView_( @@ -1369,12 +1476,18 @@ fun ComposeView( allowVoiceToContact = ::allowVoiceToContact, sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, + showSign = (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.useRelays == true, + signMessageAlertShown = chatModel.controller.appPrefs.signMessageAlertShown, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, placeholder = if (userCantSendReason.value != null) "" else placeholder ?: composeState.value.placeholder, sendMessage = { ttl -> sendMessage(ttl) resetLinkPreview() }, + sendSignedMessage = { + sendMessage(null, sign = true) + resetLinkPreview() + }, sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null, updateLiveMessage = ::updateLiveMessage, cancelLiveMessage = { @@ -1517,6 +1630,22 @@ fun ComposeView( } } } + is SharedContent.MyAddress -> { + val cInfo = chat.chatInfo + val sendAsGroup = cInfo.sendAsGroup + withBGApi { + val mc = chatModel.controller.apiShareMyAddress( + chat.remoteHostId, + cInfo.chatType, cInfo.apiId, + cInfo.groupChatScope(), sendAsGroup + ) + if (mc is MsgContent.MCChat) { + composeState.value = composeState.value.copy( + preview = ComposePreview.ChatLinkPreview(mc.chatLink, mc.ownerSig) + ) + } + } + } null -> {} } chatModel.sharedContent.value = null @@ -1555,18 +1684,12 @@ fun ComposeView( } } } else { - val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() - val relayMembers = chatModel.groupMembers.value - .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) } - .sortedBy { hostFromRelayLink(it.relayLink ?: "") } - val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress - val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } - val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } - val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } - val resolvedCount = connectedCount + removedCount + failedCount - val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size - if (total == 0 || removedCount + failedCount > 0 || resolvedCount < total) { - SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, showProgress, relayListExpanded) + subscriberRelayState?.let { s -> + val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress + val resolvedCount = s.connectedCount + s.removedCount + s.failedCount + if (s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total) { + SubscriberChannelRelayBar(s.hostnames, s.relayMembers, s.connectedCount, s.removedCount, s.failedCount, s.total, showProgress, relayListExpanded) + } } } } @@ -2032,6 +2155,33 @@ private data class OwnerRelayState( val noActiveRelays: Boolean ) +private fun subscriberRelayState(chat: Chat, chatModel: ChatModel): SubscriberRelayState? { + val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo ?: return null + if (!gInfo.useRelays || gInfo.membership.memberRole == GroupMemberRole.Owner || + gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemRejected, GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) + ) return null + val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted() + val relayMembers = chatModel.groupMembers.value + .filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) } + .sortedBy { hostFromRelayLink(it.relayLink ?: "") } + val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) } + val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null } + val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null } + val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size + val noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total + return SubscriberRelayState(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays) +} + +private data class SubscriberRelayState( + val hostnames: List<String>, + val relayMembers: List<GroupMember>, + val connectedCount: Int, + val removedCount: Int, + val failedCount: Int, + val total: Int, + val noActiveRelays: Boolean +) + private fun relayMemberRemoved(status: GroupMemberStatus?): Boolean = status in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 7c04c30f67..0276727ccc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -2,12 +2,13 @@ package chat.simplex.common.views.chat import InfoRow import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.ui.Modifier import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* @@ -54,6 +55,7 @@ fun ContactPreferencesView( if (featuresAllowed == currentFeaturesAllowed) close() else showUnsavedChangesAlert({ savePrefs(close) }, close) }, + cardScreen = true, ) { ContactPreferencesLayout( featuresAllowed, @@ -90,27 +92,27 @@ private fun ContactPreferencesLayout( TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl -> applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL)) } - SectionDividerSpaced(true) + SectionDividerSpaced() val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { applyPrefs(featuresAllowed.copy(fullDelete = it)) } - SectionDividerSpaced(true) + SectionDividerSpaced() val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) } FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) { applyPrefs(featuresAllowed.copy(reactions = it)) } - SectionDividerSpaced(true) + SectionDividerSpaced() val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { applyPrefs(featuresAllowed.copy(voice = it)) } - SectionDividerSpaced(true) + SectionDividerSpaced() val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) } FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) { applyPrefs(featuresAllowed.copy(calls = it)) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() ResetSaveButtons( reset = reset, save = savePrefs, @@ -135,7 +137,7 @@ private fun FeatureSection( ) SectionView( - feature.text.uppercase(), + feature.text, icon = feature.iconFilled(), iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red, leadingIcon = true, @@ -170,7 +172,7 @@ private fun TimedMessagesFeatureSection( ) SectionView( - ChatFeature.TimedMessages.text.uppercase(), + ChatFeature.TimedMessages.text, icon = ChatFeature.TimedMessages.iconFilled(), iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red, leadingIcon = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 0948551c7e..b116201d87 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -49,9 +49,12 @@ fun SendMsgView( sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, + showSign: Boolean = false, + signMessageAlertShown: SharedPreference<Boolean> = SharedPreference(get = { false }, set = {}), customDisappearingMessageTimePref: SharedPreference<Int>? = null, placeholder: String, sendMessage: (Int?) -> Unit, + sendSignedMessage: () -> Unit = {}, sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, @@ -207,6 +210,31 @@ fun SendMsgView( ) } } + // hidden until message signing is user-facing (recipient-only stage) +// if (showSign && !cs.editing) { +// menuItems.add { +// ItemAction( +// generalGetString(MR.strings.sign_message), +// painterResource(MR.images.ic_verified), +// onClick = { +// if (signMessageAlertShown.state.value) { +// sendSignedMessage() +// } else { +// AlertManager.shared.showAlertDialog( +// title = generalGetString(MR.strings.sign_message), +// text = generalGetString(MR.strings.sign_message_desc), +// confirmText = generalGetString(MR.strings.send_verb), +// onConfirm = { +// signMessageAlertShown.set(true) +// sendSignedMessage() +// } +// ) +// } +// showDropdown.value = false +// } +// ) +// } +// } } return menuItems diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt new file mode 100644 index 0000000000..d3e3ffbe61 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SimplexNameView.kt @@ -0,0 +1,169 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.* +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.ImageResource +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +// Renders a contact's / channel's SimpleX name with its 3-state verification indicator. +// `verification`: null = not attempted, false = failed, true = verified. +// `verify` runs the verify API, updates the model and returns (newVerification, failureReason); +// null on network error. With `autoVerify`, it runs once on open when state is null. +@Composable +fun SimplexNameView( + simplexName: String, + verified: Boolean?, + verifiable: Boolean = true, + verify: suspend () -> Pair<Boolean?, String?>? +) { + val scope = rememberCoroutineScope() + val inFlight = remember { mutableStateOf(false) } + val showSpinner = remember { mutableStateOf(false) } + + fun runVerify(manual: Boolean) { + if (inFlight.value) return + inFlight.value = true + scope.launch { + // delay the spinner so a fast result on open doesn't flash it + val spinner = launch { delay(300); if (inFlight.value) showSpinner.value = true } + val res = try { + verify() + } catch (e: Exception) { + Log.e(TAG, "verify SimplexName: ${e.stackTraceToString()}") + null + } + spinner.cancel() + inFlight.value = false + showSpinner.value = false + if (res != null) { + val (newV, reason) = res + // show the reason on a manual run, or on an inconclusive auto run (state stayed null) + if (reason != null && (manual || newV == null)) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.simplex_name_not_verified), reason) + } + } + } + } + + LaunchedEffect(Unit) { + if (verifiable && chatModel.controller.appPrefs.privacyVerifySimplexNames.get() && verified == null) runVerify(manual = false) + } + + val clipboard = LocalClipboardManager.current + val nameStyle = MaterialTheme.typography.body2.copy( + color = if (verified == true) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(top = DEFAULT_PADDING_HALF) + ) { + when { + showSpinner.value -> { + Text(simplexName, style = nameStyle) + CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp, color = MaterialTheme.colors.secondary) + } + verified == true -> + SimplexNameWithIcon(simplexName, nameStyle, MR.images.ic_check_filled, MaterialTheme.colors.primary) { + clipboard.setText(AnnotatedString(simplexName)) + showToast(generalGetString(MR.strings.copied)) + } + !verifiable -> Text(simplexName, style = nameStyle) + verified == false -> + SimplexNameWithIcon(simplexName, nameStyle, MR.images.ic_close, Color.Red) { runVerify(manual = true) } + else -> { + Text(simplexName, style = nameStyle) + Text( + stringResource(MR.strings.verify_simplex_name_action), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { runVerify(manual = true) } + ) + } + } + } +} + +// The check/cross drawable is centered in its box with ~27% padding top and bottom, so its glyph bottom sits at +// ~73% of the box height. Align that line with the text baseline so the glyph rests on the baseline; the box is +// sized so the visible glyph is about the name's cap height. Only the icon is tinted, never the name. +@Composable +private fun SimplexNameWithIcon(name: String, style: TextStyle, icon: ImageResource, tint: Color, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.clickable { onClick() } + ) { + Text(name, Modifier.alignByBaseline(), style = style) + Icon( + painterResource(icon), null, + Modifier.size(22.dp).alignBy { it.measuredHeight * 73 / 100 }, + tint = tint + ) + } +} + +@Composable +fun ContactSimplexNameView(contact: Contact, verifiable: Boolean = true) { + val domain = contact.profile.contactDomain + if (domain != null && (contact.profile.contactDomainVerified != null || domain.proof != null)) { + SimplexNameView( + simplexName = "@${domain.domain}", + verified = contact.profile.contactDomainVerified, + verifiable = verifiable, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyContactDomain(rhId, contact.contactId)?.let { (ct, reason) -> + chatModel.chatsContext.updateContact(rhId, ct) + ct.profile.contactDomainVerified to reason + } + } + ) + } +} + +@Composable +fun GroupSimplexNameView(groupInfo: GroupInfo, verifiable: Boolean = true) { + if (groupInfo.businessChat == null) { + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess + val domain = access?.groupDomainClaim?.shortName + if (domain != null && (groupInfo.groupDomainVerified != null || access.groupDomainClaim?.proof != null)) { + SimplexNameView( + simplexName = "#${domain}", + verified = groupInfo.groupDomainVerified, + verifiable = verifiable, + verify = { + val rhId = chatModel.remoteHostId() + chatModel.controller.apiVerifyGroupDomain(rhId, groupInfo.groupId)?.let { (gInfo, reason) -> + chatModel.chatsContext.updateGroup(rhId, gInfo) + gInfo.groupDomainVerified to reason + } + } + ) + } + } else { + val businessClaim = groupInfo.businessChat?.businessDomain + if (businessClaim != null && (groupInfo.groupDomainVerified != null || businessClaim.proof != null)) { + // A business presents as a contact, so the name retains its .simplex suffix; it cannot be re-verified. + SimplexNameView( + simplexName = "@${businessClaim.domain}", + verified = groupInfo.groupDomainVerified, + verifiable = false, + verify = { null } + ) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 91f7af2b95..880c15216e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp @@ -29,12 +30,14 @@ fun VerifyCodeView( connectionVerified: Boolean, verify: suspend (String?) -> Pair<Boolean, String>?, close: () -> Unit, + verifyDescription: StringResource = MR.strings.to_verify_compare, ) { if (connectionCode != null) { VerifyCodeLayout( displayName, connectionCode, connectionVerified, + verifyDescription, verifyCode = { newCode -> val res = verify(newCode) if (res != null) { @@ -54,6 +57,7 @@ private fun VerifyCodeLayout( displayName: String, connectionCode: String, connectionVerified: Boolean, + verifyDescription: StringResource, verifyCode: suspend (String?) -> Boolean, ) { ColumnWithScrollBar(Modifier.padding(horizontal = DEFAULT_PADDING)) { @@ -90,7 +94,7 @@ private fun VerifyCodeLayout( } Text( - generalGetString(MR.strings.to_verify_compare), + generalGetString(verifyDescription), Modifier.padding(bottom = DEFAULT_PADDING) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 45d336be75..7223f69f98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -5,7 +5,6 @@ import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionItemViewWithoutMinPadding -import SectionSpacer import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -161,7 +160,7 @@ fun AddGroupMembersLayout( iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight ) } - SectionSpacer() + SectionDividerSpaced() if (contactsToAdd.isEmpty() && searchText.value.text.isEmpty()) { Row( @@ -195,8 +194,8 @@ fun AddGroupMembersLayout( SectionCustomFooter { InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection) } - SectionDividerSpaced(maxTopPadding = true) - SectionView(stringResource(MR.strings.select_contacts).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.select_contacts)) { SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) { SearchRowView(searchText) } @@ -229,7 +228,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr ) { val values = GroupMemberRole.selectableRoles .filter { it <= groupInfo.membership.memberRole } - .map { it to it.text } + .map { it to it.text(isChannel = groupInfo.isChannel) } ExposedDropDownSettingRow( generalGetString(MR.strings.new_member_role), values, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt index d0c2486069..95ce003caa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt @@ -5,6 +5,7 @@ import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -30,6 +31,7 @@ data class AvailableRelay( @Composable fun AddGroupRelayView( + rhId: Long?, groupInfo: GroupInfo, existingRelayIds: Set<Long>, onRelayAdded: () -> Unit, @@ -45,7 +47,7 @@ fun AddGroupRelayView( LaunchedEffect(Unit) { try { - val servers = ChatController.getUserServers(null) + val servers = ChatController.getUserServers(rhId) if (servers != null) { val relays = mutableListOf<AvailableRelay>() for (op in servers) { @@ -79,7 +81,7 @@ fun AddGroupRelayView( if (relayIds.isEmpty()) return@AddGroupRelayLayout isAdding = true scope.launch { - addSelectedRelays(groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays -> + addSelectedRelays(rhId, groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays -> selectedRelayIds = newSelectedIds availableRelays = newAvailableRelays isAdding = false @@ -131,8 +133,8 @@ private fun AddGroupRelayLayout( fontSize = 14.sp ) } - SectionDividerSpaced(maxTopPadding = true) - SectionView(generalGetString(MR.strings.select_relays).uppercase()) { + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.select_relays)) { availableRelays.forEach { item -> val selected = item.relayId in selectedRelayIds SectionItemView( @@ -182,6 +184,7 @@ private fun AddRelaysButton(onClick: () -> Unit, disabled: Boolean) { } private suspend fun addSelectedRelays( + rhId: Long?, groupInfo: GroupInfo, relayIds: List<Long>, selectedRelayIds: Set<Long>, @@ -191,7 +194,7 @@ private suspend fun addSelectedRelays( updateState: (Set<Long>, List<AvailableRelay>) -> Unit ) { try { - val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds) + val result = ChatController.apiAddGroupRelays(rhId, groupInfo.groupId, relayIds) if (result == null) { updateState(selectedRelayIds, availableRelays) return diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt index 64f02d3376..128747c4fa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt @@ -4,22 +4,27 @@ import SectionBottomSpacer import SectionItemView import SectionView import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ownersContributorsCountStr import chat.simplex.common.views.chat.subscriberCountStr +import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable -fun ChannelMembersView( +fun ModalData.ChannelMembersView( rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, @@ -33,55 +38,96 @@ fun ChannelMembersView( && m.memberStatus != GroupMemberStatus.MemRemoved && m.memberRole != GroupMemberRole.Relay } + .sortedByDescending { it.memberRole } - ColumnWithScrollBar { - val title = if (groupInfo.isOwner) { - generalGetString(MR.strings.channel_members_title_subscribers) - } else { - generalGetString(MR.strings.channel_members_section_owners) + val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } } + val s = searchText.value.text.trim().lowercase() + val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong() + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + val title = if (groupInfo.isOwner) { + generalGetString(MR.strings.channel_members_title_subscribers) + } else { + generalGetString(MR.strings.channel_members_section_owners) + } + LazyColumnWithScrollBar( + contentPadding = PaddingValues( + top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp else topPaddingToContent(false) + ) + ) { + item { + AppBarTitle(title) } - AppBarTitle(title) - if (groupInfo.isOwner) { - val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong() - SectionView(title = subscriberCountStr(subscriberCount).uppercase()) { - SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - ChannelMemberRow(groupInfo.membership, user = true, showRole = true) - } - members.forEachIndexed { index, member -> - Divider() - SectionItemView( - click = { showMemberInfo(member) }, - minHeight = 54.dp, - padding = PaddingValues(horizontal = DEFAULT_PADDING) - ) { - ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner) - } - } - } - } else { - val owners = members.filter { it.memberRole >= GroupMemberRole.Owner } - SectionView(title = generalGetString(MR.strings.channel_members_section_owners)) { - owners.forEachIndexed { index, member -> - if (index > 0) { + val showSearch = members.size > 8 + item { + SectionView(title = subscriberCountStr(subscriberCount)) { + if (showSearch) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) + } Divider() } - SectionItemView( - click = { showMemberInfo(member) }, - minHeight = 54.dp, - padding = PaddingValues(horizontal = DEFAULT_PADDING) - ) { - ChannelMemberRow(member, user = false, showRole = false) + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel) } } } + val filtered = if (s.isEmpty()) members else members.filter { it.anyNameContains(s) } + channelMemberItems(filtered, groupInfo, GroupMemberRole.Member, dividerAboveFirst = true, showMemberInfo) + } else { + val contributors = members.filter { it.memberRole >= GroupMemberRole.Member && it.memberStatus != GroupMemberStatus.MemUnknown } + val contributorCount = contributors.size + if (groupInfo.membership.memberRole >= GroupMemberRole.Member) 1 else 0 + val withContributors = contributors.any { it.memberRole < GroupMemberRole.Owner } || + groupInfo.membership.memberRole >= GroupMemberRole.Member + val showSearch = contributors.size > 8 + val showUserRow = groupInfo.membership.memberRole >= GroupMemberRole.Member + item { + SectionView(title = ownersContributorsCountStr(contributorCount, withContributors)) { + if (showSearch) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) + } + if (showUserRow) Divider() + } + if (showUserRow) { + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel) + } + } + } + } + val filtered = if (s.isEmpty()) contributors else contributors.filter { it.anyNameContains(s) } + channelMemberItems(filtered, groupInfo, GroupMemberRole.Moderator, dividerAboveFirst = showSearch || showUserRow, showMemberInfo) + } + item { + SectionBottomSpacer() + } + } +} + +private fun LazyListScope.channelMemberItems( + members: List<GroupMember>, + groupInfo: GroupInfo, + showRoleFrom: GroupMemberRole, + dividerAboveFirst: Boolean, + showMemberInfo: (GroupMember) -> Unit +) { + itemsIndexed(members, key = { _, m -> m.groupMemberId }) { index, member -> + if (index > 0 || dividerAboveFirst) { + Divider() + } + SectionItemView( + click = { showMemberInfo(member) }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + ChannelMemberRow(member, user = false, showRole = member.memberRole >= showRoleFrom, isChannel = groupInfo.isChannel) } - SectionBottomSpacer() } } @Composable -private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean) { +private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boolean, isChannel: Boolean) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -112,7 +158,7 @@ private fun ChannelMemberRow(member: GroupMember, user: Boolean, showRole: Boole } if (showRole) { Text( - member.memberRole.text, + member.memberRole.text(isChannel = isChannel), color = MaterialTheme.colors.secondary ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index cfe9f0472d..60cc19bb1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -37,7 +37,7 @@ fun ChannelRelaysView( LaunchedEffect(Unit) { setGroupMembers(rhId, groupInfo, chatModel) if (groupInfo.isOwner) { - val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId) + val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId) ChannelRelaysModel.set(groupId = groupInfo.groupId, groupRelays = relays) } } @@ -87,8 +87,6 @@ private fun ChannelRelaysLayout( minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { - // TODO [relays] re-enable when relay management ships - /* if (groupInfo.isOwner && member.canBeRemoved(groupInfo)) { DefaultDropdownMenu(showMenu) { ItemAction(generalGetString(MR.strings.button_remove_relay), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { @@ -97,7 +95,6 @@ private fun ChannelRelaysLayout( }) } } - */ val statusText = if (groupInfo.isOwner) { ownerRelayStatusText(member, groupRelays) } else { @@ -109,16 +106,15 @@ private fun ChannelRelaysLayout( } SectionTextFooter(generalGetString(MR.strings.chat_relays_forward_messages)) } - // TODO [relays] re-enable when relay management ships - /* if (groupInfo.isOwner) { SectionView { SectionItemView(click = { // Backend gate (APIAddGroupRelays) rejects any chatRelayId already in group_relays // regardless of relayStatus, so all current rows must be excluded from the add list. val existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet() - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> AddGroupRelayView( + rhId = rhId, groupInfo = groupInfo, existingRelayIds = existingRelayIds, onRelayAdded = { withBGApi { setGroupMembers(rhId, groupInfo, chatModel) } }, @@ -139,7 +135,6 @@ private fun ChannelRelaysLayout( } } } - */ SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt new file mode 100644 index 0000000000..18a944f671 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt @@ -0,0 +1,186 @@ +package chat.simplex.common.views.chat.group + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* + +@Composable +fun ChannelWebPageView( + rhId: Long?, + groupInfo: GroupInfo, + chatModel: ChatModel, + close: () -> Unit +) { + val isChannel = groupInfo.isChannel + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess + val webPage = rememberSaveable { mutableStateOf(access?.groupWebPage ?: "") } + val allowEmbedding = rememberSaveable { mutableStateOf(access?.allowEmbedding ?: false) } + val groupRelays = remember { mutableStateListOf<GroupRelay>() } + + val dataUnchanged = webPage.value.trim() == (access?.groupWebPage ?: "") && + allowEmbedding.value == (access?.allowEmbedding ?: false) + + val save: () -> Unit = { + withBGApi { + val trimmedPage = webPage.value.trim() + val newAccess = PublicGroupAccess( + groupWebPage = trimmedPage.ifEmpty { null }, + groupDomainClaim = access?.groupDomainClaim, + domainWebPage = access?.domainWebPage ?: false, + allowEmbedding = allowEmbedding.value + ) + val gp = groupInfo.groupProfile.copy( + publicGroup = groupInfo.groupProfile.publicGroup?.copy(publicGroupAccess = newAccess) + ) + val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, gp, isChannel) + if (gInfo != null) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, gInfo) + } + close() + } + } + } + + val closeWithAlert = { + if (dataUnchanged) { + close() + } else { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.save_preferences_question), + confirmText = generalGetString(if (isChannel) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = close, + ) + } + } + + LaunchedEffect(Unit) { + val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId) + groupRelays.clear() + groupRelays.addAll(relays) + } + + BackHandler(onBack = closeWithAlert) + ModalView(close = closeWithAlert, cardScreen = true) { + ChannelWebPageLayout( + isChannel = isChannel, + webPage = webPage, + allowEmbedding = allowEmbedding, + groupRelays = groupRelays, + groupInfo = groupInfo, + dataUnchanged = dataUnchanged, + save = save + ) + } +} + +@Composable +private fun ChannelWebPageLayout( + isChannel: Boolean, + webPage: MutableState<String>, + allowEmbedding: MutableState<Boolean>, + groupRelays: List<GroupRelay>, + groupInfo: GroupInfo, + dataUnchanged: Boolean, + save: () -> Unit +) { + val clipboard = LocalClipboardManager.current + ColumnWithScrollBar { + AppBarTitle(stringResource(if (isChannel) MR.strings.channel_webpage else MR.strings.group_webpage)) + + val embedCode = embedCode(groupRelays, groupInfo) + if (embedCode != null) { + SectionTextFooter(stringResource(MR.strings.webpage_info)) + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.webpage_code)) { + SectionItemView { + Text( + embedCode, + style = MaterialTheme.typography.body2.copy(fontFamily = FontFamily.Monospace, fontSize = 12.sp), + maxLines = 6, + overflow = TextOverflow.Ellipsis + ) + } + SectionItemView({ + clipboard.setText(AnnotatedString(embedCode)) + showToast(generalGetString(MR.strings.copied)) + }) { + Icon(painterResource(MR.images.ic_content_copy), null, tint = MaterialTheme.colors.primary) + Spacer(Modifier.width(8.dp)) + Text(stringResource(MR.strings.copy_code), color = MaterialTheme.colors.primary) + } + } + SectionTextFooter(stringResource(MR.strings.webpage_code_footer)) + } else { + SectionTextFooter(stringResource(MR.strings.relays_no_web_support)) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.enter_webpage_url)) { + PlainTextEditor(webPage, placeholder = stringResource(MR.strings.web_page_url_placeholder)) + } + SectionTextFooter(stringResource(MR.strings.webpage_url_footer)) + SectionDividerSpaced() + + SectionView { + PreferenceToggle(stringResource(MR.strings.allow_anyone_to_embed), checked = allowEmbedding.value) { + allowEmbedding.value = it + } + } + SectionTextFooter(stringResource(if (allowEmbedding.value) MR.strings.embed_any_webpage_can_show else MR.strings.embed_only_your_page)) + SectionDividerSpaced() + + SectionView { + SectionItemView(save, disabled = dataUnchanged) { + Text( + stringResource(MR.strings.save_verb), + color = if (dataUnchanged) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + + SectionBottomSpacer() + } +} + +private fun embedCode(groupRelays: List<GroupRelay>, groupInfo: GroupInfo): String? { + val pg = groupInfo.groupProfile.publicGroup ?: return null + val relayDomains = groupRelays.mapNotNull { it.relayCap.webDomain } + if (relayDomains.isEmpty()) return null + val domains = relayDomains.joinToString(",") + return """<div data-simplex-channel-preview + data-channel-link="${pg.groupLink}" + data-channel-id="${pg.publicGroupId}" + data-relay-domains="$domains" + data-app-download-buttons="on" + data-color-scheme="light" +></div> +<script src="https://simplex.chat/js/channel-preview.js"></script>""" +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 770dfa64fb..49e7ac6db7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -1,21 +1,25 @@ package chat.simplex.common.views.chat.group +import CARD_PADDING import InfoRow import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView import SectionItemViewLongClickable import SectionItemViewSpaceBetween -import SectionSpacer +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.animation.* import androidx.compose.animation.core.animateDpAsState import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -113,7 +117,7 @@ fun ModalData.GroupChatInfoView( setGroupMembers(rhId, groupInfo, chatModel) if (!isActive) return@launch - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true) { close -> AddGroupMembersView(rhId, groupInfo, false, chatModel, close) } } @@ -122,13 +126,13 @@ fun ModalData.GroupChatInfoView( withBGApi { val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId) val stats = r?.second - val (_, code) = if (member.memberActive) { + val (_, code) = if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent)) && member.memberRole != GroupMemberRole.Relay) { val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, member.groupMemberId) member to memCode?.second } else { member to null } - ModalManager.end.showModalCloseable(true) { closeCurrent -> + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) { closeCurrent() @@ -169,7 +173,31 @@ fun ModalData.GroupChatInfoView( clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } + ModalManager.end.showModal(cardScreen = true) { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated, isChannel = groupInfo.useRelays, shareGroupInfo = groupInfo) } + }, + manageWebPage = { + ModalManager.end.showCustomModal { close -> ChannelWebPageView(rhId, groupInfo, chatModel, close) } + }, + setSimplexName = { + ModalManager.end.showCustomModal { close -> + val domain = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName + SetSimplexDomainView( + title = generalGetString(MR.strings.set_simplex_name), + footer = generalGetString(MR.strings.set_channel_simplex_name_footer), + placeholder = "#channelname.testing", + simplexName = if (domain == null) "" else "#$domain", + save = { domain -> + val access = groupInfo.groupProfile.publicGroup?.publicGroupAccess ?: PublicGroupAccess() + val newAccess = access.copy(groupDomainClaim = domain?.let { SimplexDomainClaim(it) }) + val gInfo = chatModel.controller.apiSetPublicGroupAccess(rhId, groupInfo.groupId, newAccess) + if (gInfo != null) { + withContext(Dispatchers.Main) { chatModel.chatsContext.updateGroup(rhId, gInfo) } + true + } else false + }, + close = close + ) + } }, onSearchClicked = onSearchClicked, deletingItems = deletingItems @@ -195,7 +223,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl } AlertManager.shared.showAlertDialog( title = generalGetString(titleId), - text = generalGetString(messageId), + text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + parseHtml = false, confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { withBGApi { @@ -229,7 +258,8 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved AlertManager.shared.showAlertDialog( title = generalGetString(titleId), - text = generalGetString(messageId), + text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + parseHtml = false, confirmText = generalGetString(MR.strings.leave_group_button), onConfirm = { withLongRunningApi(60_000) { @@ -500,6 +530,8 @@ fun ModalData.GroupChatInfoLayout( clearChat: () -> Unit, leaveGroup: () -> Unit, manageGroupLink: () -> Unit, + manageWebPage: () -> Unit, + setSimplexName: () -> Unit, close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, onSearchClicked: () -> Unit, deletingItems: State<Boolean> @@ -554,7 +586,7 @@ fun ModalData.GroupChatInfoLayout( LocalAliasEditor(chat.id, groupInfo.localAlias, isContact = false, updateValue = onLocalAliasChanged) - SectionSpacer() + SectionDividerSpaced() Box( Modifier.fillMaxWidth(), @@ -583,10 +615,10 @@ fun ModalData.GroupChatInfoLayout( } } - SectionSpacer() + SectionDividerSpaced() if (groupInfo.useRelays && groupInfo.membership.memberIncognito) { - SectionView(generalGetString(MR.strings.incognito).uppercase()) { + SectionView(generalGetString(MR.strings.incognito)) { SectionItemViewSpaceBetween { Text(generalGetString(MR.strings.incognito_random_profile)) Text(groupInfo.membership.chatViewName, color = Indigo) @@ -631,6 +663,18 @@ fun ModalData.GroupChatInfoLayout( if (!groupInfo.isOwner && channelLink != null) { SectionTextFooter(stringResource(MR.strings.you_can_share_channel_link_anybody_will_be_able_to_connect)) } + if (groupInfo.isOwner && groupLink != null) { + SectionDividerSpaced() + val channelDomain = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupDomainClaim?.shortName + SectionView(title = if (channelDomain != null) generalGetString(MR.strings.channel_simplex_name) else null) { + SettingsActionItem( + painterResource(MR.images.ic_tag), + channelDomain ?: generalGetString(MR.strings.get_simplex_name_beta), + setSimplexName, + iconColor = MaterialTheme.colors.secondary + ) + } + } } else { SectionView { if (groupInfo.canAddMembers && groupInfo.businessChat == null) { @@ -660,7 +704,7 @@ fun ModalData.GroupChatInfoLayout( } } if (anyTopSectionRowShow) { - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() } SectionView { if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) { @@ -679,7 +723,7 @@ fun ModalData.GroupChatInfoLayout( else if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs SectionTextFooter(stringResource(footerId)) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() SectionView { if (!groupInfo.useRelays) { @@ -690,7 +734,7 @@ fun ModalData.GroupChatInfoLayout( } } WallpaperButton { - ModalManager.end.showModal { + ModalManager.end.showModal(cardScreen = true) { val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { @@ -699,12 +743,12 @@ fun ModalData.GroupChatInfoLayout( } } ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems) - SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true) + SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer)) + SectionDividerSpaced() if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) { - SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1), cardShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) { if (groupInfo.canAddMembers) { val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary @@ -721,38 +765,42 @@ fun ModalData.GroupChatInfoLayout( } } SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - MemberRow(groupInfo.membership, user = true) + MemberRow(groupInfo.membership, user = true, isChannel = groupInfo.isChannel) } } } } if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) { - items(filteredMembers.value, key = { it.groupMemberId }) { member -> - Divider() - val showMenu = remember { mutableStateOf(false) } - val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator - SectionItemViewLongClickable( - click = { - if (selectedItems.value != null) { - if (canBeSelected) { - toggleItemSelection(member.groupMemberId, selectedItems) + itemsIndexed(filteredMembers.value, key = { _, m -> m.groupMemberId }) { index, member -> + val isLast = index == filteredMembers.value.lastIndex + val shape = if (isLast) RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp) else RectangleShape + Column(Modifier.padding(horizontal = CARD_PADDING).fillMaxWidth().clip(shape).background(sectionCardColor())) { + Divider() + val showMenu = remember { mutableStateOf(false) } + val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator + SectionItemViewLongClickable( + click = { + if (selectedItems.value != null) { + if (canBeSelected) { + toggleItemSelection(member.groupMemberId, selectedItems) + } + } else { + showMemberInfo(member, null) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) + } + val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) + DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) + Box(Modifier.padding(start = selectionOffset)) { + MemberRow(member, isChannel = groupInfo.isChannel) } - } else { - showMemberInfo(member, null) - } - }, - longClick = { showMenu.value = true }, - minHeight = 54.dp, - padding = PaddingValues(horizontal = DEFAULT_PADDING) - ) { - Box(contentAlignment = Alignment.CenterStart) { - androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { - SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) - } - val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) - DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) - Box(Modifier.padding(start = selectionOffset)) { - MemberRow(member) } } } @@ -760,7 +808,7 @@ fun ModalData.GroupChatInfoLayout( } item { if (!groupInfo.nextConnectPrepared && !groupInfo.useRelays) { - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() } SectionView { if (groupInfo.useRelays && (groupInfo.isOwner || activeSortedMembers.any { it.memberRole == GroupMemberRole.Relay })) { @@ -786,6 +834,13 @@ fun ModalData.GroupChatInfoLayout( } } + if (groupInfo.useRelays && groupInfo.isOwner) { + SectionDividerSpaced() + SectionView(title = stringResource(MR.strings.advanced_options)) { + ChannelWebPageButton(groupInfo, manageWebPage) + } + } + if (developerTools) { SectionDividerSpaced() SectionView(title = stringResource(MR.strings.section_title_for_console)) { @@ -924,6 +979,7 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName) ) ChatInfoDescription(cInfo, displayName, copyNameToClipboard) + GroupSimplexNameView(groupInfo) val webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage if (webPage != null) { val uriHandler = LocalUriHandler.current @@ -1036,7 +1092,7 @@ private fun AddMembersButton(titleId: StringResource, tint: Color = MaterialThem } @Composable -fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false) { +fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = true, showlocalAliasAndFullName: Boolean = false, selected: Boolean = false, isChannel: Boolean = false) { @Composable fun MemberInfo() { if (member.blocked) { @@ -1044,7 +1100,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr } else { val role = member.memberRole if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Moderator, GroupMemberRole.Observer)) { - Text(role.text, color = MaterialTheme.colors.secondary) + Text(role.text(isChannel = isChannel), color = MaterialTheme.colors.secondary) } } } @@ -1199,10 +1255,22 @@ private fun ChannelLinkButton(onClick: () -> Unit) { ) } +@Composable +private fun ChannelWebPageButton(groupInfo: GroupInfo, onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_travel_explore), + stringResource(if (groupInfo.isChannel) MR.strings.channel_webpage else MR.strings.group_webpage), + onClick, + iconColor = MaterialTheme.colors.secondary + ) +} + @Composable private fun ChannelLinkQRCodeSection(groupLink: String) { val clipboard = LocalClipboardManager.current - SimpleXLinkQRCode(connReq = groupLink) + Box(Modifier.padding(vertical = DEFAULT_PADDING_HALF)) { + SimpleXLinkQRCode(connReq = groupLink) + } SectionItemView({ clipboard.shareText(simplexChatLink(groupLink)) }) { @@ -1401,7 +1469,9 @@ fun PreviewGroupChatInfoLayout() { clearChat = {}, leaveGroup = {}, manageGroupLink = {}, + manageWebPage = {}, onSearchClicked = {}, + setSimplexName = {}, deletingItems = remember { mutableStateOf(true) } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 673f72bb4e..f6f19e9d72 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -1,7 +1,9 @@ package chat.simplex.common.views.chat.group import SectionBottomSpacer +import SectionDividerSpaced import SectionItemView +import SectionView import SectionViewWithButton import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -215,7 +217,10 @@ fun GroupLinkLayout( } } else { if (!isChannel) { - RoleSelectionRow(groupInfo, groupLinkMemberRole) + SectionView { + RoleSelectionRow(groupInfo, groupLinkMemberRole) + } + SectionDividerSpaced() } var initialLaunch by remember { mutableStateOf(true) } LaunchedEffect(groupLinkMemberRole.value) { @@ -225,69 +230,70 @@ fun GroupLinkLayout( initialLaunch = false } val showShortLink = remember { mutableStateOf(true) } - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) SectionViewWithButton( titleButton = if (!isChannel && groupLink.connLinkContact.connShortLink != null) { { ToggleShortLinkButton(showShortLink) } } else null) { - SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) - } - if (!isChannel && groupLink.shouldBeUpgraded) { + Box(Modifier.padding(vertical = DEFAULT_PADDING_HALF)) { + SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) + } + if (!isChannel && groupLink.shouldBeUpgraded) { + SettingsActionItem( + painterResource(MR.images.ic_add), + stringResource(MR.strings.upgrade_group_link), + click = { showAddShortLinkAlert(null) }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + } + val clipboard = LocalClipboardManager.current SettingsActionItem( - painterResource(MR.images.ic_add), - stringResource(MR.strings.upgrade_group_link), - click = { showAddShortLinkAlert(null) }, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) - } - val clipboard = LocalClipboardManager.current - SettingsActionItem( - painterResource(MR.images.ic_share), - stringResource(MR.strings.share_link), - click = { - if (!isChannel && groupLink.shouldBeUpgraded) { - showAddShortLinkAlert { + painterResource(MR.images.ic_share), + stringResource(MR.strings.share_link), + click = { + if (!isChannel && groupLink.shouldBeUpgraded) { + showAddShortLinkAlert { + clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) + } + } else { clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } - } else { - clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) - } - }, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) - if (shareGroupInfo != null && isChannel) { - SettingsActionItem( - painterResource(MR.images.ic_forward), - stringResource(MR.strings.share_via_chat), - click = { - chatModel.sharedContent.value = SharedContent.ChatLink(shareGroupInfo) - chatModel.chatId.value = null - ModalManager.closeAllModalsEverywhere() }, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, ) - } - if (!creatingGroup && !isChannel) { - SettingsActionItem( - painterResource(MR.images.ic_delete), - stringResource(MR.strings.delete_link), - click = deleteLink, - iconColor = Color.Red, - textColor = Color.Red, - ) - } - if (creatingGroup && close != null) { - SettingsActionItem( - painterResource(MR.images.ic_check), - stringResource(MR.strings.continue_to_next_step), - click = close, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) + if (shareGroupInfo != null && isChannel) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + click = { + chatModel.sharedContent.value = SharedContent.ChatLink(shareGroupInfo) + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + }, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + } + if (!creatingGroup && !isChannel) { + SettingsActionItem( + painterResource(MR.images.ic_delete), + stringResource(MR.strings.delete_link), + click = deleteLink, + iconColor = Color.Red, + textColor = Color.Red, + ) + } + if (creatingGroup && close != null) { + SettingsActionItem( + painterResource(MR.images.ic_check), + stringResource(MR.strings.continue_to_next_step), + click = close, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) + } } } } @@ -302,7 +308,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text } + val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text(isChannel = groupInfo.isChannel) } ExposedDropDownSettingRow( generalGetString(MR.strings.initial_member_role), values, 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 fe45be92b7..3e5f64659b 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 @@ -2,12 +2,12 @@ package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView -import SectionSpacer +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.InlineTextContent @@ -214,11 +214,12 @@ fun GroupMemberInfoView( verify = { code -> chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r -> val (verified, existingCode) = r - val copy = mem.copy( - activeConn = mem.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null - ) - ) + val code = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + val copy = if (groupInfo.useRelays) { + mem.copy(memberVerifiedCode = code) + } else { + mem.copy(activeConn = mem.activeConn?.copy(connectionCode = code)) + } withContext(Dispatchers.Main) { chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy) } @@ -229,6 +230,7 @@ fun GroupMemberInfoView( } }, close, + verifyDescription = if (groupInfo.useRelays) MR.strings.to_verify_channel_member_key else MR.strings.to_verify_compare, ) } } @@ -421,10 +423,9 @@ fun GroupMemberInfoLayout( @Composable fun ModeratorDestructiveSection() { val canBlockForAll = member.canBlockForAll(groupInfo) - // TODO [relays] re-enable when relay management ships - val canRemove = member.canBeRemoved(groupInfo) && member.memberRole != GroupMemberRole.Relay + val canRemove = member.canBeRemoved(groupInfo) if (canBlockForAll || canRemove) { - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { if (canBlockForAll) { if (member.blockedByAdmin) { @@ -446,7 +447,7 @@ fun GroupMemberInfoLayout( @Composable fun NonAdminBlockSection() { - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { if (member.blockedByAdmin) { SettingsActionItem( @@ -470,7 +471,7 @@ fun GroupMemberInfoLayout( ) { GroupMemberInfoHeader(member) } - SectionSpacer() + SectionDividerSpaced() val contactId = member.memberContactId @@ -534,7 +535,7 @@ fun GroupMemberInfoLayout( } } - SectionSpacer() + SectionDividerSpaced() } val showMemberSupportChat = !openedFromSupportChat && @@ -542,16 +543,19 @@ fun GroupMemberInfoLayout( member.memberRole != GroupMemberRole.Relay && ((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator) || member.supportChat != null) + val canVerifyCode = connectionCode != null && member.memberRole != GroupMemberRole.Relay + val canSyncConn = cStats != null && cStats.ratchetSyncAllowed - if (member.memberActive) { + if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent)) + && (showMemberSupportChat || canVerifyCode || canSyncConn)) { SectionView { if (showMemberSupportChat) { SupportChatButton() } - if (connectionCode != null && !(groupInfo.useRelays && member.memberRole == GroupMemberRole.Relay)) { + if (canVerifyCode) { VerifyCodeButton(member.verified, verifyClicked) } - if (cStats != null && cStats.ratchetSyncAllowed) { + if (canSyncConn) { SynchronizeConnectionButton(syncMemberConnection) } // } else if (developerTools) { @@ -559,15 +563,10 @@ fun GroupMemberInfoLayout( // } } SectionDividerSpaced() - } else if (groupInfo.useRelays && member.memberCurrent && showMemberSupportChat) { - SectionView { - SupportChatButton() - } - SectionDividerSpaced() } if (member.contactLink != null) { - SectionView(stringResource(MR.strings.address_section_title).uppercase()) { + SectionView(stringResource(MR.strings.address_section_title)) { SimpleXLinkQRCode(member.contactLink) val clipboard = LocalClipboardManager.current ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) } @@ -578,8 +577,8 @@ fun GroupMemberInfoLayout( } else { ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) }) } - SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(member.displayName)) } + SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(member.displayName)) SectionDividerSpaced() } @@ -597,15 +596,11 @@ fun GroupMemberInfoLayout( else if (groupInfo.businessChat == null) MR.strings.info_row_group else MR.strings.info_row_chat InfoRow(stringResource(titleId), groupInfo.displayName) - if (!groupInfo.useRelays) { - val roles = remember { member.canChangeRoleTo(groupInfo) } - if (roles != null) { - RoleSelectionRow(roles, newRole, onRoleSelected) - } else { - InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text) - } + val roles = remember { member.canChangeRoleTo(groupInfo) } + if (roles != null) { + RoleSelectionRow(roles, newRole, onRoleSelected, groupInfo.isChannel) } else { - InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text) + InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text(isChannel = groupInfo.isChannel)) } val relayLink = member.relayLink if (relayLink != null) { @@ -889,14 +884,15 @@ fun ConnectViaAddressButton(onClick: () -> Unit) { private fun RoleSelectionRow( roles: List<GroupMemberRole>, selectedRole: MutableState<GroupMemberRole>, - onSelected: (GroupMemberRole) -> Unit + onSelected: (GroupMemberRole) -> Unit, + isChannel: Boolean ) { Row( Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - val values = remember { roles.map { it to it.text } } + val values = remember { roles.map { it to it.text(isChannel = isChannel) } } ExposedDropDownSettingRow( generalGetString(MR.strings.change_role), values, @@ -957,12 +953,14 @@ fun updateMemberRoleDialog( AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), text = if (memberCurrent) { - if (groupInfo.businessChat == null) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + if (groupInfo.isChannel) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_channel), newRole.text(isChannel = groupInfo.isChannel)) + else if (groupInfo.businessChat == null) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel)) else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel)) } else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text), + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_invitation), newRole.text(isChannel = groupInfo.isChannel)), confirmText = generalGetString(MR.strings.change_verb), onDismiss = onDismiss, onConfirm = onConfirm, @@ -978,9 +976,9 @@ fun updateMembersRoleDialog( AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.change_member_role_question), text = if (groupInfo.businessChat == null) - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text) + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification), newRole.text(isChannel = groupInfo.isChannel)) else - String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text), + String.format(generalGetString(MR.strings.member_role_will_be_changed_with_notification_chat), newRole.text(isChannel = groupInfo.isChannel)), confirmText = generalGetString(MR.strings.change_verb), onConfirm = onConfirm, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt index aa737a02d3..2a5f9df8c1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt @@ -102,7 +102,6 @@ fun GroupMentions( } fun messageChanged(msg: ComposeMessage, parsedMsg: List<FormattedText>) { - removeUnusedMentions(composeState, parsedMsg) val selected = selectedMarkdown(parsedMsg, msg.selection) if (selected != null) { @@ -113,7 +112,7 @@ fun GroupMentions( isVisible.value = true mentionName.value = ft.format.memberName mentionRange.value = r - mentionMemberId.value = composeState.value.mentions[mentionName.value]?.memberId + mentionMemberId.value = if (mentionName.value in composeState.value.memberMentions) composeState.value.mentions[mentionName.value]?.memberId else null if (!chatModel.membersLoaded.value) { scope.launch { setGroupMembers(rhId, chatInfo.groupInfo, chatModel) @@ -211,7 +210,7 @@ fun GroupMentions( }, contentAlignment = Alignment.BottomStart ) { - val showMaxReachedBox = composeState.value.mentions.size >= MAX_NUMBER_OF_MENTIONS && isVisible.value && composeState.value.mentions[mentionName.value] == null + val showMaxReachedBox = composeState.value.memberMentions.size >= MAX_NUMBER_OF_MENTIONS && isVisible.value && mentionName.value !in composeState.value.memberMentions LazyColumnWithScrollBarNoAppBar( Modifier .heightIn(max = MAX_PICKER_HEIGHT) @@ -229,7 +228,7 @@ fun GroupMentions( Divider() } val mentioned = mentionMemberId.value == member.memberId - val disabled = composeState.value.mentions.size >= MAX_NUMBER_OF_MENTIONS && !mentioned + val disabled = composeState.value.memberMentions.size >= MAX_NUMBER_OF_MENTIONS && !mentioned Row( Modifier .fillMaxWidth() @@ -309,18 +308,3 @@ private fun selectedMarkdown( parsedMsg[i] to TextRange(pos, pos + parsedMsg[i].text.length) } } - -private fun removeUnusedMentions(composeState: MutableState<ComposeState>, parsedMsg: List<FormattedText>) { - val usedMentions = parsedMsg.mapNotNull { ft -> - when (ft.format) { - is Format.Mention -> ft.format.memberName - else -> null - } - }.toSet() - - if (usedMentions.size < composeState.value.mentions.size) { - composeState.value = composeState.value.copy( - mentions = composeState.value.mentions.filterKeys { it in usedMentions } - ) - } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 740349eaea..f04d63e2d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -6,9 +6,11 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView +import androidx.compose.foundation.background import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.runtime.saveable.rememberSaveable import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.stringResource @@ -64,6 +66,7 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> if (preferences == currentPreferences) close() else showUnsavedChangesAlert({ savePrefs(close) }, close, saveTextId) }, + cardScreen = true, ) { GroupPreferencesLayout( preferences, @@ -175,6 +178,12 @@ private fun GroupPreferencesLayout( } } } + @Composable fun SignMessagesPreference() { + val enableSignMessages = remember(preferences) { mutableStateOf(preferences.signMessages.enable) } + FeatureSection(GroupFeature.SignMessages, enableSignMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ -> + applyPrefs(preferences.copy(signMessages = GroupPreference(enable = enable))) + } + } ColumnWithScrollBar { val titleId = if (groupInfo.useRelays) MR.strings.channel_preferences else if (groupInfo.businessChat == null) MR.strings.group_preferences @@ -182,37 +191,42 @@ private fun GroupPreferencesLayout( AppBarTitle(stringResource(titleId)) if (!groupInfo.useRelays) { if (groupInfo.businessChat == null) { - MemberAdmissionButton(openMemberAdmission) - SectionDividerSpaced(maxBottomPadding = false) + SectionView { + MemberAdmissionButton(openMemberAdmission) + } + SectionDividerSpaced() } TimedMessagesPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() DirectMessagesPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() FullDeletePreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() ReactionsPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() VoicePreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() FilesPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() SimplexLinksPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() ReportsPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() HistoryPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() SupportPreference(disabled = true) } else { + // hidden until message signing is user-facing (recipient-only stage) +// SignMessagesPreference() +// SectionDividerSpaced() TimedMessagesPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() FullDeletePreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() ReactionsPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() HistoryPreference() - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() SupportPreference(notice = generalGetString(MR.strings.chat_with_admins_relay_note), onEnable = { revert -> AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.enable_chats_with_admins_question), @@ -225,7 +239,7 @@ private fun GroupPreferencesLayout( }) } if (groupInfo.isOwner) { - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() val saveTextId = if (groupInfo.useRelays) MR.strings.save_and_notify_channel_subscribers else MR.strings.save_and_notify_group_members ResetSaveButtons( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt index 7c9db58316..544af8ed7e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt @@ -6,7 +6,10 @@ import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView +import androidx.compose.foundation.background import androidx.compose.material.MaterialTheme +import androidx.compose.ui.Modifier +import chat.simplex.common.ui.theme.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -49,6 +52,7 @@ fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> if (admission == currentAdmission) close() else showUnsavedChangesAlert({ saveAdmission(close) }, close) }, + cardScreen = true, ) { MemberAdmissionLayout( admission, @@ -85,7 +89,7 @@ private fun MemberAdmissionLayout( } } if (groupInfo.isOwner) { - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() ResetSaveButtons( reset = reset, save = saveAdmission, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt index 3d3096b4f5..1ac6d6f048 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -79,13 +79,13 @@ fun MemberSupportChatAppBar( withBGApi { val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId) val stats = r?.second - val code = if (scopeMember_.memberActive) { + val code = if ((scopeMember_.memberActive || (groupInfo.useRelays && scopeMember_.memberCurrent)) && scopeMember_.memberRole != GroupMemberRole.Relay) { val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, scopeMember_.groupMemberId) memCode?.second } else { null } - ModalManager.end.showModalCloseable(true) { closeCurrent -> + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem -> GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) { closeCurrent() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index 7ca277df94..b685d20fb9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -150,7 +150,7 @@ private fun ModalData.MemberSupportViewLayout( ) { Box(contentAlignment = Alignment.CenterStart) { DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu) - SupportChatRow(member) + SupportChatRow(member, isChannel = groupInfo.isChannel) } } } @@ -163,7 +163,7 @@ private fun ModalData.MemberSupportViewLayout( } @Composable -fun SupportChatRow(member: GroupMember) { +fun SupportChatRow(member: GroupMember, isChannel: Boolean) { fun memberStatus(): String { return if (member.activeConn?.connStatus is ConnStatus.Failed) { generalGetString(MR.strings.member_info_member_failed) @@ -174,7 +174,7 @@ fun SupportChatRow(member: GroupMember) { } else if (member.memberPending) { member.memberStatus.text } else { - member.memberRole.text + member.memberRole.text(isChannel = isChannel) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 02bee37c24..28afc0132f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -15,7 +15,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -30,7 +32,10 @@ import java.net.URI @Composable fun CIFileView( file: CIFile?, - edited: Boolean, + meta: CIMeta, + chatTTL: Int?, + showViaProxy: Boolean, + showTimestamp: Boolean, showMenu: MutableState<Boolean>, smallView: Boolean = false, senderProfile: LocalProfile?, @@ -202,10 +207,13 @@ fun CIFileView( ) { fileIndicator() if (!smallView) { - val metaReserve = if (edited) - " " - else - " " + val secondaryColor = MaterialTheme.colors.secondary + val encrypted = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null + val metaReserve = buildAnnotatedString { + withStyle(reserveTimestampStyle) { + append(reserveSpaceForMeta(meta, chatTTL, encrypted, secondaryColor = secondaryColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp, signedFileVerified = file?.loaded)) + } + } if (file != null) { Column { Text( @@ -213,8 +221,11 @@ fun CIFileView( maxLines = 1 ) Text( - formatBytes(file.fileSize) + metaReserve, - color = MaterialTheme.colors.secondary, + buildAnnotatedString { + append(formatBytes(file.fileSize)) + append(metaReserve) + }, + color = secondaryColor, fontSize = 14.sp, maxLines = 1 ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index ed9a0e6007..7ce44475b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -12,10 +12,12 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* @@ -27,6 +29,7 @@ import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* +import kotlin.math.roundToInt @Composable fun CIImageView( @@ -176,7 +179,13 @@ fun CIImageView( .then( if (!smallView) { val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH - Modifier.width(w).aspectRatio((previewBitmap.width.toFloat() / previewBitmap.height.toFloat()).coerceAtLeast(1f / 2.33f)) + // Height follows the measured (clamped) width, not nominal w, else wide images get an empty strip below. + Modifier.width(w).layout { measurable, constraints -> + val width = constraints.maxWidth.coerceAtMost(w.roundToPx().coerceAtLeast(0)) + val height = (width * (previewBitmap.height.toFloat() / previewBitmap.width.toFloat()).coerceAtMost(2.33f)).roundToInt().coerceAtMost(constraints.maxHeight) + val placeable = measurable.measure(Constraints.fixed(width, height)) + layout(width, height) { placeable.place(0, 0) } + } } else Modifier ) .desktopModifyBlurredState(!smallView, blurred, showMenu), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index 4ec2a885e7..92f89ccad9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -13,6 +13,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.platform.appPreferences import chat.simplex.common.ui.theme.isInDarkTheme import chat.simplex.res.MR import kotlinx.datetime.Clock @@ -56,7 +57,8 @@ fun CIMetaView( showStatus = showStatus, showEdited = showEdited, showViaProxy = showViaProxy, - showTimestamp = showTimestamp + showTimestamp = showTimestamp, + signedFileVerified = chatItem.file?.loaded ) } } @@ -74,7 +76,10 @@ private fun CIMetaText( showEdited: Boolean = true, showTimestamp: Boolean, showViaProxy: Boolean, + signedFileVerified: Boolean?, ) { + val showSignature = appPreferences.privacyShowSignature.state.value + val showEncryption = appPreferences.privacyShowEncryption.state.value if (showEdited && meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) } @@ -103,10 +108,17 @@ private fun CIMetaText( StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) } } - if (encrypted != null) { + if (encrypted != null && showEncryption) { Spacer(Modifier.width(4.dp)) StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) } + if (showSignature && meta.msgVerified?.verified == true && signedFileVerified != false) { + Spacer(Modifier.width(4.dp)) + StatusIconText(painterResource(MR.images.ic_verified), color) + } else if (meta.msgVerified is MsgVerified.SigMissing) { + Spacer(Modifier.width(4.dp)) + StatusIconText(painterResource(MR.images.ic_verified_missing), Color.Red) + } if (showTimestamp) { Spacer(Modifier.width(4.dp)) @@ -123,8 +135,11 @@ fun reserveSpaceForMeta( showStatus: Boolean = true, showEdited: Boolean = true, showViaProxy: Boolean = false, - showTimestamp: Boolean + showTimestamp: Boolean, + signedFileVerified: Boolean? = null ): String { + val showSignature = appPreferences.privacyShowSignature.state.value + val showEncryption = appPreferences.privacyShowEncryption.state.value val iconSpace = " \u00A0\u00A0\u00A0" val whiteSpace = "\u00A0" var res = if (showTimestamp) "" else iconSpace @@ -162,7 +177,12 @@ fun reserveSpaceForMeta( space = whiteSpace } - if (encrypted != null) { + if (encrypted != null && showEncryption) { + appendSpace() + res += iconSpace + space = whiteSpace + } + if ((showSignature && meta.msgVerified?.verified == true && signedFileVerified != false) || meta.msgVerified is MsgVerified.SigMissing) { appendSpace() res += iconSpace space = whiteSpace diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt index f8dfba4c6c..8ca0add460 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt @@ -25,6 +25,7 @@ import chat.simplex.common.views.chat.chatViewScrollState import dev.icerock.moko.resources.StringResource import java.io.File import java.net.URI +import kotlin.math.roundToInt @Composable fun CIVideoView( @@ -38,12 +39,25 @@ fun CIVideoView( receiveFile: (Long) -> Unit ) { val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } + val preview = remember(image) { base64ToBitmap(image) } Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .then( + if (!smallView) { + val w = if (preview.width * 0.97 <= preview.height) videoViewFullWidth(LocalWindowWidth()) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH + // Size the media box from the preview aspect ratio (as CIImageView does), else the unprepared player surface + // expands to PriorityLayout's max height and shows as a black strip; height tracks the clamped width (#7223). + Modifier.width(w).layout { measurable, constraints -> + val width = constraints.maxWidth.coerceAtMost(w.roundToPx().coerceAtLeast(0)) + val height = (width * (preview.height.toFloat() / preview.width.toFloat()).coerceAtMost(2.33f)).roundToInt().coerceAtMost(constraints.maxHeight) + val placeable = measurable.measure(Constraints.fixed(width, height)) + layout(width, height) { placeable.place(0, 0) } + } + } else Modifier + ) .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { - val preview = remember(image) { base64ToBitmap(image) } val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } val sizeMultiplier = if (smallView) 0.38f else 1f if (chatModel.connectedToRemote()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 2c04911e39..bab6576646 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -12,8 +12,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* @@ -125,7 +127,7 @@ fun ChatItemView( modifier = (if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier), contentAlignment = alignment, ) { - val info = cItem.meta.itemStatus.statusInto + val info = cItem.meta.itemStatus.statusInto ?: cItem.meta.msgVerified?.sigMissingInfo val onClick = if (info != null) { { AlertManager.shared.showAlertMsg( @@ -1224,12 +1226,25 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed) val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f) - val shape = when (style) { - is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) - is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness) + return when (style) { + is ShapeStyle.Bubble -> { + // Modifier.clip of the bubble GenericShape mis-hit-tests its path on very tall + // items, dropping long-press on the lower part of the bubble (issue #6991). Clip + // in the draw pass instead — drawing is clipped identically (the press ripple + // included), with no effect on hit-test. + val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) + this.drawWithCache { + val path = Path().apply { + addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache)) + } + onDrawWithContent { + clipPath(path) { this@onDrawWithContent.drawContent() } + } + } + } + // RoundRect hit-tests correctly — no bug here, keep the antialiased Modifier.clip. + is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness)) } - - return this.clip(shape) } private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 5c07fe3abf..cbd15aca67 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -201,7 +201,7 @@ fun FramedItemView( @Composable fun ciFileView(ci: ChatItem, text: String) { - CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile) + CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile) if (text != "" || ci.meta.isLive) { CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } @@ -306,7 +306,11 @@ fun FramedItemView( horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(ci.content.text) - Text("") + Text( + reserveSpaceForMeta(ci.meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp), + color = Color.Transparent, + style = MaterialTheme.typography.body1 + ) } } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index c9f7d96f39..7e66c15937 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.item import SectionItemView import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme @@ -11,8 +12,10 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* +import androidx.compose.ui.layout.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.AnnotatedString.Range @@ -23,6 +26,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.SelectionHighlightColor import chat.simplex.common.views.helpers.* import chat.simplex.res.* @@ -39,6 +43,29 @@ fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolea } } +private fun openMarkdownModal(modal: Format.Modal) { + when (modal.modalName) { + Format.Modal.Description -> showFullProfileDescription(modal.text) + } +} + +private fun showFullProfileDescription(description: String) { + ModalManager.end.showModalCloseable { _ -> + ColumnWithScrollBar { + AppBarTitle(generalGetString(MR.strings.profile_description__field)) + MarkdownText( + description, + parseToMarkdown(description), + toggleSecrets = true, + style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp), + uriHandler = LocalUriHandler.current, + linkMode = chatModel.simplexLinkMode.value, + modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING), + ) + } + } +} + private val noTyping: AnnotatedString = AnnotatedString(" ") private val typingIndicators: List<AnnotatedString> = listOf( @@ -193,6 +220,7 @@ fun MarkdownText ( var hasLinks = false var hasSecrets = false var hasCommands = false + var hasModals = false val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) @@ -302,6 +330,13 @@ fun MarkdownText ( withStyle(ftStyle) { append(ft.text) } } } + is Format.Modal -> { + hasModals = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "MODAL", annotation = i.toString()) { + withStyle(ftStyle) { append(ft.text) } + } + } is Format.Unknown -> append(ft.text) } } @@ -315,7 +350,7 @@ fun MarkdownText ( else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } val clampedRange = selectionRange?.let { it.first .. minOf(it.last, selectableEnd) } - if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { + if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null) || hasModals) { val icon = remember { mutableStateOf(PointerIcon.Text) } ClickableText(annotatedText, style = style, selectionRange = clampedRange, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> @@ -338,13 +373,10 @@ fun MarkdownText ( withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } withAnnotation("SIMPLEX_NAME") { a -> val idx = a.item.toIntOrNull() - val nameInfo = (idx?.let { formattedText.getOrNull(it) }?.format as? Format.SimplexName)?.nameInfo - val (title, msg) = if (nameInfo?.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") + val nameText = idx?.let { formattedText.getOrNull(it) }?.text + // The name string is routed through the same connect path as a + // link; planAndConnect resolves it on the core (name target). + if (nameText != null) uriHandler.openVerifiedSimplexUri(nameText) } } if (hasSecrets) { @@ -356,20 +388,26 @@ fun MarkdownText ( if (hasCommands && sendCommandMsg != null) { withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } } + if (hasModals) { + withAnnotation("MODAL") { a -> + (a.item.toIntOrNull()?.let { formattedText.getOrNull(it)?.format } as? Format.Modal)?.let { openMarkdownModal(it) } + } + } }, onHover = { offset -> val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) } - icon.value = - if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) { - PointerIcon.Hand - } else { - PointerIcon.Text - } + val hand = hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SIMPLEX_NAME") || hasAnnotation("SECRET") || hasAnnotation("COMMAND") || hasAnnotation("MODAL") + icon.value = if (hand) PointerIcon.Hand else PointerIcon.Text + }, + onHoverExit = { + // reset icon.value too, or pointerHoverIcon re-displays a stale Hand on the next Enter + icon.value = PointerIcon.Text }, shouldConsumeEvent = { offset -> annotatedText.hasStringAnnotations(tag = "WEB_URL", start = offset, end = offset) || annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) || annotatedText.hasStringAnnotations(tag = "OTHER_URL", start = offset, end = offset) + || annotatedText.hasStringAnnotations(tag = "MODAL", start = offset, end = offset) }, onTextLayout = { onTextLayoutResult?.invoke(it) } ) @@ -397,34 +435,45 @@ fun ClickableText( onClick: (Int) -> Unit, onLongClick: (Int) -> Unit = {}, onHover: (Int) -> Unit = {}, + onHoverExit: () -> Unit = {}, shouldConsumeEvent: (Int) -> Boolean ) { val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) } - val pressIndicator = Modifier.pointerInput(onClick, onLongClick) { + // pointerInput keyed on these lambdas restarts on every recomposition (they are new + // instances each time), and a restart mid-gesture swallows the click/hover in flight; + // key on Unit and read the latest handlers via rememberUpdatedState instead + val currentOnClick = rememberUpdatedState(onClick) + val currentOnLongClick = rememberUpdatedState(onLongClick) + val currentOnHover = rememberUpdatedState(onHover) + val currentOnHoverExit = rememberUpdatedState(onHoverExit) + val currentShouldConsumeEvent = rememberUpdatedState(shouldConsumeEvent) + // to tell a moved pointer from text that shifted under a stationary pointer (see waitForUpOrCancellation) + val textCoordinates = remember { mutableStateOf<LayoutCoordinates?>(null) } + val pressIndicator = Modifier.pointerInput(Unit) { detectGesture(onLongPress = { pos -> layoutResult.value?.let { layoutResult -> - onLongClick(layoutResult.getOffsetForPosition(pos)) + currentOnLongClick.value(layoutResult.getOffsetForPosition(pos)) } }, onPress = { pos -> layoutResult.value?.let { layoutResult -> val res = tryAwaitRelease() if (res) { - onClick(layoutResult.getOffsetForPosition(pos)) + currentOnClick.value(layoutResult.getOffsetForPosition(pos)) } } - }, shouldConsumeEvent = { pos -> + }, positionInWindow = { textCoordinates.value?.positionInWindow() ?: Offset.Zero }, shouldConsumeEvent = { pos -> var consume = false layoutResult.value?.let { layoutResult -> - consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos)) + consume = currentShouldConsumeEvent.value(layoutResult.getOffsetForPosition(pos)) } consume } ) - }.pointerInput(onHover) { + }.pointerInput(Unit) { if (appPlatform.isDesktop) { - detectCursorMove { pos -> + detectCursorMove(onExit = { currentOnHoverExit.value() }) { pos -> layoutResult.value?.let { layoutResult -> - onHover(layoutResult.getOffsetForPosition(pos)) + currentOnHover.value(layoutResult.getOffsetForPosition(pos)) } } } @@ -432,7 +481,8 @@ fun ClickableText( BasicText( text = text, - modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)).then(pressIndicator), + modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)).then(pressIndicator) + .onGloballyPositioned { textCoordinates.value = it }, style = style, softWrap = softWrap, overflow = overflow, 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 0cec9ab773..ca1528a3ce 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 @@ -254,9 +254,9 @@ suspend fun apiFindMessages(chatsCtx: ChatModel.ChatsContext, ch: Chat, contentT suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope { // groupMembers loading can take a long time and if the user already closed the screen, coroutine may be canceled val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId) - val currentMembers = chatModel.groupMembers.value + val currentMembersById = chatModel.groupMembers.value.associateBy { it.id } val newMembers = groupMembers.map { newMember -> - val currentMember = currentMembers.find { it.id == newMember.id } + val currentMember = currentMembersById[newMember.id] val currentMemberStats = currentMember?.activeConn?.connectionStats val newMemberConn = newMember.activeConn if (currentMemberStats != null && newMemberConn != null && newMemberConn.connectionStats == null) { @@ -583,7 +583,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection onClick = { ModalManager.center.closeModals() ModalManager.end.closeModals() - ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> + ModalManager.center.showModalCloseable(settings = true, showClose = appPlatform.isAndroid, cardScreen = true) { close -> ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, true, close) } showMenu.value = false @@ -772,10 +772,11 @@ fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatMod fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_pending_connection__question), - text = generalGetString( + text = "${connection.displayName}\n\n" + generalGetString( if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect else MR.strings.connection_you_accepted_will_be_cancelled ), + parseHtml = false, confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { withBGApi { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index e9dec64634..68fa25d553 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1,5 +1,6 @@ package chat.simplex.common.views.chatlist +import LocalCardScreen import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -48,6 +49,7 @@ import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds @@ -181,6 +183,8 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate val showWhatsNew = shouldShowWhatsNew(chatModel) val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false if (showWhatsNew || showUpdatedConditions) { + // Requested here, so that the country is known by the time the modal opens + platform.androidLoadPlayStoreCountry() delay(1000L) ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) } } @@ -572,7 +576,7 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState> navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { NavigationButtonMenu { - ModalManager.start.showModalCloseable { close -> + ModalManager.start.showModalCloseable(cardScreen = true) { close -> SettingsView(chatModel, setPerformLA, close) } } @@ -744,7 +748,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { } @Composable -private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<String?>) { +private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, searchChatFilteredBySimplexLink: MutableState<Set<String>>, connectNameCandidate: MutableState<String?>) { Box { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { val focusRequester = remember { FocusRequester() } @@ -762,6 +766,8 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState searchText = searchText, enabled = !remember { searchShowingSimplexLink }.value, trailingContent = null, + // the clear button must line up with the filter icon it replaces, so no reduction here + reducedCloseButtonPadding = 0.dp, ) { searchText.value = searchText.value.copy(it) } @@ -790,17 +796,35 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState LaunchedEffect(Unit) { snapshotFlow { searchText.value.text } .distinctUntilChanged() - .collect { - when (val target = strConnectTarget(it.trim())) { - is ConnectTarget.Link -> { - hideKeyboard(view) - searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } - } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) - null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { + .collectLatest { + val target = strConnectTarget(it.trim()) + if (target is ConnectTarget.Link) { + hideKeyboard(view) + searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = emptySet() + connectNameCandidate.value = null + connect(target.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else { + val candidate = nameSearchCandidate(it.trim()) + connectNameCandidate.value = candidate + // clear the previous match immediately so the list falls back to text search during the debounce, + // instead of showing a stale filtered chat while the new search runs + searchChatFilteredBySimplexLink.value = emptySet() + if (candidate != null) { + // resolve the name locally on each keystroke, debounced; collectLatest cancels the in-flight + // search when the next keystroke arrives. A bare name can be a contact or a channel, so search + // both and filter every known chat found; drop the row only when both types are already known. + delay(NAME_SEARCH_DEBOUNCE_MS) + val rhId = chatModel.remoteHostId() + val inProgress = mutableStateOf(false) // background search: no spinner, no error alerts + val targets = if (candidate.startsWith("@") || candidate.startsWith("#")) listOf(candidate) else listOf("@$candidate", "#$candidate") + val ids = targets.mapNotNull { name -> + knownChatId(rhId, chatModel.controller.apiConnectPlan(rhId, name, PlanResolveMode.PRMNever, inProgress = inProgress)) + } + searchChatFilteredBySimplexLink.value = ids.toSet() + if (ids.size == targets.size) connectNameCandidate.value = null + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { if (it.isNotEmpty()) { focusRequester.requestFocus() } else { @@ -812,7 +836,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null + searchChatFilteredBySimplexLink.value = emptySet() } } } @@ -823,13 +847,13 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } -private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, cleanup: (() -> Unit)?) { +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<Set<String>>, cleanup: (() -> Unit)?) { withBGApi { planAndConnect( chatModel.remoteHostId(), link, - filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, - filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, + filterKnownGroup = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, close = null, cleanup = cleanup, ) @@ -852,8 +876,8 @@ enum class ScrollDirection { @Composable fun BoxScope.StatusBarBackground() { if (appPlatform.isAndroid) { - val finalColor = MaterialTheme.colors.background.copy(0.88f) - Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) + val bg = if (LocalCardScreen.current) canvasColorForCurrentTheme() else MaterialTheme.colors.background + Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(bg.copy(0.88f))) } } @@ -916,7 +940,8 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat // which is related to [derivedStateOf]. Using safe alternative instead // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<Set<String>>(emptySet()) } + val connectNameCandidate = remember { mutableStateOf<String?>(null) } val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value) val topPaddingToContent = topPaddingToContent(false) val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent @@ -949,13 +974,23 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat if (oneHandUI.value) { Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { Divider() - TagsView(searchText) - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + // bottom toolbar: search bar below, so on desktop the connect row goes below the tags + TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> + TagsView(searchText) + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + } + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } else { - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - TagsView(searchText) + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink, connectNameCandidate) + // top toolbar: search bar above, so on desktop the connect row goes above the tags + TagsOrConnectByName(searchText, connectNameCandidate) { candidate -> + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + Divider() + TagsView(searchText) + } Divider() } } @@ -1003,6 +1038,105 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat } } +// Default top-level part used to complete a bare name typed in the search field (search field only; +// the message parser and the wire format are unchanged). +private const val DEFAULT_NAME_TLD = "testing" +// Shortest name that offers the button, so it is discoverable but does not flash on short prefixes. +private const val MIN_NAME_LENGTH = 5 +// Wait this long after the last keystroke before the local name search runs. +internal const val NAME_SEARCH_DEBOUNCE_MS = 300L + +private val nameLabelRegex = Regex("[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*") +private fun isNameLabel(s: String): Boolean = s.length in 1..63 && nameLabelRegex.matches(s) + +// On-device candidate for connecting by SimpleX name: the string sent to the core to resolve it. +// Mirrors the domain grammar (nameLabelP/mkDomain in SimplexName.hs): an optional @/# prefix, then +// dot-separated ASCII labels; a dotless word is completed with the default top-level part. Returns +// the string to send (keeping @/# so the type is preserved), or null when the text is not a name. +internal fun nameSearchCandidate(str: String): String? { + val text = str.trim() + val prefix = text.firstOrNull()?.takeIf { it == '@' || it == '#' } + val core = if (prefix != null) text.substring(1) else text + val labels = core.split(".") + if (core.isEmpty() || labels.any { !isNameLabel(it) }) return null + return when { + labels.size > 1 -> text // already has a top-level part + core.length >= MIN_NAME_LENGTH -> "${prefix ?: ""}$core.$DEFAULT_NAME_TLD" + else -> null + } +} + +// The chat id a local (PRMNever) search resolved to — a contact, a business, or a channel — or null on a miss. +// The core returns the correct type for @ vs # (getContactToConnect / type-filtered getGroupToConnect), so no +// client-side type check is needed. +internal suspend fun knownChatId(rhId: Long?, result: ConnectionPlanResult?): String? = when (val plan = result?.connectionPlan) { + is ConnectionPlan.ContactAddress -> (plan.contactAddressPlan as? ContactAddressPlan.Known)?.contact?.let { contact -> + // a name-resolved chat may be prepared in the store but not yet listed, so add it (as the tap path does) + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } + contact.id + } + is ConnectionPlan.GroupLink -> (when (val g = plan.groupLinkPlan) { + is GroupLinkPlan.Known -> g.groupInfo + is GroupLinkPlan.OwnLink -> g.groupInfo + else -> null + })?.let { gInfo -> + if (chatModel.getGroupChat(gInfo.groupId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(gInfo, groupChatScope = null), chatItems = emptyList())) + } + gInfo.id + } + else -> null +} + +// The list tags and the connect-by-name row share one slot. When there is no name, the tags show; on +// mobile the row replaces the tags while shown. On desktop both show, arranged by the caller (which +// knows whether the search bar is above or below), passed as desktopView. +@Composable +private fun TagsOrConnectByName( + searchText: MutableState<TextFieldValue>, + connectNameCandidate: MutableState<String?>, + desktopView: @Composable (candidate: String) -> Unit, +) { + val candidate = connectNameCandidate.value + when { + candidate == null -> TagsView(searchText) + !appPlatform.isDesktop -> ConnectByNameRow(candidate, searchText, connectNameCandidate, close = null) + else -> desktopView(candidate) + } +} + +@Composable +internal fun ConnectByNameRow(name: String, searchText: MutableState<TextFieldValue>, connectNameCandidate: MutableState<String?>, close: (() -> Unit)?) { + val view = LocalMultiplatformView() + Row( + Modifier + .fillMaxWidth() + .clickable { + hideKeyboard(view) + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + name, + close = close, + cleanup = { + searchText.value = TextFieldValue() + connectNameCandidate.value = null + }, + ) + } + } + .padding(vertical = DEFAULT_PADDING_HALF), + verticalAlignment = Alignment.CenterVertically + ) { + // icon and text aligned with the search bar's icon and text (same paddings and icon size) + val icon = if (name.startsWith("@")) MR.images.ic_at else MR.images.ic_tag + Icon(painterResource(icon), null, Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.primary) + Text(String.format(generalGetString(MR.strings.connect_plan_connect_to_name), name), color = MaterialTheme.colors.primary) + } +} + @Composable private fun NoChatsView(searchText: MutableState<TextFieldValue>) { val activeFilter = remember { chatModel.activeChatTagFilter }.value @@ -1310,14 +1444,14 @@ fun ItemPresetFilterAction( fun filteredChats( searchShowingSimplexLink: State<Boolean>, - searchChatFilteredBySimplexLink: State<String?>, + searchChatFilteredBySimplexLink: State<Set<String>>, searchText: String, chats: List<Chat>, activeFilter: ActiveFilter? = null, ): List<Chat> { - val linkChatId = searchChatFilteredBySimplexLink.value - return if (linkChatId != null) { - chats.filter { it.id == linkChatId } + val linkChatIds = searchChatFilteredBySimplexLink.value + return if (linkChatIds.isNotEmpty()) { + chats.filter { it.id in linkChatIds } } else { val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() if (s.isEmpty()) 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 2c7e443b4d..fbc4c7e336 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 @@ -342,7 +342,7 @@ fun ChatPreviewView( } } is MsgContent.MCFile -> SmallContentPreviewFile { - CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) { + CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) { val user = chatModel.currentUser.value ?: return@CIFileView withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index ed1c7116e6..1c2f34e88d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -10,6 +10,7 @@ import SectionView import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -151,7 +152,7 @@ enum class PresentedServerType { @Composable private fun ServerSessionsView(sess: ServerSessions) { - SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header).uppercase()) { + SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header)) { InfoRow( generalGetString(MR.strings.servers_info_sessions_connected), numOrDash(sess.ssConnected) @@ -293,7 +294,7 @@ private fun XFTPServersListView(servers: List<XFTPServerSummary>, statsStartedAt @Composable private fun SMPStatsView(stats: AgentSMPServerStatsData, statsStartedAt: Instant, remoteHostInfo: RemoteHostInfo?) { - SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header)) { InfoRow( generalGetString(MR.strings.servers_info_messages_sent), numOrDash(stats._sentDirect + stats._sentViaProxy) @@ -329,7 +330,7 @@ private fun SMPSubscriptionsSection(totals: SMPTotals) { horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) ) { Text( - generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + generalGetString(MR.strings.servers_info_subscriptions_section_header), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp @@ -359,14 +360,14 @@ private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSumma horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) ) { Text( - generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + generalGetString(MR.strings.servers_info_subscriptions_section_header), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp ) SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess) } - Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { + SectionView { InfoRow( generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed), numOrDash(subs.ssActive) @@ -415,7 +416,7 @@ private fun reconnectServerAlert(rh: RemoteHostInfo?, server: String) { @Composable fun XFTPStatsView(stats: AgentXFTPServerStatsData, statsStartedAt: Instant, rh: RemoteHostInfo?) { - SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header)) { InfoRow( generalGetString(MR.strings.servers_info_uploaded), prettySize(stats._uploadsSize) @@ -449,7 +450,7 @@ private fun IndentedInfoRow(title: String, desc: String) { @Composable fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Instant) { - SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header).uppercase()) { + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header)) { InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_total), numOrDash(stats._sentDirect + stats._sentViaProxy)) InfoRowTwoValues(generalGetString(MR.strings.sent_directly), generalGetString(MR.strings.attempts_label), stats._sentDirect, stats._sentDirectAttempts) InfoRowTwoValues(generalGetString(MR.strings.sent_via_proxy), generalGetString(MR.strings.attempts_label), stats._sentViaProxy, stats._sentViaProxyAttempts) @@ -465,7 +466,7 @@ fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Insta SectionDividerSpaced() - SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header).uppercase()) { + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header)) { InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_received_total), numOrDash(stats._recvMsgs)) SectionItemView { Text(generalGetString(MR.strings.servers_info_detailed_statistics_receive_errors), color = MaterialTheme.colors.onBackground) @@ -483,7 +484,7 @@ fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Insta SectionDividerSpaced() - SectionView(generalGetString(MR.strings.connections).uppercase()) { + SectionView(generalGetString(MR.strings.connections)) { InfoRow(generalGetString(MR.strings.created), numOrDash(stats._connCreated)) InfoRow(generalGetString(MR.strings.secured), numOrDash(stats._connSecured)) InfoRow(generalGetString(MR.strings.completed), numOrDash(stats._connCompleted)) @@ -502,7 +503,7 @@ fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Insta @Composable fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Instant) { - SectionView(generalGetString(MR.strings.uploaded_files).uppercase()) { + SectionView(generalGetString(MR.strings.uploaded_files)) { InfoRow(generalGetString(MR.strings.size), prettySize(stats._uploadsSize)) InfoRowTwoValues(generalGetString(MR.strings.chunks_uploaded), generalGetString(MR.strings.attempts_label), stats._uploads, stats._uploadAttempts) InfoRow(generalGetString(MR.strings.upload_errors), numOrDash(stats._uploadErrs)) @@ -510,7 +511,7 @@ fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Ins InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._deleteErrs)) } SectionDividerSpaced() - SectionView(generalGetString(MR.strings.downloaded_files).uppercase()) { + SectionView(generalGetString(MR.strings.downloaded_files)) { InfoRow(generalGetString(MR.strings.size), prettySize(stats._downloadsSize)) InfoRowTwoValues(generalGetString(MR.strings.chunks_downloaded), generalGetString(MR.strings.attempts_label), stats._downloads, stats._downloadAttempts) SectionItemView { @@ -528,7 +529,7 @@ fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Ins @Composable fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { - SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SectionView(generalGetString(MR.strings.server_address)) { SelectionContainer { Text( summary.xftpServer, @@ -539,20 +540,16 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, ) ) } - if (summary.stats != null || summary.sessions != null) { - SectionDividerSpaced() - } + } - if (summary.stats != null) { - XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt) - if (summary.sessions != null) { - SectionDividerSpaced(maxTopPadding = true) - } - } + if (summary.stats != null) { + SectionDividerSpaced() + XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt) + } - if (summary.sessions != null) { - ServerSessionsView(summary.sessions) - } + if (summary.sessions != null) { + SectionDividerSpaced() + ServerSessionsView(summary.sessions) } SectionBottomSpacer() @@ -560,7 +557,7 @@ fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, @Composable fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { - SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SectionView(generalGetString(MR.strings.server_address)) { SelectionContainer { Text( summary.smpServer, @@ -571,27 +568,21 @@ fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, r ) ) } - if (summary.stats != null || summary.subs != null || summary.sessions != null) { - SectionDividerSpaced() - } + } - if (summary.stats != null) { - SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt) - if (summary.subs != null || summary.sessions != null) { - SectionDividerSpaced(maxTopPadding = true) - } - } + if (summary.stats != null) { + SectionDividerSpaced() + SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt) + } - if (summary.subs != null) { - SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh) - if (summary.sessions != null) { - SectionDividerSpaced() - } - } + if (summary.subs != null) { + SectionDividerSpaced() + SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh) + } - if (summary.sessions != null) { - ServerSessionsView(summary.sessions) - } + if (summary.sessions != null) { + SectionDividerSpaced() + ServerSessionsView(summary.sessions) } SectionBottomSpacer() @@ -605,7 +596,8 @@ fun ModalData.SMPServerSummaryView( statsStartedAt: Instant ) { ModalView( - close = close + close = close, + cardScreen = true, ) { ColumnWithScrollBar { val bottomPadding = DEFAULT_PADDING @@ -628,7 +620,8 @@ fun ModalData.DetailedXFTPStatsView( statsStartedAt: Instant ) { ModalView( - close = close + close = close, + cardScreen = true, ) { ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { @@ -652,7 +645,8 @@ fun ModalData.DetailedSMPStatsView( statsStartedAt: Instant ) { ModalView( - close = close + close = close, + cardScreen = true, ) { ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { @@ -676,7 +670,8 @@ fun ModalData.XFTPServerSummaryView( statsStartedAt: Instant ) { ModalView( - close = close + close = close, + cardScreen = true, ) { ColumnWithScrollBar { Box(contentAlignment = Alignment.Center) { @@ -839,7 +834,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta val statsStartedAt = it.statsStartedAt SMPStatsView(totals.stats, statsStartedAt, rh) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() SMPSubscriptionsSection(totals) SectionDividerSpaced() @@ -847,7 +842,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta SMPServersListView( servers = currentlyUsedSMPServers, statsStartedAt = statsStartedAt, - header = generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + header = generalGetString(MR.strings.servers_info_connected_servers_section_header), rh = rh ) SectionDividerSpaced() @@ -857,7 +852,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta SMPServersListView( servers = previouslyUsedSMPServers, statsStartedAt = statsStartedAt, - header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header), rh = rh ) SectionDividerSpaced() @@ -867,11 +862,11 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta SMPServersListView( servers = proxySMPServers, statsStartedAt = statsStartedAt, - header = generalGetString(MR.strings.servers_info_proxied_servers_section_header).uppercase(), + header = generalGetString(MR.strings.servers_info_proxied_servers_section_header), footer = generalGetString(MR.strings.servers_info_proxied_servers_section_footer), rh = rh ) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() } ServerSessionsView(totals.sessions) @@ -888,13 +883,13 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta val previouslyUsedXFTPServers = xftpSummary.previouslyUsedXFTPServers XFTPStatsView(totals.stats, statsStartedAt, rh) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() if (currentlyUsedXFTPServers.isNotEmpty()) { XFTPServersListView( currentlyUsedXFTPServers, statsStartedAt, - generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + generalGetString(MR.strings.servers_info_connected_servers_section_header), rh ) SectionDividerSpaced() @@ -904,7 +899,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta XFTPServersListView( previouslyUsedXFTPServers, statsStartedAt, - generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + generalGetString(MR.strings.servers_info_previously_connected_servers_section_header), rh ) SectionDividerSpaced() @@ -915,7 +910,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta } } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { ReconnectAllServersButton(rh) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 96af5337d0..382d126db7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -51,7 +51,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } - is SharedContent.ChatLink -> { + is SharedContent.ChatLink, is SharedContent.MyAddress -> { hasSimplexLink = true } null -> {} @@ -99,9 +99,10 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { + val sharedContent = remember { chatModel.sharedContent }.value when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward && remember { chatModel.sharedContent }.value !is SharedContent.ChatLink -> { + (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && sharedContent !is SharedContent.Forward && sharedContent !is SharedContent.ChatLink && sharedContent !is SharedContent.MyAddress -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -127,7 +128,6 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal } } else -> NavigationButtonBack(onButtonClicked = { - val sharedContent = chatModel.sharedContent.value // Drop shared content chatModel.sharedContent.value = null if (sharedContent is SharedContent.Forward) { @@ -150,6 +150,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal is SharedContent.File -> stringResource(MR.strings.share_file) is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message) is SharedContent.ChatLink -> stringResource(MR.strings.share_channel) + is SharedContent.MyAddress -> stringResource(MR.strings.share_address) null -> stringResource(MR.strings.share_message) }, color = MaterialTheme.colors.onBackground, @@ -196,8 +197,8 @@ private fun ShareList( val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !(chatModel.sharedContent.value is SharedContent.ChatLink && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } - filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted) + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready && it.chatInfo.sendMsgEnabled && !((chatModel.sharedContent.value is SharedContent.ChatLink || chatModel.sharedContent.value is SharedContent.MyAddress) && it.chatInfo is ChatInfo.Local) }.sortedByDescending { it.chatInfo is ChatInfo.Local } + filteredChats(mutableStateOf(false), mutableStateOf<Set<String>>(emptySet()), search, sorted) } } val topPaddingToContent = topPaddingToContent(false) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt index c6cc887655..fe61859937 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/TagListView.kt @@ -1,7 +1,6 @@ package chat.simplex.common.views.chatlist import SectionCustomFooter -import SectionDivider import SectionItemView import TextIconSpaced import androidx.compose.animation.core.animateDpAsState @@ -157,7 +156,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } } - SectionDivider() + Divider(Modifier.padding(horizontal = 8.dp)) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 568cdfe574..81a6b31323 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -380,7 +380,7 @@ private fun GlobalSettingsSection( SectionItemView( click = { - ModalManager.start.showModalCloseable { close -> + ModalManager.start.showModalCloseable(cardScreen = true) { close -> SettingsView(chatModel, setPerformLA, close) } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index 1c1c37b7ac..b656b8b8da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -119,7 +119,7 @@ fun DatabaseEncryptionLayout( ChatStoppedView() SectionSpacer() } - SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { + SectionView(if (migration) generalGetString(MR.strings.database_passphrase) else null) { SavePassphraseSetting( useKeychain.value, initialRandomDBPassphrase.value, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index d55d89f26b..80f97d1caf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.foundation.background import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter @@ -43,29 +44,8 @@ fun DatabaseView() { val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } - val chatArchiveFile = remember { mutableStateOf<String?>(null) } val stopped = remember { m.chatRunning }.value == false - val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - val archive = chatArchiveFile.value - if (archive != null && to != null) { - copyFileToFile(File(archive), to) {} - } - // delete no matter the database was exported or canceled the export process - if (archive != null) { - File(archive).delete() - chatArchiveFile.value = null - } - } val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } - val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> - if (to != null) { - importArchiveAlert { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - importArchive(to, appFilesCountAndSize, progressIndicator, false) - } - } - } - } val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) } Box( Modifier.fillMaxSize(), @@ -78,27 +58,10 @@ fun DatabaseView() { useKeychain.value, m.chatDbEncrypted.value, m.controller.appPrefs.storeDBPassphrase.state.value, - m.controller.appPrefs.initialRandomDBPassphrase, - importArchiveLauncher, appFilesCountAndSize, chatItemTTL, user, m.users, - startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, - stopChatAlert = { stopChatAlert(m, progressIndicator) }, - exportArchive = { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) - } - }, - deleteChatAlert = { - deleteChatAlert { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - deleteChat(m, progressIndicator) - true - } - } - }, deleteAppFilesAndMedia = { deleteFilesAndMediaAlert { stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { @@ -119,12 +82,9 @@ fun DatabaseView() { setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize) } }, - disconnectAllHosts = { - val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } - connected.forEachIndexed { index, h -> - controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) - } - } + showDatabaseManagement = { + ModalManager.start.showModal(cardScreen = true) { DatabaseManagementView() } + }, ) if (progressIndicator.value) { Box( @@ -150,27 +110,21 @@ fun DatabaseLayout( useKeyChain: Boolean, chatDbEncrypted: Boolean?, passphraseSaved: Boolean, - initialRandomDBPassphrase: SharedPreference<Boolean>, - importArchiveLauncher: FileChooserLauncher, appFilesCountAndSize: MutableState<Pair<Int, Long>>, chatItemTTL: MutableState<ChatItemTTL>, currentUser: User?, users: List<UserInfo>, - startChat: () -> Unit, - stopChatAlert: () -> Unit, - exportArchive: () -> Unit, - deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, onChatItemTTLSelected: (ChatItemTTL?) -> Unit, - disconnectAllHosts: () -> Unit, + showDatabaseManagement: () -> Unit, ) { val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.your_chat_database)) + AppBarTitle(stringResource(MR.strings.chat_data)) if (!chatModel.desktopNoUserNoRemote) { - SectionView(stringResource(MR.strings.messages_section_title).uppercase()) { + SectionView(stringResource(MR.strings.messages_section_title)) { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) } SectionTextFooter( @@ -184,85 +138,23 @@ fun DatabaseLayout( } } ) - SectionDividerSpaced(maxTopPadding = true) - } - val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } - if (chatModel.localUserCreated.value == true) { - // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: - // - database was stopped after migration and the app relaunched - // - something wrong happened with database operations and the database couldn't be launched when it should - SectionView(stringResource(MR.strings.run_chat_section)) { - if (!toggleEnabled) { - SectionItemView(disconnectAllHosts) { - Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) - } - } - RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) - } - if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() } - SectionView(stringResource(MR.strings.chat_database_section)) { - if (chatModel.localUserCreated.value != true && !toggleEnabled) { - SectionItemView(disconnectAllHosts) { - Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) - } - } + SectionView { val unencrypted = chatDbEncrypted == false SettingsActionItem( if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), - stringResource(MR.strings.database_passphrase), - click = { ModalManager.start.showModal { DatabaseEncryptionView(chatModel, false) } }, + stringResource(MR.strings.database_passphrase_and_export), + click = showDatabaseManagement, iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) - if (appPlatform.isDesktop) { - SettingsActionItem( - painterResource(MR.images.ic_folder_open), - stringResource(MR.strings.open_database_folder), - ::desktopOpenDatabaseDir, - disabled = operationsDisabled - ) - } - SettingsActionItem( - painterResource(MR.images.ic_ios_share), - stringResource(MR.strings.export_database), - click = { - if (initialRandomDBPassphrase.get()) { - exportProhibitedAlert() - ModalManager.start.showModal { - DatabaseEncryptionView(chatModel, false) - } - } else { - exportArchive() - } - }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - disabled = operationsDisabled - ) - SettingsActionItem( - painterResource(MR.images.ic_download), - stringResource(MR.strings.import_database), - { withLongRunningApi { importArchiveLauncher.launch("application/zip") } }, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) - SettingsActionItem( - painterResource(MR.images.ic_delete_forever), - stringResource(MR.strings.delete_database), - deleteChatAlert, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) } SectionDividerSpaced() - SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) { + SectionView(stringResource(MR.strings.files_and_media_section)) { val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 SectionItemView( deleteAppFilesAndMedia, @@ -286,6 +178,155 @@ fun DatabaseLayout( } } +@Composable +fun DatabaseManagementView() { + val m = chatModel + val progressIndicator = remember { mutableStateOf(false) } + val prefs = m.controller.appPrefs + val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } + val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } + val chatArchiveFile = remember { mutableStateOf<String?>(null) } + val stopped = remember { m.chatRunning }.value == false + val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val archive = chatArchiveFile.value + if (archive != null && to != null) { + copyFileToFile(File(archive), to) {} + } + // delete no matter the database was exported or canceled the export process + if (archive != null) { + File(archive).delete() + chatArchiveFile.value = null + } + } + val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } + val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + importArchiveAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + importArchive(to, appFilesCountAndSize, progressIndicator, false) + } + } + } + } + val operationsDisabled = progressIndicator.value && !m.desktopNoUserNoRemote + + Box(Modifier.fillMaxSize()) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.database_passphrase_and_export)) + + val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } + val disconnectAllHosts = { + val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } + connected.forEachIndexed { index, h -> + controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) + } + } + SectionView(stringResource(MR.strings.chat_database_section)) { + if (chatModel.localUserCreated.value != true && !toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + val unencrypted = m.chatDbEncrypted.value == false + SettingsActionItem( + if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeychain.value) painterResource(MR.images.ic_vpn_key_filled) + else painterResource(MR.images.ic_lock), + stringResource(MR.strings.database_passphrase), + click = { ModalManager.start.showModal(cardScreen = true) { DatabaseEncryptionView(chatModel, false) } }, + iconColor = if (unencrypted || (appPlatform.isDesktop && prefs.storeDBPassphrase.state.value)) WarningOrange else MaterialTheme.colors.secondary, + disabled = operationsDisabled + ) + if (appPlatform.isDesktop) { + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir, + disabled = operationsDisabled + ) + } + SettingsActionItem( + painterResource(MR.images.ic_ios_share), + stringResource(MR.strings.export_database), + click = { + if (prefs.initialRandomDBPassphrase.get()) { + exportProhibitedAlert() + ModalManager.start.showModal { + DatabaseEncryptionView(chatModel, false) + } + } else { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) + } + } + }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + disabled = operationsDisabled + ) + SettingsActionItem( + painterResource(MR.images.ic_download), + stringResource(MR.strings.import_database), + { withLongRunningApi { importArchiveLauncher.launch("application/zip") } }, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + SettingsActionItem( + painterResource(MR.images.ic_delete_forever), + stringResource(MR.strings.delete_database), + { + deleteChatAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteChat(m, progressIndicator) + true + } + } + }, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + } + + if (chatModel.localUserCreated.value == true) { + SectionDividerSpaced() + // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: + // - database was stopped after migration and the app relaunched + // - something wrong happened with database operations and the database couldn't be launched when it should + SectionView(stringResource(MR.strings.run_chat_section)) { + if (!toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + RunChatSetting( + stopped, + toggleEnabled && !progressIndicator.value, + startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, + stopChatAlert = { stopChatAlert(m, progressIndicator) } + ) + } + if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) + } + SectionBottomSpacer() + } + if (progressIndicator.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) + } + } + } +} + private fun setChatItemTTLAlert( m: ChatModel, rhId: Long?, selectedChatItemTTL: MutableState<ChatItemTTL>, progressIndicator: MutableState<Boolean>, @@ -831,19 +872,13 @@ fun PreviewDatabaseLayout() { useKeyChain = false, chatDbEncrypted = false, passphraseSaved = false, - initialRandomDBPassphrase = SharedPreference({ true }, {}), - importArchiveLauncher = rememberFileChooserLauncher(true) {}, appFilesCountAndSize = remember { mutableStateOf(0 to 0L) }, chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) }, currentUser = User.sampleData, users = listOf(UserInfo.sampleData), - startChat = {}, - stopChatAlert = {}, - exportArchive = {}, - deleteChatAlert = {}, deleteAppFilesAndMedia = {}, onChatItemTTLSelected = {}, - disconnectAllHosts = {}, + showDatabaseManagement = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index c855259ffb..f70e4d0048 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -78,6 +78,8 @@ class AlertManager { onDismissRequest: (() -> Unit)? = null, hostDevice: Pair<Long?, String>? = null, belowTextContent: @Composable (() -> Unit) = {}, + // When false, [text] is rendered as literal text — use for user-controlled content. + parseHtml: Boolean = true, buttons: @Composable () -> Unit, ) { showAlert { @@ -85,8 +87,14 @@ class AlertManager { onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { - buttons() + if (parseHtml) { + AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { + buttons() + } + } else { + AlertContent(text?.let { AnnotatedString(it) }, hostDevice, extraPadding = true) { + buttons() + } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -125,13 +133,15 @@ class AlertManager { onDismissRequest: (() -> Unit)? = null, destructive: Boolean = false, hostDevice: Pair<Long?, String>? = null, + // When false, [text] is rendered as literal text — use for user-controlled content. + parseHtml: Boolean = true, ) { showAlert { AlertDialog( onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, true) { + val buttonRow: @Composable () -> Unit = { Row( Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.SpaceBetween @@ -152,6 +162,11 @@ class AlertManager { }, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } } + if (parseHtml) { + AlertContent(text, hostDevice, true, content = buttonRow) + } else { + AlertContent(text?.let { AnnotatedString(it) }, hostDevice, true, content = buttonRow) + } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) @@ -276,10 +291,13 @@ class AlertManager { profileFullName: String, profileImage: @Composable () -> Unit, profileBadge: LocalBadge? = null, + nameCaption: String? = null, subtitle: String? = null, information: String? = null, confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat), onConfirm: (() -> Unit)? = null, + connectOtherButton: String? = null, + onConnectOther: (() -> Unit)? = null, dismissText: String = generalGetString(MR.strings.cancel_verb), onDismiss: (() -> Unit)? = null, ) { @@ -322,6 +340,17 @@ class AlertManager { modifier = Modifier.fillMaxWidth() ) + if (nameCaption != null) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text( + nameCaption, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + color = MaterialTheme.colors.secondary, + maxLines = 1, + modifier = Modifier.fillMaxWidth() + ) + } if (profileFullName.isNotEmpty() && profileFullName != profileName) { Spacer(Modifier.height(DEFAULT_PADDING_HALF)) Text( @@ -373,6 +402,14 @@ class AlertManager { Text(confirmText) } } + if (connectOtherButton != null && onConnectOther != null) { + TextButton(onClick = { + onConnectOther.invoke() + hideAlert() + }) { + Text(connectOtherButton) + } + } TextButton(onClick = { onDismiss?.invoke() hideAlert() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt index ee63846657..cf2ceaf2d6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AppBarTitle.kt @@ -31,7 +31,8 @@ fun AppBarTitle( val connection = if (enableAlphaChanges) handler?.connection else null LaunchedEffect(title) { if (enableAlphaChanges) { - handler?.title?.value = title + // the app bar shows a single line, so the line breaks of the large title are replaced with spaces + handler?.title?.value = title.replace("\n", " ") } else { handler?.connection?.scrollTrackingEnabled = false } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index cf3281f776..e1aa1f98dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -16,6 +16,7 @@ sealed class SharedContent { data class File(val text: String, val uri: URI): SharedContent() data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent() data class ChatLink(val groupInfo: GroupInfo): SharedContent() + object MyAddress: SharedContent() } enum class AnimatedViewState { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt index 9252e8b032..d55f894491 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt @@ -46,6 +46,7 @@ private val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { } suspend fun PointerInputScope.detectGesture( onLongPress: ((Offset) -> Unit)? = null, onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture, + positionInWindow: (() -> Offset)? = null, shouldConsumeEvent: (Offset) -> Boolean ) = coroutineScope { val pressScope = PressGestureScopeImpl(this@detectGesture) @@ -57,8 +58,14 @@ suspend fun PointerInputScope.detectGesture( val shouldConsume = shouldConsumeEvent(down.position) if (shouldConsume) down.consumeDownChange() - pressScope.reset() + val downPositionInWindow = positionInWindow?.let { it() + down.position } + // reset() suspends until the previous gesture's press handler released the mutex, + // and release/cancel join resetJob, so flag writes can't be reordered across gestures + // (a fast second click would otherwise drop the first click's onClick). + // Mirrors detectTapGestures in Compose 1.8.2. + val resetJob = launch { pressScope.reset() } if (onPress !== NoPressGesture) launch { + resetJob.join() pressScope.onPress(down.position) } val longPressTimeout = onLongPress?.let { @@ -67,37 +74,45 @@ suspend fun PointerInputScope.detectGesture( try { val upOrCancel: PointerInputChange? = withTimeout(longPressTimeout) { - waitForUpOrCancellation() + waitForUpOrCancellation(downPositionInWindow, positionInWindow) } if (upOrCancel == null) { - pressScope.cancel() + launch { resetJob.join(); pressScope.cancel() } } else { if (shouldConsume) upOrCancel.consumeDownChange() - pressScope.release() + launch { resetJob.join(); pressScope.release() } } } catch (_: PointerEventTimeoutCancellationException) { if (onLongPress != null) { onLongPress(down.position) if (shouldConsume) consumeUntilUp() - pressScope.cancel() + launch { resetJob.join(); pressScope.cancel() } } else { if (shouldConsume) consumeUntilUp() - pressScope.release() + launch { resetJob.join(); pressScope.release() } } } } } } -suspend fun PointerInputScope.detectCursorMove(onMove: (Offset) -> Unit = {},) = coroutineScope { - forEachGesture { - awaitPointerEventScope { +suspend fun PointerInputScope.detectCursorMove(onExit: () -> Unit = {}, onMove: (Offset) -> Unit = {},) { + // One scope for all events: re-entering awaitPointerEventScope per event loses events. + // Enter/Release update hover when the pointer is stationary (content moved under it, or a click completed). + awaitPointerEventScope { + while (true) { val event = awaitPointerEvent() - if (event.type == PointerEventType.Move) { - onMove(event.changes[0].position) + if (event.type == PointerEventType.Move || event.type == PointerEventType.Enter || event.type == PointerEventType.Release) { + val pos = event.changes[0].position + // ignore events while a button is down or outside bounds: a pressed node receives them even after the pointer leaves it + if (event.changes.none { it.pressed } && pos.x >= 0 && pos.y >= 0 && pos.x < size.width && pos.y < size.height) { + onMove(pos) + } + } else if (event.type == PointerEventType.Exit) { + onExit() } } } @@ -130,7 +145,16 @@ internal suspend fun AwaitPointerEventScope.awaitFirstDownOnPass( return event.changes[0] } -suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? { +suspend fun AwaitPointerEventScope.waitForUpOrCancellation( + downPositionInWindow: Offset? = null, + positionInWindow: (() -> Offset)? = null +): PointerInputChange? { + // out of bounds in local coordinates while stationary in window coordinates is the node + // moving under the pointer (chat list shifted after a sent message), not the pointer + // leaving the node — such a press stays valid + fun stationaryInWindow(change: PointerInputChange): Boolean = + downPositionInWindow != null && positionInWindow != null && + (positionInWindow() + change.position - downPositionInWindow).getDistance() <= viewConfiguration.touchSlop while (true) { val event = awaitPointerEvent(PointerEventPass.Main) if (event.changes.all { it.changedToUp() }) { @@ -138,7 +162,7 @@ suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange } if (event.changes.any { - it.consumed.downChange || it.isOutOfBounds(size, extendedTouchPadding) + it.consumed.downChange || (it.isOutOfBounds(size, extendedTouchPadding) && !stationaryInWindow(it)) } ) { return null @@ -159,16 +183,18 @@ private class PressGestureScopeImpl( fun cancel() { isCanceled = true - mutex.unlock() + if (mutex.isLocked) mutex.unlock() } fun release() { isReleased = true - mutex.unlock() + if (mutex.isLocked) mutex.unlock() } - fun reset() { - mutex.tryLock() + // suspends until the previous gesture's tryAwaitRelease finished (or was never started): + // the mutex is the serialization token between consecutive gestures + suspend fun reset() { + mutex.lock() isReleased = false isCanceled = false } @@ -176,6 +202,7 @@ private class PressGestureScopeImpl( override suspend fun tryAwaitRelease(): Boolean { if (!isReleased && !isCanceled) { mutex.lock() + mutex.unlock() } return isReleased && !isCanceled } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 28c81fbf56..02c0b45de4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import LocalCardScreen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.StatusBarBackground import chat.simplex.common.views.onboarding.OnboardingStage @@ -27,6 +28,7 @@ fun ModalView( showAppBar: Boolean = true, enableClose: Boolean = true, background: Color = Color.Unspecified, + cardScreen: Boolean = false, modifier: Modifier = Modifier, showSearch: Boolean = false, searchAlwaysVisible: Boolean = false, @@ -40,7 +42,9 @@ fun ModalView( } val oneHandUI = remember { derivedStateOf { if (appPrefs.onboardingStage.state.value == OnboardingStage.OnboardingComplete) appPrefs.oneHandUI.state.value else false } } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { - Box(if (background != Color.Unspecified) Modifier.background(background) else Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer)) { + val bgOverride = if (cardScreen) canvasColorForCurrentTheme() else if (background != Color.Unspecified) background else null + CompositionLocalProvider(LocalCardScreen provides cardScreen) { + Box(Modifier.themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer, overrideColor = bgOverride)) { Box(modifier = modifier) { content() } @@ -66,6 +70,7 @@ fun ModalView( } } } + } } } @@ -111,15 +116,15 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun isLastModalOpen(id: ModalViewId): Boolean = modalViews.lastOrNull()?.id == id - fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { + fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, forceAnimated: Boolean = false, cardScreen: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { showCustomModal(id = id, forceAnimated = forceAnimated) { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) + ModalView(close, showClose = showClose, cardScreen = cardScreen, endButtons = endButtons, content = { content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, cardScreen: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { showCustomModal(id = id) { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) }) + ModalView(close, showClose = showClose, cardScreen = cardScreen, endButtons = endButtons, content = { content(close) }) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index a122ddd885..cc6b1c40a4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -36,7 +36,7 @@ fun SearchTextField( placeholder: String = stringResource(MR.strings.search_verb), enabled: Boolean = true, trailingContent: @Composable (() -> Unit)? = null, - reducedCloseButtonPadding: Dp = 0.dp, + reducedCloseButtonPadding: Dp = 8.dp, onValueChange: (String) -> Unit ) { val focusRequester = remember { FocusRequester() } @@ -116,7 +116,7 @@ fun SearchTextField( trailingIcon = if (searchText.value.text.isNotEmpty() || trailingContent != null) {{ Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.offset(x = 8.dp) + modifier = Modifier.offset(x = reducedCloseButtonPadding) ) { if (searchText.value.text.isNotEmpty()) { IconButton({ 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 7ee52af784..5196a144b1 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 @@ -1,9 +1,16 @@ import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.Layout import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity @@ -12,6 +19,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* +import androidx.compose.ui.text.font.FontWeight import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.windowWidth import chat.simplex.common.ui.theme.* @@ -20,16 +28,82 @@ import chat.simplex.common.views.onboarding.SelectableCard import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR +val SectionCardShape = RoundedCornerShape(16.dp) +val CARD_PADDING = 18.dp +val ICON_TEXT_SPACING = 8.dp + +val LocalCardScreen = staticCompositionLocalOf { false } + +val itemHPadding: Dp + @Composable get() = if (LocalCardScreen.current) CARD_PADDING else DEFAULT_PADDING + @Composable -fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) { +private fun CardColumnLayout( + contentPadding: PaddingValues = PaddingValues(), + cardShape: Shape = SectionCardShape, + content: @Composable () -> Unit +) { + val dividerColor = canvasColorForCurrentTheme() + val dividerPx = with(LocalDensity.current) { 2.dp.toPx() } + val childBottoms = remember { mutableListOf<Float>() } + Layout( + content = content, + modifier = Modifier + .padding(horizontal = CARD_PADDING) + .fillMaxWidth() + .clip(cardShape) + .background(sectionCardColor()) + .padding(contentPadding) + .drawBehind { + for (i in 0 until childBottoms.size - 1) { + val y = childBottoms[i] + drawLine(dividerColor, Offset(0f, y), Offset(size.width, y), strokeWidth = dividerPx) + } + } + ) { measurables, constraints -> + val placeables = measurables.map { it.measure(constraints) } + childBottoms.clear() + var y = 0f + placeables.forEach { p -> + y += p.height + childBottoms.add(y) + } + layout(constraints.maxWidth, y.toInt()) { + var yPos = 0 + placeables.forEach { p -> + p.placeRelative(0, yPos) + yPos += p.height + } + } + } +} + +@Composable +private fun CardColumn( + contentPadding: PaddingValues = PaddingValues(), + cardShape: Shape = SectionCardShape, + content: @Composable () -> Unit +) { + if (LocalCardScreen.current) { + CardColumnLayout(contentPadding, cardShape, content) + } else { + Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() } + } +} + +@Composable +fun SectionView(title: String? = null, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, cardShape: Shape = SectionCardShape, content: (@Composable ColumnScope.() -> Unit)) { + val card = LocalCardScreen.current Column { if (title != null) { Text( title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, - modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = headerBottomPadding), fontSize = 12.sp + modifier = Modifier.padding(start = if (card) DEFAULT_PADDING + DEFAULT_PADDING_HALF else DEFAULT_PADDING, bottom = if (card) 8.dp else headerBottomPadding), + fontSize = if (card) 14.sp else 12.sp, + fontWeight = if (card) FontWeight.Medium else FontWeight.Normal ) } - Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() } + CardColumn(contentPadding, cardShape) { content() } } } @@ -40,26 +114,32 @@ fun SectionView( iconTint: Color = MaterialTheme.colors.secondary, leadingIcon: Boolean = false, padding: PaddingValues = PaddingValues(), + onIconClick: (() -> Unit)? = null, content: (@Composable ColumnScope.() -> Unit) ) { + val card = LocalCardScreen.current Column { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } - Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { - if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) - Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp) - if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint) + val interactionSource = remember { MutableInteractionSource() } + val iconClickable = if (onIconClick != null) Modifier.clickable(interactionSource = interactionSource, indication = ripple(bounded = false, radius = iconSize * 0.75f), onClick = onIconClick) else Modifier + Row(Modifier.padding(start = if (card) DEFAULT_PADDING + DEFAULT_PADDING_HALF else DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { + if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint) + Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = if (card) 14.sp else 12.sp, fontWeight = if (card) FontWeight.Medium else FontWeight.Normal) + if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint) } - Column(Modifier.padding(padding).fillMaxWidth()) { content() } + CardColumn(padding) { content() } } } @Composable fun SectionViewWithButton(title: String? = null, titleButton: (@Composable () -> Unit)?, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) { + val card = LocalCardScreen.current Column { if (title != null || titleButton != null) { - Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = headerBottomPadding).fillMaxWidth()) { + val hPadding = if (card) DEFAULT_PADDING + DEFAULT_PADDING_HALF else DEFAULT_PADDING + Row(modifier = Modifier.padding(start = hPadding, end = hPadding, bottom = if (card) 8.dp else headerBottomPadding).fillMaxWidth()) { if (title != null) { - Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp) + Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = if (card) 14.sp else 12.sp, fontWeight = if (card) FontWeight.Medium else FontWeight.Normal) } if (titleButton != null) { Spacer(modifier = Modifier.weight(1f)) @@ -67,7 +147,7 @@ fun SectionViewWithButton(title: String? = null, titleButton: (@Composable () -> } } } - Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() } + CardColumn(contentPadding) { content() } } } @@ -121,9 +201,9 @@ fun SectionItemView( disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = itemHPadding, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else - PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + PaddingValues(horizontal = itemHPadding, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier @@ -144,9 +224,9 @@ fun SectionItemViewWithoutMinPadding( disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = itemHPadding) else - PaddingValues(horizontal = DEFAULT_PADDING), + PaddingValues(horizontal = itemHPadding), content: (@Composable RowScope.() -> Unit) ) { SectionItemView(click, minHeight, disabled, extraPadding, padding, content) @@ -160,9 +240,9 @@ fun SectionItemViewLongClickable( disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = itemHPadding, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else - PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + PaddingValues(horizontal = itemHPadding, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier @@ -185,7 +265,7 @@ fun SectionItemViewSpaceBetween( click: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, - padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), + padding: PaddingValues = PaddingValues(horizontal = itemHPadding), disabled: Boolean = false, content: (@Composable RowScope.() -> Unit) ) { @@ -256,20 +336,19 @@ fun SectionCustomFooter(padding: PaddingValues = PaddingValues(start = DEFAULT_P } } -@Composable -fun SectionDivider() { - Divider(Modifier.padding(horizontal = 8.dp)) -} - @Composable fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boolean = true) { - Divider( - Modifier.padding( - start = DEFAULT_PADDING_HALF, - top = if (maxTopPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp, - end = DEFAULT_PADDING_HALF, - bottom = if (maxBottomPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp) - ) + if (LocalCardScreen.current) { + Spacer(Modifier.height(30.dp)) + } else { + Divider( + Modifier.padding( + start = DEFAULT_PADDING_HALF, + top = if (maxTopPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp, + end = DEFAULT_PADDING_HALF, + bottom = if (maxBottomPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp) + ) + } } @Composable @@ -284,11 +363,11 @@ fun SectionBottomSpacer() { @Composable fun TextIconSpaced(extraPadding: Boolean = false) { - Spacer(Modifier.padding(horizontal = if (extraPadding) 17.dp else DEFAULT_PADDING_HALF)) + Spacer(Modifier.padding(horizontal = if (extraPadding) 17.dp else if (LocalCardScreen.current) ICON_TEXT_SPACING else DEFAULT_PADDING_HALF)) } @Composable -fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING)) { +fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = itemHPadding)) { SectionItemViewSpaceBetween(padding = padding) { Row { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index e8070b5c76..8ae27ca17f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -31,9 +31,11 @@ fun TextEditor( modifier: Modifier, placeholder: String? = null, contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), + shape: Shape = RoundedCornerShape(14.dp), isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null, - enabled: Boolean = true + enabled: Boolean = true, + maxLines: Int = 5 ) { var valid by rememberSaveable { mutableStateOf(true) } var focused by rememberSaveable { mutableStateOf(false) } @@ -53,7 +55,7 @@ fun TextEditor( .fillMaxWidth() .padding(contentPadding) .heightIn(min = 52.dp) - .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(14.dp)), + .border(border = BorderStroke(1.dp, strokeColor), shape = shape), contentAlignment = Alignment.Center, ) { val textFieldModifier = modifier @@ -72,7 +74,7 @@ fun TextEditor( autoCorrect = false ), singleLine = false, - maxLines = 5, + maxLines = maxLines, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = @Composable { innerTextField -> TextFieldDefaults.TextFieldDecorationBox( @@ -102,6 +104,32 @@ fun TextEditor( } } +@Composable +fun PlainTextEditor( + value: MutableState<String>, + placeholder: String? = null, + singleLine: Boolean = true, + contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING, vertical = 12.dp), + focusRequester: FocusRequester? = null +) { + BasicTextField( + value = value.value, + onValueChange = { value.value = it }, + modifier = Modifier.fillMaxWidth() + .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) + .padding(contentPadding), + textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground), + singleLine = singleLine, + cursorBrush = SolidColor(MaterialTheme.colors.secondary), + decorationBox = { innerTextField -> + if (value.value.isEmpty() && placeholder != null) { + Text(placeholder, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + } + innerTextField() + } + ) +} + @Serializable data class ParsedFormattedText( val formattedText: List<FormattedText>? = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt index d7cdf0e2e3..c8c24d918a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -3,8 +3,8 @@ package chat.simplex.common.views.helpers import SectionBottomSpacer import SectionDividerSpaced import SectionItemView -import SectionSpacer import SectionView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme.colors @@ -108,18 +108,22 @@ fun ModalData.UserWallpaperEditor( ) } - WallpaperPresetSelector( - selectedWallpaper = wallpaperType, - baseTheme = currentTheme.base, - currentColors = { type -> - // If applying for : - // - all themes: no overrides needed - // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected - val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null - ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) - }, - onChooseType = onChooseType - ) + SectionView { + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + }, + onChooseType = onChooseType + ) + } + + SectionDividerSpaced() WallpaperSetupView( themeModeOverride.value.type, @@ -133,29 +137,30 @@ fun ModalData.UserWallpaperEditor( onTypeChange = onTypeChange, ) - SectionSpacer() + SectionDividerSpaced() - if (!globalThemeUsed.value) { - ResetToGlobalThemeButton(true) { - themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) - globalThemeUsed.value = true - withBGApi { save(applyToMode.value, null) } - } - } - - SetDefaultThemeButton { - globalThemeUsed.value = false - val lightBase = DefaultTheme.LIGHT - val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX - val mode = themeModeOverride.value.mode - withBGApi { - // Saving for both modes in one place by changing mode once per save - if (applyToMode.value == null) { - val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT - save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + SectionView { + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(true) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) } - themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) - save(themeModeOverride.value.mode, themeModeOverride.value) } } @@ -174,38 +179,40 @@ fun ModalData.UserWallpaperEditor( } } - SectionSpacer() + SectionDividerSpaced() if (showMore) { - val values by remember { mutableStateOf( - listOf( - null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), - DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), - DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + SectionView { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) ) - ) - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.chat_theme_apply_to_mode), - values, - applyToMode, - icon = null, - enabled = remember { mutableStateOf(true) }, - onSelected = { - applyToMode.value = it - if (it != null && it != CurrentColors.value.base.mode) { - val lightBase = DefaultTheme.LIGHT - val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX - ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) - } } - ) + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + } SectionDividerSpaced() AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) { withBGApi { @@ -214,7 +221,9 @@ fun ModalData.UserWallpaperEditor( } } } else { - AdvancedSettingsButton { showMore = true } + SectionView { + AdvancedSettingsButton { showMore = true } + } } SectionBottomSpacer() @@ -329,32 +338,36 @@ fun ModalData.ChatWallpaperEditor( ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } - WallpaperPresetSelector( - selectedWallpaper = currentTheme.wallpaper.type, - activeBackgroundColor = currentTheme.wallpaper.background, - activeTintColor = currentTheme.wallpaper.tint, - baseTheme = CurrentColors.collectAsState().value.base, - currentColors = { type -> currentColors(type) }, - onChooseType = { type -> - when { - type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } - type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> { - withLongRunningApi { importWallpaperLauncher.launch("image/*") } - } - type is WallpaperType.Image -> { - if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) { + SectionView { + WallpaperPresetSelector( + selectedWallpaper = currentTheme.wallpaper.type, + activeBackgroundColor = currentTheme.wallpaper.background, + activeTintColor = currentTheme.wallpaper.tint, + baseTheme = CurrentColors.collectAsState().value.base, + currentColors = { type -> currentColors(type) }, + onChooseType = { type -> + when { + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } + type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> { withLongRunningApi { importWallpaperLauncher.launch("image/*") } } + type is WallpaperType.Image -> { + if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) { + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + } + } + globalThemeUsed.value || themeModeOverride.value.type != type -> { + onTypeCopyFromSameTheme(type) + } + else -> { + onTypeChange(type) + } } - globalThemeUsed.value || themeModeOverride.value.type != type -> { - onTypeCopyFromSameTheme(type) - } - else -> { - onTypeChange(type) - } - } - }, - ) + }, + ) + } + + SectionDividerSpaced() WallpaperSetupView( themeModeOverride.value.type, @@ -368,29 +381,30 @@ fun ModalData.ChatWallpaperEditor( onTypeChange = onTypeChange, ) - SectionSpacer() + SectionDividerSpaced() - if (!globalThemeUsed.value) { - ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) { - themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) - globalThemeUsed.value = true - withBGApi { save(applyToMode.value, null) } - } - } - - SetDefaultThemeButton { - globalThemeUsed.value = false - val lightBase = DefaultTheme.LIGHT - val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX - val mode = themeModeOverride.value.mode - withBGApi { - // Saving for both modes in one place by changing mode once per save - if (applyToMode.value == null) { - val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT - save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + SectionView { + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) } - themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) - save(themeModeOverride.value.mode, themeModeOverride.value) } } @@ -409,38 +423,40 @@ fun ModalData.ChatWallpaperEditor( } } - SectionSpacer() + SectionDividerSpaced() if (showMore) { - val values by remember { mutableStateOf( - listOf( - null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), - DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), - DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + SectionView { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) ) - ) - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.chat_theme_apply_to_mode), - values, - applyToMode, - icon = null, - enabled = remember { mutableStateOf(true) }, - onSelected = { - applyToMode.value = it - if (it != null && it != CurrentColors.value.base.mode) { - val lightBase = DefaultTheme.LIGHT - val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX - ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) - } } - ) + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + } SectionDividerSpaced() AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) { withBGApi { themeModeOverride.value = it @@ -448,7 +464,9 @@ fun ModalData.ChatWallpaperEditor( } } } else { - AdvancedSettingsButton { showMore = true } + SectionView { + AdvancedSettingsButton { showMore = true } + } } SectionBottomSpacer() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 86f2f13313..3128c63234 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -110,9 +110,6 @@ fun annotatedStringResource(id: StringResource, vararg args: Any?): AnnotatedStr } } -@Composable -expect fun SetupClipboardListener() - // maximum image file size to be auto-accepted // Spec: spec/services/files.md#MAX_IMAGE_SIZE const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB @@ -256,6 +253,7 @@ expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean) fun saveFileFromUri( uri: URI, + maxBytes: Long, withAlertOnException: Boolean = true, hiddenFileNamePrefix: String? = null ): CryptoFile? { @@ -277,7 +275,7 @@ fun saveFileFromUri( val destFile = File(getAppFilePath(destFileName)) if (encrypted) { createTmpFileAndDelete { tmpFile -> - Files.copy(inputStream, tmpFile.toPath()) + copyInputStreamToFile(inputStream, tmpFile, maxBytes) try { val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath) CryptoFile(destFileName, args) @@ -288,7 +286,7 @@ fun saveFileFromUri( } } } else { - Files.copy(inputStream, destFile.toPath()) + copyInputStreamToFile(inputStream, destFile, maxBytes) CryptoFile.plain(destFileName) } } else { @@ -297,6 +295,15 @@ fun saveFileFromUri( null } + } catch (e: FileTooLargeException) { + Log.e(TAG, "Util.kt saveFileFromUri file too large: ${e.message}") + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxBytes)) + ) + } + null } catch (e: Exception) { Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}") if (withAlertOnException) showWrongUriAlert() @@ -305,6 +312,27 @@ fun saveFileFromUri( } } +class FileTooLargeException(maxBytes: Long) : IOException("file exceeds $maxBytes bytes") + +fun copyInputStreamToFile(inputStream: InputStream, destFile: File, maxBytes: Long) { + try { + destFile.outputStream().use { output -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var copied = 0L + while (true) { + val read = inputStream.read(buffer) + if (read < 0) break + if (copied > maxBytes - read) throw FileTooLargeException(maxBytes) + output.write(buffer, 0, read) + copied += read + } + } + } catch (e: Throwable) { + destFile.delete() + throw e + } +} + fun saveWallpaperFile(uri: URI): String? { val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) val destFile = File(getWallpaperFilePath(destFileName)) @@ -467,6 +495,9 @@ fun ciSenderProfile(ci: ChatItem, chatInfo: ChatInfo): LocalProfile? = when (val expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration +// Whether the file really contains a video track. Reads container metadata only, without decoding a frame. +expect suspend fun hasVideoTrack(uri: URI): Boolean + fun showWrongUriAlert() { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.non_content_uri_alert_title), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 03542ca8af..87784f009e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -1,7 +1,7 @@ package chat.simplex.common.views.migration import SectionBottomSpacer -import SectionSpacer +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.foundation.layout.* @@ -11,6 +11,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.foundation.background import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -134,6 +135,7 @@ fun MigrateFromDeviceView(close: () -> Unit) { } close() }, + cardScreen = true, ) { MigrateFromDeviceLayout( migrationState = migrationState, @@ -182,7 +184,7 @@ private fun SectionByState( @Composable private fun MutableState<MigrationFromState>.ChatStopInProgressView() { Box { - SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat)) {} ProgressView() } LaunchedEffect(Unit) { @@ -192,9 +194,8 @@ private fun MutableState<MigrationFromState>.ChatStopInProgressView() { @Composable private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) { - SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) { + SectionView(stringResource(MR.strings.error_stopping_chat)) { Text(reason) - SectionSpacer() SettingsActionItemWithContent( icon = painterResource(MR.images.ic_report_filled), text = stringResource(MR.strings.auth_stop_chat), @@ -224,9 +225,9 @@ private fun MutableState<MigrationFromState>.PassphraseConfirmationView() { val view = LocalMultiplatformView() Column { ChatStoppedView() - SectionSpacer() + SectionDividerSpaced() - SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase)) { PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true) SettingsActionItemWithContent( @@ -243,8 +244,8 @@ private fun MutableState<MigrationFromState>.PassphraseConfirmationView() { } } ) {} - SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase)) } + SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase)) } if (verifyingPassphrase.value) { ProgressView() @@ -254,7 +255,7 @@ private fun MutableState<MigrationFromState>.PassphraseConfirmationView() { @Composable private fun MutableState<MigrationFromState>.UploadConfirmationView() { - SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_ios_share), text = stringResource(MR.strings.migrate_from_device_archive_and_upload), @@ -268,7 +269,7 @@ private fun MutableState<MigrationFromState>.UploadConfirmationView() { @Composable private fun MutableState<MigrationFromState>.ArchivingView() { Box { - SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_archiving_database)) {} ProgressView() } LaunchedEffect(Unit) { @@ -279,7 +280,7 @@ private fun MutableState<MigrationFromState>.ArchivingView() { @Composable private fun MutableState<MigrationFromState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) { Box { - SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_database_init)) {} ProgressView() } LaunchedEffect(Unit) { @@ -298,7 +299,7 @@ private fun MutableState<MigrationFromState>.UploadProgressView( archivePath: String, ) { Box { - SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive)) { val ratio = uploadedBytes.toFloat() / max(totalBytes, 1) LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes))) } @@ -310,7 +311,7 @@ private fun MutableState<MigrationFromState>.UploadProgressView( @Composable private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) { - SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_upload_failed)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_ios_share), text = stringResource(MR.strings.migrate_from_device_repeat_upload), @@ -329,7 +330,7 @@ private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, @Composable private fun LinkCreationView() { Box { - SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link)) {} ProgressView() } } @@ -361,15 +362,15 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S ) } ) {} - SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted)) - SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device)) } - SectionSpacer() - SectionView(stringResource(MR.strings.show_QR_code).uppercase()) { + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device)) + SectionDividerSpaced() + SectionView(stringResource(MR.strings.show_QR_code)) { SimpleXLinkQRCode(link, onShare = {}) } - SectionSpacer() - SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link)) { LinkTextView(link, true) } } @@ -377,7 +378,7 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S @Composable private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) { Box { - SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) { + SectionView(stringResource(MR.strings.migrate_from_device_migration_complete)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_play_arrow_filled), text = stringResource(MR.strings.migrate_from_device_start_chat), @@ -410,13 +411,13 @@ private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) ) } ) {} - SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) - SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) } if (chatDeletion) { ProgressView() } } + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) + SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index cabfbf031e..f92a5e0ce4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -2,6 +2,7 @@ package chat.simplex.common.views.migration import SectionBottomSpacer import SectionItemView +import SectionDividerSpaced import SectionSpacer import SectionTextFooter import SectionView @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.foundation.background import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import chat.simplex.common.model.* @@ -148,6 +150,7 @@ fun ModalData.MigrateToDeviceView(close: () -> Unit) { close() } }, + cardScreen = true, ) { MigrateToDeviceLayout( migrationState = migrationState, @@ -201,7 +204,7 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Uni val progressIndicator = remember { mutableStateOf(false) } Column { if (appPlatform.isAndroid) { - SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) { + SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ')) { QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text -> checkUserLink(text) } @@ -209,12 +212,12 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Uni SectionSpacer() } - SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) { + SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link)) { PasteLinkView() } SectionSpacer() - SectionView(stringResource(MR.strings.chat_archive).uppercase()) { + SectionView(stringResource(MR.strings.chat_archive)) { ArchiveImportView(progressIndicator, close) } } @@ -280,7 +283,7 @@ private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, lin mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value)) } - SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_check), text = stringResource(MR.strings.migrate_to_device_apply_onion), @@ -305,7 +308,7 @@ private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, lin val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = { networkProxy.value = it }) - SectionView(stringResource(MR.strings.network_settings_title).uppercase()) { + SectionView(stringResource(MR.strings.network_settings_title)) { OnionRelatedLayout( appPreferences.developerTools.get(), networkUseSocksProxy, @@ -325,7 +328,7 @@ private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, lin @Composable private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) { Box { - SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_database_init)) {} ProgressView() } LaunchedEffect(Unit) { @@ -345,7 +348,7 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView( networkProxy: NetworkProxy? ) { Box { - SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_downloading_details)) {} ProgressView() } LaunchedEffect(Unit) { @@ -356,7 +359,7 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView( @Composable private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { Box { - SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive)) { val ratio = downloadedBytes.toFloat() / max(totalBytes, 1) LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes))) } @@ -365,7 +368,7 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { @Composable private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { - SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_download_failed)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), text = stringResource(MR.strings.migrate_to_device_repeat_download), @@ -386,7 +389,7 @@ private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, cha @Composable private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { Box { - SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_importing_archive)) {} ProgressView() } LaunchedEffect(Unit) { @@ -396,7 +399,7 @@ private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: Strin @Composable private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { - SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_import_failed)) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), text = stringResource(MR.strings.migrate_to_device_repeat_import), @@ -417,7 +420,7 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S Box { val view = LocalMultiplatformView() - SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) { + SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase)) { SavePassphraseSetting( useKeychain.value, false, @@ -489,7 +492,7 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB } else -> Tuple4(generalGetString(MR.strings.error), null, generalGetString(MR.strings.unknown_error), null) } - SectionView(header.uppercase()) { + SectionView(header) { if (button != null && confirmation != null) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), @@ -500,14 +503,14 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB } ) {} } - SectionTextFooter(footer) } + SectionTextFooter(footer) } @Composable private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) { Box { - SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {} + SectionView(stringResource(MR.strings.migrate_to_device_migrating)) {} ProgressView() } LaunchedEffect(Unit) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt index 93bb4f49db..639a5cc78e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt @@ -38,7 +38,8 @@ import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* @Composable -fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit) { +fun AddChannelView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) { + val rhId = rh?.remoteHostId val view = LocalMultiplatformView() val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() @@ -56,7 +57,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit val gInfo = groupInfo.value if (showLinkStep.value && gInfo != null) { - LinkStepView(chatModel, gInfo, groupLink, closeAll) + LinkStepView(chatModel, rhId, gInfo, groupLink, closeAll) } else if (gInfo != null) { ProgressStepView( chatModel, gInfo, groupRelays, relayListExpanded, @@ -65,9 +66,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit chatModel.creatingChannelId.value = null closeAll() withBGApi { - openGroupChat(null, gInfo.groupId) - ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) + openGroupChat(rhId, gInfo.groupId) + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> + GroupLinkView(chatModel, rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close) } } } @@ -80,9 +81,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit closeAll() withBGApi { try { - chatModel.controller.apiDeleteChat(rh = null, type = ChatType.Group, id = gInfo.apiId) + chatModel.controller.apiDeleteChat(rh = rhId, type = ChatType.Group, id = gInfo.apiId) withContext(Dispatchers.Main) { - chatModel.chatsContext.removeChat(null, gInfo.id) + chatModel.chatsContext.removeChat(rhId, gInfo.id) } } catch (e: Exception) { Log.e(TAG, "cancelChannelCreation error: ${e.message}") @@ -93,6 +94,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit } else { ProfileStepView( chatModel = chatModel, + rhId = rhId, displayName = displayName, profileImage = profileImage, chosenImage = chosenImage, @@ -120,7 +122,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit creationInProgress.value = true withBGApi { try { - val enabledRelays = chooseRandomRelays() + val enabledRelays = chooseRandomRelays(rhId) val relayIds = enabledRelays.mapNotNull { it.chatRelayId } if (relayIds.isEmpty()) { withContext(Dispatchers.Main) { @@ -130,7 +132,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit return@withBGApi } val result = chatModel.controller.apiNewPublicGroup( - rh = null, + rh = rhId, incognito = false, relayIds = relayIds, groupProfile = profile @@ -138,7 +140,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit when (result) { is ChatController.PublicGroupCreationResult.Created -> { withContext(Dispatchers.Main) { - chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo) + chatModel.chatsContext.updateGroup(rhId = rhId, result.groupInfo) chatModel.creatingChannelId.value = result.groupInfo.id groupInfo.value = result.groupInfo groupLink.value = result.groupLink @@ -178,8 +180,8 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit private const val maxRelays = 3 -private suspend fun chooseRandomRelays(): List<UserChatRelay> { - val servers = getUserServers(rh = null) ?: return emptyList() +private suspend fun chooseRandomRelays(rhId: Long?): List<UserChatRelay> { + val servers = getUserServers(rh = rhId) ?: return emptyList() // Operator relays are grouped per operator; custom relays (null operator) // are treated independently to maximize trust distribution. val operatorGroups = mutableListOf<List<UserChatRelay>>() @@ -215,8 +217,8 @@ private suspend fun chooseRandomRelays(): List<UserChatRelay> { return selected } -private suspend fun checkHasRelays(): Boolean { - val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false +private suspend fun checkHasRelays(rhId: Long?): Boolean { + val servers = try { getUserServers(rh = rhId) } catch (_: Exception) { null } ?: return false return servers.any { op -> (op.operator?.enabled ?: true) && op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null } @@ -226,6 +228,7 @@ private suspend fun checkHasRelays(): Boolean { @Composable private fun ProfileStepView( chatModel: ChatModel, + rhId: Long?, displayName: MutableState<String>, profileImage: MutableState<String?>, chosenImage: MutableState<URI?>, @@ -239,7 +242,7 @@ private fun ProfileStepView( createChannel: () -> Unit ) { LaunchedEffect(Unit) { - hasRelays.value = checkHasRelays() + hasRelays.value = checkHasRelays(rhId) } ModalBottomSheetLayout( @@ -553,6 +556,7 @@ private fun RelayRow(relay: GroupRelay, connFailed: Boolean) { @Composable private fun LinkStepView( chatModel: ChatModel, + rhId: Long?, gInfo: GroupInfo, groupLink: MutableState<GroupLink?>, closeAll: () -> Unit @@ -563,14 +567,14 @@ private fun LinkStepView( delay(500) withContext(Dispatchers.Main) { ModalManager.start.closeModals() - openGroupChat(null, gInfo.groupId) + openGroupChat(rhId, gInfo.groupId) } } } - ModalView(close = close, showClose = false) { + ModalView(close = close, showClose = false, cardScreen = true) { GroupLinkView( chatModel = chatModel, - rhId = null, + rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = { groupLink.value = it }, @@ -660,6 +664,6 @@ fun RelayProgressIndicator(active: Int, total: Int) { @Composable fun PreviewAddChannelView() { SimpleXTheme { - AddChannelView(chatModel = ChatModel, close = {}, closeAll = {}) + AddChannelView(chatModel = ChatModel, rh = null, close = {}, closeAll = {}) } } 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 a54d2e42e7..1d8da1690c 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 @@ -53,11 +53,11 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c closeAll.invoke() if (!groupInfo.incognito) { - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true) { close -> AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close) } } else { - ModalManager.end.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close -> GroupLinkView(chatModel, rhId, groupInfo, groupLink = null, onGroupLinkUpdated = null, creatingGroup = true, close = close) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index e5dbe01d68..161681c91d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -31,11 +31,6 @@ suspend fun planAndConnect( filterKnownGroup: ((GroupInfo) -> Unit)? = null, ): CompletableDeferred<Boolean> { when (val target = strConnectTarget(shortOrFullLink.trim())) { - is ConnectTarget.Name -> { - showUnsupportedNameAlert(target.nameInfo) - cleanup?.invoke() - return CompletableDeferred(false) - } is ConnectTarget.Link -> { if (target.linkType == SimplexLinkType.relay) { AlertManager.privacySensitive.showAlertMsg( @@ -46,7 +41,9 @@ suspend fun planAndConnect( return CompletableDeferred(false) } } - null -> {} + // A SimplexName falls through to apiConnectPlan, which resolves it on the + // core (the /_connect plan command accepts a name target, not only a link). + is ConnectTarget.Name, null -> {} } connectProgressManager.cancelConnectProgress() val inProgress = mutableStateOf(true) @@ -77,13 +74,19 @@ private suspend fun planAndConnectTask( cleanup?.invoke() completable.complete(!completable.isActive) } - val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig, inProgress = inProgress) + val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink, linkOwnerSig = linkOwnerSig, inProgress = inProgress) connectProgressManager.stopConnectProgress() if (!inProgress.value) { return completable } if (result != null) { - val (connectionLink, connectionPlan) = result + val (connectionLink, planSimplexName, otherSimplexName, connectionPlan) = result val target = strConnectTarget(shortOrFullLink.trim()) val linkText = if (target is ConnectTarget.Link) "<br><br><u>${target.linkText}</u>" else "" + // the name can also resolve to the other kind; its type picks the verb, its short form the label and target + val connectOtherLink = otherSimplexName?.shortStr + val connectOtherButton = otherSimplexName?.let { + val label = if (it.nameType == SimplexNameType.publicGroup) MR.strings.connect_plan_join_name else MR.strings.connect_plan_connect_to_name + generalGetString(label).format(it.shortStr) + } when (connectionPlan) { is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { is InvitationLinkPlan.Ok -> @@ -94,8 +97,8 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.invitationLinkPlan.contactSLinkData_, ownerVerification = connectionPlan.invitationLinkPlan.ownerVerification, - close, - cleanup + close = close, + cleanup = cleanup ) } else { Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, no short link data") @@ -157,6 +160,9 @@ private suspend fun planAndConnectTask( connectionLink, connectionPlan.contactAddressPlan.contactSLinkData_, ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, + planSimplexName = planSimplexName, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, close, cleanup ) @@ -169,6 +175,8 @@ private suspend fun planAndConnectTask( connectDestructive = false, cleanup, ownerVerification = connectionPlan.contactAddressPlan.ownerVerification, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } ContactAddressPlan.OwnLink -> { @@ -179,6 +187,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } ContactAddressPlan.ConnectingConfirmReconnect -> { @@ -189,6 +199,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is ContactAddressPlan.ConnectingProhibit -> { @@ -197,25 +209,40 @@ private suspend fun planAndConnectTask( if (filterKnownContact != null) { filterKnownContact(contact) } else { - showOpenKnownContactAlert(chatModel, rhId, close, contact) + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } is ContactAddressPlan.Known -> { Log.d(TAG, "planAndConnect, .ContactAddress, .Known") val contact = connectionPlan.contactAddressPlan.contact + // A name-resolved contact is prepared in the store but not yet in the + // chat list (link-prepared chats arrive via NewPreparedChat). Surface it + // so it's visible and openable; no-op if already present. + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } if (filterKnownContact != null) { filterKnownContact(contact) } else { - showOpenKnownContactAlert(chatModel, rhId, close, contact) + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } is ContactAddressPlan.ContactViaAddress -> { Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress") val contact = connectionPlan.contactAddressPlan.contact - askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) - cleanup() + // the contact is already prepared in the store, so open the existing chat instead of sending a new + // connection request; surface it in the chat list first if it is not there yet (as for Known above) + if (chatModel.getContactChat(contact.contactId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = emptyList())) + } + if (filterKnownContact != null) { + filterKnownContact(contact) + } else { + showOpenKnownContactAlert(chatModel, rhId, close, contact, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) + cleanup() + } } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { @@ -228,6 +255,9 @@ private suspend fun planAndConnectTask( connectionPlan.groupLinkPlan.groupSLinkInfo_, connectionPlan.groupLinkPlan.groupSLinkData_, ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, + planSimplexName = planSimplexName, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, close, cleanup ) @@ -240,6 +270,8 @@ private suspend fun planAndConnectTask( connectDestructive = false, cleanup = cleanup, ownerVerification = connectionPlan.groupLinkPlan.ownerVerification, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is GroupLinkPlan.OwnLink -> { @@ -248,7 +280,7 @@ private suspend fun planAndConnectTask( if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) } } GroupLinkPlan.ConnectingConfirmReconnect -> { @@ -259,6 +291,8 @@ private suspend fun planAndConnectTask( text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, connectDestructive = true, cleanup = cleanup, + connectOtherButton = connectOtherButton, + connectOtherLink = connectOtherLink, ) } is GroupLinkPlan.ConnectingProhibit -> { @@ -288,10 +322,15 @@ private suspend fun planAndConnectTask( is GroupLinkPlan.Known -> { Log.d(TAG, "planAndConnect, .GroupLink, .Known") val groupInfo = connectionPlan.groupLinkPlan.groupInfo + // Same as ContactAddress.Known: surface a name-resolved (prepared) + // group in the chat list so it's visible and openable. + if (chatModel.getGroupChat(groupInfo.groupId) == null) { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo, groupChatScope = null), chatItems = emptyList())) + } if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo) + showOpenKnownGroupAlert(chatModel, rhId, close, groupInfo, planSimplexName = planSimplexName, connectOtherButton = connectOtherButton, connectOtherLink = connectOtherLink) cleanup() } } @@ -417,6 +456,8 @@ fun askCurrentOrIncognitoProfileAlert( connectDestructive: Boolean, cleanup: (() -> Unit)?, ownerVerification: OwnerVerification? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, ) { val fullText = listOfNotNull(text, ownerVerificationMessage(ownerVerification)).joinToString("\n\n").ifEmpty { null } AlertManager.privacySensitive.showAlertDialogButtonsColumn( @@ -441,6 +482,14 @@ fun askCurrentOrIncognitoProfileAlert( }) { Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) } + if (connectOtherButton != null && connectOtherLink != null) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { planAndConnect(rhId, connectOtherLink, close = close, cleanup = cleanup) } + }) { + Text(connectOtherButton, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } SectionItemView({ AlertManager.privacySensitive.hideAlert() cleanup?.invoke() @@ -463,7 +512,11 @@ fun openChat_(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, chat: Cha val alertProfileImageSize = 138.dp -private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact) { +// For alerts that show the name inline (not as a profile with an avatar): "Alice" -> "Alice (@alice.testing)". +private fun nameWithDomain(name: String, planSimplexName: SimplexNameInfo?): String = + name + (planSimplexName?.let { " (${it.shortStr})" } ?: "") + +private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, contact: Contact, planSimplexName: SimplexNameInfo? = null, connectOtherButton: String? = null, connectOtherLink: String? = null) { AlertManager.privacySensitive.showOpenChatAlert( profileName = contact.profile.displayName, profileFullName = contact.profile.fullName, @@ -476,10 +529,13 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: }, // the alert shows the badge inline, so it skips the long-expired (ExpiredOld) badge here too profileBadge = if (contact.active && contact.profile.localBadge?.status != BadgeStatus.ExpiredOld) contact.profile.localBadge else null, + nameCaption = planSimplexName?.shortStr, confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat), onConfirm = { openKnownContact(chatModel, rhId, close, contact) }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close) } } }, onDismiss = null ) } @@ -503,11 +559,14 @@ fun ownGroupLinkConfirmConnect( groupInfo: GroupInfo, close: (() -> Unit)?, cleanup: (() -> Unit)?, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, ) { if (groupInfo.useRelays) { AlertManager.privacySensitive.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel), - text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel_vName), groupInfo.displayName), + text = String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_channel_vName), nameWithDomain(groupInfo.displayName, planSimplexName)), buttons = { Column { SectionItemView({ @@ -517,6 +576,14 @@ fun ownGroupLinkConfirmConnect( }) { Text(generalGetString(MR.strings.connect_plan_open_channel), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } + if (connectOtherButton != null && connectOtherLink != null) { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { planAndConnect(rhId, connectOtherLink, close = close, cleanup = cleanup) } + }) { + Text(connectOtherButton, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } SectionItemView({ AlertManager.privacySensitive.hideAlert() cleanup?.invoke() @@ -575,7 +642,7 @@ fun ownGroupLinkConfirmConnect( } } -private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo) { +private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, groupInfo: GroupInfo, planSimplexName: SimplexNameInfo? = null, connectOtherButton: String? = null, connectOtherLink: String? = null) { val subscriberCount = if (groupInfo.useRelays) groupInfo.groupSummary.publicMemberCount?.let { subscriberCountStr(it) } else null AlertManager.privacySensitive.showOpenChatAlert( profileName = groupInfo.groupProfile.displayName, @@ -587,6 +654,7 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( icon = groupInfo.chatIconName ) }, + nameCaption = planSimplexName?.shortStr, subtitle = subscriberCount, confirmText = generalGetString( if (groupInfo.useRelays) { @@ -600,6 +668,8 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( onConfirm = { openKnownGroup(chatModel, rhId, close, groupInfo) }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close) } } }, onDismiss = null ) } @@ -619,6 +689,9 @@ fun showPrepareContactAlert( connectionLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData, ownerVerification: OwnerVerification? = null, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -636,13 +709,14 @@ fun showPrepareContactAlert( ) }, profileBadge = if (contactShortLinkData.localBadge?.status == BadgeStatus.ExpiredOld) null else contactShortLinkData.localBadge, + nameCaption = planSimplexName?.shortStr, information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), onConfirm = { AlertManager.privacySensitive.hideAlert() ModalManager.closeAllModalsEverywhere() withBGApi { - val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData) + val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData, planSimplexName?.nameDomain) if (chat != null) { withContext(Dispatchers.Main) { ChatController.chatModel.chatsContext.addChat(chat) @@ -652,6 +726,8 @@ fun showPrepareContactAlert( cleanup?.invoke() } }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close, cleanup = cleanup) } } }, onDismiss = { cleanup?.invoke() } @@ -664,6 +740,9 @@ fun showPrepareGroupAlert( groupShortLinkInfo: GroupShortLinkInfo?, groupShortLinkData: GroupShortLinkData, ownerVerification: OwnerVerification? = null, + planSimplexName: SimplexNameInfo? = null, + connectOtherButton: String? = null, + connectOtherLink: String? = null, close: (() -> Unit)?, cleanup: (() -> Unit)? ) { @@ -679,6 +758,7 @@ fun showPrepareGroupAlert( icon = if (isChannel) MR.images.ic_bigtop_updates_circle_filled else MR.images.ic_supervised_user_circle_filled ) }, + nameCaption = planSimplexName?.shortStr, subtitle = subscriberCount, information = ownerVerificationMessage(ownerVerification), confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group), @@ -686,7 +766,7 @@ fun showPrepareGroupAlert( AlertManager.privacySensitive.hideAlert() withBGApi { val directLink = groupShortLinkInfo?.direct ?: true - val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData) + val chat = chatModel.controller.apiPrepareGroup(rhId, connectionLink, directLink = directLink, groupShortLinkData, planSimplexName?.nameDomain) if (chat != null) { withContext(Dispatchers.Main) { val relays = groupShortLinkInfo?.groupRelays @@ -703,6 +783,8 @@ fun showPrepareGroupAlert( cleanup?.invoke() } }, + connectOtherButton = connectOtherButton, + onConnectOther = connectOtherLink?.let { link -> { withBGApi { planAndConnect(rhId, link, close = close, cleanup = cleanup) } } }, onDismiss = { cleanup?.invoke() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 0f299b5187..f1bd732d87 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -7,6 +7,7 @@ import SectionView import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -130,7 +131,7 @@ private fun ContactConnectionInfoLayout( if (connLink != null && connLink.connFullLink.isNotEmpty() && contactConnection.initiated) { Spacer(Modifier.height(DEFAULT_PADDING)) SectionViewWithButton( - stringResource(MR.strings.one_time_link).uppercase(), + stringResource(MR.strings.one_time_link), titleButton = if (connLink.connShortLink == null) null else {{ ToggleShortLinkButton(showShortLink) }} ) { SimpleXCreatedLinkQRCode(connLink, short = showShortLink.value) @@ -146,7 +147,7 @@ private fun ContactConnectionInfoLayout( } SectionTextFooter(sharedProfileInfo(chatModel, contactConnection.incognito)) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() DeleteButton(deleteConnection) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 6f64fe5221..4d97e3a054 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -64,7 +64,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } }, createChannel = { - ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, close, closeAll) } + ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } }, rh = rh, close = close @@ -136,7 +136,8 @@ private fun ModalData.NewChatSheetLayout( } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<Set<String>>(emptySet()) } + val connectNameCandidate = remember { mutableStateOf<String?>(null) } val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.CONTACT_WITH_REQUEST, ContactType.REQUEST, ContactType.RECENT) } val contactTypes by remember(searchText.value.text.isEmpty()) { @@ -200,7 +201,7 @@ private fun ModalData.NewChatSheetLayout( ), Triple( painterResource(MR.images.ic_bigtop_updates), - stringResource(MR.strings.create_channel_beta_button), + stringResource(MR.strings.create_channel_button), createChannel, ) ) @@ -313,8 +314,13 @@ private fun ModalData.NewChatSheetLayout( searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) + connectNameCandidate.value?.let { candidate -> + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + } Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } @@ -325,7 +331,7 @@ private fun ModalData.NewChatSheetLayout( item { if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) - SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + SectionView(stringResource(MR.strings.contact_list_header_title), headerBottomPadding = DEFAULT_PADDING_HALF) {} Spacer(Modifier.height(DEFAULT_PADDING_HALF)) } } @@ -399,8 +405,13 @@ private fun ModalData.NewChatSheetLayout( searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) + connectNameCandidate.value?.let { candidate -> + Divider() + ConnectByNameRow(candidate, searchText, connectNameCandidate, close = close) + } Divider() } } @@ -410,7 +421,7 @@ private fun ModalData.NewChatSheetLayout( item { if (filteredContactChats.isNotEmpty() && searchText.value.text.isEmpty()) { SectionDividerSpaced() - SectionView(stringResource(MR.strings.contact_list_header_title).uppercase(), headerBottomPadding = DEFAULT_PADDING_HALF) {} + SectionView(stringResource(MR.strings.contact_list_header_title), headerBottomPadding = DEFAULT_PADDING_HALF) {} } } item { @@ -466,7 +477,8 @@ private fun ContactsSearchBar( listState: LazyListState, searchText: MutableState<TextFieldValue>, searchShowingSimplexLink: MutableState<Boolean>, - searchChatFilteredBySimplexLink: MutableState<String?>, + searchChatFilteredBySimplexLink: MutableState<Set<String>>, + connectNameCandidate: MutableState<String?>, close: () -> Unit, ) { var focused by remember { mutableStateOf(false) } @@ -485,6 +497,8 @@ private fun ContactsSearchBar( alwaysVisible = true, searchText = searchText, trailingContent = null, + // the clear button must line up with the filter icon it replaces, so no reduction here + reducedCloseButtonPadding = 0.dp, ) { searchText.value = searchText.value.copy(it) } @@ -523,21 +537,26 @@ private fun ContactsSearchBar( snapshotFlow { searchText.value.text } .distinctUntilChanged() .collect { - when (val target = strConnectTarget(it.trim())) { - is ConnectTarget.Link -> { - hideKeyboard(view) - searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect( - link = target.text, - searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, - close = close, - cleanup = { searchText.value = TextFieldValue() } - ) - } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) - null -> if (!searchShowingSimplexLink.value || it.isEmpty()) { + val target = strConnectTarget(it.trim()) + if (target is ConnectTarget.Link) { + hideKeyboard(view) + searchText.value = searchText.value.copy(target.linkText, selection = TextRange.Zero) + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = emptySet() + connectNameCandidate.value = null + connect( + link = target.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } else { + // A name is resolved only when its "Connect to …" row is tapped, not on every keystroke. The + // simplex-name filter is chat-list only: this contacts/deleted view is a scoped subset, so a + // resolved chat id (channel, business, unlisted or active-only contact) may not be present in it. + val candidate = nameSearchCandidate(it.trim()) + connectNameCandidate.value = candidate + if (candidate == null && (!searchShowingSimplexLink.value || it.isEmpty())) { if (it.isNotEmpty()) { focusRequester.requestFocus() } else { @@ -547,7 +566,7 @@ private fun ContactsSearchBar( } } searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null + searchChatFilteredBySimplexLink.value = emptySet() } } } @@ -574,12 +593,12 @@ private fun ToggleFilterButton() { } } -private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<String?>, close: () -> Unit, cleanup: (() -> Unit)?) { +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<Set<String>>, close: () -> Unit, cleanup: (() -> Unit)?) { withBGApi { planAndConnect( chatModel.remoteHostId(), link, - filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownContact = { searchChatFilteredBySimplexLink.value = setOf(it.id) }, close = close, cleanup = cleanup, ) @@ -589,15 +608,15 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< private fun filteredContactChats( showUnreadAndFavorites: Boolean, searchShowingSimplexLink: State<Boolean>, - searchChatFilteredBySimplexLink: State<String?>, + searchChatFilteredBySimplexLink: State<Set<String>>, searchText: String, contactChats: List<Chat> ): List<Chat> { - val linkChatId = searchChatFilteredBySimplexLink.value + val linkChatIds = searchChatFilteredBySimplexLink.value val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() - return if (linkChatId != null) { - contactChats.filter { it.id == linkChatId } + return if (linkChatIds.isNotEmpty()) { + contactChats.filter { it.id in linkChatIds } } else { contactChats.filter { chat -> filterChat( @@ -653,7 +672,9 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats val listState = remember { appBarHandler.listState } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf<Set<String>>(emptySet()) } + // deleted contacts are not connected to by name, so this candidate only stops per-keystroke resolution + val connectNameCandidate = remember { mutableStateOf<String?>(null) } val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value val allChats by remember(chatModel.chats.value) { derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) } @@ -696,6 +717,7 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) } else { @@ -705,6 +727,7 @@ private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats searchText = searchText, searchShowingSimplexLink = searchShowingSimplexLink, searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + connectNameCandidate = connectNameCandidate, close = close, ) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index be16ced1f5..f3006d221b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -497,7 +497,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact ) SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() }) } else { - SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { + SectionView(stringResource(MR.strings.share_this_1_time_link), headerBottomPadding = 5.dp) { LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true) } @@ -521,7 +521,7 @@ private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contact val currentUser = remember { chatModel.currentUser }.value if (currentUser != null) { - SectionView(stringResource(MR.strings.new_chat_share_profile).uppercase(), headerBottomPadding = 5.dp) { + SectionView(stringResource(MR.strings.new_chat_share_profile), headerBottomPadding = 5.dp) { SectionItemView( padding = PaddingValues( top = 0.dp, @@ -645,14 +645,14 @@ private fun ConnectView(rhId: Long?, showQRCodeScanner: MutableState<Boolean>, p ) } - SectionView(stringResource(MR.strings.paste_the_link_you_received).uppercase(), headerBottomPadding = 5.dp) { + SectionView(stringResource(MR.strings.paste_the_link_you_received), headerBottomPadding = 5.dp) { PasteLinkView(rhId, pastedLink, showQRCodeScanner, close) } if (appPlatform.isAndroid) { Spacer(Modifier.height(10.dp)) - SectionView(stringResource(MR.strings.or_scan_qr_code).uppercase(), headerBottomPadding = 5.dp) { + SectionView(stringResource(MR.strings.or_scan_qr_code), headerBottomPadding = 5.dp) { QRCodeScanner(showQRCodeScanner) { text -> val linkVerified = verifyOnly(text) if (!linkVerified) { @@ -679,7 +679,11 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState<String>, showQRC showQRCodeScanner.value = false withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } } - is ConnectTarget.Name -> showUnsupportedNameAlert(target.nameInfo) + is ConnectTarget.Name -> { + pastedLink.value = target.text + showQRCodeScanner.value = false + withBGApi { connect(rhId, target.text, close) { pastedLink.value = "" } } + } null -> AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_contact_link), text = generalGetString(MR.strings.the_text_you_pasted_is_not_a_link) @@ -824,7 +828,7 @@ fun strIsSimplexLink(str: String): Boolean { sealed class ConnectTarget { class Link(val text: String, val linkType: SimplexLinkType, val linkText: String) : ConnectTarget() - class Name(val nameInfo: SimplexNameInfo) : ConnectTarget() + class Name(val text: String, val nameInfo: SimplexNameInfo) : ConnectTarget() } fun strConnectTarget(str: String): ConnectTarget? { @@ -832,24 +836,17 @@ fun strConnectTarget(str: String): ConnectTarget? { val links = parsedMd.filter { it.format?.isSimplexLink ?: false } if (links.size == 1) { val fmt = links[0].format as Format.SimplexLink - return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText) + val text = if (fmt.showText != null) fmt.simplexUri else links[0].text + return ConnectTarget.Link(text, fmt.linkType, fmt.simplexLinkText) } if (links.isEmpty()) { - val nameInfo = parsedMd.firstNotNullOfOrNull { (it.format as? Format.SimplexName)?.nameInfo } - if (nameInfo != null) return ConnectTarget.Name(nameInfo) + val nameFt = parsedMd.firstOrNull { it.format is Format.SimplexName } + val nameInfo = (nameFt?.format as? Format.SimplexName)?.nameInfo + if (nameFt != null && nameInfo != null) return ConnectTarget.Name(nameFt.text, nameInfo) } return null } -fun showUnsupportedNameAlert(nameInfo: SimplexNameInfo) { - val (title, msg) = if (nameInfo.nameType == SimplexNameType.contact) { - generalGetString(MR.strings.unsupported_contact_name) to generalGetString(MR.strings.contact_name_requires_newer_app_version) - } else { - generalGetString(MR.strings.unsupported_channel_name) to generalGetString(MR.strings.channel_name_requires_newer_app_version) - } - AlertManager.shared.showAlertMsg(title, "$msg ${generalGetString(MR.strings.please_upgrade_the_app)}") -} - @Composable fun IncognitoToggle( incognitoPref: SharedPreference<Boolean>, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt index 2fd77b46a1..d11e396388 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/ChooseServerOperators.kt @@ -55,7 +55,7 @@ fun OnboardingConditionsView(chatModel: ChatModel) { OnboardingConditionsDesktop(selectedOperatorIds) } else { CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false, showAppBar = false) { + ModalView({}, showClose = false, showAppBar = false, cardScreen = true) { OnboardingShrinkingLayout( modifier = Modifier.fillMaxSize().themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer) .systemBarsPadding() @@ -133,7 +133,7 @@ fun OnboardingConditionsView(chatModel: ChatModel) { @Composable private fun OnboardingConditionsDesktop(selectedOperatorIds: MutableState<Set<Long>>) { CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView({}, showClose = false) { + ModalView({}, showClose = false, cardScreen = true) { ColumnWithScrollBar(horizontalAlignment = Alignment.CenterHorizontally) { Column(Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { Box(Modifier.align(Alignment.CenterHorizontally)) { @@ -184,7 +184,7 @@ fun ModalData.ChooseServerOperators( prepareChatBeforeFinishingOnboarding() } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { - ModalView(close, enableClose = selectedOperatorIds.value.isNotEmpty()) { + ModalView(close, enableClose = selectedOperatorIds.value.isNotEmpty(), cardScreen = true) { ColumnWithScrollBar( Modifier .themedBackground(bgLayerSize = LocalAppBarHandler.current?.backgroundGraphicsLayerSize, bgLayer = LocalAppBarHandler.current?.backgroundGraphicsLayer), @@ -373,7 +373,7 @@ private fun ChooseServerOperatorsInfoView() { SectionDividerSpaced() - SectionView(title = stringResource(MR.strings.onboarding_network_about_operators).uppercase()) { + SectionView(title = stringResource(MR.strings.onboarding_network_about_operators)) { chatModel.conditions.value.serverOperators.forEach { op -> ServerOperatorRow(op) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index e902b7947e..97dfcd34b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -65,7 +65,7 @@ private fun LinkAMobileLayout( Modifier.weight(0.3f), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + SectionView(generalGetString(MR.strings.this_device_name)) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index e1415d071d..6391c88035 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -1,6 +1,10 @@ package chat.simplex.common.views.onboarding import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -8,17 +12,41 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.model.* @@ -34,6 +62,7 @@ import chat.simplex.common.views.usersettings.showAddShortLinkAlert import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource +import kotlin.math.absoluteValue @Composable fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) { @@ -912,6 +941,34 @@ private val versionDescriptions: List<VersionDescription> = listOf( ), ) ), + VersionDescription( + // the trailing space differs from the previously released "v7.0", so that What's new is shown again + version = "v7.0 ", + post = null, + features = listOf( + VersionFeature.FeatureView( + icon = null, + titleId = MR.strings.v7_0_invest, + view = { modalManager -> InvestInSimpleXChatView(modalManager) } + ), + VersionFeature.FeatureDescription( + icon = MR.images.ic_alternate_email, + titleId = MR.strings.v7_0_simplex_names, + descrId = MR.strings.v7_0_simplex_names_descr + ), + VersionFeature.FeatureDescription( + icon = null, + titleId = MR.strings.v7_0_channels, + descrId = null, + subfeatures = listOf( + MR.images.ic_person_add to MR.strings.v7_0_channels_contributors, + MR.images.ic_travel_explore to MR.strings.v7_0_channels_previews, + MR.images.ic_dns to MR.strings.v7_0_channels_relays, + MR.images.ic_article to MR.strings.v7_0_channels_wider_messages, + ) + ), + ) + ), ) private val lastVersion = versionDescriptions.last().version @@ -928,6 +985,288 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { return v != lastVersion } +private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat" + +private const val CROWDFUNDING_CONTACT_URI = "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im" + +// the center modal takes the remaining width of the window, so the image is limited to its design width +private val MAX_CROWDFUNDING_IMAGE_WIDTH = DEFAULT_MIN_CENTER_MODAL_WIDTH + +// the width of the page images shipped with the desktop app, so that they are never upscaled +private val CROWDFUNDING_PAGE_IMAGE_WIDTH = DEFAULT_MIN_CENTER_MODAL_WIDTH + +// the corner radius the images are designed with, and the same radius as a share of their design width +private val CROWDFUNDING_IMAGE_CORNER_RADIUS = 12.dp +private const val CROWDFUNDING_IMAGE_CORNER_RADIUS_RATIO = 0.03f + +private class CrowdfundingLayout( + val maxImageWidth: Dp, + val imageShape: Shape, + // the modal manager that shows the page in the center of the window, or null when nothing does + private val centerOfWindow: ModalManager? +) { + fun inCenterOfWindow(modalManager: ModalManager) = modalManager === centerOfWindow +} + +// the images are designed for the width of a phone screen, which Android always gives them. On desktop +// they are limited to their own width, and their radius is scaled with them, as they are still shown +// wider than designed: a fixed radius would not only look almost square, but would also leave the corners +// baked into the jpegs visible - they have black behind them, as jpegs have no transparency +private val crowdfundingLayout = if (appPlatform.isDesktop) + CrowdfundingLayout(CROWDFUNDING_PAGE_IMAGE_WIDTH, object : Shape { + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline = + Outline.Rounded(RoundRect(size.toRect(), CornerRadius(size.width * CROWDFUNDING_IMAGE_CORNER_RADIUS_RATIO))) + }, ModalManager.center) +else + CrowdfundingLayout(Dp.Unspecified, RoundedCornerShape(CROWDFUNDING_IMAGE_CORNER_RADIUS), null) + +// Google Play policy restricts promoting investments, so Play builds only show it in the US +@Composable +fun crowdfundingAvailable(): Boolean { + if (!platform.androidIsPlayStoreBuild) return true + LaunchedEffect(Unit) { + if (androidPlayStoreCountry.value == null) platform.androidLoadPlayStoreCountry() + } + return androidPlayStoreCountry.value == "US" +} + +@Composable +private fun InvestInSimpleXChatView(modalManager: ModalManager) { + if (!crowdfundingAvailable()) return + val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } } + Column(modifier = Modifier.padding(bottom = 12.dp)) { + Text( + generalGetString(MR.strings.v7_0_invest), + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp) + ) + Text( + buildAnnotatedString { + append(generalGetString(MR.strings.v7_0_invest_descr)) + append(" ") + withStyle(SpanStyle(color = MaterialTheme.colors.primary)) { + append(generalGetString(MR.strings.learn_more)) + } + }, + fontSize = 15.sp, + modifier = Modifier + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = showGetStake + ) + ) + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(MR.images.crowdfunding_1), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .padding(top = 8.dp) + .widthIn(max = MAX_CROWDFUNDING_IMAGE_WIDTH) + .fillMaxWidth() + .clip(crowdfundingLayout.imageShape) + .pointerHoverIcon(PointerIcon.Hand) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = showGetStake + ) + ) + } + } +} + +private class CrowdfundingSlide( + val image: ImageResource, + val heading: String, + val info: String?, + val text: String, +) + +// not localized: the page is only shown to US investors, and the text duplicates the images +private val getStakeSlides: List<CrowdfundingSlide> = listOf( + CrowdfundingSlide( + MR.images.crowdfunding_1, + "The first and the only messaging network without any user IDs", + null, + "By investing, you can benefit from the company growth, and help us build the future of private and secure communications." + ), + CrowdfundingSlide( + MR.images.crowdfunding_2, + "480,000+ users joined on their own", + null, + "SimpleX users have been more than doubling every year without any paid marketing, and donated over \$650,000." + ), + CrowdfundingSlide( + MR.images.crowdfunding_3, + "Developers already bet on SimpleX success", + "Independent developers created moderation and AI bots, Telegram bridges, and a public server registry.", + "Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat." + ), + CrowdfundingSlide( + MR.images.crowdfunding_4, + "Revenue plan: free for users, channels & businesses pay", + "SimpleX Chat plans to earn from the infrastructure and services that creators, businesses and large communities need as they grow.", + "Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms on Wefunder." + ), +) + +@Composable +fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) { + val uriHandler = LocalUriHandler.current + val stopped = chatModel.chatRunning.value == false + + @Composable + fun slideImage(slide: CrowdfundingSlide) { + if (BuildConfigCommon.SIMPLEX_ASSETS) { + Image( + painterResource(slide.image), + contentDescription = null, + contentScale = ContentScale.FillWidth, + modifier = Modifier + .widthIn(max = crowdfundingLayout.maxImageWidth) + .fillMaxWidth() + .clip(crowdfundingLayout.imageShape) + .fullScreenOnClick(slide.image) + ) + } else { + Text(slide.heading, style = MaterialTheme.typography.h4, fontWeight = FontWeight.Medium) + if (slide.info != null) { + Text(slide.info, Modifier.padding(top = 4.dp), lineHeight = 24.sp) + } + } + } + + ColumnWithScrollBar(Modifier.pinchZoom().padding(horizontal = DEFAULT_PADDING)) { + // in the center of the window the page is wide enough for the title to fit on one line + val title = "Get a stake in\nSimpleX Chat" + AppBarTitle(if (inCenterOfWindow) title.replace("\n", " ") else title, withPadding = false) + // What's new already shows the image of the first slide, above the link that opens this page + if (fromSettings) { + slideImage(getStakeSlides[0]) + } + Text( + buildAnnotatedString { + append(getStakeSlides[0].text) + // only the link is clickable, the rest of the paragraph is not + withLink(LinkAnnotation.Url(WEFUNDER_URL) { uriHandler.openUriCatching(WEFUNDER_URL) }) { + withStyle(SpanStyle(color = MaterialTheme.colors.primary, fontWeight = FontWeight.Bold)) { + append(" Learn more and invest on Wefunder.") + } + } + }, + Modifier.padding(top = if (fromSettings) 8.dp else 0.dp), + lineHeight = 24.sp + ) + + getStakeSlides.drop(1).forEach { slide -> + Column(Modifier.padding(top = DEFAULT_PADDING * 1.5f)) { + slideImage(slide) + Text(slide.text, Modifier.padding(top = 8.dp), lineHeight = 24.sp) + } + } + + Column( + Modifier.fillMaxWidth().padding(top = DEFAULT_PADDING * 2), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.v7_0_invest_learn_more, + onboarding = null, + onclick = { uriHandler.openUriCatching(WEFUNDER_URL) } + ) + if (!chatModel.desktopNoUserNoRemote) { + TextButtonBelowOnboardingButton( + "or ask SimpleX team", + onClick = if (stopped) null else ({ + close() + uriHandler.openVerifiedSimplexUri(CROWDFUNDING_CONTACT_URI) + }) + ) + } + } + } +} + +// there is no pinch gesture with a mouse, so on desktop a slide is opened full screen instead +@Composable +private fun Modifier.fullScreenOnClick(image: ImageResource): Modifier { + if (!appPlatform.isDesktop) return this + return pointerHoverIcon(PointerIcon.Hand).clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + ModalManager.fullscreen.showCustomModal { close -> + BackHandler(onBack = close) + Box( + Modifier + .fillMaxSize() + .background(Color.Black) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = close), + contentAlignment = Alignment.Center + ) { + Image(painterResource(image), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize()) + } + } + } +} + +private const val MAX_PAGE_ZOOM = 5f + +/** + * The slide images contain small text that is unreadable at screen width, so the page can be pinch-zoomed. + * Android only: pinch is unavailable with a mouse. + */ +@Composable +private fun Modifier.pinchZoom(): Modifier { + if (!appPlatform.isAndroid) return this + var scale by remember { mutableStateOf(1f) } + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + var size by remember { mutableStateOf(IntSize.Zero) } + return this + .onGloballyPositioned { size = it.size } + .graphicsLayer { + scaleX = scale + scaleY = scale + translationX = offsetX + translationY = offsetY + } + .pointerInput(Unit) { + awaitEachGesture { + // the initial pass, as the scroll of the same column is applied after this modifier and would take the gesture first + awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) + var taken: Boolean? = null + do { + val event = awaitPointerEvent(PointerEventPass.Initial) + val multiTouch = event.changes.count { it.pressed } > 1 + if (multiTouch || scale > 1f) { + scale = (scale * event.calculateZoom()).coerceIn(1f, MAX_PAGE_ZOOM) + val pan = event.calculatePan() + // the page is scaled around its center, so it can be panned by half of the overflow in each direction + val maxX = size.width * (scale - 1f) / 2 + val maxY = size.height * (scale - 1f) / 2 + val pannedY = offsetY + pan.y * scale + // the clamp is applied even when the gesture is not taken: at scale 1 both bounds + // are 0, which resets the offsets after zooming back out + offsetX = (offsetX + pan.x * scale).coerceIn(-maxX, maxX) + offsetY = pannedY.coerceIn(-maxY, maxY) + // two fingers always mean zoom, taken without a touch slop: waiting for one would let + // the scroll reach its own slop first and scroll the page. A one finger drag is left + // to the scroll at the edges, decided once so it cannot alternate mid drag + if (multiTouch) taken = true + else if (taken == null && pan.y != 0f) taken = pannedY.absoluteValue < maxY + if (taken == true) event.changes.forEach { if (it.pressed) it.consume() } + } + } while (event.changes.any { it.pressed }) + } + } +} + @Composable fun CreateUpdateAddressShortLinkView(modalManager: ModalManager) { val clipboard = LocalClipboardManager.current diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 8bb84060c2..2d2de2881f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -4,10 +4,10 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionItemViewLongClickable -import SectionSpacer import SectionView import TextIconSpaced import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* @@ -29,8 +29,7 @@ import chat.simplex.common.model.ChatController.switchToLocalSession import chat.simplex.common.model.ChatModel.connectedToRemote import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.DEFAULT_PADDING -import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -53,7 +52,7 @@ fun ConnectDesktopView(close: () -> Unit) { showDisconnectDesktopAlert(close) } } - ModalView(close = closeWithAlert) { + ModalView(close = closeWithAlert, cardScreen = true) { ConnectDesktopLayout( deviceName = deviceName.value!!, close @@ -128,7 +127,7 @@ private fun ConnectDesktopLayout(deviceName: String, close: () -> Unit) { @Composable private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>, sessionAddress: MutableState<String>) { AppBarTitle(stringResource(MR.strings.connect_to_desktop)) - SectionView(stringResource(MR.strings.this_device_name).uppercase()) { + SectionView(stringResource(MR.strings.this_device_name)) { DevicesView(deviceName, remoteCtrls) { if (it != "") { setDeviceName(it) @@ -139,7 +138,7 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re SectionDividerSpaced() ScanDesktopAddressView(sessionAddress) if (controller.appPrefs.developerTools.get()) { - SectionSpacer() + SectionDividerSpaced() DesktopAddressView(sessionAddress) } } @@ -147,20 +146,22 @@ private fun ConnectDesktop(deviceName: String, remoteCtrls: SnapshotStateList<Re @Composable private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { AppBarTitle(stringResource(MR.strings.connecting_to_desktop)) - SectionView(stringResource(MR.strings.connecting_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - CtrlDeviceNameText(session, rc) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - CtrlDeviceVersionText(session) + SectionView(stringResource(MR.strings.connecting_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Column { + CtrlDeviceNameText(session, rc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + CtrlDeviceVersionText(session) + } } if (session.sessionCode != null) { - SectionSpacer() - SectionView(stringResource(MR.strings.session_code).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.session_code)) { SessionCodeText(session.sessionCode!!) } } - SectionSpacer() + SectionDividerSpaced() SectionView { DisconnectButton(onClick = ::disconnectDesktop) @@ -188,7 +189,7 @@ private fun ProgressIndicator() { @Composable private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { AppBarTitle(stringResource(MR.strings.connecting_to_desktop)) - SectionView(stringResource(MR.strings.this_device_name).uppercase()) { + SectionView(stringResource(MR.strings.this_device_name)) { DevicesView(deviceName, remoteCtrls) { if (it != "") { setDeviceName(it) @@ -197,10 +198,10 @@ private fun SearchingDesktop(deviceName: String, remoteCtrls: SnapshotStateList< } } SectionDividerSpaced() - SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.found_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.waiting_for_desktop), fontStyle = FontStyle.Italic) } - SectionSpacer() + SectionDividerSpaced() DisconnectButton(stringResource(MR.strings.scan_QR_code).replace('\n', ' '), MR.images.ic_qr_code, ::disconnectDesktop) } @@ -215,7 +216,7 @@ private fun FoundDesktop( sessionAddress: MutableState<String>, ) { AppBarTitle(stringResource(MR.strings.found_desktop)) - SectionView(stringResource(MR.strings.this_device_name).uppercase()) { + SectionView(stringResource(MR.strings.this_device_name)) { DevicesView(deviceName, remoteCtrls) { if (it != "") { setDeviceName(it) @@ -224,15 +225,17 @@ private fun FoundDesktop( } } SectionDividerSpaced() - SectionView(stringResource(MR.strings.found_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - CtrlDeviceNameText(session, rc) - CtrlDeviceVersionText(session) - if (!compatible) { - Text(stringResource(MR.strings.not_compatible), color = MaterialTheme.colors.error) + SectionView(stringResource(MR.strings.found_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Column { + CtrlDeviceNameText(session, rc) + CtrlDeviceVersionText(session) + if (!compatible) { + Text(stringResource(MR.strings.not_compatible), color = MaterialTheme.colors.error) + } } } - SectionSpacer() + SectionDividerSpaced() if (compatible) { SectionItemView({ withBGApi { confirmKnownDesktop(sessionAddress, rc) } }) { @@ -256,25 +259,26 @@ private fun FoundDesktop( @Composable private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessCode: String, remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { AppBarTitle(stringResource(MR.strings.verify_connection)) - SectionView(stringResource(MR.strings.connected_to_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - CtrlDeviceNameText(session, rc) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - CtrlDeviceVersionText(session) + SectionView(stringResource(MR.strings.connected_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Column { + CtrlDeviceNameText(session, rc) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + CtrlDeviceVersionText(session) + } } - SectionSpacer() + SectionDividerSpaced() - SectionView(stringResource(MR.strings.verify_code_with_desktop).uppercase()) { + SectionView(stringResource(MR.strings.verify_code_with_desktop)) { SessionCodeText(sessCode) + SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) { + Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary) + TextIconSpaced(false) + Text(generalGetString(MR.strings.confirm_verb)) + } } - SectionSpacer() - - SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) { - Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary) - TextIconSpaced(false) - Text(generalGetString(MR.strings.confirm_verb)) - } + SectionDividerSpaced() SectionView { DisconnectButton(onClick = ::disconnectDesktop) @@ -311,20 +315,22 @@ private fun CtrlDeviceVersionText(session: RemoteCtrlSession) { @Composable private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close: () -> Unit) { AppBarTitle(stringResource(MR.strings.connected_to_desktop)) - SectionView(stringResource(MR.strings.connected_desktop).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - Text(rc.deviceViewName) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - CtrlDeviceVersionText(session) + SectionView(stringResource(MR.strings.connected_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Column { + Text(rc.deviceViewName) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + CtrlDeviceVersionText(session) + } } if (session.sessionCode != null) { - SectionSpacer() - SectionView(stringResource(MR.strings.session_code).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.session_code)) { SessionCodeText(session.sessionCode!!) } } - SectionSpacer() + SectionDividerSpaced() SectionView { DisconnectButton { disconnectDesktop(close) } @@ -355,7 +361,7 @@ private fun DevicesView(deviceName: String, remoteCtrls: SnapshotStateList<Remot @Composable private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) { - SectionView(stringResource(MR.strings.scan_qr_code_from_desktop).uppercase()) { + SectionView(stringResource(MR.strings.scan_qr_code_from_desktop)) { QRCodeScanner { text -> sessionAddress.value = text connectDesktopAddress(sessionAddress, text) @@ -366,7 +372,7 @@ private fun ScanDesktopAddressView(sessionAddress: MutableState<String>) { @Composable private fun DesktopAddressView(sessionAddress: MutableState<String>) { val clipboard = LocalClipboardManager.current - SectionView(stringResource(MR.strings.desktop_address).uppercase()) { + SectionView(stringResource(MR.strings.desktop_address)) { if (sessionAddress.value.isEmpty()) { SettingsActionItem( painterResource(MR.images.ic_content_paste), @@ -410,7 +416,7 @@ private fun DesktopAddressView(sessionAddress: MutableState<String>) { private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.linked_desktops)) - SectionView(stringResource(MR.strings.desktop_devices).uppercase()) { + SectionView(stringResource(MR.strings.desktop_devices)) { remoteCtrls.forEach { rc -> val showMenu = rememberSaveable { mutableStateOf(false) } SectionItemViewLongClickable(click = {}, longClick = { showMenu.value = true }) { @@ -427,7 +433,7 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList<RemoteCtrlInfo>) { } SectionDividerSpaced() - SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) { + SectionView(stringResource(MR.strings.linked_desktop_options)) { PreferenceToggle(stringResource(MR.strings.verify_connections), checked = remember { controller.appPrefs.confirmRemoteSessions.state }.value) { controller.appPrefs.confirmRemoteSessions.set(it) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 1d01ab11ff..8caf038481 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -92,7 +92,7 @@ fun ConnectMobileLayout( ) { ColumnWithScrollBar { AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) - SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { + SectionView(generalGetString(MR.strings.this_device_name)) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { controller.appPrefs.offerRemoteMulticast.state }.value) { @@ -100,7 +100,7 @@ fun ConnectMobileLayout( } SectionDividerSpaced() } - SectionView(stringResource(MR.strings.devices).uppercase()) { + SectionView(stringResource(MR.strings.devices)) { if (chatModel.localUserCreated.value == true) { SettingsActionItemWithContent(text = stringResource(MR.strings.this_device), icon = painterResource(MR.images.ic_desktop), click = connectDesktop) { if (connectedHost.value == null) { @@ -215,7 +215,7 @@ private fun ConnectMobileViewLayout( Spacer(Modifier.height(DEFAULT_PADDING)) } if (deviceName != null || sessionCode != null) { - SectionView(stringResource(MR.strings.connected_mobile).uppercase()) { + SectionView(stringResource(MR.strings.connected_mobile)) { SelectionContainer { Text( deviceName ?: stringResource(MR.strings.new_mobile_device), @@ -228,7 +228,7 @@ private fun ConnectMobileViewLayout( } if (sessionCode != null) { - SectionView(stringResource(MR.strings.verify_code_on_mobile).uppercase()) { + SectionView(stringResource(MR.strings.verify_code_on_mobile)) { SelectionContainer { Text( sessionCode.substring(0, 23), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index e24c09afd0..93c5eb7756 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -1,11 +1,13 @@ package chat.simplex.common.views.usersettings +import CARD_PADDING +import LocalCardScreen import SectionBottomSpacer import SectionDividerSpaced import SectionItemView +import itemHPadding import SectionItemViewSpaceBetween import SectionItemViewWithoutMinPadding -import SectionSpacer import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -58,9 +60,9 @@ expect fun AppearanceView(m: ChatModel) object AppearanceScope { @Composable fun ProfileImageSection() { - SectionView(stringResource(MR.strings.settings_section_title_profile_images).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionView(stringResource(MR.strings.settings_section_title_profile_images), contentPadding = PaddingValues(horizontal = CARD_PADDING)) { val image = remember { chatModel.currentUser }.value?.image - Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { + Row(Modifier.padding(vertical = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { val size = 60 Box(Modifier.offset(x = -(size / 12).dp)) { if (!image.isNullOrEmpty()) { @@ -91,9 +93,10 @@ object AppearanceScope { @Composable fun AppToolbarsSection() { BoxWithConstraints { - SectionView(stringResource(MR.strings.appearance_app_toolbars).uppercase()) { + SectionView(stringResource(MR.strings.appearance_app_toolbars)) { SectionItemViewWithoutMinPadding { Box(Modifier.weight(1f)) { + var fontScale by remember { mutableStateOf(1f) } Text( stringResource(MR.strings.appearance_in_app_bars_alpha), Modifier.clickable( @@ -102,7 +105,9 @@ object AppearanceScope { ) { appPrefs.inAppBarsAlpha.set(appPrefs.inAppBarsDefaultAlpha) }, - maxLines = 1 + maxLines = 1, + fontSize = MaterialTheme.typography.body1.fontSize * fontScale, + onTextLayout = { if (it.hasVisualOverflow && fontScale > 0.5f) fontScale -= 0.05f } ) } Spacer(Modifier.padding(end = 10.dp)) @@ -175,7 +180,7 @@ object AppearanceScope { @Composable fun MessageShapeSection() { BoxWithConstraints { - SectionView(stringResource(MR.strings.settings_section_title_message_shape).uppercase()) { + SectionView(stringResource(MR.strings.settings_section_title_message_shape)) { SectionItemViewWithoutMinPadding { Text(stringResource(MR.strings.settings_message_shape_corner), Modifier.weight(1f)) Spacer(Modifier.width(10.dp)) @@ -205,8 +210,8 @@ object AppearanceScope { @Composable fun FontScaleSection() { val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } - SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + SectionView(stringResource(MR.strings.appearance_font_size), contentPadding = PaddingValues(horizontal = CARD_PADDING)) { + Row(Modifier.padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) { Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) @@ -409,26 +414,29 @@ object AppearanceScope { } if (appPlatform.isDesktop) { - val itemWidth = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 - val itemHeight = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2) / 4 + val gridPadding = 12.dp + val cardPadding = if (LocalCardScreen.current) CARD_PADDING * 2 else 0.dp + val itemSize = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - cardPadding - gridPadding * 5) / 4 val rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt() LazyVerticalGrid( columns = GridCells.Fixed(4), - Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2), - contentPadding = PaddingValues(DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), - horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + Modifier.height(itemSize * rows + gridPadding * (rows + 1)), + contentPadding = PaddingValues(gridPadding), + verticalArrangement = Arrangement.spacedBy(gridPadding), + horizontalArrangement = Arrangement.spacedBy(gridPadding), ) { - gridContent(itemWidth, itemHeight) + gridContent(itemSize, itemSize) } } else { - LazyHorizontalGrid( + val gridPadding = 14.dp + val itemSize = 81.dp + LazyHorizontalGrid( rows = GridCells.Fixed(1), - Modifier.height(80.dp + DEFAULT_PADDING * 2), - contentPadding = PaddingValues(DEFAULT_PADDING), - horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + Modifier.height(itemSize + gridPadding * 2), + contentPadding = PaddingValues(gridPadding), + horizontalArrangement = Arrangement.spacedBy(gridPadding), ) { - gridContent(80.dp, 80.dp) + gridContent(itemSize, itemSize) } } } @@ -521,9 +529,7 @@ object AppearanceScope { } SectionView(stringResource(MR.strings.settings_section_title_themes)) { - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) ThemeDestinationPicker(themeUserDestination) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> if (to != null) onImport(to) @@ -555,7 +561,6 @@ object AppearanceScope { color = if (chatModel.remoteHostId != null && themeUserDestination.value != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } - SectionSpacer() } val state: State<DefaultThemeMode?> = remember(appPrefs.currentTheme.get()) { @@ -584,23 +589,23 @@ object AppearanceScope { } saveThemeToDatabase(null) } - } - SectionItemView(click = { - val user = themeUserDestination.value - if (user == null) { - ModalManager.start.showModal { - val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> - if (to != null) onImport(to) + SectionItemView(click = { + val user = themeUserDestination.value + if (user == null) { + ModalManager.start.showModal(cardScreen = true) { + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + CustomizeThemeView { onChooseType(it, importWallpaperLauncher) } + } + } else { + ModalManager.start.showModalCloseable(cardScreen = true) { close -> + UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close) } - CustomizeThemeView { onChooseType(it, importWallpaperLauncher) } - } - } else { - ModalManager.start.showModalCloseable { close -> - UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close) } + }) { + Text(stringResource(MR.strings.customize_theme_title)) } - }) { - Text(stringResource(MR.strings.customize_theme_title)) } } @@ -626,68 +631,70 @@ object AppearanceScope { ) } - WallpaperPresetSelector( - selectedWallpaper = wallpaperType, - baseTheme = currentTheme.base, - currentColors = { type -> - ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get()) - }, - onChooseType = onChooseType - ) - - val type = MaterialTheme.wallpaper.type - if (type is WallpaperType.Image) { - SectionItemView(disabled = chatModel.remoteHostId != null, click = { - val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) - ThemeManager.saveAndApplyWallpaper(baseTheme, null) - ThemeManager.removeTheme(defaultActiveTheme?.themeId) - removeWallpaperFile(type.filename) - saveThemeToDatabase(null) - }) { - Text( - stringResource(MR.strings.theme_remove_image), - color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - ) - } - SectionSpacer() - } - - SectionView(stringResource(MR.strings.settings_section_title_chat_colors).uppercase()) { - WallpaperSetupView( - wallpaperType, - baseTheme, - MaterialTheme.wallpaper, - MaterialTheme.appColors.sentMessage, - MaterialTheme.appColors.sentQuote, - MaterialTheme.appColors.receivedMessage, - MaterialTheme.appColors.receivedQuote, - editColor = { name -> - editColor(name) - }, - onTypeChange = { type -> - ThemeManager.saveAndApplyWallpaper(baseTheme, type) - saveThemeToDatabase(null) + SectionView { + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get()) }, + onChooseType = onChooseType ) + val type = MaterialTheme.wallpaper.type + if (type is WallpaperType.Image) { + SectionItemView(disabled = chatModel.remoteHostId != null, click = { + val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, null) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(type.filename) + saveThemeToDatabase(null) + }) { + Text( + stringResource(MR.strings.theme_remove_image), + color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + } } SectionDividerSpaced() + WallpaperSetupView( + wallpaperType, + baseTheme, + MaterialTheme.wallpaper, + MaterialTheme.appColors.sentMessage, + MaterialTheme.appColors.sentQuote, + MaterialTheme.appColors.receivedMessage, + MaterialTheme.appColors.receivedQuote, + editColor = { name -> + editColor(name) + }, + onTypeChange = { type -> + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + saveThemeToDatabase(null) + }, + firstSectionTitle = stringResource(MR.strings.settings_section_title_chat_colors), + ) + SectionDividerSpaced() + CustomizeThemeColorsSection(currentTheme) { name -> editColor(name) } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() val currentOverrides = remember(currentTheme) { ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) } val canResetColors = currentTheme.base.hasChangedAnyColor(currentOverrides) if (canResetColors) { - SectionItemView({ - ThemeManager.resetAllThemeColors() - saveThemeToDatabase(null) - }) { - Text(generalGetString(MR.strings.reset_color), color = colors.primary) + SectionView { + SectionItemView({ + ThemeManager.resetAllThemeColors() + saveThemeToDatabase(null) + }) { + Text(generalGetString(MR.strings.reset_color), color = colors.primary) + } } - SectionSpacer() + SectionDividerSpaced() } SectionView { @@ -1007,7 +1014,7 @@ object AppearanceScope { SimpleXThemeOverride(currentColors()) { ChatThemePreview(theme, wallpaperImage, wallpaperType, previewBackgroundColor, previewTintColor) } - SectionSpacer() + SectionDividerSpaced() } var currentColor by remember { mutableStateOf(initialColor) } @@ -1084,7 +1091,7 @@ object AppearanceScope { }) { Text(generalGetString(MR.strings.reset_single_color), color = colors.primary) } - SectionSpacer() + SectionDividerSpaced() } } @@ -1188,75 +1195,82 @@ fun WallpaperSetupView( initialReceivedQuoteColor: Color, editColor: (ThemeColor) -> Unit, onTypeChange: (WallpaperType?) -> Unit, + firstSectionTitle: String? = null, ) { - if (wallpaperType is WallpaperType.Image) { - val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) } - val values = remember { - WallpaperScaleType.entries.map { it to generalGetString(it.text) } - } - ExposedDropDownSettingRow( - stringResource(MR.strings.wallpaper_scale), - values, - state, - onSelected = { scaleType -> - onTypeChange(wallpaperType.copy(scaleType = scaleType)) - } - ) - } + val hasWallpaperSettings = wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image - if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) { - val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) } - Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { - Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp)) - Slider( - state.value, - valueRange = 0.5f..2f, - onValueChange = { - if (wallpaperType is WallpaperType.Preset) { - onTypeChange(wallpaperType.copy(scale = it)) - } else if (wallpaperType is WallpaperType.Image) { - onTypeChange(wallpaperType.copy(scale = it)) - } + if (hasWallpaperSettings) { + SectionView(firstSectionTitle) { + if (wallpaperType is WallpaperType.Image) { + val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) } + val values = remember { + WallpaperScaleType.entries.map { it to generalGetString(it.text) } } - ) + ExposedDropDownSettingRow( + stringResource(MR.strings.wallpaper_scale), + values, + state, + onSelected = { scaleType -> + onTypeChange(wallpaperType.copy(scaleType = scaleType)) + } + ) + } + + if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) { + val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) } + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp)) + Slider( + state.value, + valueRange = 0.5f..2f, + onValueChange = { + if (wallpaperType is WallpaperType.Preset) { + onTypeChange(wallpaperType.copy(scale = it)) + } else if (wallpaperType is WallpaperType.Image) { + onTypeChange(wallpaperType.copy(scale = it)) + } + } + ) + } + } + + val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) { + val title = generalGetString(MR.strings.color_wallpaper_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor) + } + val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) { + val title = generalGetString(MR.strings.color_wallpaper_tint) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + } } + SectionDividerSpaced() } - if (wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image) { - val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background) - SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) { - val title = generalGetString(MR.strings.color_wallpaper_background) + SectionView(if (!hasWallpaperSettings) firstSectionTitle else null) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) { + val title = generalGetString(MR.strings.color_sent_message) Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor) } - val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme) - SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) { - val title = generalGetString(MR.strings.color_wallpaper_tint) + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) { + val title = generalGetString(MR.strings.color_sent_quote) Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) { + val title = generalGetString(MR.strings.color_received_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) { + val title = generalGetString(MR.strings.color_received_quote) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor) } - SectionSpacer() - } - - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) { - val title = generalGetString(MR.strings.color_sent_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) { - val title = generalGetString(MR.strings.color_sent_quote) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) { - val title = generalGetString(MR.strings.color_received_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) { - val title = generalGetString(MR.strings.color_received_quote) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index cb36e4ae1a..43863d5794 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR @Composable @@ -38,12 +39,14 @@ fun CallSettingsLayout( ) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_calls)) - val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { SectionItemView(editIceServers) { Text(stringResource(MR.strings.webrtc_ice_servers)) } - val enabled = remember { mutableStateOf(true) } - LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) + if (appPlatform.isAndroid) { + val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } + val enabled = remember { mutableStateOf(true) } + LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) + } SettingsPreferenceItem(null, stringResource(MR.strings.always_use_relay), webrtcPolicyRelay) } SectionTextFooter( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index dcb71a552d..2c729149d0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -4,9 +4,12 @@ import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionView +import androidx.compose.foundation.background import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.ui.theme.* import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -29,14 +32,14 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.start.showModalCloseable { TerminalView(false) } } } ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) - SectionTextFooter( - generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + - generalGetString(MR.strings.developer_options) - ) } + SectionTextFooter( + generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + + generalGetString(MR.strings.developer_options) + ) if (devTools.value) { - SectionDividerSpaced(maxTopPadding = true) - SectionView(stringResource(MR.strings.developer_options_section).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.developer_options_section)) { SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) { DefaultSwitch( checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG, @@ -59,15 +62,15 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> SettingsPreferenceItem(painterResource(MR.images.ic_avg_pace), stringResource(MR.strings.show_slow_api_calls), appPreferences.showSlowApiCalls) } } - SectionDividerSpaced(maxTopPadding = true) - SectionView(stringResource(MR.strings.deprecated_options_section).uppercase()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.deprecated_options_section)) { val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it }) - SectionBottomSpacer() } + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index 55bd796a3b..4a3806ab89 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -68,7 +68,7 @@ private fun HiddenProfileLayout( val passwordValid by remember { derivedStateOf { hidePassword.value == hidePassword.value.trim() } } val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } } val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } } - SectionView(stringResource(MR.strings.hidden_profile_password).uppercase()) { + SectionView(stringResource(MR.strings.hidden_profile_password)) { SectionItemViewWithoutMinPadding { PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 2fc427cd2e..91324bb39a 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 @@ -4,8 +4,11 @@ import SectionBottomSpacer import SectionTextFooter import SectionView import SectionViewSelectable +import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import chat.simplex.common.ui.theme.* import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.capitalize @@ -21,43 +24,28 @@ import kotlin.collections.ArrayList fun NotificationsSettingsView( chatModel: ChatModel, ) { - val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode -> - chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name) - chatModel.notificationPreviewMode.value = mode - } - NotificationsSettingsLayout( notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }, - notificationPreviewMode = chatModel.notificationPreviewMode, - showPage = { page -> + showNotificationsMode = { ModalManager.start.showModalCloseable(true) { - when (page) { - CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } - CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected) - } + NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } } }, ) } -enum class CurrentPage { - NOTIFICATIONS_MODE, NOTIFICATION_PREVIEW_MODE -} - @Composable fun NotificationsSettingsLayout( notificationsMode: State<NotificationsMode>, - notificationPreviewMode: State<NotificationPreviewMode>, - showPage: (CurrentPage) -> Unit, + showNotificationsMode: () -> Unit, ) { val modes = remember { notificationModes() } - val previewModes = remember { notificationPreviewModes() } ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { if (appPlatform == AppPlatform.ANDROID) { - SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { + SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), showNotificationsMode) { Text( modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "", maxLines = 1, @@ -66,17 +54,9 @@ fun NotificationsSettingsLayout( ) } } - SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { - Text( - previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - } - if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { - SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization)) - } + } + if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { + SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization)) } SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index fe9137ee35..63f3491d80 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -1,13 +1,15 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView +import SectionDividerSpaced import SectionTextFooter import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme +import androidx.compose.ui.Modifier +import chat.simplex.common.ui.theme.* import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -47,6 +49,7 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { if (preferences == currentPreferences) close() else showUnsavedChangesAlert({ savePrefs(close) }, close) }, + cardScreen = true, ) { PreferencesLayout( preferences, @@ -81,27 +84,27 @@ private fun PreferencesLayout( onTTLUpdated = onTTLUpdated ) - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) } FeatureSection(ChatFeature.FullDelete, allowFullDeletion) { applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it))) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.allow) } FeatureSection(ChatFeature.Reactions, allowReactions) { applyPrefs(preferences.copy(reactions = SimpleChatPreference(allow = it))) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) } FeatureSection(ChatFeature.Voice, allowVoice) { applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it))) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced() val allowCalls = remember(preferences) { mutableStateOf(preferences.calls.allow) } FeatureSection(ChatFeature.Calls, allowCalls) { applyPrefs(preferences.copy(calls = SimpleChatPreference(allow = it))) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() ResetSaveButtons( reset = reset, save = savePrefs, 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 2771b5ac62..59136e90d2 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 @@ -1,10 +1,11 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import SectionDividerSpaced import SectionItemView +import SectionDividerSpaced import SectionTextFooter import SectionView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -14,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp @@ -72,6 +74,48 @@ fun PrivacySettingsView( stringResource(MR.strings.sanitize_links_toggle), chatModel.controller.appPrefs.privacySanitizeLinks ) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { + appPrefs.privacyMediaBlurRadius.set(it) + } + } + + val currentUser = chatModel.currentUser.value + if (currentUser != null && !chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced() + ContacRequestsFromGroupsSection( + currentUser = currentUser, + setAutoAcceptGrpDirectInvs = { enable -> + withApi { + chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) + chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + } + } + ) + } + + SectionDividerSpaced() + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_more_horiz), + stringResource(MR.strings.more_privacy), + showSettingsModal { MorePrivacyView(it) } + ) + } + SectionBottomSpacer() + } +} + +@Composable +fun MorePrivacyView(chatModel: ChatModel) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.more_privacy)) + + SectionView(stringResource(MR.strings.settings_section_title_chats)) { SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -90,6 +134,17 @@ fun PrivacySettingsView( chatModel.draftChatId.value = null } }) + SettingsPreferenceItem( + painterResource(MR.images.ic_tag), + stringResource(MR.strings.verify_simplex_names), + chatModel.controller.appPrefs.privacyVerifySimplexNames + ) + // hidden until message signing is user-facing (recipient-only stage) +// SettingsPreferenceItem( +// painterResource(MR.images.ic_verified), +// stringResource(MR.strings.show_signature), +// chatModel.controller.appPrefs.privacyShowSignature +// ) } SectionDividerSpaced() @@ -97,11 +152,8 @@ fun PrivacySettingsView( SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) - BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { - appPrefs.privacyMediaBlurRadius.set(it) - } SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.show_encryption), chatModel.controller.appPrefs.privacyShowEncryption) } SectionTextFooter( if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) { @@ -110,9 +162,34 @@ fun PrivacySettingsView( stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers) } ) + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.notifications)) { + val previewModes = remember { notificationPreviewModes() } + val notificationPreviewMode = remember { chatModel.notificationPreviewMode } + SettingsActionItemWithContent( + painterResource(MR.images.ic_visibility_off), + stringResource(MR.strings.settings_notification_preview_mode_title), + click = { + ModalManager.start.showModalCloseable(true) { + NotificationPreviewView(notificationPreviewMode) { mode -> + chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name) + chatModel.notificationPreviewMode.value = mode + } + } + } + ) { + Text( + previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.secondary + ) + } + } val currentUser = chatModel.currentUser.value - if (currentUser != null) { + if (currentUser != null && !chatModel.desktopNoUserNoRemote) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { withLongRunningApi(slow = 60_000) { val mrs = UserMsgReceiptSettings(enable, clearOverrides) @@ -163,57 +240,40 @@ fun PrivacySettingsView( } } - fun setAutoAcceptGrpDirectInvs(enable: Boolean) { - withApi { - chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) - chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + SectionDividerSpaced() + DeliveryReceiptsSection( + currentUser = currentUser, + setOrAskSendReceiptsContacts = { enable -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Direct) { + val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (contactReceiptsOverrides == 0) { + setSendReceiptsContacts(enable, clearOverrides = false) + } else { + showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) + } + }, + setOrAskSendReceiptsGroups = { enable -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Group) { + val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (groupReceiptsOverrides == 0) { + setSendReceiptsGroups(enable, clearOverrides = false) + } else { + showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) + } } - } - - if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced(maxTopPadding = true) - ContacRequestsFromGroupsSection( - currentUser = currentUser, - setAutoAcceptGrpDirectInvs = { enable -> - setAutoAcceptGrpDirectInvs(enable) - } - ) - - SectionDividerSpaced(maxTopPadding = true) - DeliveryReceiptsSection( - currentUser = currentUser, - setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Direct) { - val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (contactReceiptsOverrides == 0) { - setSendReceiptsContacts(enable, clearOverrides = false) - } else { - showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) - } - }, - setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Group) { - val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (groupReceiptsOverrides == 0) { - setSendReceiptsGroups(enable, clearOverrides = false) - } else { - showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) - } - } - ) - } + ) } SectionBottomSpacer() } @@ -617,46 +677,46 @@ fun SimplexLockView( } } } - if (performLA.value && laMode.value == LAMode.PASSCODE) { - SectionDividerSpaced() - SectionView(stringResource(MR.strings.self_destruct_passcode).uppercase()) { - val openInfo = { - ModalManager.start.showModal { - SelfDestructInfoView() - } + } + if (performLA.value && laMode.value == LAMode.PASSCODE) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.self_destruct_passcode)) { + val openInfo = { + ModalManager.start.showModal { + SelfDestructInfoView() } - SettingsActionItemWithContent(null, null, click = openInfo) { - SharedPreferenceToggleWithIcon( - stringResource(MR.strings.enable_self_destruct), - painterResource(MR.images.ic_info), - openInfo, - remember { selfDestructPref.state }.value - ) { - toggleSelfDestruct(selfDestructPref) - } + } + SettingsActionItemWithContent(null, null, click = openInfo) { + SharedPreferenceToggleWithIcon( + stringResource(MR.strings.enable_self_destruct), + painterResource(MR.images.ic_info), + openInfo, + remember { selfDestructPref.state }.value + ) { + toggleSelfDestruct(selfDestructPref) } + } - if (remember { selfDestructPref.state }.value) { - Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { - Text( - stringResource(MR.strings.self_destruct_new_display_name), - fontSize = 16.sp, - modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) - ) - ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) - LaunchedEffect(selfDestructDisplayName.value) { - val new = selfDestructDisplayName.value - if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { - selfDestructDisplayNamePref.set(new) - } + if (remember { selfDestructPref.state }.value) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) { + Text( + stringResource(MR.strings.self_destruct_new_display_name), + fontSize = 16.sp, + modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF) + ) + ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) }) + LaunchedEffect(selfDestructDisplayName.value) { + val new = selfDestructDisplayName.value + if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) { + selfDestructDisplayNamePref.set(new) } } - SectionItemView({ changeSelfDestructPassword() }) { - Text( - stringResource(MR.strings.change_self_destruct_passcode), - color = MaterialTheme.colors.primary - ) - } + } + SectionItemView({ changeSelfDestructPassword() }) { + Text( + stringResource(MR.strings.change_self_destruct_passcode), + color = MaterialTheme.colors.primary + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt new file mode 100644 index 0000000000..7ab1ffa33d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetSimplexNameView.kt @@ -0,0 +1,185 @@ +package chat.simplex.common.views.usersettings + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionItemViewSpaceBetween +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.* +import chat.simplex.common.views.chat.item.openBrowserAlert +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.* + +// Each dot-separated label is ASCII letters/digits with single internal hyphens (mirrors simplexmq SimplexName.hs nameLabelP). +private val simplexNameLabelRegex = Regex("[A-Za-z0-9]+(-[A-Za-z0-9]+)*") + +// Set the user's own (prefix "@") or a channel's (prefix "#") SimpleX name. +// The field is prefilled with the full prefixed name; `save` receives the encoded name (or null to +// clear) and returns true on success (it shows its own error alert otherwise). +// `registerBackgroundClose` is set by the contact/start-panel call site so a desktop background click +// routes through the save-on-close prompt; the channel call site (opened via ModalManager.end) leaves it false. +@Composable +fun SetSimplexDomainView( + title: String, + footer: String, + placeholder: String, + simplexName: String, + registerBackgroundClose: Boolean = false, + broadcastWarning: String? = null, + save: suspend (String?) -> Boolean, + close: () -> Unit +) { + val name = rememberSaveable { mutableStateOf(simplexName) } + val saving = remember { mutableStateOf(false) } + val editing = rememberSaveable { mutableStateOf(simplexName.isBlank()) } + val uriHandler = LocalUriHandler.current + val clipboard = LocalClipboardManager.current + + fun addSimplexTLD(s: String): String { + return if (s.contains(".")) s else "$s.simplex" + } + + fun normalized(s: String): String? { + val t = s.trim() + return when { + t.isEmpty() -> null + t.startsWith("@") || t.startsWith("#") -> addSimplexTLD(t.substring(1).lowercase()) + else -> addSimplexTLD(t.lowercase()) + } + } + + // An empty field is valid (it means "remove the name"). Otherwise check the SimpleX-name grammar on + // the normalized value; A-Z is accepted because the core lowercases the name on accept. + fun isValidName(s: String): Boolean { + val n = normalized(s) ?: return true + if (n.length > 253) return false + val labels = n.split(".") + if (labels.size < 2) return false + return labels.all { it.length in 1..63 && simplexNameLabelRegex.matches(it) } + } + + val unchanged = normalized(name.value) == normalized(simplexName) + val isValid = isValidName(name.value) + + fun doSave(close: () -> Unit) { + withBGApi { + saving.value = true + val ok = try { save(normalized(name.value)) } catch (e: Exception) { + Log.e(TAG, "SetSimplexDomainView save: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_simplex_name), e.message ?: "") + false + } + saving.value = false + if (ok) withContext(Dispatchers.Main) { + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null + close() + } + } + } + + // Reads name.value live so it stays correct when invoked from the background-click handler registered once. + // Returns true when it consumes the close (shows the prompt), false when it lets the close proceed. + fun onClose(close: () -> Unit): Boolean { + val valid = isValidName(name.value) + val changed = normalized(name.value) != normalized(simplexName) + return if (changed && valid) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.save_simplex_name_question), + text = broadcastWarning, + confirmText = generalGetString(MR.strings.save_verb), + onConfirm = { doSave(close) }, + dismissText = generalGetString(MR.strings.exit_without_saving), + onDismiss = { + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } else { + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null + close() + false + } + } + + DisposableEffect(Unit) { + if (registerBackgroundClose) { + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) + } + } + onDispose { + if (registerBackgroundClose) chatModel.centerPanelBackgroundClickHandler = null + } + } + + ModalView(close = { onClose(close) }, cardScreen = true) { + ColumnWithScrollBar { + AppBarTitle(title) + SectionView { + if (editing.value) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + SectionItemViewSpaceBetween(click = { focusRequester.requestFocus() }) { + Box(Modifier.weight(1f)) { + PlainTextEditor(name, placeholder = placeholder, contentPadding = PaddingValues(), focusRequester = focusRequester) + } + if (!isValid) { + Icon(painterResource(MR.images.ic_error), null, tint = MaterialTheme.colors.error) + } + } + } else { + SectionItemViewSpaceBetween(click = { + clipboard.setText(AnnotatedString(name.value)) + showToast(generalGetString(MR.strings.copied)) + }) { + Text(name.value) + Icon(painterResource(MR.images.ic_content_copy), stringResource(MR.strings.copy_verb), tint = MaterialTheme.colors.secondary) + } + } + } + SectionTextFooter(footer) + SectionDividerSpaced() + SectionView { + if (editing.value) { + SettingsActionItem( + painterResource(MR.images.ic_open_in_new), + stringResource(MR.strings.register_test_name), + { openBrowserAlert("https://github.com/simplex-chat/simplex-chat/blob/master/docs/guide/register-simplex-name.md", uriHandler) }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary + ) + SectionItemView({ if (broadcastWarning != null && !unchanged) AlertManager.shared.showAlertDialog(title = broadcastWarning, confirmText = generalGetString(MR.strings.save_verb), onConfirm = { doSave(close) }) else doSave(close) }, disabled = unchanged || saving.value || !isValid) { + Text( + stringResource(MR.strings.save_verb), + color = if (unchanged || saving.value || !isValid) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } else { + SectionItemView({ name.value = ""; editing.value = true }) { + Text(stringResource(MR.strings.remove_name), color = MaterialTheme.colors.primary) + } + } + } + SectionBottomSpacer() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 22270ea5bb..8c134cb361 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -1,8 +1,9 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import SectionDividerSpaced +import itemHPadding import SectionItemView +import SectionDividerSpaced import SectionView import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview @@ -21,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* @@ -29,29 +29,30 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.database.DatabaseView import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrateFromDeviceView +import chat.simplex.common.views.onboarding.GetStakeView import chat.simplex.common.views.onboarding.SimpleXInfo import chat.simplex.common.views.onboarding.WhatsNewView +import chat.simplex.common.views.onboarding.crowdfundingAvailable import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView import chat.simplex.res.MR @Composable fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) { - val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false + val showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } } SettingsLayout( stopped, chatModel.chatDbEncrypted.value == true, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, - remember { chatModel.controller.appPrefs.notificationsMode.state }, - user?.displayName, setPerformLA = setPerformLA, showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, - showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, + showSettingsModal = showSettingsModal, showSettingsModalWithSearch = { modalView -> ModalManager.start.showCustomModal { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, + cardScreen = true, showSearch = true, searchAlwaysVisible = true, onSearchValueChanged = { search.value = it }, @@ -60,12 +61,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( }, showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, showVersion = { - withBGApi { - val info = chatModel.controller.apiGetVersion() - if (info != null) { - ModalManager.start.showModal { VersionInfoView(info) } - } - } + ModalManager.start.showModal(cardScreen = true) { VersionInfoView(showSettingsModal, ::doWithAuth) } }, withAuth = ::doWithAuth, ) @@ -82,8 +78,6 @@ fun SettingsLayout( stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, - notificationsMode: State<NotificationsMode>, - userDisplayName: String?, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -96,30 +90,63 @@ fun SettingsLayout( LaunchedEffect(Unit) { hideKeyboard(view) } - val uriHandler = LocalUriHandler.current + val notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state } ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SectionView { SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_chat_database)) { + SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.your_privacy), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.help_and_support), showSettingsModal { HelpAndSupportView(it, showModal, showCustomModal) }) DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped) SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped) } - SectionDividerSpaced() + SectionView(stringResource(MR.strings.advanced_settings)) { + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) + if (appPlatform == AppPlatform.ANDROID) { + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) + AppShutdownItem() + AppVersionItem(showVersion) + } + + if (crowdfundingAvailable()) { + SectionDividerSpaced() + SectionView(stringResource(MR.strings.v7_0_invest)) { + SettingsActionItem( + painterResource(MR.images.ic_redeem), + stringResource(MR.strings.v7_0_crowdfunding), + { ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = true, close = close) } } + ) + } + } + SectionBottomSpacer() + } +} + +@Composable +fun HelpAndSupportView( + chatModel: ChatModel, + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), +) { + val uriHandler = LocalUriHandler.current + val stopped = chatModel.chatRunning.value == false + val userDisplayName = chatModel.currentUser.value?.displayName ?: "" + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.help_and_support)) + SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_contact)) { if (!chatModel.desktopNoUserNoRemote) { SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) } @@ -127,27 +154,29 @@ fun SettingsLayout( } SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_support)) { - if (!BuildConfigCommon.ANDROID_BUNDLE) { + SectionView(stringResource(MR.strings.settings_section_title_support_project)) { + if (!platform.androidIsPlayStoreBuild) { ContributeItem(uriHandler) } - RateAppItem(uriHandler) + if (appPlatform.isAndroid) { + RateAppItem(uriHandler) + } StarOnGithubItem(uriHandler) } - SectionDividerSpaced() - - SettingsSectionApp(showSettingsModal, showVersion, withAuth) SectionBottomSpacer() } } @Composable -expect fun SettingsSectionApp( +expect fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) +// Shutdown is only available on Android; on desktop the app is closed via the window. +@Composable +expect fun AppShutdownItem() + @Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { SectionItemView(openDatabaseView) { Row( @@ -158,11 +187,11 @@ expect fun SettingsSectionApp( Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Icon( painterResource(MR.images.ic_database), - contentDescription = stringResource(MR.strings.database_passphrase_and_export), + contentDescription = stringResource(MR.strings.chat_data), tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange, ) TextIconSpaced(false) - Text(stringResource(MR.strings.database_passphrase_and_export)) + Text(stringResource(MR.strings.chat_data)) } if (stopped) { Icon( @@ -206,7 +235,7 @@ fun ChatLockItem( } } -@Composable private fun ContributeItem(uriHandler: UriHandler) { +@Composable fun ContributeItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) { Icon( painterResource(MR.images.ic_keyboard), @@ -218,7 +247,7 @@ fun ChatLockItem( } } -@Composable private fun RateAppItem(uriHandler: UriHandler) { +@Composable fun RateAppItem(uriHandler: UriHandler) { SectionItemView({ runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") } .onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") } @@ -234,7 +263,7 @@ fun ChatLockItem( } } -@Composable private fun StarOnGithubItem(uriHandler: UriHandler) { +@Composable fun StarOnGithubItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), @@ -349,9 +378,9 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: ( click, extraPadding = extraPadding, padding = if (extraPadding && icon != null) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = itemHPadding) else - PaddingValues(horizontal = DEFAULT_PADDING), + PaddingValues(horizontal = itemHPadding), disabled = disabled ) { if (icon != null) { @@ -485,8 +514,6 @@ fun PreviewSettingsLayout() { stopped = false, encrypted = false, passphraseSaved = false, - notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) }, - userDisplayName = "Alice", setPerformLA = { _ -> }, showModal = { {} }, showSettingsModal = { {} }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index e5c731f3b2..87660c7e65 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionCardShape import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -8,6 +9,7 @@ import SectionView import SectionViewWithButton import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.ui.layout.ContentScale import androidx.compose.foundation.shape.RoundedCornerShape @@ -32,6 +34,8 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.newchat.* import chat.simplex.common.BuildConfigCommon import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Composable fun UserAddressView( @@ -171,7 +175,7 @@ fun UserAddressView( ) } - ModalView(close = close) { + ModalView(close = close, cardScreen = true) { showLayout() } @@ -301,16 +305,16 @@ private fun UserAddressLayout( ) { if (userAddress == null) { if (!onboarding) { - SectionView(generalGetString(MR.strings.for_social_media).uppercase()) { + SectionView(generalGetString(MR.strings.for_social_media)) { CreateAddressButton(createAddress) } SectionDividerSpaced() - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + SectionView(generalGetString(MR.strings.or_to_share_privately)) { CreateOneTimeLinkButton() } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() SectionView { LearnMoreButton(learnMore) } @@ -336,7 +340,7 @@ private fun UserAddressLayout( val savedAddressSettingsState = remember { mutableStateOf(addressSettingsState.value) } SectionViewWithButton( - stringResource(MR.strings.for_social_media).uppercase(), + stringResource(MR.strings.for_social_media), titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null ) { SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value) @@ -350,29 +354,69 @@ private fun UserAddressLayout( share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) } } + ShareViaChatButton { + val shareViaChat = { + chatModel.sharedContent.value = SharedContent.MyAddress + chatModel.chatId.value = null + ModalManager.closeAllModalsEverywhere() + } + if (userAddress.shouldBeUpgraded) showAddShortLinkAlert { shareViaChat() } else shareViaChat() + } // ShareViaEmailButton { sendEmail(userAddress) } BusinessAddressToggle(addressSettingsState) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAddressSettings) - - if (addressSettingsState.value.businessAddress) { - SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) - } + } + if (addressSettingsState.value.businessAddress) { + SectionTextFooter(stringResource(MR.strings.add_your_team_members_to_conversations)) } - SectionDividerSpaced(maxTopPadding = addressSettingsState.value.businessAddress) - SectionView(generalGetString(MR.strings.or_to_share_privately).uppercase()) { + SectionDividerSpaced() + val domain = user?.profile?.contactDomain?.domain + SectionView(title = if (domain != null) generalGetString(MR.strings.your_simplex_name) else null) { + SettingsActionItem( + painterResource(MR.images.ic_at), + if (domain != null) "$domain" else generalGetString(MR.strings.get_simplex_name_beta), + click = { + ModalManager.start.showCustomModal { close -> + SetSimplexDomainView( + title = generalGetString(MR.strings.set_simplex_name), + footer = generalGetString(MR.strings.set_user_simplex_name_footer), + placeholder = "@yourname.testing", + simplexName = if (domain == null) "" else "@$domain", + registerBackgroundClose = true, + broadcastWarning = generalGetString(MR.strings.profile_update_will_be_sent_to_contacts), + save = { simplexDomain -> + try { + val u = chatModel.controller.apiSetUserDomain(user?.remoteHostId, simplexDomain) + withContext(Dispatchers.Main) { chatModel.updateUser(u) } + true + } catch (e: Exception) { + Log.e(TAG, "apiSetUserDomain: ${e.message}") + false + } + }, + close = close + ) + } + }, + iconColor = MaterialTheme.colors.secondary + ) + } + + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.or_to_share_privately)) { CreateOneTimeLinkButton() } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { LearnMoreButton(learnMore) } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { DeleteAddressButton(deleteAddress) - SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) } + SectionTextFooter(stringResource(MR.strings.your_contacts_will_remain_connected)) } } } @@ -402,6 +446,17 @@ private fun AddShortLinkButton(text: String, onClick: () -> Unit) { ) } +@Composable +private fun ShareViaChatButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_forward), + stringResource(MR.strings.share_via_chat), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + @Composable private fun CreateOneTimeLinkButton() { val closeAll = { ModalManager.start.closeModals() } @@ -495,7 +550,7 @@ private fun ModalData.UserAddressSettings( } } - ModalView(close = { onClose(close) }) { + ModalView(close = { onClose(close) }, cardScreen = true) { ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.address_settings), hostDevice(user?.remoteHostId)) Column( @@ -512,10 +567,10 @@ private fun ModalData.UserAddressSettings( } SectionDividerSpaced() - SectionView(stringResource(MR.strings.address_welcome_message).uppercase()) { + SectionView(stringResource(MR.strings.address_welcome_message)) { AutoReplyEditor(addressSettingsState) } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionDividerSpaced() saveAddressSettingsButton(addressSettingsState.value == savedAddressSettingsState.value) { saveAddressSettings(addressSettingsState.value, savedAddressSettingsState) @@ -699,7 +754,13 @@ private fun AcceptIncognitoToggle(addressSettingsState: MutableState<AddressSett @Composable private fun AutoReplyEditor(addressSettingsState: MutableState<AddressSettingsState>) { val autoReply = rememberSaveable { mutableStateOf(addressSettingsState.value.autoReply) } - TextEditor(autoReply, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional)) + TextEditor( + autoReply, + Modifier.height(100.dp), + placeholder = stringResource(MR.strings.enter_welcome_message_optional), + contentPadding = PaddingValues(), + shape = SectionCardShape + ) LaunchedEffect(autoReply.value) { if (autoReply.value != addressSettingsState.value.autoReply) { addressSettingsState.value = AddressSettingsState( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index 45cdee6108..f2a4cc7dac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -24,6 +24,7 @@ import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.platform.* import chat.simplex.common.views.* import chat.simplex.res.MR +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.net.URI @@ -39,10 +40,10 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { var profile by remember { mutableStateOf(user.profile.toProfile()) } UserProfileLayout( profile = profile, - close, - saveProfile = { displayName, fullName, shortDescr, image -> + close = close, + saveProfile = { displayName, fullName, shortDescr, description, image -> withBGApi { - val updatedProfile = profile.copy(displayName = displayName.trim(), fullName = fullName.trim(), shortDescr = shortDescr.trim().ifEmpty { null }, image = image) + val updatedProfile = profile.copy(displayName = displayName.trim(), fullName = fullName.trim(), shortDescr = shortDescr.trim().ifEmpty { null }, description = description.trim().ifEmpty { null }, image = image) val updated = chatModel.controller.apiUpdateProfile(user.remoteHostId, updatedProfile) if (updated != null) { val (newProfile, _) = updated @@ -60,12 +61,13 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { fun UserProfileLayout( profile: Profile, close: () -> Unit, - saveProfile: (String, String, String, String?) -> Unit, + saveProfile: (String, String, String, String, String?) -> Unit, ) { val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = remember { mutableStateOf(profile.displayName) } val fullName = remember { mutableStateOf(profile.fullName) } val shortDescr = remember { mutableStateOf(profile.shortDescr ?: "") } + val description = remember { mutableStateOf(profile.description ?: "") } val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) } val profileImage = rememberSaveable { mutableStateOf(profile.image) } val scope = rememberCoroutineScope() @@ -73,6 +75,8 @@ fun UserProfileLayout( val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } val focusRequester = remember { FocusRequester() } + val descrFocusRequester = remember { FocusRequester() } + var editingDescription by remember { mutableStateOf(false) } ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), sheetContent = { @@ -86,19 +90,75 @@ fun UserProfileLayout( sheetState = bottomSheetModalState, sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) ) { - val dataUnchanged = + fun dataUnchanged(): Boolean = displayName.value.trim() == profile.displayName && fullName.value.trim() == profile.fullName && shortDescr.value.trim() == (profile.shortDescr ?: "") && + description.value.trim() == (profile.description ?: "") && profile.image == profileImage.value - val closeWithAlert = { - if (dataUnchanged || !canSaveProfile(displayName.value, shortDescr.value, profile)) { - close() - } else { - showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) }, close) + fun onClose(close: () -> Unit): Boolean = if (dataUnchanged() || !canSaveProfile(displayName.value, shortDescr.value, profile)) { + chatModel.centerPanelBackgroundClickHandler = null + close() + false + } else { + showUnsavedChangesAlert( + { + chatModel.centerPanelBackgroundClickHandler = null + saveProfile(displayName.value, fullName.value, shortDescr.value, description.value, profileImage.value) + }, + { + chatModel.centerPanelBackgroundClickHandler = null + close() + } + ) + true + } + DisposableEffect(Unit) { + onDispose { chatModel.centerPanelBackgroundClickHandler = null } + } + LaunchedEffect(Unit) { + chatModel.centerPanelBackgroundClickHandler = { + onClose(close = { ModalManager.start.closeModals() }) } } - ModalView(close = closeWithAlert) { + LaunchedEffect(editingDescription) { + if (editingDescription) { + delay(200) + descrFocusRequester.requestFocus() + } + } + ModalView(close = if (editingDescription) ({ editingDescription = false }) else ({ onClose(close) })) { + if (editingDescription) { + // app bar is top (default) or bottom (one-handed) — mirror ColumnWithScrollBar's spacers + // so the entry area never runs under the app bar, keyboard, or system bars + val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state } + Column(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING)) { + if (oneHandUI.value) { + Spacer(Modifier.padding(top = DEFAULT_PADDING + 5.dp).windowInsetsTopHeight(WindowInsets.statusBars)) + } else { + Spacer(Modifier.statusBarsPadding().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + } + AppBarTitle(stringResource(MR.strings.profile_description__field), withPadding = false) + // weight goes on the Box (a direct Column child); TextEditor forwards its modifier + // to the inner BasicTextField, where weight would be ignored + Box(Modifier.weight(1f, fill = false).padding(bottom = DEFAULT_PADDING)) { + TextEditor( + description, + Modifier.heightIn(min = 140.dp), + placeholder = stringResource(MR.strings.enter_description_optional), + contentPadding = PaddingValues(), + focusRequester = descrFocusRequester, + maxLines = Int.MAX_VALUE + ) + } + if (oneHandUI.value) { + Spacer(Modifier.navigationBarsPadding().padding(bottom = AppBarHeight * fontSizeSqrtMultiplier)) + } else { + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) + } + } + return@ModalView + } ColumnWithScrollBar( Modifier .padding(horizontal = DEFAULT_PADDING), @@ -168,8 +228,15 @@ fun UserProfileLayout( ProfileNameField(shortDescr) Spacer(Modifier.height(DEFAULT_PADDING)) - val enabled = !dataUnchanged && canSaveProfile(displayName.value, shortDescr.value, profile) - val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, shortDescr.value, profileImage.value) } + Text( + stringResource(if (description.value.isBlank()) MR.strings.add_description else MR.strings.edit_description), + color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { editingDescription = true } + ) + + Spacer(Modifier.height(DEFAULT_PADDING)) + val enabled = !dataUnchanged() && canSaveProfile(displayName.value, shortDescr.value, profile) + val saveModifier: Modifier = Modifier.clickable(enabled) { saveProfile(displayName.value, fullName.value, shortDescr.value, description.value, profileImage.value) } val saveColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary Text( stringResource(MR.strings.save_and_notify_contacts), @@ -248,7 +315,7 @@ fun PreviewUserProfileLayoutEditOff() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _, _ -> } + saveProfile = { _, _, _, _, _ -> } ) } } @@ -264,7 +331,7 @@ fun PreviewUserProfileLayoutEditOn() { UserProfileLayout( profile = Profile.sampleData, close = {}, - saveProfile = { _, _, _, _ -> } + saveProfile = { _, _, _, _, _ -> } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index d7ddb6b950..ac21fb6b23 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -1,7 +1,6 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer -import SectionDivider import SectionItemView import SectionItemViewSpaceBetween import SectionItemViewWithoutMinPadding @@ -177,7 +176,7 @@ private fun UserProfilesLayout( SectionView { for (user in filteredUsers) { UserView(user, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile) - SectionDivider() + Divider(Modifier.padding(horizontal = 8.dp)) } if (searchTextOrPassword.value.trim().isEmpty()) { SectionItemView(addUser, minHeight = 68.dp) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 52addd146b..5070c3c0aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,33 +1,55 @@ package chat.simplex.common.views.usersettings +import SectionBottomSpacer +import SectionDividerSpaced +import SectionView +import itemHPadding +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.ChatModel import chat.simplex.common.model.CoreVersionInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.appPlatform -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.AppBarTitle import chat.simplex.res.MR @Composable -fun VersionInfoView(info: CoreVersionInfo) { - ColumnWithScrollBar( - Modifier.padding(horizontal = DEFAULT_PADDING), - ) { - AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) - if (appPlatform.isAndroid) { - Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) - Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) - } else { - Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME)) - Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE)) +fun VersionInfoView( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, +) { + val versionInfo = remember { mutableStateOf<CoreVersionInfo?>(null) } + LaunchedEffect(Unit) { + versionInfo.value = chatModel.controller.apiGetVersion() + } + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.app_version_title)) + SectionView { + Column(Modifier.padding(horizontal = itemHPadding, vertical = DEFAULT_PADDING_HALF)) { + if (appPlatform.isAndroid) { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) + Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) + } else { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME)) + Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE)) + } + versionInfo.value?.let { info -> + Text(String.format(stringResource(MR.strings.core_version), info.version)) + val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit + Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit)) + } + } } - Text(String.format(stringResource(MR.strings.core_version), info.version)) - val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit - Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit)) + SectionDividerSpaced() + + AdvancedSettingsAppSection(showSettingsModal, withAuth) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 8c38070c98..42746006a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -8,6 +8,7 @@ import SectionTextFooter import SectionView import SectionViewSelectableCards import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -158,6 +159,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - }, close) } }, + cardScreen = true, ) { AdvancedNetworkSettingsLayout( currentRemoteHost = currentRemoteHost, @@ -234,13 +236,13 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy) } SectionTextFooter(stringResource(MR.strings.private_routing_explanation)) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() - SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) { + SectionView(stringResource(MR.strings.network_session_mode_transport_isolation)) { SessionModePicker(sessionMode, showModal, updateSessionMode) } SectionDividerSpaced() - SectionView(stringResource(MR.strings.network_smp_web_port_section_title).uppercase()) { + SectionView(stringResource(MR.strings.network_smp_web_port_section_title)) { ExposedDropDownSettingRow( stringResource(MR.strings.network_smp_web_port_toggle), SMPWebPortServers.entries.map { it to stringResource(it.text) }, @@ -251,9 +253,9 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - if (smpWebPortServers.value == SMPWebPortServers.Preset) stringResource(MR.strings.network_smp_web_port_preset_footer) else String.format(stringResource(MR.strings.network_smp_web_port_footer), if (smpWebPortServers.value == SMPWebPortServers.All) "443" else "5223") ) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() - SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { + SectionView(stringResource(MR.strings.network_option_tcp_connection)) { SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeoutInteractive, @@ -330,7 +332,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() - } } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { SectionItemView(reset, disabled = resetDisabled) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt index 1c68e780dc..9a2d7f8e61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings.networkAndServers import SectionBottomSpacer +import SectionCardShape import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween @@ -10,9 +11,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.sp import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource @@ -149,7 +148,8 @@ fun ChatRelayView( text = generalGetString(MR.strings.check_relay_address) ) } - } + }, + cardScreen = true, ) { ChatRelayLayout( relayToEdit, @@ -182,7 +182,7 @@ private fun ChatRelayLayout( @Composable private fun PresetRelay(relay: MutableState<UserChatRelay>, testing: MutableState<Boolean>) { - SectionView(stringResource(MR.strings.preset_relay_address).uppercase()) { + SectionView(stringResource(MR.strings.preset_relay_address)) { SelectionContainer { Text( relay.value.address, @@ -192,7 +192,7 @@ private fun PresetRelay(relay: MutableState<UserChatRelay>, testing: MutableStat } } SectionDividerSpaced() - SectionView(stringResource(MR.strings.preset_relay_name).uppercase()) { + SectionView(stringResource(MR.strings.preset_relay_name)) { SectionItemView { Text(relay.value.displayName) } @@ -229,43 +229,35 @@ private fun CustomRelay( } SectionView( - stringResource(MR.strings.your_relay_address).uppercase(), + stringResource(MR.strings.your_relay_address), icon = painterResource(MR.images.ic_error), iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent, ) { TextEditor( relayAddress, - Modifier.height(144.dp) + Modifier.height(144.dp), + contentPadding = PaddingValues(), + shape = SectionCardShape ) } SectionDividerSpaced(maxTopPadding = true) - Column { - val iconSize = with(LocalDensity.current) { 21.sp.toDp() } - Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) { - Text( - stringResource(MR.strings.your_relay_name).uppercase(), - color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp - ) - IconButton( - onClick = { if (!validName.value) showInvalidRelayNameAlert(relayName) }, - enabled = !validName.value, - modifier = Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize) - ) { - Icon( - painterResource(MR.images.ic_error), null, - tint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent - ) - } - } - Column(Modifier.fillMaxWidth()) { - TextEditor( - relayName, - Modifier, - placeholder = generalGetString(MR.strings.enter_relay_name), - enabled = relay.value.tested != true - ) - } + SectionView( + stringResource(MR.strings.your_relay_name), + icon = painterResource(MR.images.ic_error), + iconTint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent, + onIconClick = if (!validName.value) { + { showInvalidRelayNameAlert(relayName) } + } else null + ) { + TextEditor( + relayName, + Modifier, + placeholder = generalGetString(MR.strings.enter_relay_name), + contentPadding = PaddingValues(), + shape = SectionCardShape, + enabled = relay.value.tested != true + ) } if (relay.value.tested != true) { SectionTextFooter(annotatedStringResource(MR.strings.test_relay_to_retrieve_name)) @@ -291,7 +283,7 @@ private fun UseRelaySection( testing: MutableState<Boolean> ) { val scope = rememberCoroutineScope() - SectionView(stringResource(MR.strings.use_relay).uppercase()) { + SectionView(stringResource(MR.strings.use_relay)) { SectionItemViewSpaceBetween( click = { testing.value = true @@ -377,7 +369,7 @@ fun ModalData.NewChatRelayView( ModalView(close = { addChatRelay(relayToEdit.value, userServers, serverErrors, serverWarnings, rhId, close) - }) { + }, cardScreen = true) { NewChatRelayLayout(relayToEdit) } } 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 a62a58cb10..8241422d0a 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 @@ -9,6 +9,7 @@ import SectionTextFooter import SectionView import SectionViewSelectable import TextIconSpaced +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.* @@ -84,7 +85,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { onClose(close = { ModalManager.start.closeModals() }) } } - ModalView(close = { onClose(closeNetworkAndServers) }) { + ModalView(close = { onClose(closeNetworkAndServers) }, cardScreen = true) { NetworkAndServersLayout( currentRemoteHost = currentRemoteHost, networkUseSocksProxy = networkUseSocksProxy, @@ -210,7 +211,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { AppBarTitle(stringResource(MR.strings.network_and_servers)) // TODO: Review this and socks. if (!chatModel.desktopNoUserNoRemote) { - SectionView(generalGetString(MR.strings.network_preset_servers_title).uppercase()) { + SectionView(generalGetString(MR.strings.network_preset_servers_title)) { userServers.value.forEachIndexed { index, srv -> srv.operator?.let { ServerOperatorRow(index, it, currUserServers, userServers, serverErrors, serverWarnings, currentRemoteHost?.remoteHostId) } } @@ -262,36 +263,37 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) } }) SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) - if (networkUseSocksProxy.value) { - SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) - SectionDividerSpaced(maxTopPadding = true) - } else { - SectionDividerSpaced(maxBottomPadding = false) - } } } - val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) - - SectionItemView( - { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, - disabled = saveDisabled, - ) { - Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + if (currentRemoteHost == null && networkUseSocksProxy.value) { + SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) } - val serversErr = globalServersError(serverErrors.value) - if (serversErr != null) { - SectionCustomFooter { - ServersErrorFooter(serversErr) + + SectionDividerSpaced() + SectionView { + val saveDisabled = !serversCanBeSaved(currUserServers.value, userServers.value, serverErrors.value) + SectionItemView( + { scope.launch { saveServers(rhId = currentRemoteHost?.remoteHostId, currUserServers, userServers) } }, + disabled = saveDisabled, + ) { + Text(stringResource(MR.strings.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) + } + } + val serversErrs = globalServersErrors(serverErrors.value) + if (serversErrs.isNotEmpty()) { + serversErrs.forEach { err -> + SectionCustomFooter { + ServersErrorFooter(err) + } } } else if (serverErrors.value.isNotEmpty()) { SectionCustomFooter { ServersErrorFooter(generalGetString(MR.strings.errors_in_servers_configuration)) } } - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversWarn != null) { + globalServersWarnings(serverWarnings.value).forEach { warn -> SectionCustomFooter { - ServersWarningFooter(serversWarn) + ServersWarningFooter(warn) } } @@ -303,7 +305,7 @@ fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { if (appPlatform.isAndroid) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.settings_section_title_network_connection).uppercase()) { + SectionView(generalGetString(MR.strings.settings_section_title_network_connection)) { val info = remember { chatModel.networkInfo }.value SettingsActionItemWithContent(icon = null, info.networkType.text) { Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = if (info.online) Color.Green else MaterialTheme.colors.error) @@ -466,10 +468,11 @@ fun SocksProxySettings( ) } }, + cardScreen = true, ) { ColumnWithScrollBar { AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) - SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { + SectionView(stringResource(MR.strings.network_socks_proxy)) { Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { DefaultConfigurableTextField( hostUnsaved, @@ -492,12 +495,12 @@ fun SocksProxySettings( UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) { onionHosts.value = it } - SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) } + SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() - SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) { + SectionView(stringResource(MR.strings.network_proxy_auth)) { PreferenceToggle( stringResource(MR.strings.network_proxy_random_credentials), checked = proxyAuthRandomUnsaved.value, @@ -523,10 +526,10 @@ fun SocksProxySettings( ) } } - SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode)) } + SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode)) - SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true) + SectionDividerSpaced() SectionView { SectionItemView({ @@ -952,23 +955,11 @@ fun serversCanBeSaved( return userServers != currUserServers && serverErrors.isEmpty() } -fun globalServersError(serverErrors: List<UserServersError>): String? { - for (err in serverErrors) { - if (err.globalError != null) { - return err.globalError - } - } - return null -} +fun globalServersErrors(serverErrors: List<UserServersError>): List<String> = + serverErrors.mapNotNull { it.globalError } -fun globalServersWarning(serverWarnings: List<UserServersWarning>): String? { - for (warn in serverWarnings) { - if (warn.globalWarning != null) { - return warn.globalWarning - } - } - return null -} +fun globalServersWarnings(serverWarnings: List<UserServersWarning>): List<String> = + serverWarnings.mapNotNull { it.globalWarning } fun globalSMPServersError(serverErrors: List<UserServersError>): String? { for (err in serverErrors) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index 9e11b9a932..53277c9ccd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* +import androidx.compose.foundation.background import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -181,7 +182,7 @@ fun OperatorViewLayout( val duplicateHosts = findDuplicateHosts(serverErrors.value) Column { - SectionView(generalGetString(MR.strings.operator).uppercase()) { + SectionView(generalGetString(MR.strings.operator)) { SectionItemView({ ModalManager.start.showModalCloseable { _ -> OperatorInfoView(operator) } }) { Row( Modifier.fillMaxWidth(), @@ -210,15 +211,19 @@ fun OperatorViewLayout( rhId = rhId ) } - val serversErr = globalServersError(serverErrors.value) - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversErr != null) { - SectionCustomFooter { - ServersErrorFooter(serversErr) + val serversErrs = globalServersErrors(serverErrors.value) + val serversWarns = globalServersWarnings(serverWarnings.value) + if (serversErrs.isNotEmpty()) { + serversErrs.forEach { err -> + SectionCustomFooter { + ServersErrorFooter(err) + } } - } else if (serversWarn != null) { - SectionCustomFooter { - ServersWarningFooter(serversWarn) + } else if (serversWarns.isNotEmpty()) { + serversWarns.forEach { warn -> + SectionCustomFooter { + ServersWarningFooter(warn) + } } } else { val footerText = when (val c = operator.conditionsAcceptance) { @@ -238,7 +243,7 @@ fun OperatorViewLayout( if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) { val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value) SectionDividerSpaced() - SectionView(generalGetString(MR.strings.chat_relays).uppercase()) { + SectionView(generalGetString(MR.strings.chat_relays)) { userServers.value[operatorIndex].chatRelays.forEachIndexed { index, relay -> if (!relay.deleted) { ChatRelayViewLink(relay, duplicateRelayAddresses) { @@ -252,7 +257,7 @@ fun OperatorViewLayout( if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.operator_use_for_messages).uppercase()) { + SectionView(generalGetString(MR.strings.operator_use_for_messages)) { SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text( stringResource(MR.strings.operator_use_for_messages_receiving), @@ -266,7 +271,7 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false) ) ) } @@ -286,7 +291,27 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled) + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(proxy = enabled) ?: ServerRoles(storage = false, proxy = enabled, names = false) + ) + ) + } + } + ) + } + SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text( + stringResource(MR.strings.operator_use_for_names), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = userServers.value[operatorIndex].operator_.smpRoles.names, + onCheckedChange = { enabled -> + userServers.value = userServers.value.toMutableList().apply { + this[operatorIndex] = this[operatorIndex].copy( + operator = this[operatorIndex].operator?.copy( + smpRoles = this[operatorIndex].operator?.smpRoles?.copy(names = enabled) ?: ServerRoles(storage = false, proxy = false, names = enabled) ) ) } @@ -306,7 +331,7 @@ fun OperatorViewLayout( // Preset servers can't be deleted if (userServers.value[operatorIndex].smpServers.any { it.preset }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + SectionView(generalGetString(MR.strings.message_servers)) { userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> if (!server.preset) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { @@ -340,7 +365,7 @@ fun OperatorViewLayout( if (userServers.value[operatorIndex].smpServers.any { !it.preset && !it.deleted }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.operator_added_message_servers).uppercase()) { + SectionView(generalGetString(MR.strings.operator_added_message_servers)) { userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> if (server.deleted || server.preset) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { @@ -356,7 +381,7 @@ fun OperatorViewLayout( if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.operator_use_for_files).uppercase()) { + SectionView(generalGetString(MR.strings.operator_use_for_files)) { SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text( stringResource(MR.strings.operator_use_for_sending), @@ -370,7 +395,7 @@ fun OperatorViewLayout( userServers.value = userServers.value.toMutableList().apply { this[operatorIndex] = this[operatorIndex].copy( operator = this[operatorIndex].operator?.copy( - xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false) + xftpRoles = this[operatorIndex].operator?.xftpRoles?.copy(storage = enabled) ?: ServerRoles(storage = enabled, proxy = false, names = false) ) ) } @@ -389,7 +414,7 @@ fun OperatorViewLayout( // Preset servers can't be deleted if (userServers.value[operatorIndex].xftpServers.any { it.preset }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + SectionView(generalGetString(MR.strings.media_and_file_servers)) { userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> if (!server.preset) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { @@ -423,7 +448,7 @@ fun OperatorViewLayout( if (userServers.value[operatorIndex].xftpServers.any { !it.preset && !it.deleted}) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.operator_added_xftp_servers).uppercase()) { + SectionView(generalGetString(MR.strings.operator_added_xftp_servers)) { userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> if (server.deleted || server.preset) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { @@ -490,7 +515,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) { } } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() val uriHandler = LocalUriHandler.current SectionView { @@ -507,7 +532,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) { val selfhost = serverOperator.info.selfhost if (selfhost != null) { - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() SectionView { SectionItemView { val (text, link) = selfhost diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt index 01630a2b52..3c8cc9e0ce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServerView.kt @@ -5,6 +5,7 @@ import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween import SectionView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* @@ -18,6 +19,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -80,12 +82,14 @@ fun ProtocolServerView( ) } } - } + }, + cardScreen = true, ) { Box { ProtocolServerLayout( draftServer, serverProtocol, + userServers, testing.value, testServer = { testing.value = true @@ -118,6 +122,7 @@ fun ProtocolServerView( private fun ProtocolServerLayout( server: MutableState<UserServer>, serverProtocol: ServerProtocol, + userServers: MutableState<List<UserOperatorServers>>, testing: Boolean, testServer: () -> Unit, onDelete: () -> Unit, @@ -128,7 +133,7 @@ private fun ProtocolServerLayout( if (server.value.preset) { PresetServer(server, testing, testServer) } else { - CustomServer(server, testing, testServer, onDelete) + CustomServer(server, testing, testServer, onDelete, serverProtocol, userServers) } SectionBottomSpacer() } @@ -140,7 +145,7 @@ private fun PresetServer( testing: Boolean, testServer: () -> Unit ) { - SectionView(stringResource(MR.strings.smp_servers_preset_address).uppercase()) { + SectionView(stringResource(MR.strings.smp_servers_preset_address)) { SelectionContainer { Text( server.value.server, @@ -162,17 +167,13 @@ fun CustomServer( testing: Boolean, testServer: () -> Unit, onDelete: (() -> Unit)?, + serverProtocol: ServerProtocol? = null, + userServers: MutableState<List<UserOperatorServers>>? = null ) { val serverAddress = remember { mutableStateOf(server.value.server) } - val valid = remember { - derivedStateOf { - with(parseServerAddress(serverAddress.value)) { - this?.valid == true - } - } - } + val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } } SectionView( - stringResource(MR.strings.smp_servers_your_server_address).uppercase(), + stringResource(MR.strings.smp_servers_your_server_address), icon = painterResource(MR.images.ic_error), iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent, ) { @@ -190,18 +191,56 @@ fun CustomServer( } } } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() UseServerSection(server, valid.value, testing, testServer, onDelete) + val op = remember(server.value.server) { serverProtocolAndOperator(server.value, userServers?.value ?: listOf())?.second } + if (serverProtocol == ServerProtocol.SMP && server.value.enabled && (!server.value.preset || server.value.roles != ServerRolesOverride())) { + SectionDividerSpaced() + ServerRolesSection(server, op?.smpRoles ?: ServerRoles.noOperatorDefault) + } + if (valid.value) { SectionDividerSpaced() - SectionView(stringResource(MR.strings.smp_servers_add_to_another_device).uppercase()) { + SectionView(stringResource(MR.strings.smp_servers_add_to_another_device)) { QRCode(serverAddress.value, small = true) } } } +@Composable +private fun ServerRolesSection(server: MutableState<UserServer>, inherited: ServerRoles) { + SectionView(stringResource(MR.strings.operator_use_for_messages)) { + RoleDropDown(stringResource(MR.strings.operator_use_for_messages_receiving), server.value.roles.storage, defaultOn = inherited.storage) { + server.value = server.value.copy(roles = server.value.roles.copy(storage = it)) + } + RoleDropDown(stringResource(MR.strings.operator_use_for_messages_private_routing), server.value.roles.proxy, defaultOn = inherited.proxy) { + server.value = server.value.copy(roles = server.value.roles.copy(proxy = it)) + } + RoleDropDown(stringResource(MR.strings.operator_use_for_names), server.value.roles.names, defaultOn = inherited.names) { + server.value = server.value.copy(roles = server.value.roles.copy(names = it)) + } + } +} + +@Composable +private fun RoleDropDown(title: String, value: Boolean?, defaultOn: Boolean, onSelected: (Boolean?) -> Unit) { + val values = remember(defaultOn, appPrefs.appLanguage.state.value) { + listOf( + null to String.format(generalGetString(MR.strings.chat_preferences_default), generalGetString(if (defaultOn) MR.strings.chat_preferences_yes else MR.strings.chat_preferences_no)), + true to generalGetString(MR.strings.chat_preferences_yes), + false to generalGetString(MR.strings.chat_preferences_no) + ) + } + ExposedDropDownSettingRow( + title, + values, + rememberUpdatedState(value), + onSelected = onSelected + ) +} + @Composable private fun UseServerSection( server: MutableState<UserServer>, @@ -210,7 +249,7 @@ private fun UseServerSection( testServer: () -> Unit, onDelete: (() -> Unit)? = null, ) { - SectionView(stringResource(MR.strings.smp_servers_use_server).uppercase()) { + SectionView(stringResource(MR.strings.smp_servers_use_server)) { SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) { Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) ShowTestStatus(server.value) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt index 3be2456b72..63365bd080 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ProtocolServersView.kt @@ -7,9 +7,11 @@ import SectionItemView import SectionTextFooter import SectionView import androidx.compose.foundation.layout.* +import androidx.compose.foundation.background import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import chat.simplex.common.ui.theme.* import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -86,7 +88,7 @@ fun YourServersViewLayout( Column { if (userServers.value[operatorIndex].chatRelays.any { !it.deleted }) { val duplicateRelayAddresses = findDuplicateRelayAddresses(serverErrors.value) - SectionView(generalGetString(MR.strings.chat_relays).uppercase()) { + SectionView(generalGetString(MR.strings.chat_relays)) { userServers.value[operatorIndex].chatRelays.forEachIndexed { i, relay -> if (relay.deleted) return@forEachIndexed ChatRelayViewLink(relay, duplicateRelayAddresses) { @@ -99,7 +101,7 @@ fun YourServersViewLayout( if (userServers.value[operatorIndex].smpServers.any { !it.deleted }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.message_servers).uppercase()) { + SectionView(generalGetString(MR.strings.message_servers)) { userServers.value[operatorIndex].smpServers.forEachIndexed { i, server -> if (server.deleted) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.SMP) }) { @@ -133,7 +135,7 @@ fun YourServersViewLayout( if (userServers.value[operatorIndex].xftpServers.any { !it.deleted }) { SectionDividerSpaced() - SectionView(generalGetString(MR.strings.media_and_file_servers).uppercase()) { + SectionView(generalGetString(MR.strings.media_and_file_servers)) { userServers.value[operatorIndex].xftpServers.forEachIndexed { i, server -> if (server.deleted) return@forEachIndexed SectionItemView({ navigateToProtocolView(i, server, ServerProtocol.XFTP) }) { @@ -170,7 +172,7 @@ fun YourServersViewLayout( userServers.value[operatorIndex].xftpServers.any { !it.deleted } || userServers.value[operatorIndex].chatRelays.any { !it.deleted } ) { - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionDividerSpaced() } SectionView { @@ -183,19 +185,17 @@ fun YourServersViewLayout( iconColor = if (testing.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } - val serversErr = globalServersError(serverErrors.value) - if (serversErr != null) { + globalServersErrors(serverErrors.value).forEach { err -> SectionCustomFooter { - ServersErrorFooter(serversErr) + ServersErrorFooter(err) } } - val serversWarn = globalServersWarning(serverWarnings.value) - if (serversWarn != null) { + globalServersWarnings(serverWarnings.value).forEach { warn -> SectionCustomFooter { - ServersWarningFooter(serversWarn) + ServersWarningFooter(warn) } } - SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + SectionDividerSpaced() SectionView { TestServersButton( 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 bd9c5d6881..9d9ea0cadd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -32,13 +32,13 @@ <string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string> <string name="network_enable_socks_info">الوصول إلى الخوادم عبر وسيط SOCKS على المنفذ %d؟ يجب بدء تشغيل الوسيط قبل تفعيل هذا الخيار.</string> <string name="smp_servers_add">أضف خادم</string> - <string name="network_settings">إعدادات الشبكة المتقدمة</string> - <string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string> - <string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> - <string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</string> + <string name="network_settings">إعدادات الشبكة المتقدّمة</string> + <string name="all_group_members_will_remain_connected">سيظل جميع أعضاء المجموعة متصلين.</string> + <string name="allow_disappearing_messages_only_if">اسمح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> + <string name="allow_irreversible_message_deletion_only_if">اسمح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</string> <string name="group_member_role_admin">المُدير</string> <string name="users_add">أضف ملف التعريف</string> - <string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string> + <string name="allow_direct_messages">اسمح بإرسال رسائل مباشرة إلى الأعضاء.</string> <string name="accept_contact_incognito_button">اقبل التخفي</string> <string name="button_add_welcome_message">أضِف رسالة ترحيب</string> <string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string> @@ -46,56 +46,56 @@ <string name="accept_connection_request__question">قبول طلب الاتصال؟</string> <string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string> <string name="callstatus_accepted">قُبلت المكالمة</string> - <string name="allow_calls_only_if">السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string> + <string name="allow_calls_only_if">اسمح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="keychain_is_storing_securely">يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل.</string> <string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم، ويفتح التطبيق كالمعتاد.</string> <string name="answer_call">أجب الاتصال</string> <string name="chat_preferences_always">دائمًا</string> - <string name="allow_to_send_disappearing">السماح بإرسال رسائل تختفي.</string> - <string name="allow_to_send_voice">السماح بإرسال رسائل صوتية.</string> + <string name="allow_to_send_disappearing">اسمح بإرسال رسائل تختفي.</string> + <string name="allow_to_send_voice">اسمح بإرسال رسائل صوتية.</string> <string name="settings_section_title_app">تطبيق</string> <string name="color_secondary_variant">ثانوي إضافي</string> - <string name="allow_your_contacts_adding_message_reactions">السماح لجهات اتصالك بإضافة ردود الفعل للرسالة.</string> - <string name="allow_your_contacts_to_call">السماح لجهات اتصالك بالاتصال بك.</string> - <string name="allow_message_reactions">السماح بردود الفعل على الرسائل.</string> - <string name="v5_1_self_destruct_passcode_descr">يتم مسح جميع البيانات عند إدخالها.</string> + <string name="allow_your_contacts_adding_message_reactions">اسمح لجهات اتصالك بإضافة ردود الفعل للرسالة.</string> + <string name="allow_your_contacts_to_call">اسمح لجهات اتصالك بالاتصال بك.</string> + <string name="allow_message_reactions">اسمح بردود الفعل على الرسائل.</string> + <string name="v5_1_self_destruct_passcode_descr">تُمسح جميع البيانات عند إدخالها.</string> <string name="keychain_allows_to_receive_ntfs">سيتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن بعد إعادة تشغيل التطبيق أو تغيير عبارة المرور - سيسمح بإستلام الإشعارات.</string> - <string name="allow_your_contacts_to_send_disappearing_messages">السماح لجهات اتصالك بإرسال رسائل تختفي.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">اسمح لجهات اتصالك بإرسال رسائل تختفي.</string> <string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="v5_0_app_passcode">رمز مرور التطبيق</string> <string name="notifications_mode_service">يعمل دائمًا</string> <string name="notifications_mode_off_desc">يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية</string> <string name="allow_voice_messages_question">السماح بالرسائل الصوتية؟</string> - <string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات اتصالك متصلة.</string> - <string name="always_use_relay">استخدم الموجه دائمًا</string> + <string name="all_your_contacts_will_remain_connected">ستظل جميع جهات اتصالك متصلة.</string> + <string name="always_use_relay">استخدم المُرحل دائمًا</string> <string name="full_backup">النسخ الاحتياطي لبيانات التطبيق</string> <string name="all_app_data_will_be_cleared">حُذفت جميع بيانات التطبيق.</string> - <string name="allow_to_delete_messages">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> + <string name="allow_to_delete_messages">اسمح بحذف الرسائل المُرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="allow_your_contacts_to_send_voice_messages">اسمح لجهات اتصالك بإرسال رسائل صوتية.</string> <string name="learn_more_about_address">عن عنوان SimpleX</string> <string name="app_version_code">بناء التطبيق: %s</string> <string name="appearance_settings">المظهر</string> <string name="add_address_to_your_profile">أضف عنوانًا إلى ملف تعريفك، حتى تتمكن جهات اتصالك على SimpleX من مشاركته مع أشخاص آخرين. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك على SimpleX.</string> - <string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string> - <string name="settings_section_title_icon">رمز التطبيق</string> + <string name="all_your_contacts_will_remain_connected_update_sent">ستظل جميع جهات اتصالك متصلة. سيتم إرسال تحديث ملف التعريف إلى جهات اتصالك.</string> + <string name="settings_section_title_icon">أيقونة التطبيق</string> <string name="address_section_title">عنوان</string> <string name="allow_your_contacts_irreversibly_delete">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> - <string name="auth_unavailable">المصادقة غير متاحة</string> - <string name="back">رجوع</string> + <string name="auth_unavailable">الاستيثاق غير متاح</string> + <string name="back">ارجع</string> <string name="invite_prohibited">لا يمكن دعوة جهة اتصال!</string> - <string name="icon_descr_cancel_image_preview">إلغاء معاينة الصورة</string> + <string name="icon_descr_cancel_image_preview">ألغِ معاينة الصورة</string> <string name="use_camera_button">الكاميرا</string> - <string name="icon_descr_cancel_link_preview">إلغاء معاينة الروابط</string> + <string name="icon_descr_cancel_link_preview">ألغِ معاينة الروابط</string> <string name="network_session_mode_user_description"><![CDATA[سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل ملف تعريف دردشة لديك في التطبيق </b>.]]></string> - <string name="feature_cancelled_item">ألغيت %s</string> + <string name="feature_cancelled_item">أُلغيَ %s</string> <string name="one_time_link_short">رابط لمرة واحدة</string> <string name="send_disappearing_message_5_minutes">5 دقائق</string> - <string name="authentication_cancelled">ألغيت المصادقة</string> + <string name="authentication_cancelled">أُلغيَ الاستيثاق</string> <string name="notifications_mode_service_desc">تعمل خدمة الخلفية دائمًا - سيتم عرض الإشعارات بمجرد توفر الرسائل.</string> <string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string> <string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string> - <string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string> + <string name="icon_descr_call_progress">مكالمتك جارية</string> <string name="cannot_receive_file">لا يمكن استلام الملف</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>جيد للبطارية</b>. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string> <string name="bold_text">عريض</string> @@ -117,8 +117,8 @@ <string name="impossible_to_recover_passphrase"><![CDATA[<b>يُرجى الملاحظة</b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string> <string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه. (24 ساعة)</string> <string name="v4_2_auto_accept_contact_requests">قبول طلبات الاتصال تلقائيًا</string> - <string name="la_auth_failed">فشلت المصادقة</string> - <string name="la_authenticate">مصادقة</string> + <string name="la_auth_failed">فشل الاستيثاق</string> + <string name="la_authenticate">استوثق</string> <string name="send_disappearing_message_1_minute">1 دقيقة</string> <string name="send_disappearing_message_30_seconds">30 ثانية</string> <string name="icon_descr_cancel_live_message">ألغِ الرسالة الحيّة</string> @@ -137,19 +137,19 @@ <string name="callstatus_error">خطأ في الاتصال</string> <string name="turning_off_service_and_periodic">تحسين البطارية نشط، مما يؤدي إلى إيقاف تشغيل خدمة الخلفية والطلبات الدورية للرسائل الجديدة. يمكنك إعادة تفعيلها عبر الإعدادات.</string> <string name="database_initialization_error_title">لا يمكن تهيئة قاعدة البيانات</string> - <string name="attach">إرفاق</string> - <string name="icon_descr_asked_to_receive">طلب لاستلام الصورة</string> + <string name="attach">أرفق</string> + <string name="icon_descr_asked_to_receive">طُلب استلام الصورة</string> <string name="app_version_name">إصدار التطبيق: v%s</string> <string name="auto_accept_contact">قبول تلقائي</string> <string name="settings_section_title_calls">المكالمات</string> <string name="alert_title_cant_invite_contacts">لا يمكن دعوة جهات الاتصال!</string> <string name="rcv_conn_event_switch_queue_phase_completed">غُيِّر العنوان من أجلك</string> - <string name="icon_descr_video_asked_to_receive">طلب لاستلام الفيديو</string> - <string name="callstatus_in_progress">مكالمتك تحت الإجراء</string> + <string name="icon_descr_video_asked_to_receive">طُلب استلام الفيديو</string> + <string name="callstatus_in_progress">مكالمتك جارية</string> <string name="change_database_passphrase_question">تغيير عبارة مرور قاعدة البيانات؟</string> <string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string> - <string name="icon_descr_cancel_file_preview">إلغاء معاينة الملف</string> - <string name="app_version_title">نسخة التطبيق</string> + <string name="icon_descr_cancel_file_preview">ألغِ معاينة الملف</string> + <string name="app_version_title">نُسخة التطبيق</string> <string name="color_background">الخلفية</string> <string name="audio_video_calls">مكالمات الصوت/الفيديو</string> <string name="v5_1_better_messages">رسائل أفضل</string> @@ -163,7 +163,7 @@ <string name="feature_enabled_for_you">مفعّلة لك</string> <string name="contacts_can_mark_messages_for_deletion">يمكن لجهات الاتصال تحديد الرسائل لحذفها؛ ستتمكن من مشاهدتها.</string> <string name="display_name_connecting">يتصل…</string> - <string name="connection_error_auth">خطأ في الإتصال (المصادقة)</string> + <string name="connection_error_auth">أُزيل رابط الاتصال</string> <string name="error_deleting_contact">خطأ في حذف جهة الاتصال</string> <string name="notification_preview_somebody">جهة الاتصال مخفية:</string> <string name="copy_verb">انسخ</string> @@ -214,7 +214,7 @@ <string name="callstate_connecting">يتصل…</string> <string name="callstate_ended">انتهى</string> <string name="callstate_connected">متصل</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d تخطت الرسائل</string> + <string name="alert_text_decryption_error_too_many_skipped">تم تخطّي %1$d رسالة.</string> <string name="enable_lock">فعّل القفل</string> <string name="confirm_passcode">تأكيد رمز المرور</string> <string name="error_deleting_database">خطأ في حذف قاعدة بيانات الدردشة</string> @@ -231,12 +231,12 @@ <string name="contact_wants_to_connect_via_call">%1$s يريد التواصل معك عبر</string> <string name="snd_conn_event_switch_queue_phase_changing">جارِ تغيير العنوان…</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">جارِ تغيير العنوان ل%s…</string> - <string name="allow_to_send_files">السماح بإرسال الملفات والوسائط.</string> + <string name="allow_to_send_files">اسمح بإرسال الملفات والوسائط.</string> <string name="enter_welcome_message_optional">أدخل رسالة ترحيب… (اختياري)</string> <string name="snd_conn_event_ratchet_sync_agreed">وافق التعمية ل%s</string> <string name="snd_conn_event_ratchet_sync_allowed">سمح بإعادة التفاوض على التعمية ل%s</string> <string name="error_accepting_contact_request">خطأ في قبول طلب جهة الاتصال</string> - <string name="status_contact_has_no_e2e_encryption">ليس لدى جهة الاتصال التعمية بين الطريفين</string> + <string name="status_contact_has_no_e2e_encryption">ليس لدى جهة الاتصال التعمية بين الطرفين</string> <string name="change_self_destruct_mode">تغيير وضع التدمير الذاتي</string> <string name="change_self_destruct_passcode">تغيير رمز المرور التدمير الذاتي</string> <string name="confirm_database_upgrades">تأكيد ترقيات قاعدة البيانات</string> @@ -261,10 +261,10 @@ <string name="chat_is_running">الدردشة قيد التشغيل</string> <string name="chat_database_imported">استُوردت قاعدة بيانات الدردشة</string> <string name="error_changing_address">خطأ في تغيير العنوان</string> - <string name="integrity_msg_skipped">%1$d رسائل تخطت</string> + <string name="integrity_msg_skipped">%1$d رسالة مُتخطّاة</string> <string name="change_lock_mode">تغيير وضع القفل</string> <string name="enabled_self_destruct_passcode">فعّل رمز التدمير الذاتي</string> - <string name="change_member_role_question">تغيير دور المجموعة؟</string> + <string name="change_member_role_question">تغيير الدور؟</string> <string name="chat_preferences">تفضيلات الدردشة</string> <string name="enter_correct_passphrase">أدخل عبارة المرور الصحيحة.</string> <string name="rcv_group_event_member_connected">متصل</string> @@ -312,7 +312,7 @@ <string name="create_address_and_let_people_connect">أنشئ عنوانًا للسماح للأشخاص بالتواصل معك.</string> <string name="smp_servers_enter_manually">أدخل الخادم يدويًا</string> <string name="colored_text">ملون</string> - <string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطريفين</string> + <string name="status_contact_has_e2e_encryption">لدى جهة الاتصال التعمية بين الطرفين</string> <string name="create_profile_button">أنشئ</string> <string name="create_your_profile">أنشئ ملف تعريفك</string> <string name="icon_descr_call_connecting">مكالمة جارية</string> @@ -448,7 +448,7 @@ <string name="v4_2_group_links">روابط المجموعة</string> <string name="v4_6_hidden_chat_profiles">ملفات تعريف الدردشة المخفية</string> <string name="full_name__field">الاسم الكامل:</string> - <string name="alert_message_group_invitation_expired">لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل.</string> + <string name="alert_message_group_invitation_expired">لم تعد دعوة المجموعة صالحة، أُزيلت بواسطة المرسل.</string> <string name="group_link">رابط المجموعة</string> <string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="group_full_name_field">الاسم الكامل للمجموعة:</string> @@ -598,7 +598,7 @@ <string name="v4_4_disappearing_messages">رسائل تختفي</string> <string name="failed_to_create_user_duplicate_title">اسم العرض مكرر!</string> <string name="smp_server_test_disconnect">قطع الاتصال</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">مصادقة الجهاز غير مفعّلة. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل مصادقة الجهاز.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">استيثاق الجهاز غير مفعّل. يمكنك تشغيل قفل SimpleX عبر الإعدادات، بمجرد تفعيل استيثاق الجهاز.</string> <string name="smp_server_test_download_file">نزّل الملف</string> <string name="auth_disable_simplex_lock">عطّل قفل SimpleX</string> <string name="edit_verb">حرّر</string> @@ -608,7 +608,7 @@ <string name="integrity_msg_duplicate">كرر الرسالة</string> <string name="share_text_disappears_at">يختفي في: %s</string> <string name="disappearing_prohibited_in_this_chat">الرسائل المختفية ممنوعة في هذه الدردشة.</string> - <string name="status_e2e_encrypted">مُعمّى بين الطريفين</string> + <string name="status_e2e_encrypted">مُعمّى بين الطرفين</string> <string name="icon_descr_edited">حُرّر</string> <string name="downgrade_and_open_chat">الرجوع إلى إصدار سابق وفتح الدردشة</string> <string name="direct_messages">رسائل مباشرة</string> @@ -618,7 +618,7 @@ <string name="settings_section_title_device">الجهاز</string> <string name="ttl_week">%d أسبوع</string> <string name="display_name_cannot_contain_whitespace">لا يمكن أن يحتوي اسم العرض على مسافة فارغة.</string> - <string name="encrypted_video_call">مكالمة فيديو مُعمّاة بين الطريفين</string> + <string name="encrypted_video_call">مكالمة فيديو مُعمّاة بين الطرفين</string> <string name="direct_messages_are_prohibited_in_group">يُمنع إرسال الرسائل المباشرة بين الأعضاء في هذه المجموعة.</string> <string name="ttl_hour">%d ساعة</string> <string name="ttl_h">%d ساعة</string> @@ -643,7 +643,7 @@ <string name="dont_enable_receipts">لا تُفعل</string> <string name="la_minutes">%d دقائق</string> <string name="la_seconds">%d ثواني</string> - <string name="encrypted_audio_call">مكالمة صوتية مُعمّاة بين الطريفين</string> + <string name="encrypted_audio_call">مكالمة صوتية مُعمّاة بين الطرفين</string> <string name="ttl_sec">%d ثانية</string> <string name="icon_descr_server_status_disconnected">قُطع الاتصال</string> <string name="disappearing_message">رسالة تختفي</string> @@ -738,7 +738,7 @@ <string name="v4_5_message_draft">مسودة الرسالة</string> <string name="v4_5_multiple_chat_profiles">ملفات تعريف دردشة متعددة</string> <string name="settings_notification_preview_title">معاينة الإشعار</string> - <string name="status_no_e2e_encryption">لا يوجد تعمية بين الطريفين</string> + <string name="status_no_e2e_encryption">لا يوجد تعمية بين الطرفين</string> <string name="chat_preferences_no">لا</string> <string name="notification_preview_new_message">رسالة جديدة</string> <string name="images_limit_desc">يمكن إرسال 10 صور فقط في نفس الوقت</string> @@ -832,7 +832,7 @@ <string name="revoke_file__confirm">اسحب الوصول</string> <string name="reveal_verb">اكشف</string> <string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string> - <string name="reject_contact_button">رفض</string> + <string name="reject_contact_button">ارفض</string> <string name="rate_the_app">قيم التطبيق</string> <string name="port_verb">منفذ</string> <string name="save_auto_accept_settings">احفظ إعدادات عنوان SimpleX</string> @@ -851,7 +851,7 @@ <string name="prohibit_sending_files">امنع إرسال الملفات والوسائط.</string> <string name="callstate_received_answer">استلمت إجابة…</string> <string name="read_more_in_github_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">مستودع GitHub</font>.]]></string> - <string name="reject">رفض</string> + <string name="reject">ارفض</string> <string name="relay_server_protects_ip">يحمي خادم المُرحل عنوان IP الخاص بك، ولكن يمكنه مراقبة مُدّة المكالمة.</string> <string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string> <string name="restore_database_alert_title">استعادة النسخة الاحتياطية لقاعدة البيانات؟</string> @@ -936,8 +936,8 @@ <string name="sender_cancelled_file_transfer">أُلغيَ المرسل نقل الملف.</string> <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(امسح أو ألصق من الحافظة)</string> <string name="network_option_seconds_label">ثانية</string> - <string name="sender_may_have_deleted_the_connection_request">ربما حذف المرسل طلب الاتصال.</string> - <string name="scan_QR_code">مسح رمز QR</string> + <string name="sender_may_have_deleted_the_connection_request">حذف المُرسل طلب الاتصال.</string> + <string name="scan_QR_code">امسح رمز QR</string> <string name="send_us_an_email">أرسل لنا بريداً</string> <string name="scan_code_from_contacts_app">مسح رمز الأمان من تطبيق جهة الاتصال</string> <string name="share_invitation_link">مشاركة رابط ذو استخدام واحد</string> @@ -972,7 +972,7 @@ <string name="scan_code">مسح الرمز</string> <string name="chat_with_the_founder">أرسل أسئلة وأفكار</string> <string name="share_address_with_contacts_question">مشاركة العنوان مع جهات اتصال SimpleX؟</string> - <string name="share_address">شارك العنوان</string> + <string name="share_address">شارك العنوان…</string> <string name="save_welcome_message_question">حفظ رسالة الترحيب؟</string> <string name="smp_servers_save">احفظ الخوادم</string> <string name="settings_section_title_delivery_receipts">أرسل إيصالات التسليم إلى</string> @@ -993,7 +993,7 @@ <string name="set_contact_name">تعيين اسم جهة الاتصال</string> <string name="icon_descr_settings">الإعدادات</string> <string name="smp_save_servers_question">حفظ الخوادم؟</string> - <string name="smp_servers_scan_qr">مسح رمز QR الخادم</string> + <string name="smp_servers_scan_qr">امسح رمز QR الخادم</string> <string name="security_code">رمز الأمان</string> <string name="save_preferences_question">حفظ التفضيلات؟</string> <string name="save_settings_question">حفظ الإعدادات؟</string> @@ -1069,7 +1069,7 @@ <string name="language_system">النظام</string> <string name="theme">السمة</string> <string name="to_start_a_new_chat_help_header">لبدء محادثة جديدة</string> - <string name="to_verify_compare">للتحقق من التعمية بين الطريفين مع جهة اتصالك، قارن (أو امسح) الرمز الموجود على أجهزتك.</string> + <string name="to_verify_compare">للتحقق من التعمية بين الطرفين مع جهة اتصالك، قارن (أو امسح) الرمز الموجود على أجهزتك.</string> <string name="group_is_decentralized">لامركزية بالكامل – مرئية للأعضاء فقط.</string> <string name="theme_system">النظام</string> <string name="error_smp_test_failed_at_step">فشل الاختبار في الخطوة %s.</string> @@ -1091,7 +1091,7 @@ <string name="scan_qr_to_connect_to_contact">للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق.</string> <string name="smp_servers_test_servers">اختبر الخوادم</string> <string name="first_platform_without_user_ids">لا معرّفات مُستخدم</string> - <string name="settings_section_title_support">دعم SIMPLEX CHAT</string> + <string name="settings_section_title_support">دعم SimpleX Chat</string> <string name="switch_verb">بدِّل</string> <string name="color_title">العنوان الرئيسي</string> <string name="moderate_message_will_be_marked_warning">سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء.</string> @@ -1197,7 +1197,7 @@ <string name="v4_3_irreversible_message_deletion_desc">يمكن أن تسمح جهات اتصالك بحذف الرسائل بالكامل.</string> <string name="unknown_message_format">تنسيق رسالة غير معروف</string> <string name="description_via_one_time_link">عبر رابط لمرة واحدة</string> - <string name="video_call_no_encryption">مكالمة الفيديو ليست مُعمّاة بين الطريفين</string> + <string name="video_call_no_encryption">مكالمة الفيديو ليست مُعمّاة بين الطرفين</string> <string name="snd_conn_event_switch_queue_phase_completed">غيّرتَ العنوان</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">ستكون متصلاً عندما يكون جهاز جهة اتصالك متصلاً بالإنترنت، يُرجى الانتظار أو التحقق لاحقًا!</string> <string name="snd_group_event_user_left">غادرت</string> @@ -1265,7 +1265,7 @@ <string name="v5_0_large_files_support">مقاطع فيديو وملفات تصل إلى 1 جيجا بايت</string> <string name="v5_1_better_messages_descr">- رسائل صوتية تصل إلى 5 دقائق.\n- الوقت المخصص لتختفي.\n- تحرير التاريخ.</string> <string name="you_can_enable_delivery_receipts_later">يمكنك تفعيلة لاحقًا عبر الإعدادات</string> - <string name="you_can_enable_delivery_receipts_later_alert">يمكنك تفعيلها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string> + <string name="you_can_enable_delivery_receipts_later_alert">يمكنك تفعيلها لاحقًا من خلال إعدادات خصوصيتك في التطبيق.</string> <string name="description_via_group_link">عبر رابط المجموعة</string> <string name="description_you_shared_one_time_link_incognito">لقد شاركت رابط لمرة واحدة متخفي</string> <string name="simplex_link_mode_browser">عبر المتصفح</string> @@ -1273,8 +1273,7 @@ <string name="upgrade_and_open_chat">رقِّ وافتح الدردشة</string> <string name="button_welcome_message">رسالة الترحيب</string> <string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string> - <string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو استُخدم هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. -\nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء رابط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string> + <string name="connection_error_auth_desc">أُزيلت جهة اتصالك هذا الرابط، أو أنه كان رابطًا لمرة واحدة وقد استُخدِم بالفعل.\nللتواصل، اطلب من جهة اتصالك إنشاء رابط جديد.</string> <string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف دردشتك\nإلى جهة اتصالك</string> <string name="user_unhide">إلغاء الإخفاء</string> <string name="incognito_random_profile">ملفك التعريفي العشوائي</string> @@ -1326,7 +1325,7 @@ <string name="paste_the_link_you_received_to_connect_with_your_contact">ألصِق الرابط المُستلَم للتواصل مع جهة اتصالك…</string> <string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك التعريفي %1$s.</string> <string name="system_restricted_background_in_call_desc">قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.</string> - <string name="turn_off_battery_optimization_button">سماح</string> + <string name="turn_off_battery_optimization_button">اسمح</string> <string name="system_restricted_background_in_call_title">لا مكالمات في الخلفية</string> <string name="system_restricted_background_warn"><![CDATA[لتفعيل الإشعارات، يُرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> <string name="system_restricted_background_in_call_warn"><![CDATA[لإجراء مكالمات في الخلفية، يُرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> @@ -1393,7 +1392,7 @@ <string name="delete_messages__question">حذف %d رسالة؟</string> <string name="connect_with_contact_name_question">اتصل مع %1$s؟</string> <string name="blocked_items_description">%d رسالة محظورة</string> - <string name="block_member_button">حظر العضو</string> + <string name="block_member_button">احظر العضو</string> <string name="connected_mobile">الجوّال متصل</string> <string name="delete_and_notify_contact">احذف وإشعار جهة الاتصال</string> <string name="desktop_connection_terminated">انتهى الاتصال</string> @@ -1401,7 +1400,7 @@ <string name="disconnect_remote_host">قطع الاتصال</string> <string name="block_member_question">حظر العضو؟</string> <string name="rcv_group_events_count">%d أحداث مجموعة</string> - <string name="group_member_role_author">الكاتب</string> + <string name="group_member_role_author">المؤلف</string> <string name="connected_to_mobile">متصل بالجوّال</string> <string name="devices">الأجهزة</string> <string name="multicast_discoverable_via_local_network">مُكتشف عبر الشبكة المحلية</string> @@ -1470,7 +1469,7 @@ <string name="paste_desktop_address">ألصق عنوان سطح المكتب</string> <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[هذا هو الرابط الخاص بك للمجموعة <b>%1$s</b>!]]></string> <string name="verify_code_with_desktop">تحقق من الرمز مع سطح المكتب</string> - <string name="scan_qr_code_from_desktop">مسح رمز QR من سطح المكتب</string> + <string name="scan_qr_code_from_desktop">امسح رمز QR من سطح المكتب</string> <string name="unblock_member_confirmation">ألغِ الحظر</string> <string name="v5_4_more_things_descr">- إشعار اختياريًا جهات الاتصال المحذوفة.\n- أسماء الملفات التعريفية بمسافات.\n- و اكثر!</string> <string name="non_content_uri_alert_title">مسار الملف غير صالح</string> @@ -1501,7 +1500,7 @@ <string name="disable_sending_recent_history">لا ترسل التاريخ للأعضاء الجدد.</string> <string name="or_show_this_qr_code">أو أظهر هذا الرمز</string> <string name="recent_history_is_sent_to_new_members">يتم إرسال ما يصل إلى 100 رسالة أخيرة إلى الأعضاء الجدد.</string> - <string name="code_you_scanned_is_not_simplex_link_qr_code">الرمز الذي مسحته ضوئيًا ليس رمز QR لرابط SimpleX.</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">الرمز الذي مسحته ليس رمز QR لرابط SimpleX.</string> <string name="the_text_you_pasted_is_not_a_link">النص الذي لصقته ليس رابط SimpleX.</string> <string name="enable_camera_access">فعّل الوصول إلى الكاميرا</string> <string name="you_can_view_invitation_link_again">يمكنك عرض رابط الدعوة مرة أخرى في تفاصيل الاتصال.</string> @@ -1622,9 +1621,9 @@ <string name="migrate_to_device_downloading_archive">جارِ تنزيل الأرشيف</string> <string name="migrate_to_device_bytes_downloaded">نُزّل %s</string> <string name="migrate_to_device_download_failed">فشل التنزيل</string> - <string name="migrate_to_device_repeat_download">كرر التنزيل</string> + <string name="migrate_to_device_repeat_download">كرّر التنزيل</string> <string name="migrate_to_device_import_failed">فشل الاستيراد</string> - <string name="migrate_to_device_repeat_import">كرر الاستيراد</string> + <string name="migrate_to_device_repeat_import">كرّر الاستيراد</string> <string name="migrate_to_device_enter_passphrase">أدخل عبارة المرور</string> <string name="migrate_to_device_file_delete_or_link_invalid">حُذف الملف أو الرابط غير صالح</string> <string name="migrate_to_device_chat_migrated">رحّلت الدردشة!</string> @@ -1677,7 +1676,7 @@ <string name="migrate_from_device_error_exporting_archive">حدث خطأ أثناء تصدير قاعدة بيانات الدردشة</string> <string name="migrate_from_device_chat_should_be_stopped">للاستمرار، يجب إيقاف الدردشة.</string> <string name="migrate_from_device_or_share_this_file_link">أو شارك رابط الملف هذا بشكل آمن</string> - <string name="migrate_from_device_repeat_upload">كرر الرفع</string> + <string name="migrate_from_device_repeat_upload">كرّر الرفع</string> <string name="migrate_from_device_stopping_chat">جارِ إيقاف الدردشة</string> <string name="migrate_from_device_bytes_uploaded">رُفع %s</string> <string name="migrate_from_device_upload_failed">فشل الرفع</string> @@ -1807,9 +1806,7 @@ <string name="theme_destination_app_theme">سمة التطبيق</string> <string name="v5_8_safe_files_descr">تأكيد الملفات من خوادم غير معروفة.</string> <string name="chat_theme_reset_to_user_theme">صفّر إلى سمة المستخدم</string> - <string name="message_queue_info_server_info">معلومات قائمة انتظار الخادم: %1$s -\n -\nآخر رسالة تم استلامها: %2$s</string> + <string name="message_queue_info_server_info">معلومات قائمة انتظار الخادم: %1$s \n \nآخر رسالة مُستلمة: %2$s</string> <string name="info_row_debug_delivery">تسليم التصحيح</string> <string name="message_queue_info">معلومات قائمة انتظار الرسائل</string> <string name="v5_8_private_routing_descr">احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات اتصالك. \nفعّل في إعدادات *الشبكة والخوادم*.</string> @@ -1902,7 +1899,7 @@ <string name="sent_via_proxy">مُرسَل عبر الوسيط</string> <string name="subscribed">مشترك</string> <string name="subscription_errors">أخطاء الاشتراك</string> - <string name="upload_errors">رفع الأخطاء</string> + <string name="upload_errors">أخطاء الرفع</string> <string name="app_check_for_updates">التمس التحديثات</string> <string name="acknowledgement_errors">أخطاء معترف بها</string> <string name="app_check_for_updates_download_completed_title">نُزّل تحديث التطبيق</string> @@ -2166,7 +2163,7 @@ <string name="message_deleted_or_not_received_error_title">لا توجد رسالة</string> <string name="no_message_servers_configured_for_receiving">لا يوجد خوادم لاستلام الرسائل.</string> <string name="v6_2_improved_chat_navigation_descr">- فتح الدردشة عند أول رسالة غير مقروءة.\n- الانتقال إلى الرسائل المقتبسة.</string> - <string name="you_can_set_connection_name_to_remember">يمكنك تعيين اسم الاتصال، لتذكر الأشخاص الذين تمت مشاركة الرابط معهم.</string> + <string name="you_can_set_connection_name_to_remember">يمكنك تعيين اسم الاتصال، لتتذكر الأشخاص الذين شاركتَ الرابط معهم.</string> <string name="onboarding_network_operators_review_later">راجع لاحقًا</string> <string name="error_server_protocol_changed">تغيّر بروتوكول الخادم.</string> <string name="share_address_publicly">شارك العنوان علناً</string> @@ -2216,7 +2213,7 @@ <string name="maximum_message_size_reached_non_text">يُرجى تقليل حجم الرسالة أو إزالة الوسائط ثم إرسالها مرة أخرى.</string> <string name="maximum_message_size_reached_forwarding">يمكنك نسخ الرسالة وتقليل حجمها لإرسالها.</string> <string name="onboarding_network_operators_cant_see_who_talks_to_whom">عندما يتم تفعيل أكثر من مُشغل واحد، لن يكون لدى أي منهم بيانات تعريفية لمعرفة مَن يتواصل مع مَن.</string> - <string name="member_role_will_be_changed_with_notification_chat">سيتم تغيير الدور إلى %s. وسيتم إشعار الجميع في الدردشة.</string> + <string name="member_role_will_be_changed_with_notification_chat">سيتم تغيير الدور إلى "%s". وسيتم إشعار الجميع في الدردشة.</string> <string name="chat_main_profile_sent">سيتم إرسال ملف تعريفك للدردشة إلى أعضاء الدردشة</string> <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">ستتوقف عن تلقي الرسائل من هذه الدردشة. سيتم حفظ سجل الدردشة.</string> <string name="onboarding_network_about_operators">عن المُشغلين</string> @@ -2487,13 +2484,13 @@ <string name="share_old_link_alert_button">شارك الرابط القديم</string> <string name="share_group_profile_via_link_alert_text">سيكون الرابط قصيراً، وسيتم مشاركة الملف التعريفي للمجموعة عبر الرابط.</string> <string name="upgrade_group_link">رقِّ رابط المجموعة</string> - <string name="settings_section_title_contact_requests_from_groups">طلبات الاتصال من المجموعات</string> + <string name="settings_section_title_contact_requests_from_groups">طلبات التواصل من المجموعات</string> <string name="member_is_deleted_cant_accept_request">حُذف العضو - لا يمكن قبول الطلب</string> <string name="rcv_direct_event_group_inv_link_received">طُلب اتصال من المجموعة %1$s</string> <string name="this_setting_is_for_your_current_profile">هذا الإعداد لملف تعريفك الحالي</string> <string name="allow_files_and_media_only_if">اسمح بالملفات والوسائط فقط إذا سمح جهة اتصالك بذلك.</string> <string name="allow_your_contacts_to_send_files_and_media">اسمح لجهات اتصالك بإرسال الملفات والوسائط.</string> - <string name="chat_banner_bot">بوت</string> + <string name="chat_banner_bot">روبوت</string> <string name="both_you_and_your_contact_can_send_files">يمكنك أنت وجهة اتصالك إرسال الملفات والوسائط.</string> <string name="files_prohibited_in_this_chat">يُمنع إرسال الملفات والوسائط في هذه الدردشة.</string> <string name="only_you_can_send_files">يمكنك أنت فقط إرسال الملفات والوسائط.</string> @@ -2577,7 +2574,6 @@ <string name="relay_conn_status_connecting">يتصل</string> <string name="create_channel_title">أنشئ قناة عامة</string> <string name="create_channel_button">أنشئ قناة عامة</string> - <string name="create_channel_beta_button">أنشئ قناة عامة (تجريبي)</string> <string name="creating_channel">ينشئ قناة</string> <string name="rcv_channel_events_count">%d أحداث القناة</string> <string name="relay_test_step_decode_link">فك ترميز الرابط</string> @@ -2609,7 +2605,7 @@ <string name="connect_plan_open_channel">افتح قناة</string> <string name="connect_plan_open_new_channel">افتح قناة جديدة</string> <string name="member_info_section_title_owner">المالك</string> - <string name="channel_members_section_owners">المالكون</string> + <string name="channel_members_section_owners">المالكين والمساهمين</string> <string name="preset_relay_address">عنوان المُرحل مسبق الضبط</string> <string name="preset_relay_name">اسم المُرحل مسبق الضبط</string> <string name="group_member_role_relay">مُرحل</string> @@ -2746,7 +2742,6 @@ <string name="share_channel">شارك القناة…</string> <string name="share_via_chat">شارك عبر الدردشة</string> <string name="owner_verification_failed">⚠️ فشل التحقق من التوقيع: %s.</string> - <string name="chat_link_signed">(موقّع)</string> <string name="group_reports_subscriber_reports">بلاغات المشترك</string> <string name="group_members_can_add_message_reactions_channel">يمكن للمشتركين إضافة ردود الفعل على الرسائل.</string> <string name="members_can_chat_with_admins_channel">يمكن للمشتركين الدردشة مع المُدراء.</string> @@ -2797,10 +2792,10 @@ <string name="error_deleting_message">خطأ في حذف الرسالة</string> <string name="from_history">من السجل</string> <string name="close_behavior_dialog_text">إذا اخترت أغلِق، فلن تُستلم الرسائل.\nيمكنك تغيير ذلك لاحقًا من إعدادات المظهر.</string> - <string name="appearance_minimize_to_tray_desc">أبقِ SimpleX يعمل في الخلفية لاستلام الرسائل.</string> + <string name="appearance_minimize_to_tray_desc">يعمل في الخلفية لاستلام الرسائل</string> <string name="close_behavior_dialog_minimize">صغّر إلى اللوحة</string> <string name="close_behavior_dialog_title">تصغير إلى اللوحة؟</string> - <string name="appearance_minimize_to_tray">صغّر إلى اللوحة عند إغلاق النافذة</string> + <string name="appearance_minimize_to_tray">أغلِق إلى اللوحة</string> <string name="tray_quit">أنهِ SimpleX</string> <string name="tray_show">أظهر SimpleX</string> <string name="tray_tooltip">SimpleX</string> @@ -2810,4 +2805,105 @@ <string name="relay_status_rejected">رُفض</string> <string name="member_info_relay_status_rejected_by_operator">رُفض بواسطة مُشغل المُرحل</string> <string name="member_info_status">الحالة</string> + <string name="channel_owner_count_singular">%1$d مالك</string> + <string name="channel_owner_count_plural">%1$d مالكون</string> + <string name="settings_section_title_about">عن</string> + <string name="advanced_options">خيارات متقدّمة</string> + <string name="advanced_settings">إعدادات متقدّمة</string> + <string name="allow_anyone_to_embed">اسمح لأي شخص بالتضمين</string> + <string name="embed_any_webpage_can_show">يمكن لأي صفحة ويب عرض المعاينة.</string> + <string name="chat_data">بيانات الدردشة</string> + <string name="channel_name_requires_newer_app_version">يتطلب الاتصال عبر اسم القناة إصدارًا أحدث من التطبيق.</string> + <string name="contact_name_requires_newer_app_version">يتطلب الاتصال عبر اسم جهة الاتصال إصدارًا أحدث من التطبيق.</string> + <string name="settings_section_title_contact">تواصل</string> + <string name="group_member_role_member_channel">مساهم</string> + <string name="copy_code">انسخ الرمز</string> + <string name="webpage_info">أنشئ صفحة ويب لعرض معاينة قناتك للزوار قبل اشتراكهم. يمكنك استضافتها بنفسك أو استخدام أي خدمة استضافة ثابتة.</string> + <string name="enter_webpage_url">أدخل عنوان URL لصفحة الويب</string> + <string name="group_webpage">صفحة ويب المجموعة</string> + <string name="help_and_support">المساعدة والدعم</string> + <string name="web_page_url_placeholder">https://</string> + <string name="webpage_url_footer">سيتم عرضه للمشتركين واستخدامه للسماح بتحميل المعاينة.</string> + <string name="more_privacy">مزيد من الخصوصية</string> + <string name="embed_only_your_page">لا يمكن عرض المعاينة إلا على صفحتك المذكورة أعلاه.</string> + <string name="please_upgrade_the_app">يُرجى ترقية التطبيق.</string> + <string name="channel_owners_contributors_count">%1$d مالكين ومساهمين</string> + <string name="badge_supported_simplex">%1$s دعم تطبيق SimpleX Chat. انتهت صلاحية الشارة في %2$s.</string> + <string name="relay_status_acknowledged_roster">قائمة المعترف بهم</string> + <string name="webpage_code_footer">أضف هذا الرمز إلى صفحة الويب الخاصة بك. سيُظهر هذا الرمز معاينة لقناتك أو مجموعتك.</string> + <string name="app_update_required">يلزم تحديث التطبيق</string> + <string name="badge_unknown_key_title">تعذّر التحقق من الشارة</string> + <string name="channel_webpage">صفحة القناة</string> + <string name="badge_invested">استثمر %s في حملة التمويل الجماعي لـ SimpleX Chat.</string> + <string name="badge_supports_simplex">%s يدعم SimpleX Chat.</string> + <string name="group_member_role_observer_channel">مشترك</string> + <string name="settings_section_title_support_project">ادعم المشروع</string> + <string name="badge_unknown_key_desc">الشارة موقّعة بمفتاح لا يتعرف عليه هذا الإصدار من التطبيق. حدِّث التطبيق للتحقق من هذه الشارة.</string> + <string name="member_role_will_be_changed_with_notification_channel">سيتم تغيير الدور إلى "%s". وسيتم إشعار جميع المشاركين في القناة بذلك.</string> + <string name="badge_unverified_desc">تعذّر التحقق من صحة هذه الشارة، وقد لا تكون أصلية.</string> + <string name="group_link_requires_newer_version">تتطلب هذه المجموعة إصدارًا أحدث من التطبيق. يُرجى تحديث التطبيق للانضمام إليها.</string> + <string name="unsupported_channel_name">اسم قناة غير مدعوم</string> + <string name="unsupported_contact_name">اسم جهة اتصال غير مدعوم</string> + <string name="badge_unverified_title">شارة غير متحقق منها</string> + <string name="relays_no_web_support">لا تدعم خوادم ترحيل الدردشة المستعملة صفحات الويب.</string> + <string name="webpage_code">رمز صفحة الويب</string> + <string name="badge_support_from_v7">يمكنك دعم SimpleX بدءًا من الإصدار 7 من التطبيق.</string> + <string name="error_saving_simplex_name">خطأ في حفظ الاسم</string> + <string name="set_user_simplex_name_footer">اسمح للناس بالتواصل معك عبر الاسم المسجَّل في عنوان SimpleX الخاص بك.</string> + <string name="set_channel_simplex_name_footer">اسمح للأشخاص بالانضمام باستخدام الاسم المسجَّل عبر رابط هذه القناة.</string> + <string name="simplex_name_not_found">لم يُعثر على الاسم</string> + <string name="simplex_name_no_servers_desc">لم تكوِّن أي من خوادمك لحل أسماء SimpleX. اضبط الخوادم أو استخدم رابط الاتصال.</string> + <string name="no_names_servers_enabled">لا توجد خوادم لحلّ الأسماء.</string> + <string name="simplex_name_no_valid_link">لا رابط صالح</string> + <string name="simplex_name_resolver_error_desc">خطأ المحلّل: %1$s</string> + <string name="simplex_name_server_no_resolver_desc">الخادم %1$s لا يدعم تحليل الأسماء. اضبط الخوادم أو استخدم رابط الاتصال.</string> + <string name="set_simplex_name">عيِّن اسم SimpleX</string> + <string name="simplex_name">اسم SimpleX</string> + <string name="simplex_name_error">خطأ في اسم SimpleX</string> + <string name="simplex_name_not_verified">لم يتحقق مِن اسم SimpleX</string> + <string name="simplex_name_no_valid_link_desc">اسم SimpleX %1$s مسجَّل، لكنه لا يحتوي على رابط صالح.</string> + <string name="simplex_name_unconfirmed_desc">اسم SimpleX %1$s مسجَّل، لكنه لم يُضف إلى ملف التعريف. يُرجى إضافته إلى عنوانك أو ملفك التعريفي للقناة، إذا كنت المالك.</string> + <string name="simplex_name_owner_no_channel_link">سُجَّل الاسم %1$s في SimpleX دون رابط القناة. أضف رابط القناة إلى الاسم عبر صفحة التسجيل.</string> + <string name="simplex_name_owner_no_address">سُجَّل الاسم %1$s في SimpleX دون عنوان SimpleX. أضف عنوان SimpleX الخاص بك إلى الاسم عبر صفحة التسجيل.</string> + <string name="simplex_name_not_found_desc">اسم SimpleX هذا غير مُسجَّل. يُرجى التحقق من الاسم.</string> + <string name="operator_use_for_names">لتحليل الاسم</string> + <string name="simplex_name_unconfirmed">اسم غير مؤكد</string> + <string name="verify_simplex_name_action">تحقق من الاسم</string> + <string name="verify_simplex_names">تحقق من أسماء SimpleX</string> + <string name="your_simplex_name">اسم SimpleX الخاص بك</string> + <string name="channel_simplex_name">اسم SimpleX للقناة</string> + <string name="connect_plan_connect_to_name">اتصل بـ %s</string> + <string name="do_not_require_message_signatures">لا تشترط التوقيع على الرسائل.</string> + <string name="get_simplex_name_beta">احصل على اسم SimpleX (تجريبي)</string> + <string name="connect_plan_join_name">انضم للقناة %s</string> + <string name="message_signatures_are_not_required">توقيع الرسالة ليس إلزاميًا.</string> + <string name="message_signatures_are_required">مطلوب توقيع الرسالة.</string> + <string name="register_test_name">سجِّل اسم اختبار</string> + <string name="remove_name">أزِل الاسم</string> + <string name="require_message_signatures">تطلب توقيع الرسائل.</string> + <string name="save_simplex_name_question">احفظ اسم SimpleX؟</string> + <string name="show_encryption">أظهر التعمية</string> + <string name="show_signature">أظهر التوقيع</string> + <string name="signature_missing_alert_title">التوقيع مفقود</string> + <string name="info_row_signed">موقَّع</string> + <string name="info_row_signed_verified">موقَّع ومتحقق منه</string> + <string name="sign_message_desc">التوقيع يثبت أنك مَن كاتب هذه الرسالة ولا يمكن إنكار ذلك لاحقًا.</string> + <string name="sign_message">وقِّع الرسالة</string> + <string name="sign_messages">وقِّع الرسائل</string> + <string name="signature_missing_alert_desc">طلبت القناة توقيع هذه الرسالة، لكن التوقيع غير موجود.</string> + <string name="add_description">أضف وصف</string> + <string name="profile_description__field">الوصف</string> + <string name="edit_description">حرّر الوصف</string> + <string name="enter_description_optional">أدِخل وصف (اختياري)</string> + <string name="error_sharing_address">خطأ في مشاركة العنوان</string> + <string name="to_verify_channel_member_key">للتحقق من المفاتيح مع هذا المشترك، قارن (أو امسح ضوئيًا) الرمز الموجود على أجهزتك.</string> + <string name="v7_0_channels_contributors">أضف المساهمين.</string> + <string name="v7_0_channels">قنوات أفضل 📢</string> + <string name="v7_0_channels_previews">أنشئ معاينة الويب.</string> + <string name="v7_0_channels_wider_messages">أسهل في القراءة.</string> + <string name="v7_0_channels_relays">أدِر مُرحلاتك.</string> + <string name="v7_0_simplex_names">أسماء SimpleX (تجريبي)</string> + <string name="v7_0_simplex_names_descr">أسماء لقناتك أو لشركتك.</string> + <string name="info_row_file_servers">خوادم الملفات</string> + <string name="share_text_file_servers">خوادم الملفات: %s</string> </resources> 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 d6d31dd4d1..739467b3ff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -15,6 +15,8 @@ <string name="connect_via_link_verb">Connect</string> <string name="connect_via_link_incognito">Connect incognito</string> <string name="connect_plan_open_chat">Open chat</string> + <string name="connect_plan_join_name">Join channel %s</string> + <string name="connect_plan_connect_to_name">Connect to %s</string> <string name="connect_plan_open_new_chat">Open new chat</string> <string name="connect_plan_open_group">Open group</string> <string name="connect_plan_open_new_group">Open new group</string> @@ -146,6 +148,7 @@ <string name="for_chat_profile">For chat profile %s:</string> <string name="errors_in_servers_configuration">Errors in servers configuration.</string> <string name="no_chat_relays_enabled">No chat relays enabled.</string> + <string name="no_names_servers_enabled">No servers to resolve names.</string> <string name="server_warning">Server warning</string> <string name="error_accepting_operator_conditions">Error accepting conditions</string> <string name="blocking_reason_spam">Spam</string> @@ -199,19 +202,29 @@ <string name="channel_name_requires_newer_app_version">Connecting via channel name requires a newer app version.</string> <string name="contact_name_requires_newer_app_version">Connecting via contact name requires a newer app version.</string> <string name="please_upgrade_the_app">Please upgrade the app.</string> + <string name="simplex_name_error">SimpleX name error</string> + <string name="simplex_name_no_servers_desc">None of your servers are set to resolve SimpleX names. Configure servers, or use a connection link.</string> + <string name="simplex_name_server_no_resolver_desc">Server %1$s does not support name resolution. Configure servers, or use a connection link.</string> + <string name="simplex_name_not_found">Name not found</string> + <string name="simplex_name_not_found_desc">This SimpleX name is not registered. Please check the name.</string> + <string name="simplex_name_resolver_error_desc">Resolver error: %1$s</string> + <string name="simplex_name_no_valid_link">No valid link</string> + <string name="simplex_name_no_valid_link_desc">The SimpleX name %1$s is registered, but it has no valid link.</string> + <string name="simplex_name_unconfirmed">Unconfirmed name</string> + <string name="simplex_name_unconfirmed_desc">The SimpleX name %1$s is registered, but not added to profile. Please add it to your address or channel profile, if you are the owner.</string> <string name="channel_temporarily_unavailable">Channel temporarily unavailable</string> <string name="channel_no_active_relays_try_later">Channel has no active relays. Please try to join later.</string> <string name="app_update_required">App update required</string> <string name="group_link_requires_newer_version">This group requires a newer version of the app. Please update the app to join.</string> - <string name="connection_error_auth">Connection error (AUTH)</string> - <string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string> + <string name="connection_error_auth">Connection link removed</string> + <string name="connection_error_auth_desc">Your contact removed this link, or it was a one-time link that was already used.\nTo connect, ask your contact to create a new link.</string> <string name="connection_error_blocked">Connection blocked</string> <string name="connection_error_blocked_desc">Connection is blocked by server operator:\n%1$s.</string> <string name="connection_error_quota">Undelivered messages</string> <string name="connection_error_quota_desc">The connection reached the limit of undelivered messages, your contact may be offline.</string> <string name="error_accepting_contact_request">Error accepting contact request</string> <string name="error_rejecting_contact_request">Error rejecting contact request</string> - <string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string> + <string name="sender_may_have_deleted_the_connection_request">The sender deleted the connection request.</string> <string name="error_deleting_contact">Error deleting contact</string> <string name="error_deleting_group">Error deleting group</string> <string name="error_deleting_note_folder">Error deleting private notes</string> @@ -380,6 +393,11 @@ <string name="reply_verb">Reply</string> <string name="share_verb">Share</string> <string name="copy_verb">Copy</string> + <string name="save_simplex_name_question">Save SimpleX name?</string> + <string name="get_simplex_name_beta">Get SimpleX name (BETA)</string> + <string name="channel_simplex_name">Channel SimpleX name</string> + <string name="register_test_name">How to register a test name</string> + <string name="remove_name">Remove name</string> <string name="save_verb">Save</string> <string name="edit_verb">Edit</string> <string name="info_menu">Info</string> @@ -556,6 +574,7 @@ <string name="forward_message">Forward message…</string> <string name="forward_multiple">Forward messages…</string> <string name="share_channel">Share channel…</string> + <string name="share_address">Share address…</string> <string name="cannot_share_message_alert_title">Cannot send message</string> <string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string> <string name="share_via_chat">Share via chat</string> @@ -566,8 +585,8 @@ <string name="chat_link_contact_address">Contact address</string> <string name="chat_link_one_time">One-time link</string> <string name="chat_link_from_owner">(from owner)</string> - <string name="chat_link_signed">(signed)</string> <string name="error_sharing_channel">Error sharing channel</string> + <string name="error_sharing_address">Error sharing address</string> <string name="owner_verification_passed">Link signature verified.</string> <string name="owner_verification_failed">⚠️ Signature verification failed: %s.</string> @@ -741,6 +760,19 @@ <string name="send_disappearing_message_5_minutes">5 minutes</string> <string name="send_disappearing_message_custom_time">Custom time</string> <string name="send_disappearing_message_send">Send</string> + <string name="sign_message">Sign message</string> + <string name="sign_message_desc">Signing proves you authored this message and can\'t be denied later.</string> + <string name="info_row_signed">Signed</string> + <string name="info_row_signed_verified">Signed & verified</string> + <string name="sign_messages">Sign messages</string> + <string name="require_message_signatures">Require signing messages.</string> + <string name="do_not_require_message_signatures">Do not require signing messages.</string> + <string name="message_signatures_are_required">Message signing is required.</string> + <string name="message_signatures_are_not_required">Message signing is not required.</string> + <string name="show_signature">Show signature</string> + <string name="show_encryption">Show encryption</string> + <string name="signature_missing_alert_title">Signature missing</string> + <string name="signature_missing_alert_desc">The channel required this message to be signed, but the signature is missing.</string> <string name="live_message">Live message!</string> <string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string> <string name="send_verb">Send</string> @@ -930,6 +962,17 @@ <string name="one_time_link">One-time invitation link</string> <string name="one_time_link_short">1-time link</string> <string name="simplex_address">SimpleX address</string> + <string name="verify_simplex_name_action">Verify name</string> + <string name="verify_simplex_names">Verify SimpleX names</string> + <string name="simplex_name_not_verified">SimpleX name not verified</string> + <string name="simplex_name">SimpleX name</string> + <string name="your_simplex_name">Your SimpleX name</string> + <string name="set_simplex_name">Set SimpleX name</string> + <string name="error_saving_simplex_name">Error saving name</string> + <string name="simplex_name_owner_no_channel_link">The SimpleX name %1$s is registered without channel link. Add channel link to the name via the registration page.</string> + <string name="simplex_name_owner_no_address">The SimpleX name %1$s is registered without SimpleX address. Add your SimpleX address to the name via the registration page.</string> + <string name="set_user_simplex_name_footer">Let people connect to you via name registered with your SimpleX address.</string> + <string name="set_channel_simplex_name_footer">Let people join via name registered with this channel link.</string> <string name="or_show_this_qr_code">Or show this code</string> <string name="full_link_button_text">Full link</string> <string name="short_link_button_text">Short link</string> @@ -971,6 +1014,7 @@ <string name="mark_code_verified">Mark verified</string> <string name="clear_verification">Clear verification</string> <string name="to_verify_compare">To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</string> + <string name="to_verify_channel_member_key">To verify keys with this subscriber, compare (or scan) the code on your devices.</string> <string name="is_verified">%s is verified</string> <string name="is_not_verified">%s is not verified</string> @@ -1108,7 +1152,7 @@ <string name="network_smp_web_port_off">Off</string> <string name="appearance_settings">Appearance</string> <string name="customize_theme_title">Customize theme</string> - <string name="theme_colors_section_title">INTERFACE COLORS</string> + <string name="theme_colors_section_title">Interface colors</string> <string name="app_version_title">App version</string> <string name="app_version_name">App version: v%s</string> <string name="app_version_code">App build: %s</string> @@ -1205,6 +1249,10 @@ <string name="full_name__field">Full name:</string> <string name="short_descr__field">Bio:</string> <string name="bio_too_large">Bio too large</string> + <string name="profile_description__field">Description</string> + <string name="add_description">Add description</string> + <string name="edit_description">Edit description</string> + <string name="enter_description_optional">Enter description (optional)</string> <string name="your_current_profile">Your current profile</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</string> <string name="edit_image">Edit image</string> @@ -1544,26 +1592,33 @@ <string name="privacy_chat_list_open_clean_web_link">Open clean link</string> <!-- Settings sections --> - <string name="settings_section_title_you">YOU</string> - <string name="settings_section_title_settings">SETTINGS</string> - <string name="settings_section_title_chat_database">CHAT DATABASE</string> - <string name="settings_section_title_help">HELP</string> - <string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string> - <string name="settings_section_title_app">APP</string> - <string name="settings_section_title_device">DEVICE</string> - <string name="settings_section_title_chats">CHATS</string> - <string name="settings_section_title_files">FILES</string> - <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> - <string name="settings_section_title_contact_requests_from_groups">CONTACT REQUESTS FROM GROUPS</string> + <string name="settings_section_title_you">You</string> + <string name="settings_section_title_settings">Settings</string> + <string name="settings_section_title_chat_database">Chat database</string> + <string name="settings_section_title_help">Help</string> + <string name="settings_section_title_support">Support SimpleX Chat</string> + <string name="settings_section_title_app">App</string> + <string name="settings_section_title_device">Device</string> + <string name="settings_section_title_chats">Chats</string> + <string name="settings_section_title_files">Files</string> + <string name="settings_section_title_delivery_receipts">Send delivery receipts to</string> + <string name="settings_section_title_contact_requests_from_groups">Contact requests from groups</string> + <string name="settings_section_title_about">About</string> + <string name="settings_section_title_contact">Contact</string> + <string name="settings_section_title_support_project">Support the project</string> + <string name="chat_data">Chat data</string> + <string name="help_and_support">Help & support</string> + <string name="more_privacy">More privacy</string> + <string name="advanced_settings">Advanced settings</string> <string name="settings_restart_app">Restart</string> <string name="settings_shutdown">Shutdown</string> <string name="settings_developer_tools">Developer tools</string> <string name="settings_experimental_features">Experimental features</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> - <string name="settings_section_title_interface" translatable="false">INTERFACE</string> + <string name="settings_section_title_socks">SOCKS proxy</string> + <string name="settings_section_title_interface" translatable="false">Interface</string> <string name="settings_section_title_language" translatable="false">LANGUAGE</string> - <string name="settings_section_title_icon">APP ICON</string> - <string name="settings_section_title_themes">THEMES</string> + <string name="settings_section_title_icon">App icon</string> + <string name="settings_section_title_themes">Themes</string> <string name="settings_section_title_profile_images">Profile images</string> <string name="settings_section_title_message_shape">Message shape</string> <string name="settings_message_shape_corner">Corner</string> @@ -1571,21 +1626,21 @@ <string name="settings_section_title_chat_theme">Chat theme</string> <string name="settings_section_title_user_theme">Profile theme</string> <string name="settings_section_title_chat_colors">Chat colors</string> - <string name="settings_section_title_messages">MESSAGES AND FILES</string> - <string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string> - <string name="settings_section_title_calls">CALLS</string> + <string name="settings_section_title_messages">Messages and files</string> + <string name="settings_section_title_private_message_routing">Private message routing</string> + <string name="settings_section_title_calls">Calls</string> <string name="settings_section_title_network_connection">Network connection</string> <string name="settings_section_title_incognito">Incognito mode</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="settings_section_title_use_from_desktop">Use from desktop</string> <!-- DatabaseView.kt --> <string name="your_chat_database">Your chat database</string> - <string name="run_chat_section">RUN CHAT</string> + <string name="run_chat_section">Run chat</string> <string name="remote_hosts_section">Remote mobiles</string> <string name="chat_is_running">Chat is running</string> <string name="chat_is_stopped">Chat is stopped</string> - <string name="chat_database_section">CHAT DATABASE</string> + <string name="chat_database_section">Chat database</string> <string name="database_passphrase">Database passphrase</string> <string name="export_database">Export database</string> <string name="import_database">Import database</string> @@ -1843,8 +1898,10 @@ <!-- GroupMemberRole --> <string name="group_member_role_observer">observer</string> + <string name="group_member_role_observer_channel">subscriber</string> <string name="group_member_role_author">author</string> <string name="group_member_role_member">member</string> + <string name="group_member_role_member_channel">contributor</string> <string name="group_member_role_moderator">moderator</string> <string name="group_member_role_admin">admin</string> <string name="group_member_role_owner">owner</string> @@ -1894,7 +1951,7 @@ <string name="button_add_members">Invite members</string> <string name="button_add_team_members">Add team members</string> <string name="button_add_friends">Add friends</string> - <string name="group_info_section_title_num_members">%1$s MEMBERS</string> + <string name="group_info_section_title_num_members">%1$s members</string> <string name="group_info_member_you">you: %1$s</string> <string name="button_delete_group">Delete group</string> <string name="button_delete_channel">Delete channel</string> @@ -1918,6 +1975,20 @@ <string name="button_welcome_message">Welcome message</string> <string name="group_link">Group link</string> <string name="channel_link">Channel link</string> + <string name="channel_webpage">Channel webpage</string> + <string name="group_webpage">Group webpage</string> + <string name="advanced_options">Advanced options</string> + <string name="web_page_url_placeholder">https://</string> + <string name="allow_anyone_to_embed">Allow anyone to embed</string> + <string name="enter_webpage_url">Enter webpage URL</string> + <string name="webpage_url_footer">It will be shown to subscribers and used to allow loading the preview.</string> + <string name="webpage_code">Webpage code</string> + <string name="webpage_code_footer">Add this code to your webpage. It will display the preview of your channel / group.</string> + <string name="copy_code">Copy code</string> + <string name="webpage_info">Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting.</string> + <string name="relays_no_web_support">Used chat relays do not support webpages.</string> + <string name="embed_any_webpage_can_show">Any webpage can show the preview.</string> + <string name="embed_only_your_page">Only your page above can show the preview.</string> <string name="create_group_link">Create group link</string> <string name="button_create_group_link">Create link</string> <string name="delete_link_question">Delete link?</string> @@ -1947,13 +2018,14 @@ <string name="button_channel_relays">Chat relays</string> <!-- Chat / Chat item info --> - <string name="section_title_for_console">FOR CONSOLE</string> + <string name="section_title_for_console">For console</string> <string name="info_row_local_name">Local name</string> <string name="info_row_database_id">Database ID</string> <string name="info_row_debug_delivery">Debug delivery</string> <string name="info_row_updated_at">Record updated at</string> <string name="info_row_message_status">Message status</string> <string name="info_row_file_status">File status</string> + <string name="info_row_file_servers">File servers</string> <string name="info_row_sent_at">Sent at</string> <string name="info_row_created_at">Created at</string> <string name="info_row_received_at">Received at</string> @@ -1964,6 +2036,7 @@ <string name="share_text_updated_at">Record updated at: %s</string> <string name="share_text_message_status">Message status: %s</string> <string name="share_text_file_status">File status: %s</string> + <string name="share_text_file_servers">File servers: %s</string> <string name="share_text_sent_at">Sent at: %s</string> <string name="share_text_created_at">Created at: %s</string> <string name="share_text_received_at">Received at: %s</string> @@ -2017,14 +2090,15 @@ <string name="member_info_member_disabled">disabled</string> <string name="member_info_member_failed">failed</string> <string name="member_info_member_inactive">inactive</string> - <string name="member_info_section_title_member">MEMBER</string> + <string name="member_info_section_title_member">Member</string> <string name="role_in_group">Role</string> <string name="change_role">Change role</string> <string name="change_verb">Change</string> <string name="switch_verb">Switch</string> - <string name="change_member_role_question">Change group role?</string> + <string name="change_member_role_question">Change role?</string> <string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string> <string name="member_role_will_be_changed_with_notification_chat">The role will be changed to "%s". Everyone in the chat will be notified.</string> + <string name="member_role_will_be_changed_with_notification_channel">The role will be changed to "%s". Everyone in the channel will be notified.</string> <string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string> <string name="connect_via_member_address_alert_title">Connect directly?</string> <string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string> @@ -2034,7 +2108,7 @@ <string name="info_row_group">Group</string> <string name="info_row_chat">Chat</string> <string name="info_row_connection">Connection</string> - <string name="info_row_connection_failed">CONNECTION FAILED</string> + <string name="info_row_connection_failed">Connection failed</string> <string name="conn_level_desc_direct">direct</string> <string name="conn_level_desc_indirect">indirect (%1$s)</string> <string name="message_queue_info">Message queue info</string> @@ -2063,7 +2137,7 @@ <string name="message_too_large">Message too large</string> <!-- ConnectionStats --> - <string name="conn_stats_section_title_servers">SERVERS</string> + <string name="conn_stats_section_title_servers">Servers</string> <string name="receiving_via">Receiving via</string> <string name="sending_via">Sending via</string> <string name="network_status">Network status</string> @@ -2129,6 +2203,7 @@ <string name="operator_use_for_messages">Use for messages</string> <string name="operator_use_for_messages_receiving">To receive</string> <string name="operator_use_for_messages_private_routing">For private routing</string> + <string name="operator_use_for_names">To resolve names</string> <string name="operator_added_message_servers">Added message servers</string> <string name="operator_use_for_files">Use for files</string> <string name="operator_use_for_sending">To send</string> @@ -2661,6 +2736,17 @@ <string name="v6_5_safe_web_links_descr">- opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking.</string> <string name="v6_5_non_profit_governance">Non-profit governance</string> <string name="v6_5_non_profit_governance_descr">To make SimpleX Network last.</string> + <string name="v7_0_invest" translatable="false">You can now invest in SimpleX Chat! 🚀</string> + <string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string> + <string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string> + <string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string> + <string name="v7_0_simplex_names">SimpleX public names (BETA)</string> + <string name="v7_0_simplex_names_descr">Public names for your channel or business.</string> + <string name="v7_0_channels">Better channels 📢</string> + <string name="v7_0_channels_previews">Create web preview.</string> + <string name="v7_0_channels_relays">Manage your relays.</string> + <string name="v7_0_channels_contributors">Add contributors.</string> + <string name="v7_0_channels_wider_messages">Easier to read.</string> <string name="view_updated_conditions">View updated conditions</string> <!-- CustomTimePicker --> @@ -2681,7 +2767,7 @@ <string name="dont_enable_receipts">Don\'t enable</string> <string name="you_can_enable_delivery_receipts_later">You can enable later via Settings</string> <string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string> - <string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string> + <string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Your privacy settings.</string> <string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string> <!-- Remote access --> @@ -2952,9 +3038,12 @@ <!-- ChannelMembersView.kt --> <string name="channel_members_title_subscribers">Subscribers</string> - <string name="channel_members_section_owners">Owners</string> + <string name="channel_members_section_owners">Owners & contributors</string> <string name="channel_subscriber_count_singular">%1$d subscriber</string> <string name="channel_subscriber_count_plural">%1$d subscribers</string> + <string name="channel_owner_count_singular">%1$d owner</string> + <string name="channel_owner_count_plural">%1$d owners</string> + <string name="channel_owners_contributors_count">%1$d owners & contributors</string> <string name="channel_member_you">you</string> <!-- ChatRelayView.kt --> @@ -3001,6 +3090,7 @@ <string name="relay_status_new">new</string> <string name="relay_status_invited">invited</string> <string name="relay_status_accepted">accepted</string> + <string name="relay_status_acknowledged_roster">acknowledged roster</string> <string name="relay_status_active">active</string> <string name="relay_status_inactive">inactive</string> <string name="relay_status_rejected">rejected</string> @@ -3027,9 +3117,9 @@ <string name="relay_bar_subscriber_waiting">Waiting for channel owner to add relays.</string> <!-- GroupMemberInfoView.kt channel-related --> - <string name="member_info_section_title_relay">RELAY</string> - <string name="member_info_section_title_owner">OWNER</string> - <string name="member_info_section_title_subscriber">SUBSCRIBER</string> + <string name="member_info_section_title_relay">Relay</string> + <string name="member_info_section_title_owner">Owner</string> + <string name="member_info_section_title_subscriber">Subscriber</string> <string name="info_row_channel">Channel</string> <string name="info_row_relay_link">Relay link</string> <string name="info_row_relay_address">Relay address</string> @@ -3047,7 +3137,6 @@ <!-- AddChannelView.kt --> <string name="create_channel_title">Create public channel</string> <string name="create_channel_button">Create public channel</string> - <string name="create_channel_beta_button">Create public channel (BETA)</string> <string name="channel_display_name_field">Channel name</string> <string name="creating_channel">Creating channel</string> <string name="error_creating_channel">Error creating channel</string> 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 c691447b32..9676bc0199 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -83,7 +83,7 @@ <string name="keychain_is_storing_securely">Android Keystore се използва за сигурно съхраняване на паролата - тоа позволява на услугата за известия да работи.</string> <string name="empty_chat_profile_is_created">Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено.</string> <string name="notifications_mode_off_desc">Приложението може да получава известия само когато работи, няма да се стартира услуга във фонов режим</string> - <string name="settings_section_title_icon">ИКОНА НА ПРИЛОЖЕНИЕТО</string> + <string name="settings_section_title_icon">Икона на приложението</string> <string name="la_authenticate">Идентифицирай</string> <string name="turning_off_service_and_periodic">Оптимизацията на батерията е активна, изключват се фоновата услуга и периодичните заявки за нови съобщения. Можете да ги активирате отново през настройките.</string> <string name="network_session_mode_user_description"><![CDATA[Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.]]></string> @@ -169,20 +169,20 @@ <string name="call_connection_via_relay">чрез реле</string> <string name="icon_descr_video_call">видео разговор</string> <string name="your_calls">Вашите обаждания</string> - <string name="settings_section_title_app">ПРИЛОЖЕНИЕ</string> + <string name="settings_section_title_app">Приложение</string> <string name="full_backup">Резервно копие на данните от приложението</string> <string name="app_passcode_replaced_with_self_destruct">Кода за достъп до приложение се заменя с код за самоунищожение.</string> <string name="auto_accept_images">Автоматично приемане на изображения</string> <string name="authentication_cancelled">Идентификацията е отменена</string> <string name="send_link_previews">Изпрати визуализация на линковете</string> - <string name="settings_section_title_calls">ОБАЖДАНИЯ</string> + <string name="settings_section_title_calls">Обаждания</string> <string name="keychain_allows_to_receive_ntfs">Android Keystore ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на известия.</string> <string name="change_database_passphrase_question">Промяна на паролата на базата данни\?</string> <string name="rcv_group_event_changed_member_role">променена ролята от %s на %s</string> <string name="invite_prohibited">Не може да покани контакта!</string> <string name="rcv_conn_event_switch_queue_phase_completed">адреса за изпращане е променен</string> <string name="change_verb">Промени</string> - <string name="change_member_role_question">Промяна на груповата роля\?</string> + <string name="change_member_role_question">Промяна на груповата роля?</string> <string name="you_will_still_receive_calls_and_ntfs">Все още ще получавате обаждания и известия от заглушени профили, когато са активни.</string> <string name="allow_disappearing_messages_only_if">Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</string> <string name="allow_your_contacts_irreversibly_delete">Позволи на вашите контакти да изтриват необратимо изпратените съобщения. (24 часа)</string> @@ -217,13 +217,13 @@ <string name="auth_device_authentication_is_disabled_turning_off">Идентификацията на устройството е деактивирано. Изключване на SimpleX заключване.</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Идентификацията на устройството не е активирана. Можете да включите SimpleX заключване през Настройки, след като активирате идентификацията на устройството.</string> <string name="auth_simplex_lock_turned_on">SimpleX заключване е включено</string> - <string name="connection_error_auth">Грешка при свързване (AUTH)</string> + <string name="connection_error_auth">Грешка при свързване</string> <string name="display_name_connecting">свързване…</string> <string name="switch_receiving_address">Промени адреса за получаване</string> <string name="chat_database_deleted">Базата данни е изтрита</string> <string name="chat_is_running">Чатът работи</string> <string name="chat_is_stopped">Чатът е спрян</string> - <string name="chat_database_section">БАЗА ДАННИ</string> + <string name="chat_database_section">База данни</string> <string name="chat_database_imported">Базата данни е импортирана</string> <string name="confirm_new_passphrase">Потвърди новата парола…</string> <string name="confirm_database_upgrades">Потвърди актуализаациите на базата данни</string> @@ -314,13 +314,13 @@ <string name="change_lock_mode">Промяна на режима на заключване</string> <string name="change_self_destruct_mode">Промени режима на самоунищожение</string> <string name="change_self_destruct_passcode">Промени кода за достъп за самоунищожение</string> - <string name="settings_section_title_chats">ЧАТОВЕ</string> + <string name="settings_section_title_chats">Чатове</string> <string name="rcv_conn_event_switch_queue_phase_changing">промяна на адреса…</string> <string name="maximum_supported_file_size">В момента максималният поддържан размер на файла е %1$s.</string> <string name="info_row_database_id">ID в базата данни</string> <string name="share_text_database_id">ID в базата данни: %d</string> <string name="receipts_section_contacts">Контакти</string> - <string name="settings_section_title_themes">ТЕМИ</string> + <string name="settings_section_title_themes">Теми</string> <string name="set_password_to_export_desc">Базата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране.</string> <string name="database_passphrase">Парола за базата данни</string> <string name="delete_database">Изтрий базата данни</string> @@ -406,7 +406,7 @@ <string name="developer_options">Идентификатори в базата данни и опция за изолация на транспорта.</string> <string name="delete_address">Изтрий адрес</string> <string name="delete_address__question">Изтрий адрес\?</string> - <string name="theme_colors_section_title">ЦВЕТОВЕ НА ИНТЕРФЕЙСА</string> + <string name="theme_colors_section_title">Цветове на интерфейса</string> <string name="create_profile_button">Създай</string> <string name="create_profile">Създай профил</string> <string name="delete_image">Изтрий изображение</string> @@ -446,7 +446,7 @@ <string name="receipts_contacts_title_enable">Активирай потвърждениeто\?</string> <string name="receipts_contacts_override_disabled">Изпращането на потвърждениe за доставка е деактивирано за %d контакта</string> <string name="receipts_contacts_override_enabled">Изпращането на потвърждениe е активирано за %d контакта</string> - <string name="settings_section_title_device">УСТРОЙСТВО</string> + <string name="settings_section_title_device">Устройство</string> <string name="receipts_contacts_disable_keep_overrides">Деактивиране (запазване на промените)</string> <string name="total_files_count_and_size">%d файл(а) с общ размер от %s</string> <string name="encrypt_database">Криптирай</string> @@ -476,7 +476,7 @@ <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string> <string name="send_receipts">Изпращане на потвърждениe за доставка</string> <string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string> - <string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за Поверителност и сигурност на приложението.</string> <string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string> <string name="enter_correct_passphrase">Въведи правилна парола.</string> <string name="feature_enabled_for_you">активирано за вас</string> @@ -485,7 +485,7 @@ <string name="receipts_section_description_1">Те могат да бъдат променени в настройките за всеки контакт и група.</string> <string name="settings_developer_tools">Инструменти за разработчици</string> <string name="receipts_contacts_disable_for_all">Деактивиране за всички</string> - <string name="settings_section_title_delivery_receipts">ИЗПРАЩАЙТЕ ПОТВЪРЖДЕНИE ЗА ДОСТАВКА НА</string> + <string name="settings_section_title_delivery_receipts">Изпращайте потвърждение за доставка на</string> <string name="delete_messages_after">Изтрий съобщенията след</string> <string name="chat_item_ttl_seconds">%s секунда(и)</string> <string name="delete_messages">Изтрий съобщенията</string> @@ -622,7 +622,7 @@ <string name="full_name__field">Пълно име:</string> <string name="exit_without_saving">Изход без запазване</string> <string name="hidden_profile_password">Парола за скрит профил</string> - <string name="settings_section_title_experimenta">ЕКСПЕРИМЕНТАЛЕН</string> + <string name="settings_section_title_experimenta">Експериментален</string> <string name="file_with_path">Файл: %s</string> <string name="icon_descr_expand_role">Разшири избора на роля</string> <string name="fix_connection_question">Поправи връзката\?</string> @@ -632,7 +632,7 @@ <string name="v4_4_disappearing_messages_desc">Изпратените съобщения ще бъдат изтрити след зададеното време.</string> <string name="group_link">Групов линк</string> <string name="files_and_media">Файлове и медия</string> - <string name="section_title_for_console">ЗА КОНЗОЛАТА</string> + <string name="section_title_for_console">За конзолата</string> <string name="group_preferences">Групови настройки</string> <string name="icon_descr_file">Файл</string> <string name="file_not_found">Файлът не е намерен</string> @@ -643,7 +643,7 @@ <string name="v5_2_favourites_filter_descr">Филтрирайте непрочетените и любимите чатове.</string> <string name="group_members_can_send_dms">Членовете могат да изпращат лични съобщения.</string> <string name="icon_descr_help">помощ</string> - <string name="settings_section_title_help">ПОМОЩ</string> + <string name="settings_section_title_help">Помощ</string> <string name="email_invite_body">Здравей, \nСвържи се с мен през SimpleX Chat: %s</string> <string name="group_members_can_add_message_reactions">Членовете могат да добавят реакции към съобщенията.</string> @@ -805,7 +805,7 @@ <string name="onboarding_notifications_mode_off">Когато приложението работи</string> <string name="onboarding_notifications_mode_periodic">Периодично</string> <string name="paste_the_link_you_received">Постави получения линк</string> - <string name="settings_section_title_messages">СЪОБЩЕНИЯ И ФАЙЛОВЕ</string> + <string name="settings_section_title_messages">Съобщения и файлове</string> <string name="no_received_app_files">Няма получени или изпратени файлове</string> <string name="notifications_will_be_hidden">Известията ще се доставят само докато приложението не е спряно!</string> <string name="remove_passphrase_from_keychain">Премахване на парола от Keystore\?</string> @@ -987,7 +987,7 @@ <string name="lock_mode">Режим на заключване</string> <string name="alert_text_fragment_please_report_to_developers">Моля, докладвайте го на разработчиците.</string> <string name="protect_app_screen">Защити екрана на приложението</string> - <string name="member_info_section_title_member">ЧЛЕН</string> + <string name="member_info_section_title_member">Член</string> <string name="remove_member_confirmation">Премахване</string> <string name="network_option_ping_count">PING бройка</string> <string name="only_your_contact_can_add_message_reactions">Само вашият контакт може да добавя реакции на съобщенията.</string> @@ -1042,8 +1042,8 @@ <string name="share_with_contacts">Сподели с контактите</string> <string name="stop_sharing">Спри споделянето</string> <string name="stop_sharing_address">Спри споделянето на адреса\?</string> - <string name="settings_section_title_settings">НАСТРОЙКИ</string> - <string name="run_chat_section">СТАРТИРАНЕ НА ЧАТ</string> + <string name="settings_section_title_settings">Настройки</string> + <string name="run_chat_section">Стартиране на чат</string> <string name="text_field_set_contact_placeholder">Задай име на контакт…</string> <string name="no_info_on_delivery">Няма информация за доставката</string> <string name="revoke_file__title">Отзови файл\?</string> @@ -1067,7 +1067,7 @@ <string name="receipts_groups_override_enabled">Изпращането на потвърждениe за доставка е разрешено за %d групи</string> <string name="restart_the_app_to_use_imported_chat_database">Рестартирайте приложението, за да използвате импортирана база данни.</string> <string name="send_receipts_disabled_alert_msg">Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща.</string> - <string name="conn_stats_section_title_servers">СЪРВЪРИ</string> + <string name="conn_stats_section_title_servers">Сървъри</string> <string name="recipient_colon_delivery_status">%s: %s</string> <string name="delivery">Доставка</string> <string name="receipts_groups_enable_keep_overrides">Активиране (запазване на груповите промени)</string> @@ -1093,7 +1093,7 @@ <string name="share_image">Сподели медия…</string> <string name="simplex_address">SimpleX адрес</string> <string name="v4_2_security_assessment_desc">Сигурността на SimpleX Chat беше одитирана от Trail of Bits.</string> - <string name="settings_section_title_socks">SOCKS ПРОКСИ</string> + <string name="settings_section_title_socks">SOCKS прокси</string> <string name="settings_restart_app">Рестартиране</string> <string name="settings_shutdown">Изключване</string> <string name="restart_the_app_to_create_a_new_chat_profile">Рестартирайте приложението, за да създадете нов чат профил.</string> @@ -1152,7 +1152,7 @@ <string name="color_title">Заглавие</string> <string name="to_share_with_your_contact">(за споделяне с вашия контакт)</string> <string name="alert_message_no_group">Тази група вече не съществува.</string> - <string name="settings_section_title_support">ПОДКРЕПЕТЕ SIMPLEX CHAT</string> + <string name="settings_section_title_support">Подкрепете SimpleX Chat</string> <string name="contact_sent_large_file">Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%1$s).</string> <string name="in_developing_desc">Тази функция все още не се поддържа. Опитайте следващата версия.</string> <string name="tap_to_start_new_chat">Докосни за започване на нов чат</string> @@ -1231,7 +1231,7 @@ <string name="snd_conn_event_switch_queue_phase_completed">адреса за получаване е променен</string> <string name="you_can_share_this_address_with_your_contacts">Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s.</string> <string name="unfavorite_chat">Премахни от любимите</string> - <string name="settings_section_title_you">ВИЕ</string> + <string name="settings_section_title_you">Вие</string> <string name="your_chat_database">Вашата база данни</string> <string name="icon_descr_waiting_for_image">Изчаква се получаването на изображението</string> <string name="waiting_for_image">Изчаква се получаването на изображението</string> @@ -1294,8 +1294,7 @@ <string name="description_you_shared_one_time_link">споделихте еднократен линк за връзка</string> <string name="description_you_shared_one_time_link_incognito">споделихте еднократен инкогнито линк за връзка</string> <string name="description_via_one_time_link">чрез еднократен линк за връзка</string> - <string name="connection_error_auth_desc">Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. -\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</string> + <string name="connection_error_auth_desc">Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. \nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</string> <string name="smp_server_test_upload_file">Качи файл</string> <string name="you_are_already_connected_to_vName_via_this_link">Вече сте вече свързани с %1$s.</string> <string name="la_notice_turn_on">Включи</string> @@ -1775,7 +1774,7 @@ <string name="private_routing_explanation">За да защити вашия IP адрес, поверително рутиране използва вашите SMP сървъри за доставяне на съобщения.</string> <string name="forward_alert_forward_messages_without_files">Препращане на съобщенията без файловете?</string> <string name="network_smp_proxy_mode_unknown">Неизвестни сървъри</string> - <string name="settings_section_title_files">ФАЙЛОВЕ</string> + <string name="settings_section_title_files">Файлове</string> <string name="chat_list_always_visible">Показване на списъка на чатовете в нов прозорец</string> <string name="color_mode_system">Системна</string> <string name="color_mode_dark">Тъмна</string> @@ -1800,7 +1799,7 @@ <string name="snd_error_proxy">Препращащ сървър: %1$s\nГрешка: %2$s</string> <string name="srv_error_version">Версията на сървъра е несъвместима с мрежовите настройки.</string> <string name="protect_ip_address">Защити IP адреса</string> - <string name="settings_section_title_private_message_routing">ПОВЕРИТЕЛНО РУТИРАНЕ НА СЪОБЩЕНИЯ</string> + <string name="settings_section_title_private_message_routing">Поверително рутиране на съобщения</string> <string name="app_will_ask_to_confirm_unknown_file_servers">Приложението ще поиска потвърждение за изтегляния от неизвестни файлови сървъри (с изключение на .onion сървъри или когато SOCKS прокси е активирано).</string> <string name="ci_status_other_error">Грешка: %1$s</string> <string name="forward_files_not_accepted_receive_files">Изтегляне</string> @@ -2038,7 +2037,7 @@ <string name="app_check_for_updates_button_download">Изтегли %s (%s)</string> <string name="app_check_for_updates_button_skip">Пропусни тази версия</string> <string name="app_check_for_updates_notice_title">Провери за актуализации</string> - <string name="settings_section_title_chat_database">БАЗА ДАННИ</string> + <string name="settings_section_title_chat_database">База данни</string> <string name="you_can_still_send_messages_to_contact">Можете да изпращате съобщения до %1$s от архивираните контакти.</string> <string name="chat_bottom_bar">Достъпен панел</string> <string name="cant_send_message_to_member_alert_title">Изпращането на съобщения на груповия член не е налично</string> @@ -2501,7 +2500,7 @@ <string name="share_old_link_alert_button">Сподели стар линк</string> <string name="share_group_profile_via_link_alert_text">Линкът ще бъде кратък и профилът на групата ще бъде споделен чрез него.</string> <string name="upgrade_group_link">Обнови групов линк</string> - <string name="settings_section_title_contact_requests_from_groups">ЗАЯВКИ ЗА КОНТАКТ ОТ ГРУПИ</string> + <string name="settings_section_title_contact_requests_from_groups">Заявки за контакт от групи</string> <string name="member_is_deleted_cant_accept_request">Членът е изтрит - не може да се приеме заявката</string> <string name="rcv_direct_event_group_inv_link_received">заявка за връзка от група %1$s</string> <string name="this_setting_is_for_your_current_profile">Тази настройка е за текущия профил</string> 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 7ab3f5a381..ce38afb836 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -126,10 +126,10 @@ <string name="all_app_data_will_be_cleared">S\'eliminaran totes les dades de l\'aplicació.</string> <string name="empty_chat_profile_is_created">Es crea un perfil de xat buit amb el nom proporcionat i l\'aplicació s\'obre com de costum.</string> <string name="app_passcode_replaced_with_self_destruct">La contrasenya de l\'aplicació es substitueix per una contrasenya d\'autodestrucció.</string> - <string name="settings_section_title_app">APLICACIÓ</string> - <string name="settings_section_title_icon">ICONA APLICACIÓ</string> + <string name="settings_section_title_app">Aplicació</string> + <string name="settings_section_title_icon">Icona aplicació</string> <string name="privacy_media_blur_radius">Desenfocar els mitjans</string> - <string name="settings_section_title_calls">TRUCADES</string> + <string name="settings_section_title_calls">Trucades</string> <string name="keychain_is_storing_securely">Android Keystore s\'utilitza per emmagatzemar de manera segura la frase de contrasenya: permet que el servei de notificacions funcioni.</string> <string name="keychain_allows_to_receive_ntfs">Android Keystore s\'utilitzarà per emmagatzemar de manera segura la frase de contrasenya després de reiniciar l\'aplicació o canviar la frase de contrasenya; permetrà rebre notificacions.</string> <string name="cannot_access_keychain">No es pot accedir a Keystore per desar la contrasenya de la base de dades</string> @@ -383,7 +383,7 @@ <string name="receipts_contacts_title_disable">Desactivar rebuts?</string> <string name="receipts_groups_title_disable">Desactivar rebuts per a grups?</string> <string name="settings_developer_tools">Eines per a desenvolupadors</string> - <string name="settings_section_title_device">DISPOSITIU</string> + <string name="settings_section_title_device">Dispositiu</string> <string name="set_password_to_export_desc">La base de dades es xifra amb una contrasenya aleatòria. Si us plau, canvieu-la abans d\'exportar.</string> <string name="database_passphrase">Contrasenya de la base de dades</string> <string name="delete_chat_profile_question">Voleu suprimir el perfil?</string> @@ -514,8 +514,8 @@ <string name="change_self_destruct_mode">Canvia el mode l\'autodestrucció</string> <string name="change_self_destruct_passcode">Canvia el codi d\'autodestrucció</string> <string name="confirm_passcode">Confirmeu el codi d\'accés</string> - <string name="settings_section_title_chat_database">BASE DE DADES DELS XATS</string> - <string name="settings_section_title_chats">XATS</string> + <string name="settings_section_title_chat_database">Base de dades dels xats</string> + <string name="settings_section_title_chats">Xats</string> <string name="settings_section_title_chat_theme">Tema del xat</string> <string name="settings_section_title_chat_colors">Colors del xat</string> <string name="chat_database_deleted">Base de dades suprimida</string> @@ -551,7 +551,7 @@ <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[També us podeu connectar fent clic a l\'enllaç. Si s\'obre al navegador, feu clic al botó <b>Obre a l\'aplicació mòbil</b>.]]></string> <string name="error_saving_ICE_servers">Error en desar els servidors ICE</string> <string name="network_proxy_incorrect_config_title">Error en desar el servidor intermediari</string> - <string name="chat_database_section">BASE DE DADES DELS XATS</string> + <string name="chat_database_section">Base de dades dels xats</string> <string name="chat_is_running">El xat s\'està executant</string> <string name="chat_is_stopped">El xat està aturat</string> <string name="error_with_info">Error: %s</string> @@ -575,7 +575,7 @@ <string name="connection_error">Error de connexió</string> <string name="connection_timeout">Temps de connexió enhaurit</string> <string name="contact_already_exists">El contacte la existeix</string> - <string name="connection_error_auth">Error de connexió (AUTH)</string> + <string name="connection_error_auth">Error de connexió</string> <string name="notification_preview_mode_contact">Nom del contacte</string> <string name="contact_deleted">Contacte eliminat!</string> <string name="alert_title_contact_connection_pending">El contacte encara no s\'hi ha connectat!</string> @@ -742,7 +742,7 @@ <string name="contact_connection_pending">s\'està connectant…</string> <string name="onboarding_network_operators_conditions_will_be_accepted">Les condicions s\'acceptaran per als operadors habilitats després de 30 dies.</string> <string name="system_restricted_background_desc">SimpleX no pot funcionar en segon pla. Només rebreu les notificacions quan obriu l\'aplicació.</string> - <string name="ntf_channel_calls">Trucades de SimpleX chat</string> + <string name="ntf_channel_calls">Trucades de SimpleX Chat</string> <string name="ntf_channel_messages">Missatges de xat de SimpleX</string> <string name="icon_descr_sent_msg_status_sent">enviat</string> <string name="icon_descr_received_msg_status_unread">per llegir</string> @@ -781,7 +781,7 @@ <string name="show_dev_options">Mostra:</string> <string name="hide_dev_options">Amaga:</string> <string name="theme_simplex">SimpleX</string> - <string name="v4_2_security_assessment_desc">La seguretat de SimpleX chat ha estat auditada per Trail of Bits.</string> + <string name="v4_2_security_assessment_desc">La seguretat de SimpleX Chat ha estat auditada per Trail of Bits.</string> <string name="email_invite_subject">Parlem a SimpleX Chat</string> <string name="invalid_name">El nom no és vàlid!</string> <string name="italic_text">cursiva</string> @@ -794,7 +794,7 @@ <string name="member_info_member_inactive">inactiu</string> <string name="chat_theme_apply_to_light_mode">Mode clar</string> <string name="v5_4_incognito_groups">Grups d\'incògnit</string> - <string name="member_info_section_title_member">MEMBRE</string> + <string name="member_info_section_title_member">Membre</string> <string name="join_group_question">Voleu unir-vos al grup?</string> <string name="leave_group_button">Surt</string> <string name="leave_chat_question">Voleu sortir del xat?</string> @@ -966,8 +966,8 @@ <string name="receipts_contacts_title_enable">Activar els rebuts?</string> <string name="enable_self_destruct">Activar autodestrucció</string> <string name="receipts_groups_title_enable">Activar els rebuts per a grups?</string> - <string name="settings_section_title_files">FITXERS</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_files">Fitxers</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="export_database">Exportar base de dades</string> <string name="encrypt_database">Xifrar</string> <string name="file_with_path">Fitxer: %s</string> @@ -981,7 +981,7 @@ <string name="alert_title_no_group">Grup no trobat!</string> <string name="conn_event_ratchet_sync_required">es requereix renegociar el xifratge</string> <string name="group_member_status_group_deleted">grup esborrat</string> - <string name="section_title_for_console">PER A CONSOLA</string> + <string name="section_title_for_console">Per a consola</string> <string name="fix_connection_question">Arreglar connexió?</string> <string name="fix_connection_not_supported_by_group_member">Correcció no suportada per membre del grup</string> <string name="group_full_name_field">Nom complet del grup:</string> @@ -1055,7 +1055,7 @@ <string name="permissions_grant">Donar permís(os) per fer trucades</string> <string name="audio_device_wired_headphones">Auriculars</string> <string name="encrypt_local_files">Xifra fitxers locals</string> - <string name="settings_section_title_help">AJUT</string> + <string name="settings_section_title_help">Ajut</string> <string name="files_and_media_section">Arxius i mitjans</string> <string name="encrypt_database_question">Xifrar base de dades?</string> <string name="encrypted_database">Base de dades xifrada</string> @@ -1109,7 +1109,7 @@ <string name="compose_message_placeholder">Missatge</string> <string name="maximum_message_size_title">El missatge és massa llarg!</string> <string name="info_view_message_button">missatge</string> - <string name="settings_section_title_messages">MISSATGES I FITXERS</string> + <string name="settings_section_title_messages">Missatges i fitxers</string> <string name="messages_section_title">Missatges</string> <string name="info_row_message_status">Estat del missatge</string> <string name="share_text_message_status">Estat del missatge: %s</string> @@ -1266,15 +1266,15 @@ <string name="receipts_contacts_override_disabled">L\'enviament de rebuts està desactivat per a %d contactes</string> <string name="receipts_contacts_override_enabled">L\'enviament de rebuts està habilitat per a %d contactes</string> <string name="receipts_groups_override_enabled">L\'enviament de rebuts està habilitat per a %d grups</string> - <string name="settings_section_title_delivery_receipts">ENVIAR ELS REBUS DE LLIURAMENT A</string> + <string name="settings_section_title_delivery_receipts">Enviar els rebus de lliurament a</string> <string name="receipts_groups_override_disabled">L\'enviament de rebuts està desactivat per a %d grups</string> <string name="settings_restart_app">Reiniciar</string> - <string name="settings_section_title_socks">SERVIDOR INTERMEDIARI SOCKS</string> + <string name="settings_section_title_socks">Servidor intermediari SOCKS</string> <string name="settings_section_title_profile_images">Imatges de perfil</string> - <string name="settings_section_title_themes">TEMES</string> + <string name="settings_section_title_themes">Temes</string> <string name="settings_message_shape_tail">Cua</string> <string name="settings_section_title_message_shape">Forma del missatge</string> - <string name="run_chat_section">EXECUTAR SIMPLEX</string> + <string name="run_chat_section">Executar SimpleX</string> <string name="settings_section_title_use_from_desktop">Usar des d\'ordinador</string> <string name="your_chat_database">Base de dades de xat</string> <string name="import_database">Importar base de dades</string> @@ -1899,7 +1899,7 @@ <string name="network_enable_socks">Utilitzar servidor intermediari SOCKS?</string> <string name="network_use_onion_hosts_prefer">Si disponibles</string> <string name="network_proxy_auth_mode_username_password">Les vostres credencials es podrien enviar sense xifrar.</string> - <string name="theme_colors_section_title">COLORS DE LA INTERFÍCIE</string> + <string name="theme_colors_section_title">Colors de la interfície</string> <string name="update_network_smp_proxy_fallback_question">Alternativa d\'encaminament de missatges</string> <string name="update_network_smp_proxy_mode_question">Mode d\'encaminament de missatges</string> <string name="app_check_for_updates_button_open">Obrir ubicació del fitxer</string> @@ -1949,12 +1949,12 @@ <string name="receipts_section_description">Aquesta configuració és per al vostre perfil actual</string> <string name="receipts_section_description_1">Es pot canviar a la configuració de contacte i grup.</string> <string name="privacy_media_blur_radius_off">No</string> - <string name="settings_section_title_settings">CONFIGURACIÓ</string> + <string name="settings_section_title_settings">Configuració</string> <string name="privacy_media_blur_radius_soft">Tou</string> <string name="privacy_media_blur_radius_strong">Fort</string> - <string name="settings_section_title_support">SUPORT SIMPLEX XAT</string> + <string name="settings_section_title_support">Suport SimpleX Chat</string> <string name="settings_section_title_network_connection">Connexió a la xarxa</string> - <string name="settings_section_title_private_message_routing">ENCAMINAMENT DE MISSATGES PRIVAT</string> + <string name="settings_section_title_private_message_routing">Encaminament de missatges privat</string> <string name="chat_item_ttl_none">mai</string> <string name="no_received_app_files">No s\'han rebut ni enviats fitxers</string> <string name="restart_the_app_to_create_a_new_chat_profile">Reinicieu l\'aplicació per crear un perfil de xat nou.</string> @@ -2024,7 +2024,7 @@ <string name="group_welcome_preview">Vista prèvia</string> <string name="receiving_via">Rebent via</string> <string name="save_and_update_group_profile">Desa i actualitza el perfil del grup</string> - <string name="conn_stats_section_title_servers">SERVIDORS</string> + <string name="conn_stats_section_title_servers">Servidors</string> <string name="group_welcome_title">Missatge de benvinguda</string> <string name="welcome_message_is_too_long">El missatge de benvinguda és massa llarg</string> <string name="your_servers">Els teus servidors</string> @@ -2183,7 +2183,7 @@ <string name="smp_servers">Servidors SMP</string> <string name="xftp_servers">Servidors XFTP</string> <string name="audio_device_speaker">Altaveu</string> - <string name="settings_section_title_you">VÓS</string> + <string name="settings_section_title_you">Vós</string> <string name="chat_item_ttl_seconds">%s segon(s)</string> <string name="you_are_invited_to_group">"Heu estat convidat a un grup"</string> <string name="rcv_group_event_1_member_connected">%s connectat</string> @@ -2474,7 +2474,7 @@ <string name="share_old_link_alert_button">Compartir l\'enllaç antic</string> <string name="share_group_profile_via_link_alert_text">L\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell.</string> <string name="upgrade_group_link">Actualitzar l\'enllaç del grup</string> - <string name="settings_section_title_contact_requests_from_groups">SOL·LICITUDS DE CONTACTE DE GRUPS</string> + <string name="settings_section_title_contact_requests_from_groups">Sol·licituds de contacte de grups</string> <string name="member_is_deleted_cant_accept_request">Membre eliminat(da); no es pot acceptar la sol·licitud.</string> <string name="rcv_direct_event_group_inv_link_received">connexió sol·licitada del grup %1$s</string> <string name="this_setting_is_for_your_current_profile">Aquesta configuració és per al perfil actual</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index c727cc1cb0..5160d28feb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -40,11 +40,11 @@ <string name="button_create_group_link">Vytvořit odkaz</string> <string name="delete_link_question">Smazat odkaz\?</string> <string name="button_send_direct_message">Odeslat přímou zprávu</string> - <string name="member_info_section_title_member">ČLEN</string> - <string name="change_member_role_question">Změnit roli ve skupině\?</string> + <string name="member_info_section_title_member">Člen</string> + <string name="change_member_role_question">Změnit roli ve skupině?</string> <string name="info_row_connection">Připoj</string> <string name="conn_level_desc_indirect">nepřímé (%1$s)</string> - <string name="conn_stats_section_title_servers">SERVERY</string> + <string name="conn_stats_section_title_servers">Servery</string> <string name="receiving_via">Příjímáno přes</string> <string name="create_secret_group_title">Vytvoření tajné skupiny</string> <string name="group_display_name_field">Zadejte název skupiny:</string> @@ -259,16 +259,16 @@ <string name="icon_descr_speaker_on">Reproduktor zapnut</string> <string name="icon_descr_call_progress">Probíhající hovor</string> <string name="auto_accept_images">Automaticky přijímat obrázky</string> - <string name="settings_section_title_settings">NASTAVENÍ</string> - <string name="settings_section_title_help">NÁPOVĚDA</string> - <string name="settings_section_title_device">ZAŘÍZENÍ</string> - <string name="settings_section_title_chats">KONVERZACE</string> + <string name="settings_section_title_settings">Nastavení</string> + <string name="settings_section_title_help">Nápověda</string> + <string name="settings_section_title_device">Zařízení</string> + <string name="settings_section_title_chats">Konverzace</string> <string name="settings_experimental_features">Experimentální funkce</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> - <string name="settings_section_title_icon">IKONA APLIKACE</string> - <string name="settings_section_title_themes">TÉMATA</string> - <string name="settings_section_title_messages">ZPRÁVY A SOUBORY</string> - <string name="settings_section_title_calls">VOLÁNÍ</string> + <string name="settings_section_title_socks">SOCKS proxy</string> + <string name="settings_section_title_icon">Ikona aplikace</string> + <string name="settings_section_title_themes">Témata</string> + <string name="settings_section_title_messages">Zprávy a soubory</string> + <string name="settings_section_title_calls">Volání</string> <string name="export_database">Export databáze</string> <string name="import_database">Import databáze</string> <string name="delete_database">Smazat databázi</string> @@ -353,7 +353,7 @@ <string name="info_row_group">Skupina</string> <string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string> <string name="accept_feature_set_1_day">Nastavit 1 den</string> - <string name="connection_error_auth">Chyba spojení (AUTH)</string> + <string name="connection_error_auth">Chyba spojení</string> <string name="sender_may_have_deleted_the_connection_request">Odesílatel možná smazal požadavek připojení</string> <string name="error_smp_test_server_auth">Server vyžaduje autorizaci pro vytvoření front, zkontrolujte heslo.</string> <string name="smp_server_test_delete_queue">Odstranit frontu</string> @@ -511,8 +511,7 @@ <string name="error_receiving_file">Chyba při příjmu souboru</string> <string name="error_creating_address">Chyba při vytváření adresy</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</string> - <string name="connection_error_auth_desc">Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji. -\nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti.</string> + <string name="connection_error_auth_desc">Pokud váš kontakt neodstranil připojení nebo tento odkaz již nebyl použit, může se jednat o chybu – nahlaste ji. \nChcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu na připojení a zkontrolujte, zda máte stabilní připojení k síti.</string> <string name="error_deleting_contact">Chyba mazání kontaktu</string> <string name="error_deleting_group">Chyba mazání skupiny</string> <string name="error_deleting_contact_request">Chyba mazání žádosti kontaktu</string> @@ -700,15 +699,15 @@ \n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech. \n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze. \n3. Spojení je kompromitováno.</string> - <string name="settings_section_title_you">VY</string> - <string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string> + <string name="settings_section_title_you">Vy</string> + <string name="settings_section_title_support">Podpořit SimpleX Chat</string> <string name="settings_developer_tools">Nástroje pro vývojáře</string> <string name="settings_section_title_incognito">Inkognito mód</string> <string name="your_chat_database">Vaše chat databáze</string> - <string name="run_chat_section">SPUSTIT CHAT</string> + <string name="run_chat_section">Spustit chat</string> <string name="chat_is_running">Chat je spuštěn</string> <string name="chat_is_stopped">Chat je zastaven</string> - <string name="chat_database_section">DATABÁZE CHATU</string> + <string name="chat_database_section">Databáze chatu</string> <string name="database_passphrase">přístupová fráze k databázi</string> <string name="new_database_archive">Archiv nové databáze</string> <string name="old_database_archive">Archiv staré databáze</string> @@ -821,7 +820,7 @@ <string name="icon_descr_contact_checked">Zkontrolované kontakty</string> <string name="num_contacts_selected">%d kontakt(y) vybrán(y)</string> <string name="button_add_members">Pozvat členy</string> - <string name="group_info_section_title_num_members">%1$s ČLENŮ</string> + <string name="group_info_section_title_num_members">%1$s členů</string> <string name="group_info_member_you">vy: %1$s</string> <string name="button_delete_group">Smazat skupinu</string> <string name="delete_group_question">Smazat skupinu\?</string> @@ -836,7 +835,7 @@ <string name="error_creating_link_for_group">Chyba při vytváření odkazu skupiny</string> <string name="error_deleting_link_for_group">Chyba při odstraňování odkazu skupiny</string> <string name="only_group_owners_can_change_prefs">Předvolby skupiny mohou měnit pouze vlastníci skupiny.</string> - <string name="section_title_for_console">PRO KONSOLE</string> + <string name="section_title_for_console">Pro konsole</string> <string name="info_row_local_name">Místní název</string> <string name="info_row_database_id">ID databáze</string> <string name="button_remove_member">Odstranit člena</string> @@ -991,7 +990,7 @@ <string name="upgrade_and_open_chat">Zvýšit a otevřít chat</string> <string name="hide_dev_options">Skrýt:</string> <string name="show_developer_options">Zobrazit možnosti vývojáře</string> - <string name="settings_section_title_experimenta">POKUSNÝ</string> + <string name="settings_section_title_experimenta">Pokusný</string> <string name="image_will_be_received_when_contact_completes_uploading">Obrázek bude přijat, až kontakt dokončí jeho nahrání.</string> <string name="show_dev_options">Zobrazit:</string> <string name="developer_options">ID databáze a možnost Izolace přenosu.</string> @@ -1164,7 +1163,7 @@ <string name="you_can_accept_or_reject_connection">Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Přečtěte si více v <font color="#0088ff">Uživatelské příručce</font>.]]></string> <string name="simplex_address">Adresa SimpleX</string> - <string name="theme_colors_section_title">BARVY MOTIVU</string> + <string name="theme_colors_section_title">Barvy motivu</string> <string name="customize_theme_title">Přizpůsobit motiv</string> <string name="profile_update_will_be_sent_to_contacts">Aktualizace profilu bude zaslána vašim kontaktům.</string> <string name="share_address_with_contacts_question">Sdílet adresu s kontakty?</string> @@ -1301,7 +1300,7 @@ <string name="in_reply_to">V odpovědi na</string> <string name="no_history">Žádná historie</string> <string name="network_option_protocol_timeout_per_kb">Časový limit protokolu na KB</string> - <string name="settings_section_title_delivery_receipts">ZASLAT POTVRZENÍ O DORUČENÍ NA</string> + <string name="settings_section_title_delivery_receipts">Zaslat potvrzení o doručení na</string> <string name="v5_2_message_delivery_receipts_descr">Druhé zaškrtnutí jsme přehlédli! ✅</string> <string name="switch_receiving_address_desc">Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</string> <string name="choose_file_title">Vybrat soubor</string> @@ -1802,8 +1801,8 @@ \nProsím sdělte jakékoli další problémy vývojářům.</string> <string name="network_smp_proxy_fallback_prohibit">Ne</string> <string name="network_smp_proxy_fallback_prohibit_description">NEposílejte zprávy přímo, i když váš nebo cílový server nepodporuje soukromé směrování.</string> - <string name="settings_section_title_files">SOUBORY</string> - <string name="settings_section_title_private_message_routing">SOUKROMÉ SMĚROVÁNÍ ZPRÁV</string> + <string name="settings_section_title_files">Soubory</string> + <string name="settings_section_title_private_message_routing">Soukromé směrování zpráv</string> <string name="settings_section_title_user_theme">Téma profilu</string> <string name="color_received_quote">Přijata odpověď</string> <string name="reset_single_color">Obnovit barvu</string> @@ -1877,7 +1876,7 @@ <string name="app_check_for_updates_button_remind_later">Připomenout později</string> <string name="app_check_for_updates_notice_title">Zkontrolovat aktualizace</string> <string name="privacy_media_blur_radius_off">Vypnuto</string> - <string name="settings_section_title_chat_database">CHAT DATABÁZE</string> + <string name="settings_section_title_chat_database">Chat databáze</string> <string name="member_info_member_disabled">vypnut</string> <string name="message_queue_info_server_info">info fronty serveru: %1$s\n\nposlední obdržená zpráva: %2$s</string> <string name="network_options_save_and_reconnect">Uložit a připojit znovu</string> @@ -2361,7 +2360,7 @@ <string name="members_will_be_removed_from_group_cannot_be_undone">Členové budou odstraněny ze skupiny - toto nelze zvrátit!</string> <string name="button_remove_members_question">Odebrat členy?</string> <string name="members_will_be_removed_from_chat_cannot_be_undone">Členové budou odstraněny z chatu - toto nelze zvrátit!</string> - <string name="onboarding_conditions_by_using_you_agree">Použitím SimpleX chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam.</string> + <string name="onboarding_conditions_by_using_you_agree">Použitím SimpleX Chatu souhlasíte že:\n- ve veřejných skupinách budete zasílat pouze legální obsah.\n- budete respektovat ostatní uživatele – žádný spam.</string> <string name="onboarding_conditions_accept">Přijmout</string> <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Zásady ochrany soukromí a podmínky používání.</string> <string name="onboarding_conditions_private_chats_not_accessible">Soukromé konverzace, skupiny a kontakty nejsou přístupné provozovatelům serverů.</string> @@ -2429,7 +2428,7 @@ <string name="connect_plan_open_new_group">Otevřít novou skupinu</string> <string name="compose_view_connect">Připojit</string> <string name="v6_4_connect_faster">Připojte se rychleji! 🚀</string> - <string name="settings_section_title_contact_requests_from_groups">POŽADAVKY NA PŘIPOJENÍ ZE SKUPIN</string> + <string name="settings_section_title_contact_requests_from_groups">Požadavky na připojení ze skupin</string> <string name="contact_should_accept">kontakt by měl přijmout…</string> <string name="v6_4_1_short_address_create">Vytvořit vaši adresu</string> <string name="group_descr_too_large">Popis příliš dlouhý</string> @@ -2608,7 +2607,6 @@ <string name="connect_via_link_or_qr_code">Připojení přes odkaz nebo QR kód</string> <string name="create_channel_title">Vytvořit veřejný kanál</string> <string name="create_channel_button">Vytvořit veřejný kanál</string> - <string name="create_channel_beta_button">Vytvořit veřejný kanál (BETA)</string> <string name="connect_with_someone">Vytvořte odkaz</string> <string name="create_your_public_address">Vytvořte si veřejnou adresu</string> <string name="creating_channel">Vytvářím kanál</string> @@ -2649,7 +2647,7 @@ <string name="relay_status_inactive">neaktivní</string> <string name="invalid_relay_address">Špatná relé adresa!</string> <string name="invalid_relay_name">Neplatné jméno relé!</string> - <string name="relay_status_invited">pozván</string> + <string name="relay_status_invited">pozvané</string> <string name="invite_someone_privately">Pozvat soukromě</string> <string name="compose_view_join_channel">Připojit ke kanálu</string> <string name="button_leave_channel">Opustit kanál</string> @@ -2745,7 +2743,6 @@ <string name="share_via_chat">Sdílet pomocí chatu</string> <string name="tray_show">Zobrazit SimpleX</string> <string name="owner_verification_failed">⚠️ Ověření podpisu selhalo: %s.</string> - <string name="chat_link_signed">(podepsán)</string> <string name="tray_tooltip">SimpleX</string> <string name="tray_tooltip_unread">SimpleX - %d nepřečteno</string> <string name="member_info_section_title_subscriber">ODBĚRATEL</string> @@ -2770,4 +2767,11 @@ <string name="alert_text_msg_reception_error">Aplikace odstranila tuto zprávu po %1$d pokusech o přijetí.</string> <string name="connection_reached_limit_of_undelivered_messages">Připojení dosáhlo limitu nedoručených zpráv</string> <string name="onboarding_first_network">První síť, kde vy vlastníte\nvaše kontakty a skupiny.</string> + <string name="channel_owner_count_singular">%1$d vlastník</string> + <string name="channel_owner_count_plural">%1$d vlastníci</string> + <string name="channel_owners_contributors_count">%1$d vlastníků & přispěvatelů</string> + <string name="badge_supported_simplex">%1$s podpořilo SimpleX Chat. Odznak prošel %2$s.</string> + <string name="settings_section_title_about">O aplikaci</string> + <string name="relay_status_rejected">odmítnuto</string> + <string name="member_info_relay_status_rejected_by_operator">odmítnuto operátorem relé</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml index 38507cc228..f23a95defb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/da/strings.xml @@ -56,7 +56,7 @@ <string name="acknowledged">Anerkendt</string> <string name="acknowledgement_errors">Bekræftelsesfejl</string> <string name="servers_info_subscriptions_connections_subscribed">Aktive forbindelser</string> - <string name="add_address_to_your_profile">Tilføj adresse til din profil, så dine kontakter kan dele den med andre. Profilopdateringen sendes til dine kontakter.</string> + <string name="add_address_to_your_profile">Tilføj en adresse til din profil, så dine SimpleX-kontakter kan dele den med andre. Dine kontakter vil modtage din opdaterede profil.</string> <string name="add_contact_tab">Tilføj kontakt</string> <string name="operator_added_xftp_servers">Tilføjede medie- og filservere</string> <string name="operator_added_message_servers">Tilføjede beskedservere</string> @@ -157,7 +157,7 @@ <string name="report_reason_other">En anden grund</string> <string name="answer_call">Svaropkald</string> <string name="opensource_protocol_and_code_anybody_can_run_servers">Alle kan være vært for servere.</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="onboarding_notifications_mode_service_desc_short">App løber altid i baggrunden</string> <string name="app_version_code">App Build: %s</string> <string name="notifications_mode_off_desc">App kan kun modtage meddelelser, når den kører, ingen baggrundstjeneste startes</string> @@ -305,7 +305,7 @@ <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Kontroller venligst, at du har brugt det korrekte link, eller bed din kontaktperson om at sende dig et nyt.</string> <string name="unsupported_connection_link">Ikke-understøttet forbindelseslink</string> <string name="link_requires_newer_app_version_please_upgrade">Dette link kræver en nyere appversion. Opgrader appen, eller bed din kontaktperson om at sende et kompatibelt link.</string> - <string name="connection_error_auth">Forbindelsesfejl (AUTH)</string> + <string name="connection_error_auth">Forbindelsesfejl</string> <string name="connection_error_auth_desc">Medmindre din kontaktperson har slettet forbindelsen, eller dette link allerede er i brug, kan det være en fejl - rapporter det.\nFor at oprette forbindelse skal du bede din kontaktperson om at oprette et nyt forbindelseslink og kontrollere, at du har en stabil netværksforbindelse.</string> <string name="connection_error_blocked">Forbindelse blokeret</string> <string name="connection_error_blocked_desc">Forbindelsen er blokeret af serveroperatøren:\n%1$s.</string> @@ -720,7 +720,7 @@ <string name="allow_your_contacts_to_send_files_and_media">Lad dine kontakter sende filer og medier.</string> <string name="appearance_settings">Udseende</string> <string name="v5_3_encrypt_local_files_descr">App krypterer nye lokale filer (undtagen videoer).</string> - <string name="settings_section_title_icon">Appikon</string> + <string name="settings_section_title_icon">App ikon</string> <string name="migrate_to_device_apply_onion">Anvende</string> <string name="chat_theme_apply_to_mode">Ansøg på</string> <string name="la_app_passcode">App adgangskode</string> @@ -861,4 +861,25 @@ <string name="group_member_status_introduced">Tilslutning (introduceret)</string> <string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Vælg <i>Overfør fra en anden enhed</i> på den nye enhed og scan QR-koden.]]></string> <string name="migrate_from_another_device">Overfør fra en anden enhed</string> + <string name="relay_bar_active">%1$d/%2$d relays aktive</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d relays aktive, %3$d fejl</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d relays aktive, %3$d fejlet</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d relays aktive, %3$d fjernet</string> + <string name="relay_bar_connected">%1$d/%2$d relays forbundet</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d relays forbundet, %3$d fejl</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d relays forbundet, %3$d fejl</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d relays forbundet, %3$d fjernet</string> + <string name="channel_owner_count_singular">%1$d ejer</string> + <string name="channel_owner_count_plural">%1$d ejere</string> + <string name="relay_status_active">aktiv</string> + <string name="add_button">Tilføj</string> + <string name="add_relay_button">Tilføj relay</string> + <string name="add_relays_title">Tilføj relays</string> + <string name="relay_bar_owner_no_delivery">Tilføj relays for at genoprette beskedleveringen.</string> + <string name="webpage_code_footer">Tilføj denne kode til din hjemmeside. Den viser en forhåndsvisning af din kanal eller gruppe.</string> + <string name="advanced_options">Avancerede indstillinger</string> + <string name="advanced_settings">Avancerede indstillinger</string> + <string name="another_instance_title">Appen kører allerede</string> + <string name="app_update_required">App\'en skal opdateres</string> + <string name="chat_link_business_address">Virksomhedsadresse</string> </resources> 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 42049da403..9774107fcf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -66,11 +66,10 @@ <string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit %1$s verbunden.</string> <string name="invalid_connection_link">Ungültiger Verbindungslink</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben, oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string> - <string name="connection_error_auth">Verbindungsfehler (AUTH)</string> - <string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln – bitte melden Sie ihn uns. -\nBitten Sie Ihren Kontakt darum, einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können, und stellen Sie sicher, dass Sie eine stabile Netzwerkverbindung haben.</string> + <string name="connection_error_auth">Verbindungslink entfernt</string> + <string name="connection_error_auth_desc">Ihr Kontakt hat diesen Link entfernt oder es war ein Einmal‑Link, welcher bereits verwendet wurde.\nUm sich zu verbinden, bitten Sie Ihren Kontakt, einen neuen Link zu erstellen.</string> <string name="error_accepting_contact_request">Fehler beim Annehmen der Kontaktanfrage</string> - <string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string> + <string name="sender_may_have_deleted_the_connection_request">Der Absender hat die Verbindungsanfrage gelöscht.</string> <string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string> <string name="error_deleting_group">Fehler beim Löschen der Gruppe</string> <string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string> @@ -344,7 +343,7 @@ <!-- settings - SettingsView.kt --> <string name="your_settings">Einstellungen</string> <string name="your_simplex_contact_address">Ihre SimpleX-Adresse</string> - <string name="database_passphrase_and_export">Datenbank-Passwort & -Export</string> + <string name="database_passphrase_and_export">Datenbank-Passwort und -Export</string> <string name="about_simplex_chat">Über SimpleX Chat</string> <string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string> <string name="markdown_help">Markdown-Hilfe</string> @@ -389,7 +388,7 @@ <string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC-ICE-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string> <string name="save_servers_button">Speichern</string> - <string name="network_and_servers">Netzwerk & Server</string> + <string name="network_and_servers">Netzwerk und Server</string> <string name="network_settings">Erweiterte Netzwerkeinstellungen</string> <string name="network_settings_title">Erweiterte Einstellungen</string> <string name="network_enable_socks">SOCKS-Proxy verwenden?</string> @@ -493,7 +492,7 @@ <string name="icon_descr_video_call">Videoanruf</string> <string name="icon_descr_audio_call">Audioanruf</string> <!-- Call settings --> - <string name="settings_audio_video_calls">Audio- & Videoanrufe</string> + <string name="settings_audio_video_calls">Audio- und Videoanrufe</string> <string name="your_calls">Ihre Anrufe</string> <string name="always_use_relay">Immer über einen Router verbinden</string> <string name="call_on_lock_screen">Anrufe auf Sperrbildschirm:</string> @@ -542,33 +541,33 @@ \n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde. \n3. Die Verbindung wurde kompromittiert.</string> <!-- Privacy settings --> - <string name="privacy_and_security">Datenschutz & Sicherheit</string> + <string name="privacy_and_security">Datenschutz und Sicherheit</string> <string name="your_privacy">Privatsphäre</string> <string name="protect_app_screen">App-Bildschirm schützen</string> <string name="auto_accept_images">Bilder automatisch akzeptieren</string> <string name="send_link_previews">Linkvorschau senden</string> <string name="full_backup">App-Datensicherung</string> <!-- Settings sections --> - <string name="settings_section_title_you">MEINE DATEN</string> - <string name="settings_section_title_settings">EINSTELLUNGEN</string> - <string name="settings_section_title_help">HILFE</string> - <string name="settings_section_title_support">UNTERSTÜTZUNG VON SIMPLEX CHAT</string> - <string name="settings_section_title_device">GERÄT</string> - <string name="settings_section_title_chats">CHATS</string> + <string name="settings_section_title_you">Meine Daten</string> + <string name="settings_section_title_settings">Einstellungen</string> + <string name="settings_section_title_help">Hilfe</string> + <string name="settings_section_title_support">Unterstützung von SimpleX Chat</string> + <string name="settings_section_title_device">Gerät</string> + <string name="settings_section_title_chats">Chats</string> <string name="settings_developer_tools">Entwicklertools</string> <string name="settings_experimental_features">Experimentelle Funktionen</string> - <string name="settings_section_title_socks">SOCKS-PROXY</string> - <string name="settings_section_title_icon">APP-ICON</string> - <string name="settings_section_title_themes">DESIGN</string> - <string name="settings_section_title_messages">NACHRICHTEN und DATEIEN</string> - <string name="settings_section_title_calls">CALLS</string> + <string name="settings_section_title_socks">SOCKS-Proxy</string> + <string name="settings_section_title_icon">App-Icon</string> + <string name="settings_section_title_themes">Design</string> + <string name="settings_section_title_messages">Nachrichten und Dateien</string> + <string name="settings_section_title_calls">Calls</string> <string name="settings_section_title_incognito">Inkognito-Modus</string> <!-- DatabaseView.kt --> <string name="your_chat_database">Chat-Datenbank</string> - <string name="run_chat_section">CHAT STARTEN</string> + <string name="run_chat_section">Chat starten</string> <string name="chat_is_running">Der Chat läuft</string> <string name="chat_is_stopped">Der Chat ist beendet</string> - <string name="chat_database_section">CHAT-DATENBANK</string> + <string name="chat_database_section">Chat-Datenbank</string> <string name="database_passphrase">Datenbank-Passwort</string> <string name="export_database">Datenbank exportieren</string> <string name="import_database">Datenbank importieren</string> @@ -747,7 +746,7 @@ <string name="invite_prohibited_description">Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden.</string> <!-- GroupChatInfoView.kt --> <string name="button_add_members">Mitglieder einladen</string> - <string name="group_info_section_title_num_members">%1$s MITGLIEDER</string> + <string name="group_info_section_title_num_members">%1$s Mitglieder</string> <string name="group_info_member_you">Sie: %1$s</string> <string name="button_delete_group">Gruppe löschen</string> <string name="delete_group_question">Gruppe löschen?</string> @@ -765,7 +764,7 @@ <string name="error_deleting_link_for_group">Fehler beim Löschen des Gruppen-Links</string> <string name="only_group_owners_can_change_prefs">Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden.</string> <!-- For Console chat info section --> - <string name="section_title_for_console">FÜR KONSOLE</string> + <string name="section_title_for_console">Für Konsole</string> <string name="info_row_local_name">Lokaler Name</string> <string name="info_row_database_id">Datenbank-ID</string> <!-- GroupMemberInfoView.kt --> @@ -773,13 +772,13 @@ <string name="button_send_direct_message">Direktnachricht senden</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!</string> <string name="remove_member_confirmation">Entfernen</string> - <string name="member_info_section_title_member">MITGLIED</string> + <string name="member_info_section_title_member">Mitglied</string> <string name="role_in_group">Rolle</string> <string name="change_role">Rolle ändern</string> <string name="change_verb">Ändern</string> <string name="switch_verb">Wechseln</string> - <string name="change_member_role_question">Die Mitgliederrolle ändern?</string> - <string name="member_role_will_be_changed_with_notification">Die Rolle wird auf %s geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string> + <string name="change_member_role_question">Rolle ändern?</string> + <string name="member_role_will_be_changed_with_notification">Die Rolle wird auf %s geändert. Alle Gruppenmitglieder werden benachrichtigt.</string> <string name="member_role_will_be_changed_with_invitation">Die Rolle wird auf %s geändert. Das Mitglied wird eine neue Einladung erhalten.</string> <string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string> <string name="error_changing_role">Fehler beim Ändern der Rolle</string> @@ -788,7 +787,7 @@ <string name="conn_level_desc_direct">direkt</string> <string name="conn_level_desc_indirect">indirekt (%1$s)</string> <!-- ConnectionStats --> - <string name="conn_stats_section_title_servers">SERVER</string> + <string name="conn_stats_section_title_servers">Server</string> <string name="receiving_via">Empfangen über</string> <string name="sending_via">Senden über</string> <string name="network_status">Netzwerkstatus</string> @@ -977,7 +976,7 @@ <string name="network_option_ping_count">PING-Zähler</string> <string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string> <string name="smp_servers_per_user">Nachrichten-Server für neue Verbindungen über Ihr aktuelles Chat-Profil</string> - <string name="files_and_media_section">Dateien & Medien</string> + <string name="files_and_media_section">Dateien und Medien</string> <string name="network_session_mode_transport_isolation">Transport-Isolation</string> <string name="users_delete_question">Chat-Profil löschen\?</string> <string name="error_deleting_user">Fehler beim Löschen des Benutzerprofils</string> @@ -1005,8 +1004,8 @@ <string name="v4_5_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string> <string name="v4_5_multiple_chat_profiles_descr">Unterschiedliche Namen, Avatare und Transport-Isolation.</string> <string name="v4_5_transport_isolation">Transport-Isolation</string> - <string name="v4_5_italian_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string> - <string name="v4_4_french_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string> + <string name="v4_5_italian_interface_descr">Dank der Nutzer - Wirken Sie per Weblate mit!</string> + <string name="v4_4_french_interface_descr">Dank der Nutzer - Wirken Sie per Weblate mit!</string> <string name="v4_5_private_filenames_descr">Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen.</string> <string name="moderated_description">Moderiert</string> <string name="moderated_item_description">Von %s moderiert</string> @@ -1051,7 +1050,7 @@ <string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string> <string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string> <string name="v4_6_group_welcome_message_descr">Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!</string> - <string name="v4_6_chinese_spanish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string> + <string name="v4_6_chinese_spanish_interface_descr">Dank der Nutzer - Wirken Sie per Weblate mit!</string> <string name="v4_6_group_moderation">Gruppenmoderation</string> <string name="v4_6_hidden_chat_profiles">Verborgene Chat-Profile</string> <string name="user_hide">Verberge</string> @@ -1065,7 +1064,7 @@ <string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string> <string name="show_dev_options">Anzeigen:</string> <string name="show_developer_options">Entwickleroptionen anzeigen</string> - <string name="settings_section_title_experimenta">EXPERIMENTELL</string> + <string name="settings_section_title_experimenta">Experimentell</string> <string name="database_upgrade">Datenbank-Aktualisierung</string> <string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string> <string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string> @@ -1172,12 +1171,12 @@ <string name="v5_0_large_files_support_descr">Schnell und ohne zu warten, bis der Kontakt online ist!</string> <string name="v5_0_polish_interface">Polnische Bedienoberfläche</string> <string name="v5_0_app_passcode_descr">Anstelle der System-Authentifizierung festlegen.</string> - <string name="v5_0_polish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string> + <string name="v5_0_polish_interface_descr">Dank der Nutzer - Wirken Sie per Weblate mit!</string> <string name="only_your_contact_can_make_calls">Nur Ihr Kontakt kann Anrufe tätigen.</string> <string name="v5_0_app_passcode">App-Zugangscode</string> <string name="calls_prohibited_with_this_contact">Audio-/Video-Anrufe sind nicht erlaubt.</string> <string name="address_section_title">Adresse</string> - <string name="share_address">Adresse teilen</string> + <string name="share_address">Adresse teilen…</string> <string name="export_theme">Design exportieren</string> <string name="import_theme_error">Fehler beim Importieren des Designs</string> <string name="color_title">Bezeichnung</string> @@ -1189,7 +1188,7 @@ <string name="you_can_accept_or_reject_connection">Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.</string> <string name="you_wont_lose_your_contacts_if_delete_address">Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string> <string name="customize_theme_title">Design anpassen</string> - <string name="theme_colors_section_title">INTERFACE-FARBEN</string> + <string name="theme_colors_section_title">Interface-Farben</string> <string name="add_address_to_your_profile">Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre SimpleX-Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre SimpleX-Kontakte gesendet.</string> <string name="all_your_contacts_will_remain_connected_update_sent">Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string> <string name="create_address_and_let_people_connect">Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.</string> @@ -1216,7 +1215,7 @@ <string name="color_secondary_variant">Zweite Akzentfarbe</string> <string name="color_background">Hintergrund-Farbe</string> <string name="import_theme">Design importieren</string> - <string name="color_surface">Menüs & Benachrichtigungen</string> + <string name="color_surface">Menüs und Benachrichtigungen</string> <string name="color_received_message">Empfangene Nachricht</string> <string name="color_secondary">Zweite Farbe</string> <string name="color_sent_message">Gesendete Nachricht</string> @@ -1287,7 +1286,7 @@ <string name="v5_1_japanese_portuguese_interface">Japanische und portugiesische Bedienoberfläche</string> <string name="custom_time_unit_minutes">Minuten</string> <string name="custom_time_unit_seconds">Sekunden</string> - <string name="whats_new_thanks_to_users_contribute_weblate">Dank der Nutzer - Tragen Sie per Weblate bei!</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Dank der Nutzer - Wirken Sie per Weblate mit!</string> <string name="v5_1_better_messages">Verbesserungen bei Nachrichten</string> <string name="v5_1_custom_themes_descr">Farbdesigns anpassen und weitergeben.</string> <string name="custom_time_unit_days">Tage</string> @@ -1309,7 +1308,7 @@ <string name="non_fatal_errors_occured_during_import">Während des Imports sind nicht schwerwiegende Fehler aufgetreten:</string> <string name="shutdown_alert_question">Herunterfahren\?</string> <string name="shutdown_alert_desc">Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="settings_restart_app">Neustart</string> <string name="settings_shutdown">Herunterfahren</string> <string name="error_aborting_address_change">Fehler beim Beenden des Adresswechsels</string> @@ -1372,7 +1371,7 @@ <string name="receipts_contacts_title_enable">Bestätigungen aktivieren\?</string> <string name="receipts_contacts_override_enabled">Das Senden von Bestätigungen an %d Kontakte ist aktiviert</string> <string name="receipts_contacts_enable_for_all">Für alle aktivieren</string> - <string name="settings_section_title_delivery_receipts">EMPFANGSBESTÄTIGUNGEN SENDEN AN</string> + <string name="settings_section_title_delivery_receipts">Empfangsbestätigungen senden an</string> <string name="receipts_contacts_disable_keep_overrides">Deaktivieren (vorgenommene Einstellungen bleiben erhalten)</string> <string name="send_receipts">Bestätigungen senden</string> <string name="v5_2_fix_encryption">Ihre Verbindungen beibehalten</string> @@ -1384,7 +1383,7 @@ <string name="v5_2_more_things_descr">- Stabilere Zustellung von Nachrichten.\n- Ein bisschen verbesserte Gruppen.\n- Und mehr!</string> <string name="dont_enable_receipts">Nicht aktivieren</string> <string name="sending_delivery_receipts_will_be_enabled">Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert.</string> - <string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in Ihren Privatsphäre-Einstellungen der App aktivieren.</string> <string name="choose_file_title">Datei auswählen</string> <string name="in_developing_title">Kommt bald!</string> <string name="no_selected_chat">Kein Chat ausgewählt</string> @@ -1440,7 +1439,7 @@ <string name="socks_proxy_setting_limitations"><![CDATA[<b>Bitte beachten Sie</b>: Die Nachrichten- und Datei-Router sind per SOCKS-Proxy verbunden. Anrufe nutzen eine direkte Verbindung.]]></string> <string name="encrypt_local_files">Lokale Dateien verschlüsseln</string> <string name="rcv_group_event_open_chat">Öffnen</string> - <string name="v5_3_encrypt_local_files">Gespeicherte Dateien & Medien verschlüsseln</string> + <string name="v5_3_encrypt_local_files">Gespeicherte Dateien und Medien verschlüsseln</string> <string name="error_creating_member_contact">Fehler beim Anlegen eines Mitglied-Kontaktes</string> <string name="v5_3_new_desktop_app">Neue Desktop-App!</string> <string name="v5_3_new_interface_languages">6 neue Sprachen für die Bedienoberfläche</string> @@ -1851,13 +1850,13 @@ <string name="network_smp_proxy_fallback_allow_downgrade">Herabstufung erlauben</string> <string name="network_smp_proxy_mode_always_description">Immer privates Routing nutzen.</string> <string name="network_smp_proxy_fallback_prohibit_description">Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> - <string name="settings_section_title_private_message_routing">PRIVATES NACHRICHTEN-ROUTING</string> + <string name="settings_section_title_private_message_routing">Privates Nachrichten-Routing</string> <string name="network_smp_proxy_fallback_allow_protected_description">Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> <string name="network_smp_proxy_fallback_allow_description">Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt.</string> <string name="private_routing_explanation">Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Router genutzt.</string> <string name="network_smp_proxy_mode_unprotected_description">Bei unbekannten Servern privates Routing nutzen, wenn Ihre IP-Adresse nicht geschützt ist.</string> <string name="protect_ip_address">IP-Adresse schützen</string> - <string name="settings_section_title_files">DATEIEN</string> + <string name="settings_section_title_files">Dateien</string> <string name="app_will_ask_to_confirm_unknown_file_servers">Die App wird bei unbekannten Datei-Servern nach einer Download-Bestätigung fragen (außer bei .onion oder wenn ein SOCKS-Proxy aktiviert ist).</string> <string name="file_not_approved_title">Unbekannte Server!</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein.</string> @@ -1902,7 +1901,7 @@ <string name="v5_8_chat_themes_descr">Gestalten Sie Ihre Chats unterschiedlich!</string> <string name="v5_8_chat_themes">Neue Chat-Designs</string> <string name="v5_8_private_routing">Privates Nachrichten-Routing 🚀</string> - <string name="v5_8_private_routing_descr">Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen.</string> + <string name="v5_8_private_routing_descr">Schützen Sie Ihre IP-Adresse vor den Nachrichten-Routern, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk und Server* Einstellungen.</string> <string name="v5_8_safe_files">Dateien sicher herunterladen</string> <string name="v5_8_message_delivery_descr">Mit reduziertem Akkuverbrauch.</string> <string name="message_queue_info_none">Keine Information</string> @@ -2124,7 +2123,7 @@ <string name="new_message">Neue Nachricht</string> <string name="error_parsing_uri_desc">Bitte überprüfen Sie, ob der SimpleX-Link korrekt ist.</string> <string name="error_parsing_uri_title">Ungültiger Link</string> - <string name="settings_section_title_chat_database">CHAT-DATENBANK</string> + <string name="settings_section_title_chat_database">Chat-Datenbank</string> <string name="switching_profile_error_title">Fehler beim Wechseln des Profils</string> <string name="delete_messages_cannot_be_undone_warning">Die Nachrichten werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string> <string name="new_chat_share_profile">Profil teilen</string> @@ -2305,7 +2304,7 @@ <string name="maximum_message_size_reached_non_text">Bitte verkleinern Sie die Nachrichten-Größe oder entfernen Sie Medien und versenden Sie diese erneut.</string> <string name="only_chat_owners_can_change_prefs">Präferenzen können nur von Chat-Eigentümern geändert werden.</string> <string name="maximum_message_size_reached_text">Bitte verkleinern Sie die Nachrichten-Größe und versenden Sie diese erneut.</string> - <string name="member_role_will_be_changed_with_notification_chat">Die Rolle wird auf %s geändert. Im Chat wird Jeder darüber informiert.</string> + <string name="member_role_will_be_changed_with_notification_chat">Die Rolle wird auf %s geändert. Im Chat wird jeder darüber informiert.</string> <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Sie werden von diesem Chat keine Nachrichten mehr erhalten. Der Nachrichtenverlauf wird beibehalten.</string> <string name="maximum_message_size_reached_forwarding">Sie können die Nachricht kopieren und verkleinern, um sie zu versenden.</string> <string name="button_delete_chat">Chat löschen</string> @@ -2581,7 +2580,7 @@ <string name="share_old_link_alert_button">Alten Link teilen</string> <string name="share_group_profile_via_link_alert_text">Der Link wird gekürzt sein, und das Gruppen-Profil wird über den Link geteilt.</string> <string name="upgrade_group_link">Gruppen-Link aktualisieren</string> - <string name="settings_section_title_contact_requests_from_groups">KONTAKTANFRAGEN VON GRUPPEN</string> + <string name="settings_section_title_contact_requests_from_groups">Kontaktanfragen von Gruppen</string> <string name="member_is_deleted_cant_accept_request">Mitglied ist gelöscht - Anfrage kann nicht angenommen werden</string> <string name="rcv_direct_event_group_inv_link_received">Angefragte Verbindung von Gruppe %1$s</string> <string name="this_setting_is_for_your_current_profile">Diese Einstellung gilt für Ihr aktuelles Profil</string> @@ -2624,7 +2623,7 @@ <string name="placeholder_search_voice_messages">Sprachnachrichten suchen</string> <string name="content_filter_videos">Videos</string> <string name="content_filter_voice_messages">Sprachnachrichten</string> - <string name="info_row_connection_failed">VERBINDUNG FEHLGESCHLAGEN</string> + <string name="info_row_connection_failed">Verbindung fehlgeschlagen</string> <string name="member_info_member_failed">Fehlgeschlagen</string> <string name="down_migration_warning_chat_relays">Kanäle, welche Sie erstellt haben oder denen Sie beigetreten sind, werden dauerhaft deaktiviert.</string> <string name="relay_bar_active">%1$d/%2$d Relais aktiv</string> @@ -2663,7 +2662,6 @@ <string name="relay_conn_status_connecting">Verbinde</string> <string name="create_channel_title">Öffentlichen Kanal erstellen</string> <string name="create_channel_button">Öffentlichen Kanal erstellen</string> - <string name="create_channel_beta_button">Öffentlichen Kanal erstellen (BETA)</string> <string name="creating_channel">Kanal wird erstellt</string> <string name="relay_test_step_decode_link">Link dekodieren</string> <string name="button_delete_channel">Kanal löschen</string> @@ -2693,12 +2691,12 @@ <string name="not_all_relays_connected">Es sind nicht alle Relais verbunden</string> <string name="connect_plan_open_channel">Kanal öffnen</string> <string name="connect_plan_open_new_channel">Neuen Kanal öffnen</string> - <string name="member_info_section_title_owner">EIGENTÜMER</string> - <string name="channel_members_section_owners">Eigentümer</string> + <string name="member_info_section_title_owner">Eigentümer</string> + <string name="channel_members_section_owners">Eigentümer und Mitwirkende</string> <string name="preset_relay_address">Voreingestellte Relais-Adresse</string> <string name="preset_relay_name">Voreingestellter Relais-Name</string> <string name="group_member_role_relay">Relais</string> - <string name="member_info_section_title_relay">RELAIS</string> + <string name="member_info_section_title_relay">Relais</string> <string name="info_row_relay_address">Relais-Adresse</string> <string name="relay_address_alert_title">Relais-Adresse</string> <string name="relay_connection_failed">Relais-Verbindung fehlgeschlagen</string> @@ -2709,7 +2707,7 @@ <string name="error_relay_test_server_auth">Der Server erfordert eine Autorisierung, um eine Verbindung zum Router herzustellen. Bitte Passwort überprüfen.</string> <string name="server_warning">Serverwarnung</string> <string name="share_relay_address">Relais-Adresse teilen</string> - <string name="member_info_section_title_subscriber">ABONNENT</string> + <string name="member_info_section_title_subscriber">Abonnent</string> <string name="channel_members_title_subscribers">Abonnenten</string> <string name="relay_section_footer_owner">Abonnenten verbinden sich über den Relais‑Link mit dem Kanal.\nDie Relais-Adresse wurde zur Einrichtung dieses Relais für diesen Kanal verwendet.</string> <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">Abonnent wird aus dem Kanal entfernt. Dies kann nicht rückgängig gemacht werden!</string> @@ -2783,7 +2781,6 @@ <string name="share_channel">Kanal teilen…</string> <string name="share_via_chat">Per Chat teilen</string> <string name="owner_verification_failed">⚠️ Signaturüberprüfung fehlgeschlagen: %s.</string> - <string name="chat_link_signed">(signiert)</string> <string name="tap_to_open">Zum Öffnen tippen</string> <string name="connection_reached_limit_of_undelivered_messages">Die Verbindung hat das Limit für nicht zugestellte Nachrichten erreicht</string> <string name="channel_preferences">Kanal-Präferenzen</string> @@ -2814,10 +2811,10 @@ <string name="onboarding_post_address">Diese Adresse in Ihrem Social‑Media‑Profil, auf Ihrer Webseite oder in Ihrer E‑Mail‑Signatur verwenden.</string> <string name="v6_5_invite_friends_descr">Wir haben das Verbinden für neue Nutzer vereinfacht.</string> <string name="your_public_address">Ihre öffentliche Adresse</string> - <string name="why_built_heading">Sie wurden ohne eine Benutzerkennung geboren.</string> + <string name="why_built_heading">Sie wurden ohne ein Benutzerkonto geboren.</string> <string name="why_built_p1">Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich.</string> <string name="why_built_p2">Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein.</string> - <string name="why_built_p3">Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist.</string> + <string name="why_built_p3">Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzerkonten, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist.</string> <string name="why_built_p4">Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän.</string> <string name="why_built_p5">Ihre Kommunikation gehört Ihnen, so wie es immer war, bevor es das Internet gab. Das Netzwerk ist kein Ort, den Sie besuchen. Es ist ein Ort, den Sie erschaffen und besitzen und Niemand kann es Ihnen nehmen, egal ob Sie es privat oder öffentlich machen.</string> <string name="why_built_p6">Die älteste Freiheit des Menschen - mit einem anderen Menschen sprechen zu können, ohne beobachtet zu werden - gestützt auf einer Infrastruktur, die Sie nicht verraten kann.</string> @@ -2856,7 +2853,7 @@ <string name="migrate">Migrieren</string> <string name="onboarding_network_commitments">Netzwerk‑Verpflichtungen</string> <string name="onboarding_network_routers_cannot_know">Netzwerk‑Router können nicht erkennen,\nwer mit wem kommuniziert</string> - <string name="onboarding_no_account">Kein Account. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung.</string> + <string name="onboarding_no_account">Kein Benutzerkonto. Keine Telefonnummer. Keine E‑Mail. Keine ID.\nDie sicherste Verschlüsselung.</string> <string name="onboarding_on_your_phone">Auf Ihrem Gerät, nicht auf Servern.</string> <string name="open_external_link_title">Externen Link öffnen?</string> <string name="onboarding_private_and_secure">Private und sichere Kommunikation.</string> @@ -2890,10 +2887,10 @@ <string name="last_active_relay_warning">Dies ist das letzte aktive Relais. Wenn Sie es entfernen, können keine Nachrichten mehr an Abonnenten zugestellt werden.</string> <string name="close_behavior_dialog_close">App schließen</string> <string name="close_behavior_dialog_text">Wenn Sie \"Schließen\" auswählen, werden keine Nachrichten mehr empfangen.\nSie können dies später in den Einstellungen unter \"Darstellung\" ändern.</string> - <string name="appearance_minimize_to_tray_desc">SimpleX im Hintergrund weiter ausführen, um Nachrichten zu empfangen.</string> + <string name="appearance_minimize_to_tray_desc">Läuft im Hintergrund ab, um Nachrichten zu empfangen.</string> <string name="close_behavior_dialog_minimize">In den Infobereich minimieren</string> <string name="close_behavior_dialog_title">In den Infobereich minimieren?</string> - <string name="appearance_minimize_to_tray">Beim Schließen des Fensters in den Infobereich minimieren</string> + <string name="appearance_minimize_to_tray">In den Infobereich minimieren</string> <string name="tray_quit">SimpleX beenden</string> <string name="tray_show">SimpleX anzeigen</string> <string name="tray_tooltip">SimpleX</string> @@ -2905,4 +2902,105 @@ <string name="relay_status_rejected">Abgelehnt</string> <string name="member_info_status">Status</string> <string name="member_info_relay_status_rejected_by_operator">Vom Relais-Betreiber abgelehnt</string> + <string name="channel_owner_count_singular">%1$d Eigentümer</string> + <string name="channel_owner_count_plural">%1$d Eigentümer</string> + <string name="channel_owners_contributors_count">%1$d Eigentümer und Mitwirkende</string> + <string name="badge_supported_simplex">%1$s hat SimpleX Chat unterstützt. Das Abzeichen ist am %2$s abgelaufen.</string> + <string name="settings_section_title_about">Über</string> + <string name="relay_status_acknowledged_roster">Bestätigter Relaisbestand</string> + <string name="webpage_code_footer">Fügen Sie diesen Code in Ihre Webseite ein. Er zeigt die Vorschau Ihres Kanals / Ihrer Gruppe an.</string> + <string name="advanced_options">Erweiterte Optionen</string> + <string name="advanced_settings">Erweiterte Einstellungen</string> + <string name="allow_anyone_to_embed">Einbetten für alle erlauben</string> + <string name="embed_any_webpage_can_show">Eine Vorschau ist auf jeder Webseite möglich.</string> + <string name="app_update_required">Aktualisierung der App erforderlich</string> + <string name="badge_unknown_key_title">Abzeichen ist nicht verifizierbar</string> + <string name="channel_webpage">Kanal-Webseite</string> + <string name="chat_data">Chat-Daten</string> + <string name="channel_name_requires_newer_app_version">Die Verbindung über den Kanalnamen erfordert eine neuere App‑Version.</string> + <string name="contact_name_requires_newer_app_version">Die Verbindung über den Kontaktnamen erfordert eine neuere App‑Version.</string> + <string name="settings_section_title_contact">Kontakt</string> + <string name="group_member_role_member_channel">Mitwirkender</string> + <string name="copy_code">Code kopieren</string> + <string name="webpage_info">Erstellen Sie eine Webseite, die Besuchern Ihren Kanal als Vorschau zeigt, bevor sie ihn abonnieren. Hosten Sie die Seite selbst oder nutzen Sie beliebiges statisches Hosting.</string> + <string name="enter_webpage_url">URL der Webseite eingeben</string> + <string name="group_webpage">Webseite der Gruppe</string> + <string name="help_and_support">Hilfe & Unterstützung</string> + <string name="web_page_url_placeholder">https://</string> + <string name="webpage_url_footer">Dies wird Abonnenten angezeigt und zum Laden der Vorschau genutzt.</string> + <string name="more_privacy">Weitere Privatsphäre</string> + <string name="embed_only_your_page">Nur Ihre oben genannte Seite kann die Vorschau anzeigen.</string> + <string name="please_upgrade_the_app">Bitte die App aktualisieren.</string> + <string name="badge_invested">%s hat sich am SimpleX Chat-Crowdfunding beteiligt.</string> + <string name="badge_supports_simplex">%s unterstützt SimpleX Chat.</string> + <string name="group_member_role_observer_channel">Abonnent</string> + <string name="settings_section_title_support_project">Unterstützen Sie das Projekt</string> + <string name="badge_unknown_key_desc">Das Abzeichen ist mit einem Schlüssel signiert, den diese App‑Version nicht erkennt. Aktualisieren Sie die App, um dieses Abzeichen zu verifizieren.</string> + <string name="member_role_will_be_changed_with_notification_channel">Die Rolle wird auf %s geändert. Alle Abonnenten werden benachrichtigt.</string> + <string name="badge_unverified_desc">Dieses Abzeichen konnte nicht verifiziert werden und ist möglicherweise nicht echt.</string> + <string name="group_link_requires_newer_version">Diese Gruppe erfordert eine neuere App‑Version. Bitte aktualisieren Sie die App, um beizutreten.</string> + <string name="unsupported_channel_name">Kanalname wird nicht unterstützt</string> + <string name="unsupported_contact_name">Kontaktname wird nicht unterstützt</string> + <string name="badge_unverified_title">Abzeichen nicht verifiziert</string> + <string name="relays_no_web_support">Die verwendeten Chat‑Relais unterstützen keine Webseiten.</string> + <string name="webpage_code">Webseiten-Code</string> + <string name="badge_support_from_v7">Sie können SimpleX ab der App-Version v7 unterstützen.</string> + <string name="error_saving_simplex_name">Fehler beim Speichern des Namens</string> + <string name="set_user_simplex_name_footer">Lassen Sie sich über den mit Ihrer SimpleX‑Adresse registrierten Namen verbinden.</string> + <string name="set_channel_simplex_name_footer">Ermöglichen Sie Beitritte über den mit diesem Kanal‑Link registrierten Namen.</string> + <string name="simplex_name_not_found">Name wurde nicht gefunden</string> + <string name="simplex_name_no_servers_desc">Keiner Ihrer Server ist zum Auflösen von SimpleX‑Namen konfiguriert. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink.</string> + <string name="no_names_servers_enabled">Keine Server für die Namensauflösung konfiguriert.</string> + <string name="simplex_name_no_valid_link">Kein gültiger Link</string> + <string name="simplex_name_resolver_error_desc">Namensauflösungs-Fehler: %1$s</string> + <string name="simplex_name_server_no_resolver_desc">Der Server %1$s unterstützt keine Namensauflösung. Konfigurieren Sie Server oder verwenden Sie einen Verbindungslink.</string> + <string name="set_simplex_name">SimpleX-Name einrichten</string> + <string name="simplex_name">SimpleX-Name</string> + <string name="simplex_name_error">Fehler beim SimpleX-Namen</string> + <string name="simplex_name_not_verified">SimpleX-Name ist nicht verifiziert</string> + <string name="simplex_name_no_valid_link_desc">Der SimpleX-Name %1$s wurde registriert, aber er hat keinen gültigen Link.</string> + <string name="simplex_name_unconfirmed_desc">Der SimpleX‑Name %1$s wurde registriert, jedoch nicht in Ihrem Profil hinterlegt. Bitte zu Ihrer Adresse oder zum Kanalprofil hinzufügen, sofern Sie der Besitzer sind.</string> + <string name="simplex_name_owner_no_channel_link">Der SimpleX‑Name %1$s wurde ohne Kanal‑Link registriert. Fügen Sie den Kanal‑Link über die Registrierungsseite hinzu.</string> + <string name="simplex_name_owner_no_address">Der SimpleX‑Name %1$s wurde ohne SimpleX-Adresse registriert. Fügen Sie die SimpleX-Adresse über die Registrierungsseite hinzu.</string> + <string name="simplex_name_not_found_desc">Dieser SimpleX-Name wurde nicht registriert. Bitte überprüfen Sie den Namen.</string> + <string name="operator_use_for_names">Für Namensauflösung</string> + <string name="simplex_name_unconfirmed">Unbestätigter Name</string> + <string name="verify_simplex_name_action">Name überprüfen</string> + <string name="verify_simplex_names">SimpleX-Namen überprüfen</string> + <string name="your_simplex_name">Ihr SimpleX-Name</string> + <string name="connect_plan_connect_to_name">Mit %s verbinden</string> + <string name="connect_plan_join_name">Kanal %s beitreten</string> + <string name="do_not_require_message_signatures">Signatur für Nachrichten nicht erforderlich.</string> + <string name="message_signatures_are_not_required">Nachrichten müssen nicht signiert werden.</string> + <string name="message_signatures_are_required">Nachrichten müssen signiert werden.</string> + <string name="require_message_signatures">Signatur für Nachrichten erforderlich.</string> + <string name="show_encryption">Verschlüsselung anzeigen</string> + <string name="show_signature">Signatur anzeigen</string> + <string name="signature_missing_alert_title">Signatur fehlt</string> + <string name="info_row_signed">Signiert</string> + <string name="info_row_signed_verified">Signiert und verifiziert</string> + <string name="sign_message_desc">Die Signatur bestätigt, dass Sie diese Nachricht verfasst haben und sie später nicht abstreiten können.</string> + <string name="sign_message">Nachricht signieren</string> + <string name="sign_messages">Nachrichten signieren</string> + <string name="signature_missing_alert_desc">Der Kanal verlangt für diese Nachricht eine Signatur, welche aber fehlt.</string> + <string name="channel_simplex_name">Im Kanal genutzter SimpleX-Name</string> + <string name="get_simplex_name_beta">SimpleX-Name erhalten (BETA)</string> + <string name="register_test_name">Wie man einen Test-Namen registriert</string> + <string name="remove_name">Name entfernen</string> + <string name="save_simplex_name_question">SimpleX-Name speichern?</string> + <string name="add_description">Beschreibung hinzufügen</string> + <string name="profile_description__field">Beschreibung</string> + <string name="edit_description">Beschreibung bearbeiten</string> + <string name="enter_description_optional">Beschreibung eingeben (optional)</string> + <string name="error_sharing_address">Fehler beim Teilen der Adresse</string> + <string name="to_verify_channel_member_key">Für die Überprüfung der Schlüssel mit diesem Abonnenten vergleichen oder scannen Sie den Code auf Ihren Geräten.</string> + <string name="v7_0_channels_contributors">Mitwirkende hinzufügen.</string> + <string name="v7_0_channels">Verbesserte Kanäle 📢</string> + <string name="v7_0_channels_previews">Eine Web-Vorschau erstellen.</string> + <string name="v7_0_channels_wider_messages">Einfacher zu lesen.</string> + <string name="v7_0_channels_relays">Ihre Relais verwalten.</string> + <string name="v7_0_simplex_names_descr">Öffentliche Namen für Ihren Kanal oder Ihr Unternehmen.</string> + <string name="v7_0_simplex_names">Öffentliche SimpleX-Namen (BETA)</string> + <string name="info_row_file_servers">Datei-Server</string> + <string name="share_text_file_servers">Datei-Server: %s</string> </resources> 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 47cfd90ad6..390913c5f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -86,7 +86,7 @@ <string name="your_ICE_servers">Ο ΙCE διακομιστής σου</string> <string name="v5_0_app_passcode">Κωδικός πρόσβασης εφαρμογής</string> <string name="connect_via_member_address_alert_desc">Αίτημα σύνδεσης θα σταλεί σε αυτό το μέλος της ομάδας.</string> - <string name="settings_section_title_icon">ΟΙΚΟΝΑ ΕΦΑΡΜΟΓΗΣ</string> + <string name="settings_section_title_icon">Εικόνα εφαρμογής</string> <string name="settings_section_title_app">Εφαρμογή</string> <string name="your_settings">Οι ρυθμίσεις σου</string> <string name="app_version_name">Έκδοση εφαρμογής: v%s</string> @@ -105,7 +105,7 @@ <string name="change_verb">Άλλαξε</string> <string name="available_in_v51">\nΔιαθέσιμο στην έκδοση 5.1</string> <string name="icon_descr_call_ended">Τέλος κλήσης</string> - <string name="settings_section_title_calls">ΚΛΗΣΕΙΣ</string> + <string name="settings_section_title_calls">Κλήσεις</string> <string name="auto_accept_contact">Αυτόματη αποδοχή</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d αποτυχία κρυπτογράφησης μηνύματος</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">αλλαγή διεύθυνσης για %s…</string> @@ -252,7 +252,7 @@ <string name="icon_descr_audio_on">Eνεργοποίηση ήχου</string> <string name="alert_title_msg_bad_hash">Κακό μήνυμα hash</string> <string name="privacy_media_blur_radius">Θάμπωση των μέσων</string> - <string name="settings_section_title_chat_database">ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ</string> + <string name="settings_section_title_chat_database">Βάση δεδομένων συνομιλίας</string> <string name="keychain_is_storing_securely">Το Android Keystore χρησιμοποιείται για την ασφαλή αποθήκευση της φράσης πρόσβασης - επιτρέπει την υπηρεσία ειδοποιήσεων να λειτουργεί.</string> <string name="member_info_member_blocked">αποκλεισμένος</string> <string name="member_blocked_by_admin">Αποκλεισμένος από τον διαχειριστή</string> @@ -288,7 +288,7 @@ <string name="deleted_chats">Αρχειοθετημένες επαφές</string> <string name="migrate_from_device_cancel_migration">Ακύρωση μεταφοράς</string> <string name="settings_section_title_chat_colors">Χρώματα συνομιλίας</string> - <string name="chat_database_section">ΒΑΣΗ ΔΕΔΟΜΕΝΩΝ ΣΥΝΟΜΙΛΙΑΣ</string> + <string name="chat_database_section">Βάση δεδομένων συνομιλίας</string> <string name="chat_is_running">Η συνομιλία εκτελείται</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Παρακαλώ σημείωσε</b>: ΔΕΝ θα μπορείς να ανακτήσεις ή να αλλάξεις τη φράση πρόσβασης εάν τη χάσεις.]]></string> <string name="block_for_all">Αποκλεισμός για όλους</string> @@ -377,7 +377,7 @@ <string name="integrity_msg_bad_id">κακό αναγνωριστικό μηνύματος</string> <string name="answer_call">Απάντηση κλήσης</string> <string name="alert_title_msg_bad_id">Κακό αναγνωριστικό μηνύματος</string> - <string name="settings_section_title_chats">ΣΥΝΟΜΙΛΙΕΣ</string> + <string name="settings_section_title_chats">Συνομιλίες</string> <string name="chat_database_imported">Η βάση δεδεδομένων της συνομιλίας εισάχθηκε</string> <string name="snd_conn_event_ratchet_sync_started">συμφωνία κρυπτογράφησης για %s…</string> <string name="allow_calls_question">Να επιτραπούν οι κλήσεις;</string> @@ -600,7 +600,7 @@ <string name="notification_preview_somebody">Κρυμμένη επαφή:</string> <string name="cant_call_contact_deleted_alert_text">Η επαφή διαγράφηκε.</string> <string name="cant_send_message_contact_not_ready">η επαφή δεν είναι έτοιμη</string> - <string name="settings_section_title_contact_requests_from_groups">ΑΙΤΗΣΕΙΣ ΕΠΑΦΩΝ ΑΠΟ ΟΜΑΔΕΣ</string> + <string name="settings_section_title_contact_requests_from_groups">Αιτήσεις επαφών από ομάδες</string> <string name="chat_list_contacts">Επαφές</string> <string name="contact_should_accept">η επαφή πρέπει να αποδεχτεί…</string> <string name="delete_contact_cannot_undo_warning">Η επαφή θα διαγραφεί – αυτή η ενέργεια δεν μπορεί να αναιρεθεί!</string> @@ -759,7 +759,7 @@ <string name="servers_info_details">Λεπτομέρειες</string> <string name="developer_options_section">Επιλογές προγραμματιστή</string> <string name="settings_developer_tools">Εργαλεία προγραμματιστή</string> - <string name="settings_section_title_device">ΣΥΣΚΕΥΗ</string> + <string name="settings_section_title_device">Συσκευή</string> <string name="auth_device_authentication_is_disabled_turning_off">Η επαλήθευση συσκευής είναι απενεργοποιημένη. Απενεργοποιείται το SimpleX Lock.</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Η επαλήθευση συσκευής δεν είναι ενεργοποιημένη. Μπορείς να ενεργοποιήσεις το SimpleX Lock από τις Ρυθμίσεις, αφού πρώτα ενεργοποιήσεις την επαλήθευση συσκευής.</string> <string name="devices">Συσκευές</string> @@ -927,7 +927,7 @@ <string name="report_archive_for_all_moderators">Για όλους τους διαχειριστές</string> <string name="v6_2_network_decentralization_enable_flux_reason">για καλύτερη ιδιωτικότητα μεταδεδομένων</string> <string name="for_chat_profile">Για το προφίλ συνομιλίας %s:</string> - <string name="section_title_for_console">ΓΙΑ ΚΟΝΣΟΛΑ</string> + <string name="section_title_for_console">Για κονσόλα</string> <string name="for_everybody">Για όλους</string> <string name="onboarding_network_operators_app_will_use_for_routing">Για παράδειγμα, αν η επαφή σου λαμβάνει μηνύματα μέσω κάποιου SimpleX Chat διακομιιστή, η εφαρμογή σου θα τα παραδίδει μέσω ενός Flux διακομιστή.</string> <string name="report_archive_for_me">Για μένα</string> @@ -986,7 +986,7 @@ <string name="icon_descr_hang_up">Τερματισμός κλήσης</string> <string name="audio_device_wired_headphones">Ακουστικά</string> <string name="icon_descr_help">βοήθεια</string> - <string name="settings_section_title_help">ΒΟΗΘΕΙΑ</string> + <string name="settings_section_title_help">Βοήθεια</string> <string name="v6_3_reports_descr">Βοήθησε τους διαχειριστές να διαχειρίζονται τις ομάδες τους.</string> <string name="email_invite_body">Γεια σου!\nΣυνδέσου μαζί μου μέσω SimpleX Chat: %s</string> <string name="notification_preview_mode_hidden">Κρυφό</string> @@ -1069,7 +1069,7 @@ <string name="icon_descr_instant_notifications">Άμεσες ειδοποιήσεις</string> <string name="service_notifications">Άμεσες ειδοποιήσεις!</string> <string name="service_notifications_disabled">Οι άμεσες ειδοποιήσεις είναι απενεργοποιημένες!</string> - <string name="theme_colors_section_title">ΧΡΩΜΑΤΑ ΔΙΕΠΑΦΗΣ</string> + <string name="theme_colors_section_title">Χρώματα διεπαφής</string> <string name="agent_internal_error_title">Εσωτερικό σφάλμα</string> <string name="invalid_chat">μη έγκυρη συνομιλία</string> <string name="invalid_connection_link">Μη έγκυρος σύνδεσμος</string> @@ -1175,7 +1175,7 @@ <string name="media_and_file_servers">Διακομιστές πολυμέσων & αρχείων</string> <string name="privacy_media_blur_radius_medium">Μεσαίο</string> <string name="group_member_role_member">μέλος</string> - <string name="member_info_section_title_member">ΜΕΛΟΣ</string> + <string name="member_info_section_title_member">Μέλος</string> <string name="past_member_vName">Μέλος %1$s</string> <string name="profile_update_event_member_name_changed">το μέλος %1$s άλλαξε σε %2$s</string> <string name="member_admission">Εγγραφή μέλους</string> @@ -1219,7 +1219,7 @@ <string name="update_network_smp_proxy_fallback_question">Εναλλακτική δρομολόγηση μηνυμάτων</string> <string name="update_network_smp_proxy_mode_question">Λειτουργία δρομολόγησης μηνυμάτων</string> <string name="messages_section_title">Μηνύματα</string> - <string name="settings_section_title_messages">ΜΗΝΥΜΑΤΑ ΚΑΙ ΑΡΧΕΙΑ</string> + <string name="settings_section_title_messages">Μηνύματα και αρχεία</string> <string name="message_servers">Διακομιστές μηνυμάτων</string> <string name="unblock_member_desc">Θα εμφανιστούν τα μηνύματα από το %s!</string> <string name="unblock_members_desc">Θα εμφανιστούν τα μηνύματα από αυτά τα μέλη!</string> @@ -1332,7 +1332,7 @@ <string name="image_decoding_exception_desc">Η εικόνα δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε μια άλλη εικόνα ή επικοινώνησε με τους προγραμματιστές.</string> <string name="share_group_profile_via_link_alert_text">Ο σύνδεσμος θα είναι σύντομος και το προφίλ της ομάδας θα κοινοποιηθεί μέσω αυτού.</string> <string name="theme">Θέμα</string> - <string name="settings_section_title_themes">ΘΕΜΑΤΑ</string> + <string name="settings_section_title_themes">Θέματα</string> <string name="moderate_messages_will_be_deleted_warning">Τα μηνύματα θα διαγραφούν για όλα τα μέλη.</string> <string name="moderate_messages_will_be_marked_warning">Τα μηνύματα θα επισημαίνονται ως ελεγχόμενα για όλα τα μέλη.</string> <string name="moderate_message_will_be_deleted_warning">Το μήνυμα θα διαγραφεί για όλα τα μέλη.</string> @@ -1582,7 +1582,7 @@ <string name="exit_without_saving">Έξοδος χωρίς αποθήκευση</string> <string name="expand_verb">Επέκτεινε</string> <string name="icon_descr_expand_role">Επέκταση επιλογής ρόλου</string> - <string name="settings_section_title_experimenta">ΠΕΙΡΑΜΑΤΙΚΟ</string> + <string name="settings_section_title_experimenta">Πειραματικό</string> <string name="settings_experimental_features">Πειραματικά χαρακτηριστικά</string> <string name="expired_label">έληξε</string> <string name="export_database">Εξαγωγή της βάσης δεδομένων</string> @@ -1604,7 +1604,7 @@ <string name="file_error_no_file">Το αρχείο δεν βρέθηκε - πιθανότατα το αρχείο διαγράφηκε ή ακυρώθηκε.</string> <string name="file_with_path">Αρχείο: %s</string> <string name="servers_info_files_tab">Αρχεία</string> - <string name="settings_section_title_files">ΑΡΧΕΙΑ</string> + <string name="settings_section_title_files">Αρχεία</string> <string name="files_and_media">Αρχεία και πολυμέσα</string> <string name="files_are_prohibited_in_group">Απαγορεύονται τα αρχεία και τα πολυμέσα.</string> <string name="files_prohibited_in_this_chat">Τα αρχεία και τα πολυμέσα, απαγορεύονται σε αυτήν τη συνομιλία.</string> @@ -1863,7 +1863,7 @@ <string name="v4_5_private_filenames">Ιδιωτικά ονόματα αρχείων</string> <string name="v6_3_private_media_file_names">Ιδιωτικά ονόματα αρχείων πολυμέσων.</string> <string name="v5_8_private_routing">Δρομολόγηση ιδιωτικών μηνυμάτων 🚀</string> - <string name="settings_section_title_private_message_routing">ΔΡΟΜΟΛΟΓΗΣΗ ΙΔΙΩΤΙΚΩΝ ΜΗΝΥΜΑΤΩΝ</string> + <string name="settings_section_title_private_message_routing">Δρομολόγηση ιδιωτικών μηνυμάτων</string> <string name="note_folder_local_display_name">Ιδιωτικές σημειώσεις</string> <string name="v5_5_private_notes">Ιδιωτικές σημειώσεις</string> <string name="onboarding_notifications_mode_title">Ιδιωτικές ειδοποιήσεις</string> @@ -2032,7 +2032,7 @@ <string name="revoke_file__action">Ανάκληση αρχείου</string> <string name="revoke_file__title">Ανάκληση αρχείου;</string> <string name="role_in_group">Ρόλος</string> - <string name="run_chat_section">ΕΚΚΙΝΗΣΗ ΣΥΝΟΜΙΛΙΑΣ</string> + <string name="run_chat_section">Εκκίνηση συνομιλίας</string> <string name="notifications_mode_off">Εκτελείται όταν η εφαρμογή είναι ανοιχτή</string> <string name="v5_8_safe_files">Ασφαλής λήψη αρχείων</string> <string name="v5_6_safer_groups">Ασφαλέστερες ομάδες</string> @@ -2098,7 +2098,7 @@ <string name="send_disappearing_message_send">Απέστειλε</string> <string name="send_live_message_desc">Στείλε ένα ζωντανό μήνυμα - θα ενημερώνεται για τον παραλήπτη ή τους παραλήπτες καθώς το πληκτρολογείς.</string> <string name="compose_view_send_contact_request_alert_question">Αποστολή αιτήματος επαφής;</string> - <string name="settings_section_title_delivery_receipts">ΑΠΟΣΤΟΛΗ ΑΝΑΦΟΡΩΝ ΠΑΡΑΔΟΣΗΣ ΣΕ</string> + <string name="settings_section_title_delivery_receipts">Αποστολή αναφορών παράδοσης σε</string> <string name="button_send_direct_message">Αποστολή άμεσου μηνύματος</string> <string name="compose_send_direct_message_to_connect">Στείλε άμεσο μήνυμα για να συνδεθείς</string> <string name="send_disappearing_message">Αποστολή μηνύματος που εξαφανίζεται</string> @@ -2153,7 +2153,7 @@ <string name="message_queue_info_server_info">πληροφορίες ουράς διακομιστή: %1$s\n\nτελευταίο ληφθέν μήνυμα: %2$s</string> <string name="error_smp_test_server_auth">Ο διακομιστής απαιτεί εξουσιοδότηση για τη δημιουργία ουρών, έλεγξε τον κωδικό.</string> <string name="error_xftp_test_server_auth">Ο διακομιστής απαιτεί εξουσιοδότηση για ανέβασμα αρχείων, έλεγξε τον κωδικό.</string> - <string name="conn_stats_section_title_servers">ΔΙΑΚΟΜΙΣΤΕΣ</string> + <string name="conn_stats_section_title_servers">Διακομιστές</string> <string name="servers_info">Πληροφορίες διακομιστών</string> <string name="servers_info_reset_stats_alert_message">Θα γίνει επαναφορά στα στατιστικά στοιχεία των διακομιστών - αυτή η ενέργεια δεν μπορεί να αναιρεθεί!</string> <string name="smp_servers_test_failed">Η δοκιμή του διακομιστή απέτυχε!</string> @@ -2179,7 +2179,7 @@ <string name="v4_6_group_welcome_message_descr">Όρισε το εμφανιζόμενο μήνυμα για τα νέα μέλη!</string> <string name="icon_descr_settings">Ρυθμίσεις</string> <string name="toolbar_settings">Ρυθμίσεις</string> - <string name="settings_section_title_settings">ΡΥΘΜΙΣΕΙΣ</string> + <string name="settings_section_title_settings">Ρυθμίσεις</string> <string name="setup_database_passphrase">Όρισε τη φράση πρόσβασης της βάσης δεδομένων</string> <string name="v5_7_shape_profile_images">Διαμόρφωση εικόνων προφίλ</string> <string name="share_verb">Διαμοίρασε</string> @@ -2260,7 +2260,7 @@ <string name="smp_server">Διακομιστής SMP</string> <string name="smp_servers">Διακομιστές SMP</string> <string name="network_socks_proxy">Διακομιστής μεσολάβησης SOCKS</string> - <string name="settings_section_title_socks">ΔΙΑΚΟΜΙΣΤΗΣ ΜΕΣΟΛΑΒΗΣΗΣ SOCKS</string> + <string name="settings_section_title_socks">Διακομιστής μεσολάβησης SOCKS</string> <string name="network_socks_proxy_settings">Ρυθμίσεις διακομιστή μεσολάβησης SOCKS</string> <string name="privacy_media_blur_radius_soft">Απαλό</string> <string name="chat_database_exported_not_all_files">Κάποιο/α αρχείο/α δεν εξήχθησαν</string> @@ -2309,7 +2309,7 @@ <string name="subscription_results_ignored">Η εγγραφή αγνοήθηκε</string> <string name="migrate_from_device_bytes_uploaded">%s ανεβασμένα</string> <string name="v4_6_audio_video_calls_descr">Υποστήριξη bluetooth και άλλων βελτιώσεων.</string> - <string name="settings_section_title_support">ΥΠΟΣΤΗΡΙΞΗ SIMPLEX CHAT</string> + <string name="settings_section_title_support">Υποστήριξη SimpleX Chat</string> <string name="switch_verb">Ενάλλαξε</string> <string name="v6_1_better_calls_descr">Εναλλαγή ήχου και βίντεο κατά τη διάρκεια της κλήσης.</string> <string name="v6_1_switch_chat_profile_descr">Αλλαγή προφίλ συνομιλίας για προσκλήσεις 1-χρήσης.</string> @@ -2414,7 +2414,7 @@ <string name="network_smp_proxy_fallback_allow">Ναι</string> <string name="privacy_chat_list_open_links_yes">Ναι</string> <string name="sender_you_pronoun">εσύ</string> - <string name="settings_section_title_you">ΕΣΥ</string> + <string name="settings_section_title_you">Εσύ</string> <string name="group_info_member_you">εσύ: %1$s</string> <string name="you_accepted_connection">Αποδέχθηκες τη σύνδεση</string> <string name="snd_group_event_member_accepted">αποδέχθηκες αυτό το μέλος</string> 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 8be594f9e2..4e3ce8e810 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -20,7 +20,7 @@ <string name="allow_your_contacts_to_send_voice_messages">Permites que tus contactos envien mensajes de voz.</string> <string name="chat_preferences_always">siempre</string> <string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string> - <string name="settings_section_title_icon">ICONO DE LA APLICACIÓN</string> + <string name="settings_section_title_icon">Icono de la aplicación</string> <string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string> <string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Se puede desactivar en la configuración</b>. En ese caso las notificaciones se seguirán mostrando mientras la aplicación esté en funcionamiento.]]></string> @@ -152,7 +152,7 @@ <string name="delete_after">Eliminar en</string> <string name="ttl_sec">%d seg</string> <string name="contact_already_exists">El contácto ya existe</string> - <string name="connection_error_auth">Error de conexión (Autenticación)</string> + <string name="connection_error_auth">Enlace de conexión eliminado</string> <string name="for_me_only">Eliminar para mí</string> <string name="icon_descr_server_status_disconnected">Desconectado</string> <string name="icon_descr_server_status_connected">Conectado</string> @@ -200,7 +200,7 @@ <string name="smp_servers_delete_server">Eliminar servidor</string> <string name="display_name">Introduce tu nombre:</string> <string name="callstate_connected">conectado</string> - <string name="settings_section_title_device">DISPOSITIVO</string> + <string name="settings_section_title_device">Dispositivo</string> <string name="database_passphrase">Contraseña base de datos</string> <string name="delete_database">Eliminar base de datos</string> <string name="delete_files_and_media_all">Eliminar todos los archivos</string> @@ -243,11 +243,11 @@ <string name="core_version">Core versión: v%s</string> <string name="delete_image">Eliminar imagen</string> <string name="edit_image">Editar imagen</string> - <string name="settings_section_title_chats">CHATS</string> + <string name="settings_section_title_chats">Chats</string> <string name="change_verb">Cambiar</string> <string name="notifications_mode_periodic_desc">Se realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutos</string> <string name="clear_contacts_selection_button">Limpiar</string> - <string name="change_member_role_question">¿Cambiar rol?</string> + <string name="change_member_role_question">¿Cambiar el rol?</string> <string name="v4_4_verify_connection_security_desc">Compara los códigos de seguridad con tus contactos</string> <string name="choose_file">Archivo</string> <string name="clear_verb">Vaciar</string> @@ -274,7 +274,7 @@ <string name="chat_preferences">Preferencias generales</string> <string name="feature_cancelled_item">cancelado %s</string> <string name="chat_is_stopped">SimpleX está parado</string> - <string name="settings_section_title_calls">LLAMADAS</string> + <string name="settings_section_title_calls">Llamadas</string> <string name="chat_is_running">SimpleX está en ejecución</string> <string name="rcv_conn_event_switch_queue_phase_changing">está cambiando de servidor…</string> <string name="chat_with_developers">habla con los desarrolladores</string> @@ -295,7 +295,7 @@ <string name="call_on_lock_screen">Llamadas en la ventana de bloqueo</string> <string name="alert_title_cant_invite_contacts">¡No se pueden invitar contactos!</string> <string name="chat_console">Consola de Chat</string> - <string name="chat_database_section">BASE DE DATOS DE SIMPLEX</string> + <string name="chat_database_section">Base de datos de SimpleX</string> <string name="chat_database_deleted">Base de datos eliminada</string> <string name="chat_database_imported">Base de datos importada</string> <string name="smp_servers_check_address">Comprueba la dirección del servidor e inténtalo de nuevo.</string> @@ -342,7 +342,7 @@ <string name="error_changing_address">Error al cambiar dirección</string> <string name="error_saving_file">Error al guardar archivo</string> <string name="icon_descr_server_status_error">Error</string> - <string name="from_gallery_button">De la galería</string> + <string name="from_gallery_button">Galería</string> <string name="gallery_image_button">Imagen</string> <string name="gallery_video_button">Vídeo</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si has recibido un enlace de invitación a SimpleX Chat puedes abrirlo en tu navegador:</string> @@ -393,15 +393,15 @@ <string name="file_not_found">Archivo no encontrado</string> <string name="how_to_use_simplex_chat">Guía de uso</string> <string name="callstate_ended">finalizado</string> - <string name="settings_section_title_help">AYUDA</string> + <string name="settings_section_title_help">Ayuda</string> <string name="export_database">Exportar base de datos</string> <string name="error_exporting_chat_database">Error al exportar base de datos</string> <string name="error_starting_chat">Error al iniciar Chat</string> <string name="rcv_group_event_invited_via_your_group_link">se ha unido mediante tu enlace de grupo</string> <string name="error_updating_link_for_group">Error al actualizar enlace de grupo</string> - <string name="section_title_for_console">PARA CONSOLA</string> + <string name="section_title_for_console">Para consola</string> <string name="error_changing_role">Error al cambiar rol</string> - <string name="conn_stats_section_title_servers">SERVIDORES</string> + <string name="conn_stats_section_title_servers">Servidores</string> <string name="group_display_name_field">Nombre del grupo:</string> <string name="group_preferences">Preferencias del grupo</string> <string name="group_members_can_send_dms">Los miembros pueden enviar mensajes directos.</string> @@ -455,7 +455,7 @@ \n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos. \n3. La conexión ha sido comprometida.</string> <string name="notification_preview_mode_message">Contacto y texto</string> - <string name="member_info_section_title_member">MIEMBRO</string> + <string name="member_info_section_title_member">Miembro</string> <string name="chat_item_ttl_none">nunca</string> <string name="network_use_onion_hosts_no_desc">No se usarán hosts .onion</string> <string name="settings_notification_preview_title">Vista previa de notificaciones</string> @@ -524,7 +524,7 @@ <string name="video_call_no_encryption">videollamada (sin cifrar)</string> <string name="status_no_e2e_encryption">sin cifrar</string> <string name="import_database">Importar base de datos</string> - <string name="settings_section_title_messages">MENSAJES Y ARCHIVOS</string> + <string name="settings_section_title_messages">Mensajes y archivos</string> <string name="import_database_question">¿Importar base de datos\?</string> <string name="no_received_app_files">Sin archivos recibidos o enviados</string> <string name="messages_section_title">Mensajes</string> @@ -569,7 +569,7 @@ <string name="smp_servers_invalid_address">¡Dirección de servidor no válida!</string> <string name="make_private_connection">Establecer una conexión privada</string> <string name="network_error_desc">Comprueba tu conexión de red con %1$s e inténtalo de nuevo.</string> - <string name="sender_may_have_deleted_the_connection_request">El remitente puede haber eliminado la solicitud de conexión.</string> + <string name="sender_may_have_deleted_the_connection_request">El remitente ha eliminado la solicitud de conexión.</string> <string name="error_smp_test_certificate">La huella en la dirección del servidor no coincide con el certificado.</string> <string name="reply_verb">Responder</string> <string name="save_passphrase_in_keychain">Guardar contraseña en Keystore</string> @@ -661,7 +661,7 @@ <string name="prohibit_sending_disappearing_messages">No se permiten mensajes temporales.</string> <string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string> <string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string> - <string name="run_chat_section">EJECUTAR SIMPLEX</string> + <string name="run_chat_section">Ejecutar SimpleX</string> <string name="restart_the_app_to_use_imported_chat_database">Reinicia la aplicación para poder usar la base de datos importada.</string> <string name="enter_correct_current_passphrase">Introduce la contraseña actual correcta.</string> <string name="feature_received_prohibited">recepción no permitida</string> @@ -692,7 +692,7 @@ <string name="save_and_notify_contact">Guardar y notificar contacto</string> <string name="save_preferences_question">¿Guardar preferencias\?</string> <string name="save_and_notify_group_members">Guardar y notificar grupo</string> - <string name="connection_error_auth_desc">A menos que tu contacto haya eliminado la conexión o el enlace se haya usado, podría ser un error. Por favor, notifícalo. \nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red.</string> + <string name="connection_error_auth_desc">Tu contacto ha eliminado el enlace, o era un enlace de un solo uso que ya se ha usado.\nPara conectarte pide a tu contacto que cree otro enlace.</string> <string name="periodic_notifications_desc">La aplicación recoge nuevos mensajes periódicamente lo que consume un pequeño porcentaje de batería al día. La aplicación no usa notificaciones push por tanto los datos de tu dispositivo no se envían a los servidores push.</string> <string name="la_notice_title_simplex_lock">Bloqueo SimpleX</string> <string name="auth_unlock">Desbloquear</string> @@ -703,8 +703,8 @@ <string name="network_session_mode_transport_isolation">Aislamiento de transporte</string> <string name="strikethrough_text">tachado</string> <string name="use_chat">Abrir SimpleX</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> - <string name="settings_section_title_themes">TEMAS</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> + <string name="settings_section_title_themes">Temas</string> <string name="stop_chat_confirmation">Parar</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción es irreversible. Tu perfil, contactos, mensajes y archivos se perderán.</string> <string name="skip_inviting_button">Omitir invitación a miembros</string> @@ -714,7 +714,7 @@ <string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string> <string name="set_contact_name">Escribe un nombre para el contacto</string> <string name="unknown_error">Error desconocido</string> - <string name="member_role_will_be_changed_with_notification">El rol cambiará a %s. Se notificará en el grupo.</string> + <string name="member_role_will_be_changed_with_notification">El rol cambiará a %s y se notificará en el grupo.</string> <string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string> <string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string> <string name="ntf_channel_messages">Mensajes de chat SimpleX</string> @@ -786,7 +786,7 @@ <string name="profile_is_only_shared_with_your_contacts">El perfil sólo se comparte con tus contactos.</string> <string name="callstate_starting">inicializando…</string> <string name="alert_title_skipped_messages">Mensajes omitidos</string> - <string name="settings_section_title_settings">CONFIGURACIÓN</string> + <string name="settings_section_title_settings">Configuración</string> <string name="stop_chat_question">¿Parar SimpleX?</string> <string name="chat_item_ttl_seconds">%s segundo(s)</string> <string name="group_invitation_tap_to_join">Pulsa para unirte</string> @@ -794,7 +794,7 @@ <string name="network_option_tcp_connection_timeout">Timeout de la conexión TCP</string> <string name="theme">Tema</string> <string name="set_group_preferences">Establece preferencias de grupo</string> - <string name="settings_section_title_support">SOPORTE SIMPLEX CHAT</string> + <string name="settings_section_title_support">Soporte SimpleX Chat</string> <string name="set_password_to_export">Escribe la contraseña para exportar</string> <string name="update_database">Actualizar</string> <string name="update_database_passphrase">Actualizar contraseña base de datos</string> @@ -899,12 +899,12 @@ <string name="your_calls">Llamadas</string> <string name="your_ice_servers">Servidores ICE</string> <string name="your_privacy">Privacidad</string> - <string name="settings_section_title_you">MIS DATOS</string> + <string name="settings_section_title_you">Mis datos</string> <string name="your_chat_database">Base de datos</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string> <string name="you_sent_group_invitation">Has enviado una invitación de grupo</string> <string name="num_contacts_selected">%d contacto(s) seleccionado(s)</string> - <string name="group_info_section_title_num_members"> %1$s MIEMBROS</string> + <string name="group_info_section_title_num_members">%1$s miembros</string> <string name="voice_prohibited_in_this_chat">Los mensajes de voz no están permitidos en este chat.</string> <string name="whats_new">Novedades</string> <string name="you_have_to_enter_passphrase_every_time">La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación.</string> @@ -925,7 +925,7 @@ <string name="integrity_msg_skipped">%1$d mensaje(s) omitido(s)</string> <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Dejarás de recibir mensajes del grupo. El historial del chat se conservará.</string> <string name="view_security_code">Mostrar código de seguridad</string> - <string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</string> + <string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz, antes debes permitir que tu contacto pueda enviarlos.</string> <string name="voice_messages_prohibited">¡Mensajes de voz no permitidos!</string> <string name="group_main_profile_sent">Tu perfil será enviado a los miembros del grupo</string> <string name="icon_descr_address">Dirección SimpleX</string> @@ -990,7 +990,7 @@ <string name="incompatible_database_version">Versión de base de datos incompatible</string> <string name="confirm_database_upgrades">Confirmar actualizaciones de la bases de datos</string> <string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="developer_options">IDs de la base de datos y opciones de aislamiento de transporte.</string> <string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando el contacto termine de subirlo.</string> <string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando el contacto termine de subirla.</string> @@ -1108,7 +1108,7 @@ <string name="one_time_link_short">Enlace de un solo uso</string> <string name="simplex_address">Dirección SimpleX</string> <string name="you_can_accept_or_reject_connection">Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud.</string> - <string name="share_address">Compartir dirección</string> + <string name="share_address">Compartir dirección…</string> <string name="enter_welcome_message">Deja un mensaje de bienvenida…</string> <string name="theme_simplex">SimpleX</string> <string name="color_primary_variant">Color adicional</string> @@ -1142,7 +1142,7 @@ <string name="color_sent_message">Mensaje enviado</string> <string name="stop_sharing">Dejar de compartir</string> <string name="stop_sharing_address">¿Dejar de compartir la dirección\?</string> - <string name="theme_colors_section_title">COLORES DE LA INTERFAZ</string> + <string name="theme_colors_section_title">Colores de la interfaz</string> <string name="you_can_create_it_later">Puedes crearla más tarde</string> <string name="share_address_with_contacts_question">¿Compartir la dirección con los contactos SimpleX?</string> <string name="share_with_contacts">Compartir con contactos SimpleX</string> @@ -1229,7 +1229,7 @@ <string name="item_info_no_text">sin texto</string> <string name="non_fatal_errors_occured_during_import">Han ocurrido algunos errores no críticos durante la importación:</string> <string name="shutdown_alert_question">¿Salir de SimpleX?</string> - <string name="settings_section_title_app">APLICACIÓN</string> + <string name="settings_section_title_app">Aplicación</string> <string name="settings_restart_app">Reiniciar</string> <string name="settings_shutdown">Salir</string> <string name="shutdown_alert_desc">Las notificaciones dejarán de funcionar hasta que vuelvas a iniciar la aplicación</string> @@ -1291,7 +1291,7 @@ <string name="receipts_contacts_enable_for_all">Activar para todos</string> <string name="receipts_contacts_enable_keep_overrides">Activar (conservar anulaciones)</string> <string name="receipts_contacts_disable_for_all">Desactivar para todos</string> - <string name="settings_section_title_delivery_receipts">ENVIAR CONFIRMACIONES DE ENTREGA A</string> + <string name="settings_section_title_delivery_receipts">Enviar confirmaciones de entrega a</string> <string name="delivery_receipts_are_disabled">¡Las confirmaciones de entrega están desactivadas!</string> <string name="dont_enable_receipts">No activar</string> <string name="error_enabling_delivery_receipts">¡Error al activar confirmaciones de entrega!</string> @@ -1306,7 +1306,7 @@ <string name="sending_delivery_receipts_will_be_enabled">El envío de confirmaciones de entrega se activará para todos los contactos.</string> <string name="v5_2_message_delivery_receipts_descr">¡El doble check que nos faltaba! ✅</string> <string name="you_can_enable_delivery_receipts_later">Puedes activar más tarde en Configuración</string> - <string name="you_can_enable_delivery_receipts_later_alert">Puedes activarlos más tarde en la configuración de Privacidad y Seguridad.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Puedes habilitarlas más tarde a través de la aplicación, en la sección de configuración de privacidad.</string> <string name="v5_2_more_things">Algunas cosas más</string> <string name="choose_file_title">Selecciona un archivo</string> <string name="no_selected_chat">Ningún chat seleccionado</string> @@ -1511,7 +1511,7 @@ <string name="recent_history_is_not_sent_to_new_members">El historial no se envía a miembros nuevos.</string> <string name="retry_verb">Reintentar</string> <string name="camera_not_available">Cámara no disponible</string> - <string name="enable_sending_recent_history">Se envían hasta 100 mensajes más recientes a los miembros nuevos.</string> + <string name="enable_sending_recent_history">Se envían los 100 últimos mensajes a los miembros nuevos.</string> <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Añadir contacto</b>: crea un enlace de invitación nuevo o usa un enlace recibido.]]></string> <string name="disable_sending_recent_history">No se envía el historial a los miembros nuevos.</string> <string name="or_show_this_qr_code">O muestra el código QR</string> @@ -1776,7 +1776,7 @@ <string name="network_smp_proxy_mode_always_description">Usar siempre enrutamiento privado.</string> <string name="message_delivery_warning_title">Aviso de entrega de mensaje</string> <string name="network_smp_proxy_mode_never">Nunca</string> - <string name="settings_section_title_private_message_routing">ENRUTAMIENTO PRIVADO DE MENSAJES</string> + <string name="settings_section_title_private_message_routing">Enrutamiento privado de mensajes</string> <string name="srv_error_host">La dirección del servidor es incompatible con la configuración de la red.</string> <string name="network_smp_proxy_mode_unprotected">Con IP desprotegida</string> <string name="snd_error_auth">Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada</string> @@ -1787,7 +1787,7 @@ \n%1$s.</string> <string name="protect_ip_address">Proteger dirección IP</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos.</string> - <string name="settings_section_title_files">ARCHIVOS</string> + <string name="settings_section_title_files">Archivos</string> <string name="app_will_ask_to_confirm_unknown_file_servers">La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion o cuando esté habilitado el proxy SOCKS).</string> <string name="settings_section_title_chat_colors">Colores del chat</string> <string name="settings_section_title_chat_theme">Tema del chat</string> @@ -2053,7 +2053,7 @@ <string name="error_parsing_uri_desc">Por favor, comprueba que el enlace SimpleX es correcto.</string> <string name="forward_files_in_progress_desc">%1$d archivo(s) se está(n) descargando todavía.</string> <string name="n_other_file_errors">%1$d otro(s) error(es) de archivo.</string> - <string name="settings_section_title_chat_database">BASE DE DATOS</string> + <string name="settings_section_title_chat_database">Base de datos</string> <string name="error_forwarding_messages">Error en reenvío de mensajes</string> <string name="forward_alert_title_messages_to_forward">¿Reenviar %1$s mensaje(s)?</string> <string name="forward_multiple">Reenviar mensajes…</string> @@ -2220,7 +2220,7 @@ <string name="delete_chat_for_self_cannot_undo_warning">El chat será eliminado para tí. ¡No puede deshacerse!</string> <string name="only_chat_owners_can_change_prefs">Sólo los propietarios del chat pueden cambiar las preferencias.</string> <string name="member_will_be_removed_from_chat_cannot_be_undone">El miembro será eliminado del chat. ¡No puede deshacerse!</string> - <string name="member_role_will_be_changed_with_notification_chat">El rol cambiará a %s. Se notificará en el chat.</string> + <string name="member_role_will_be_changed_with_notification_chat">El rol cambiará a "%s" y se notificará en el chat.</string> <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Dejarás de recibir mensajes del chat. El historial del chat se conservará.</string> <string name="how_it_helps_privacy">Cómo ayuda a la privacidad</string> <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién.</string> @@ -2304,7 +2304,7 @@ <string name="report_compose_reason_header_profile">Informar del perfil de un miembro: sólo los moderadores del grupo lo verán.</string> <string name="report_reason_other">Otro motivo</string> <string name="report_item_archived">informes archivados</string> - <string name="report_reason_community">Violación de las normas de la comunidad</string> + <string name="report_reason_community">Violación de las normas</string> <string name="report_reason_illegal">Contenido inapropiado</string> <string name="report_reason_profile">Perfil inapropiado</string> <string name="report_item_visibility_moderators">Solo el remitente y el moderador pueden verlo</string> @@ -2508,7 +2508,7 @@ <string name="share_old_link_alert_button">Compartir enlace antiguo</string> <string name="share_group_profile_via_link_alert_text">El enlace será corto y el perfil del grupo se compartirá mediante el enlace.</string> <string name="upgrade_group_link">Actualizar enlace de grupo</string> - <string name="settings_section_title_contact_requests_from_groups">SOLICITUDES DE CONTACTO EN GRUPOS</string> + <string name="settings_section_title_contact_requests_from_groups">Solicitudes de contacto en grupos</string> <string name="rcv_direct_event_group_inv_link_received">conexión solicitada desde el grupo %1$s</string> <string name="this_setting_is_for_your_current_profile">Esta configuración se aplica al perfil actual</string> <string name="member_is_deleted_cant_accept_request">Miembro eliminado, no puede aceptar solicitudes</string> @@ -2597,10 +2597,9 @@ <string name="snd_channel_event_channel_profile_updated">perfil del canal actualizado</string> <string name="delete_channel_for_all_subscribers_cannot_undo_warning">El canal será eliminado para todos los suscriptores. ¡No puede deshacerse!</string> <string name="delete_channel_for_self_cannot_undo_warning">El canal será eliminado para tí. ¡No puede deshacerse!</string> - <string name="info_row_connection_failed">CONEXIÓN FALLIDA</string> + <string name="info_row_connection_failed">Conexión fallida</string> <string name="create_channel_title">Crear canal público</string> <string name="create_channel_button">Crear canal público</string> - <string name="create_channel_beta_button">Crear canal público (BETA)</string> <string name="creating_channel">Creando canal</string> <string name="rcv_channel_events_count">%d eventos del canal</string> <string name="button_delete_channel">Eliminar canal</string> @@ -2631,12 +2630,12 @@ <string name="not_all_relays_connected">Hay servidores no conectados</string> <string name="connect_plan_open_channel">Abrir canal</string> <string name="connect_plan_open_new_channel">Abrir canal nuevo</string> - <string name="member_info_section_title_owner">PROPIETARIO</string> - <string name="channel_members_section_owners">Propietarios</string> + <string name="member_info_section_title_owner">Propietario</string> + <string name="channel_members_section_owners">Propietarios y colaboradores</string> <string name="preset_relay_address">Direcciones predefinidas</string> <string name="preset_relay_name">Nombres predefinidos</string> <string name="group_member_role_relay">servidor</string> - <string name="member_info_section_title_relay">SERVIDOR</string> + <string name="member_info_section_title_relay">Servidor</string> <string name="info_row_relay_address">Dirección servidor</string> <string name="relay_address_alert_title">Dirección del servidor</string> <string name="info_row_relay_link">Enlace servidor</string> @@ -2647,7 +2646,7 @@ <string name="error_relay_test_server_auth">El servidor requiere autorización para conectar con el servidor, comprueba la contraseña.</string> <string name="server_warning">Alerta del servidor</string> <string name="share_relay_address">Compartir dirección del servidor</string> - <string name="member_info_section_title_subscriber">SUSCRIPTOR</string> + <string name="member_info_section_title_subscriber">Suscriptor</string> <string name="channel_members_title_subscribers">Suscriptores</string> <string name="relay_section_footer_owner">Los suscriptores usan el enlace del servidor para conectarse a los canales.\nLa dirección del servidor se usó para establecer el servidor para el canal.</string> <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">El suscriptor será eliminado del canal. ¡No puede deshacerse!</string> @@ -2664,9 +2663,9 @@ <string name="voice_recording_not_supported">La grabación de voz no es compatible con tu plataforma</string> <string name="wait_verb">Espera</string> <string name="relay_test_step_wait_response">Espera respuesta</string> - <string name="channel_member_you">tu</string> + <string name="channel_member_you">tú</string> <string name="you_are_subscriber">eres suscriptor</string> - <string name="you_can_share_channel_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o código QR. Cualquiera podrá unirse al canal.</string> + <string name="you_can_share_channel_link_anybody_will_be_able_to_connect">Puedes compartir el enlace o código QR. Cualquiera podrá unirse al canal.</string> <string name="relay_section_footer_subscriber">Te conectaste al canal mediante este enlace de servidor.</string> <string name="chat_banner_your_channel">Tu canal</string> <string name="connect_plan_this_is_your_link_for_channel">Tu canal</string> @@ -2683,21 +2682,21 @@ <string name="why_built_p5">Tus conversaciones te pertenecen, tal como ha sido siempre antes de la llegada de internet. Tu red no es un lugar que visitas. Es un lugar que has creado, te pertenece y nadie te la podrá quitar, ya sea pública o privada.</string> <string name="why_built_p6">La libertad más antigua del ser humano, la de hablar con otra persona sin ser observado, materializada sobre una infraestructura que no puede traicionarla.</string> <string name="why_built_p7">Porque hemos destruido el poder de saber quien eres. De manera que tu poder nunca se pueda arrebatar.</string> - <string name="why_built_tagline">Se libre en tu red.</string> + <string name="why_built_tagline">Sé libre en tu red.</string> <!-- channel preferences (subscribers) --> <string name="group_reports_subscriber_reports">Informes de suscriptores</string> <string name="allow_direct_messages_channel">Se permiten mensajes directos entre suscriptores.</string> <string name="prohibit_direct_messages_channel">No se permiten mensajes directos entre suscriptores.</string> - <string name="enable_sending_recent_history_channel">Se envían hasta 100 mensajes más recientes a los suscriptores nuevos.</string> + <string name="enable_sending_recent_history_channel">Se envían los 100 últimos mensajes a los suscriptores nuevos.</string> <string name="disable_sending_recent_history_channel">No se envía el historial a los suscriptores nuevos.</string> - <string name="group_members_can_send_disappearing_channel">Los suscriptores del canal pueden enviar mensajes temporales.</string> - <string name="group_members_can_send_dms_channel">Los suscriptores del canal pueden enviar mensajes directos.</string> - <string name="direct_messages_are_prohibited_channel">Los mensajes directos entre suscriptores del canal no están permitidos.</string> - <string name="group_members_can_delete_channel">Los suscriptores del canal pueden eliminar mensajes de forma irreversible. (24 horas)</string> + <string name="group_members_can_send_disappearing_channel">Los suscriptores pueden enviar mensajes temporales.</string> + <string name="group_members_can_send_dms_channel">Los suscriptores pueden enviar mensajes directos.</string> + <string name="direct_messages_are_prohibited_channel">Los mensajes directos entre suscriptores no están permitidos.</string> + <string name="group_members_can_delete_channel">Los suscriptores pueden eliminar mensajes de forma irreversible. (24 horas)</string> <string name="group_members_can_add_message_reactions_channel">Los suscriptores pueden añadir reacciones a los mensajes.</string> - <string name="group_members_can_send_voice_channel">Los suscriptores del canal pueden enviar mensajes de voz.</string> - <string name="group_members_can_send_files_channel">Los suscriptores del canal pueden enviar archivos y multimedia.</string> - <string name="group_members_can_send_simplex_links_channel">Los suscriptores del canal pueden enviar enlaces SimpleX.</string> + <string name="group_members_can_send_voice_channel">Los suscriptores pueden enviar mensajes de voz.</string> + <string name="group_members_can_send_files_channel">Los suscriptores pueden enviar archivos y multimedia.</string> + <string name="group_members_can_send_simplex_links_channel">Los suscriptores pueden enviar enlaces SimpleX.</string> <string name="group_members_can_send_reports_channel">Los suscriptores pueden informar de mensajes a los moderadores.</string> <string name="recent_history_is_sent_to_new_members_channel">Hasta 100 últimos mensajes son enviados a los suscriptores nuevos.</string> <string name="recent_history_is_not_sent_to_new_members_channel">El historial no se envía a suscriptores nuevos.</string> @@ -2710,11 +2709,11 @@ <string name="relay_bar_relays_removed">%1$d servidores eliminados</string> <string name="relay_bar_owner_no_delivery">Añadir servidores pare retomar el envío.</string> <string name="a_link_for_one_person">Enlace para un solo contacto</string> - <string name="allow_chat_with_admins">Permitir que los miembros chateen con administradores.</string> - <string name="allow_chat_with_admins_channel">Permitir que los suscriptores chateen con administradores.</string> + <string name="allow_chat_with_admins">Permite que los miembros chateen con los administradores.</string> + <string name="allow_chat_with_admins_channel">Permite que los suscriptores chateen con los administradores.</string> <string name="relay_bar_all_relays_failed">Todos los servidores han fallado</string> <string name="relay_bar_all_relays_removed">Todos los servidores eliminados</string> - <string name="onboarding_be_free">Se libre\nen tu red</string> + <string name="onboarding_be_free">Sé libre\nen tu red</string> <string name="chat_link_business_address">Dirección empresarial</string> <string name="cant_broadcast_message">no puedes retransmitir</string> <string name="channel_no_active_relays_try_later">El canal no tiene servidores activos. Por favor, intenta unirte más tarde.</string> @@ -2755,7 +2754,7 @@ <string name="onboarding_no_account">Sin cuenta. Sin teléfono. Sin email. Sin ID.\nEl cifrado más seguro.</string> <string name="relay_bar_no_active_relays">Sin servidores activos</string> <string name="chat_link_one_time">Enlace de un solo uso</string> - <string name="only_channel_owners_can_change_prefs">Sólo los propietarios pueden modificar las preferencias de los canales.</string> + <string name="only_channel_owners_can_change_prefs">Sólo los propietarios pueden modificar las preferencias del canal.</string> <string name="onboarding_on_your_phone">En tu teléfono, no en el servidor.</string> <string name="open_external_link_title">¿Abrir enlace externo?</string> <string name="onboarding_or_show_qr_code">O muestra el código QR en persona o por videollamada.</string> @@ -2775,9 +2774,8 @@ <string name="onboarding_configure_notifications">Configurar notificaciones</string> <string name="onboarding_configure_routers">Configurar routers</string> <string name="share_channel">Compartir canal…</string> - <string name="share_via_chat">Compartir mediante chat</string> + <string name="share_via_chat">Compartir en chat</string> <string name="owner_verification_failed">⚠️ Verificación de firma fallida: %s.</string> - <string name="chat_link_signed">(firmado)</string> <string name="members_can_chat_with_admins_channel">Los suscriptores pueden chatear con los administradores.</string> <string name="talk_to_someone">Para comunicarte</string> <string name="tap_to_open">Pulsa para abrir</string> @@ -2809,7 +2807,7 @@ <string name="error_deleting_message">Error al eliminar mensaje</string> <string name="from_history">Del historial</string> <string name="close_behavior_dialog_text">Si eliges Cerrar, los mensajes no serán recibidos.\nPuedes cambiarlo más tarde desde el menú Apariencia.</string> - <string name="appearance_minimize_to_tray_desc">Mantener Simplex en segundo plano para recibir mensajes.</string> + <string name="appearance_minimize_to_tray_desc">Se ejecuta en segundo plano para recibir mensajes</string> <string name="close_behavior_dialog_minimize">Minimizar</string> <string name="close_behavior_dialog_title">Minimizar?</string> <string name="appearance_minimize_to_tray">Minimizar al cerrar la ventana</string> @@ -2820,11 +2818,117 @@ <string name="relays_added_format">Servidores añadidos %1$s.</string> <string name="relay_will_be_removed_from_channel">El servidor será eliminado del canal. ¡No puede deshacerse!</string> <string name="relay_conn_status_removed">eliminado</string> - <string name="button_remove_relay">Eliminar servidor</string> + <string name="button_remove_relay">Quitar servidor</string> <string name="button_remove_relay_question">¿Eliminar el servidor?</string> <string name="select_relays">Seleccionar servidores</string> <string name="tray_show">Ver SimpleX</string> <string name="tray_tooltip">SimpleX</string> <string name="tray_tooltip_unread">SimpleX — %d no leído</string> <string name="last_active_relay_warning">Este es el último servidor activo. Si lo eliminas los mensajes no llegarán a los suscriptores.</string> + <string name="settings_section_title_contact">Contacto</string> + <string name="group_member_role_member_channel">colaborador</string> + <string name="copy_code">Copiar código</string> + <string name="webpage_info">Crea una página web para mostrar la vista previa de tu canal a las visitas antes de suscribirse. Alójala tú mismo o usa cualquier servicio de alojamiento estático.</string> + <string name="enter_webpage_url">Introduce la URL de la web</string> + <string name="group_webpage">Web del grupo</string> + <string name="help_and_support">Ayuda y asistencia</string> + <string name="web_page_url_placeholder">https://</string> + <string name="webpage_url_footer">Se mostrará a los suscriptores y se usará para permitir la carga de la vista previa.</string> + <string name="more_privacy">Más privacidad</string> + <string name="embed_only_your_page">Solo la página superior puede mostrar la vista previa.</string> + <string name="please_upgrade_the_app">Por favor, actualiza la aplicación.</string> + <string name="relay_status_rejected">rechazado</string> + <string name="member_info_relay_status_rejected_by_operator">rechazado por el operador del servidor</string> + <string name="badge_invested">%s ha participado en la financiación colectiva de SimpleX Chat.</string> + <string name="badge_supports_simplex">%s apoya a SimpleX Chat.</string> + <string name="member_info_status">Estado</string> + <string name="group_member_role_observer_channel">suscriptor</string> + <string name="settings_section_title_support_project">Apoyar el proyecto</string> + <string name="badge_unknown_key_desc">La insignia está firmada con una clave que esta versión de la aplicación no reconoce. Actualiza la aplicación para verificar la insignia.</string> + <string name="member_role_will_be_changed_with_notification_channel">El rol cambiará a "%s" y se notificará en el canal.</string> + <string name="badge_unverified_desc">No se ha podido verificar la insignia, podría no ser auténtica.</string> + <string name="channel_owner_count_singular">%1$d propietario</string> + <string name="channel_owner_count_plural">%1$d propietarios</string> + <string name="channel_owners_contributors_count">%1$d propietarios y colaboradores</string> + <string name="badge_supported_simplex">%1$s ha apoyado a SimpleX Chat. La insignia caducó el %2$s.</string> + <string name="settings_section_title_about">Acerca de</string> + <string name="relay_status_acknowledged_roster">lista confirmada</string> + <string name="webpage_code_footer">Añade este código a tu web. Mostrará una vista previa de tu canal o grupo.</string> + <string name="advanced_options">Opciones avanzadas</string> + <string name="advanced_settings">Configuración avanzada</string> + <string name="allow_anyone_to_embed">Permitir que cualquiera pueda añadirlo a su web</string> + <string name="another_instance_not_responding">Puede que haya otra instancia de la aplicación en ejecución o que no se haya cerrado correctamente. ¿Iniciar de todas formas?</string> + <string name="embed_any_webpage_can_show">Cualquier página web puede mostrar la vista previa.</string> + <string name="another_instance_title">La aplicación ya está ejecutándose</string> + <string name="app_update_required">Es necesario actualizar la aplicación</string> + <string name="badge_unknown_key_title">No se pudo verificar la insignia</string> + <string name="channel_webpage">Web del canal</string> + <string name="chat_data">Datos del chat</string> + <string name="unsupported_channel_name">Nombre de canal no compatible</string> + <string name="unsupported_contact_name">Nombre de contacto no compatible</string> + <string name="channel_name_requires_newer_app_version">Para conectarte mediante el nombre del canal es necesaria una versión más reciente de la aplicación.</string> + <string name="contact_name_requires_newer_app_version">Para conectarse mediante el nombre de un contacto es necesaria una versión más reciente de la aplicación.</string> + <string name="group_link_requires_newer_version">Este grupo requiere una versión más reciente de la aplicación. Por favor, actualizala para unirte.</string> + <string name="webpage_code">Código web</string> + <string name="relays_no_web_support">Los servidores usados no admiten páginas web.</string> + <string name="badge_support_from_v7">Puedes apoyar SimpleX desde la versión 7.</string> + <string name="badge_unverified_title">Insignia sin verificar</string> + <string name="connect_plan_connect_to_name">Conectar con %s</string> + <string name="error_saving_simplex_name">Error al guardar el nombre</string> + <string name="connect_plan_join_name">Unirte al canal %s</string> + <string name="set_user_simplex_name_footer">Permitir el contacto mediante tu nombre registrado contra tu dirección SimpleX.</string> + <string name="set_channel_simplex_name_footer">Permitir unirse al canal mediante el nombre registrado con este enlace.</string> + <string name="simplex_name_not_found">Nombre no encontrado</string> + <string name="simplex_name_no_servers_desc">No tienes servidores configurados para resolver nombres SimpleX. Hazlo, o usa un enlace para conectarte.</string> + <string name="no_names_servers_enabled">Sin servidores para resolver nombres.</string> + <string name="simplex_name_no_valid_link">Ningún enlace válido</string> + <string name="simplex_name_resolver_error_desc">Error de resolución: %1$s</string> + <string name="simplex_name_server_no_resolver_desc">El servidor %1$s no admite la resolución de nombres. Configura un servidor, o usa un enlace para conectarte.</string> + <string name="set_simplex_name">Escribe el nombre SimpleX</string> + <string name="simplex_name">Nombre SimpleX</string> + <string name="simplex_name_error">Error del nombre SimpleX</string> + <string name="simplex_name_not_verified">Nombre SimpleX no verificado</string> + <string name="simplex_name_no_valid_link_desc">El nombre SimpleX %1$s está registrado, pero no tiene un enlace válido.</string> + <string name="simplex_name_unconfirmed_desc">El nombre SimpleX %1$s está registrado, pero no se ha añadido a un perfil. Por favor, si eres el propietario, añádelo a tu dirección o al perfil del canal.</string> + <string name="simplex_name_owner_no_channel_link">El nombre SimpleX %1$s está registrado sin un enlace de canal. Añádelo en la página de registro.</string> + <string name="simplex_name_owner_no_address">El nombre SimpleX %1$s está registrado sin una dirección SimpleX. Añádela en la página de registro.</string> + <string name="simplex_name_not_found_desc">El nombre SimpleX no está registrado. Por favor, comprueba el nombre.</string> + <string name="operator_use_for_names">Para resolver nombres</string> + <string name="simplex_name_unconfirmed">Nombre sin confirmar</string> + <string name="verify_simplex_name_action">Verificar nombre</string> + <string name="verify_simplex_names">Verificar nombres SimpleX</string> + <string name="your_simplex_name">Mi nombre SimpleX</string> + <string name="save_simplex_name_question">¿Guardar el nombre SimpleX?</string> + <string name="get_simplex_name_beta">Obtener nombre SimpleX (BETA)</string> + <string name="remove_name">Eliminar nombre</string> + <string name="sign_message">Firmar mensaje</string> + <string name="sign_message_desc">La firma prueba que eres el autor del mensaje sin posibilidad de repudio.</string> + <string name="info_row_signed">Firmado</string> + <string name="info_row_signed_verified">Firmado y verificado</string> + <string name="sign_messages">Firmar mensajes</string> + <string name="require_message_signatures">Requerir la firma de mensajes.</string> + <string name="do_not_require_message_signatures">No requerir la firma de mensajes.</string> + <string name="message_signatures_are_required">Los mensajes firmados son obligatorios.</string> + <string name="message_signatures_are_not_required">Los mensajes firmados no son obligatorios.</string> + <string name="show_signature">Mostrar firma</string> + <string name="show_encryption">Mostrar cifrado</string> + <string name="signature_missing_alert_title">Falta la firma</string> + <string name="signature_missing_alert_desc">El canal requiere que el mensaje esté firmado, pero falta la firma.</string> + <string name="channel_simplex_name">Nombre SimpleX del canal</string> + <string name="register_test_name">Cómo registrar un nombre de prueba</string> + <string name="add_description">Añadir descripción</string> + <string name="profile_description__field">Descripción</string> + <string name="edit_description">Editar descripción</string> + <string name="enter_description_optional">Introduce descripción (opcional)</string> + <string name="error_sharing_address">Error compartiendo dirección</string> + <string name="to_verify_channel_member_key">Para verificar las claves con este suscriptor, compara (o escanea) el código en ambos dispositivos.</string> + <string name="v7_0_channels_contributors">Añade colaboradores.</string> + <string name="v7_0_channels">Canales mejorados 📢</string> + <string name="v7_0_channels_previews">Crea previsualizaciones web.</string> + <string name="v7_0_channels_wider_messages">Fácil de leer.</string> + <string name="info_row_file_servers">Servidores de archivos</string> + <string name="share_text_file_servers">Servidores de archivos: %s</string> + <string name="v7_0_channels_relays">Gestiona tus servidores.</string> + <string name="v7_0_simplex_names_descr">Nombres públicos para tu canal o negocio.</string> + <string name="v7_0_simplex_names">Nombres públicos SimpleX (BETA)</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 3f7d4ff025..263b36d404 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -141,8 +141,7 @@ <string name="smp_server_test_delete_queue">حذف صف</string> <string name="smp_server_test_create_file">ایجاد فایل</string> <string name="error_smp_test_server_auth">سرور برای ایجاد صف‌ها نیاز به مجوز دارد، گذرواژه را بررسی کنید.</string> - <string name="connection_error_auth_desc">مگر اینکه مخاطبتان اتصال را حذف کرده یا این لینک قبلا استفاده شده باشد، ممکن است این یک اشکال باشد - لطفا آن را گزارش دهید. -\nبرای متصل شدن، لطفا از مخاطبتان بخواهید لینک اتصال دیگری ایجاد کند و بررسی کنید که اتصال شبکه باثباتی دارید.</string> + <string name="connection_error_auth_desc">مگر اینکه مخاطبتان اتصال را حذف کرده یا این لینک قبلا استفاده شده باشد، ممکن است این یک اشکال باشد - لطفا آن را گزارش دهید. \nبرای متصل شدن، لطفا از مخاطبتان بخواهید لینک اتصال دیگری ایجاد کند و بررسی کنید که اتصال شبکه باثباتی دارید.</string> <string name="possible_slow_function_title">عملکرد کند</string> <string name="possible_slow_function_desc">اجرای این عملکرد زمان زیادی می‌گیرد: %1$d ثانیه: %2$s</string> <string name="icon_descr_instant_notifications">اعلان‌های آنی</string> @@ -781,7 +780,7 @@ <string name="la_mode_off">خاموش</string> <string name="receipts_groups_override_enabled">ارسال رسید برای %d گروه فعال است</string> <string name="receipts_groups_override_disabled">ارسال رسید برای %d گروه غیرفعال است</string> - <string name="settings_section_title_support">حمایت از SIMPLEX CHAT</string> + <string name="settings_section_title_support">حمایت از SimpleX Chat</string> <string name="settings_section_title_socks">پروکسی SOCKS</string> <string name="settings_section_title_use_from_desktop">استفاده از کامپیوتر</string> <string name="new_database_archive">آرشیو پایگاه داده جدید</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 24634192ec..f3c7a95ef4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -39,9 +39,9 @@ <string name="allow_to_delete_messages">Salli lähetettyjen viestien peruuttamaton poistaminen.</string> <string name="allow_to_send_disappearing">Salli katoavien viestien lähettäminen.</string> <string name="v5_1_self_destruct_passcode_descr">Kaikki tiedot poistetaan, kun se syötetään.</string> - <string name="settings_section_title_icon">SOVELLUKSEN KUVAKE</string> + <string name="settings_section_title_icon">Sovelluksen kuvake</string> <string name="full_backup">Sovelluksen tietojen varmuuskopiointi</string> - <string name="settings_section_title_calls">PUHELUT</string> + <string name="settings_section_title_calls">Puhelut</string> <string name="icon_descr_video_asked_to_receive">Pyydettiin videon vastaanottamista</string> <string name="la_authenticate">Tunnistaudu</string> <string name="auth_unavailable">Tunnistautuminen ei ole käytettävissä</string> @@ -54,12 +54,12 @@ <string name="database_encryption_will_be_updated">Tietokannan salauksen tunnuslause päivitetään ja tallennetaan Keystoreen.</string> <string name="users_delete_profile_for">Poista keskusteluprofiili käyttäjälle</string> <string name="deleted_description">poistettu</string> - <string name="settings_section_title_device">LAITE</string> + <string name="settings_section_title_device">Laite</string> <string name="ttl_h">%dh</string> <string name="connection_error">Yhteysvirhe</string> <string name="cannot_receive_file">Tiedostoa ei voi vastaanottaa</string> <string name="contact_already_exists">Kontakti on jo olemassa</string> - <string name="connection_error_auth">Yhteysvirhe (AUTH)</string> + <string name="connection_error_auth">Yhteysvirhe</string> <string name="smp_server_test_create_file">Luo tiedosto</string> <string name="smp_server_test_create_queue">Luo jono</string> <string name="smp_server_test_delete_queue">Poista jono</string> @@ -124,7 +124,7 @@ <string name="database_passphrase_is_required">Keskustelun avaamiseen tarvitaan tietokannan tunnuslause.</string> <string name="invite_prohibited">Kontaktia ei voi kutsua!</string> <string name="clear_contacts_selection_button">Tyhjennä</string> - <string name="change_member_role_question">Vaihdetaanko ryhmäroolia\?</string> + <string name="change_member_role_question">Vaihdetaanko ryhmäroolia?</string> <string name="color_secondary_variant">Toissijainen lisäsävy</string> <string name="color_background">Tausta</string> <string name="v4_4_verify_connection_security_desc">Vertaa turvakoodeja kontaktiesi kanssa.</string> @@ -153,7 +153,7 @@ <string name="change_self_destruct_mode">Vaihda itsetuhotilaa</string> <string name="change_self_destruct_passcode">Vaihda itsetuhoutuva pääsykoodi</string> <string name="app_passcode_replaced_with_self_destruct">Sovelluksen salasana korvataan itsetuhoutuvalla pääsykoodilla.</string> - <string name="chat_database_section">KESKUSTELUJEN TIETOKANTA</string> + <string name="chat_database_section">Keskustelujen tietokanta</string> <string name="settings_developer_tools">Kehittäjän työkalut</string> <string name="cannot_access_keychain">Ei pääsyä Keystoreen tietokannan salasanan tallentamiseksi</string> <string name="share_text_database_id">Tietokannan tunnus: %d</string> @@ -308,7 +308,7 @@ <string name="auto_accept_images">Hyväksy kuvat automaattisesti</string> <string name="alert_title_msg_bad_id">Virheellinen viestin tunniste</string> <string name="change_lock_mode">Vaihda lukitustilaa</string> - <string name="settings_section_title_chats">KESKUSTELUT</string> + <string name="settings_section_title_chats">Keskustelut</string> <string name="all_group_members_will_remain_connected">Kaikki ryhmän jäsenet pysyvät yhteydessä.</string> <string name="alert_title_cant_invite_contacts">Kontaktia ei voi kutsua!</string> <string name="group_member_status_complete">valmis</string> @@ -453,7 +453,7 @@ <string name="error_starting_chat">Virhe käynnistettäessä keskustelua</string> <string name="error_stopping_chat">Virhe keskustelun lopettamisessa</string> <string name="error_changing_message_deletion">Virhe asetuksen muuttamisessa</string> - <string name="settings_section_title_experimenta">KOKEELLINEN</string> + <string name="settings_section_title_experimenta">Kokeellinen</string> <string name="hide_dev_options">Piilota:</string> <string name="how_it_works">Kuinka se toimii</string> <string name="encrypted_video_call">e2e-salattu videopuhelu</string> @@ -526,7 +526,7 @@ <string name="image_saved">Kuva tallennettu galleriaan</string> <string name="image_will_be_received_when_contact_completes_uploading">Kuva vastaanotetaan, kun kontaktisi on ladannut sen.</string> <string name="choose_file">Tiedosto</string> - <string name="settings_section_title_help">APUA</string> + <string name="settings_section_title_help">Apua</string> <string name="error_encrypting_database">Virhe tietokannan salauksessa</string> <string name="downgrade_and_open_chat">Alenna ja avaa chat</string> <string name="icon_descr_group_inactive">Ei-aktiivinen ryhmä</string> @@ -576,7 +576,7 @@ <string name="immune_to_spam_and_abuse">Immuuni roskapostille ja väärinkäytöksille</string> <string name="error_exporting_chat_database">Virhe vietäessä keskustelujen tietokantaa</string> <string name="user_hide">Piilota</string> - <string name="section_title_for_console">KONSOLIIN</string> + <string name="section_title_for_console">Konsoliin</string> <string name="group_member_status_group_deleted">poistettu ryhmä</string> <string name="snd_group_event_group_profile_updated">ryhmäprofiili päivitetty</string> <string name="alert_title_group_invitation_expired">Vanhentunut kutsu!</string> @@ -655,7 +655,7 @@ <string name="network_option_ping_interval">PING-väli</string> <string name="users_delete_with_connections">Profiili- ja palvelinyhteydet</string> <string name="set_group_preferences">Aseta ryhmän asetukset</string> - <string name="conn_stats_section_title_servers">PALVELIMET</string> + <string name="conn_stats_section_title_servers">Palvelimet</string> <string name="save_and_notify_contact">Tallenna ja ilmoita kontaktille</string> <string name="save_and_notify_contacts">Tallenna ja ilmoita kontakteille</string> <string name="alert_title_skipped_messages">Ohitetut viestit</string> @@ -715,7 +715,7 @@ <string name="self_destruct">Itsetuho</string> <string name="self_destruct_passcode_changed">Itsetuhoutuva pääsykoodi vaihdettu!</string> <string name="self_destruct_passcode_enabled">Itsetuhoutuva pääsykoodi käytössä!</string> - <string name="settings_section_title_socks">SUKAT VÄLITYSPALVELIN</string> + <string name="settings_section_title_socks">SOCKS välityspalvelin</string> <string name="new_database_archive">Uusi tietokanta-arkisto</string> <string name="no_received_app_files">Ei vastaanotettuja tai lähetettyjä tiedostoja</string> <string name="remove_passphrase_from_keychain">Poistetaanko tunnuslause Keystoresta\?</string> @@ -752,7 +752,7 @@ <string name="save_and_notify_group_members">Tallenna ja ilmoita ryhmän jäsenille</string> <string name="stop_chat_confirmation">Lopeta</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</string> - <string name="run_chat_section">SUORITA CHAT</string> + <string name="run_chat_section">Suorita chat</string> <string name="set_password_to_export">Aseta tunnuslause vientiä varten</string> <string name="enter_correct_current_passphrase">Anna oikea nykyinen tunnuslause.</string> <string name="restore_database_alert_confirm">Palauta</string> @@ -836,7 +836,7 @@ <string name="open_simplex_chat_to_accept_call">Avaa SimpleX Chat hyväksyäksesi puhelun</string> <string name="status_no_e2e_encryption">ei e2e-salausta</string> <string name="settings_section_title_support">TUE SIMPLEX CHATia</string> - <string name="settings_section_title_messages">VIESTIT JA TIEDOSTOT</string> + <string name="settings_section_title_messages">Viestit ja tiedostot</string> <string name="share_address">Jaa osoite</string> <string name="users_delete_data_only">Vain paikalliset profiilitiedot</string> <string name="color_received_message">Vastaanotettu viesti</string> @@ -867,7 +867,7 @@ <string name="ok">OK</string> <string name="no_details">ei tietoja</string> <string name="add_contact">Kertakutsulinkki</string> - <string name="settings_section_title_settings">ASETUKSET</string> + <string name="settings_section_title_settings">Asetukset</string> <string name="new_passphrase">Uusi tunnuslause…</string> <string name="restore_database">Palauta tietokannan varmuuskopio</string> <string name="database_backup_can_be_restored">Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun.</string> @@ -967,7 +967,7 @@ <string name="share_text_updated_at">Päivitetty: %s</string> <string name="info_row_sent_at">Lähetetty klo</string> <string name="share_text_sent_at">Lähetetty: %s</string> - <string name="member_info_section_title_member">JÄSEN</string> + <string name="member_info_section_title_member">Jäsen</string> <string name="share_text_moderated_at">Moderoitu klo: %s</string> <string name="current_version_timestamp">%s (nykyinen)</string> <string name="switch_verb">Vaihda</string> @@ -1004,8 +1004,7 @@ <string name="custom_time_unit_seconds">sekuntia</string> <string name="whats_new_thanks_to_users_contribute_weblate">Kiitos käyttäjille – osallistu Weblaten kautta!</string> <string name="you_are_already_connected_to_vName_via_this_link">Olet jo muodostanut yhteyden %1$s kanssa.</string> - <string name="connection_error_auth_desc">Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä. -\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</string> + <string name="connection_error_auth_desc">Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä. \nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</string> <string name="enter_passphrase_notification_desc">Jos haluat saada ilmoituksia, kirjoita tietokannan tunnuslause</string> <string name="la_notice_turn_on">Kytke päälle</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Suojaa tietosi ottamalla SimpleX Lock käyttöön. @@ -1061,7 +1060,7 @@ <string name="you_control_your_chat">Hallitset keskustelujasi!</string> <string name="your_current_profile">Nykyinen profiilisi</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Profiilisi tallennetaan laitteeseesi ja jaetaan vain kontaktiesi kanssa. SimpleX -palvelimet eivät näe profiiliasi.</string> - <string name="settings_section_title_themes">TEEMAT</string> + <string name="settings_section_title_themes">Teemat</string> <string name="messages_section_description">Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä</string> <string name="delete_files_and_media_desc">Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät.</string> <string name="unknown_error">Tuntematon virhe</string> @@ -1095,7 +1094,7 @@ <string name="your_contacts_will_remain_connected">Kontaktisi pysyvät yhdistettyinä.</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi.</string> <string name="icon_descr_video_call">videopuhelu</string> - <string name="group_info_section_title_num_members"> %1$s JÄSENET</string> + <string name="group_info_section_title_num_members">%1$s JÄSENET</string> <string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string> <string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun.</string> <string name="voice_prohibited_in_this_chat">Ääniviestit ovat kiellettyjä tässä keskustelussa.</string> @@ -1116,7 +1115,7 @@ <string name="your_SMP_servers">SMP-palvelimesi</string> <string name="your_XFTP_servers">XFTP-palvelimesi</string> <string name="use_simplex_chat_servers__question">Käytä SimpleX Chat palvelimia\?</string> - <string name="theme_colors_section_title">KÄYTTÖLIITTYMÄN VÄRIT</string> + <string name="theme_colors_section_title">Käyttöliittymän värit</string> <string name="update_network_session_mode_question">Päivitä kuljetuksen eristystila\?</string> <string name="you_can_create_it_later">Voit luoda sen myöhemmin</string> <string name="to_reveal_profile_enter_password">Voit paljastaa piilotetun profiilisi kirjoittamalla koko salasanan Keskusteluprofiilit-sivun hakukenttään.</string> @@ -1151,7 +1150,7 @@ <string name="icon_descr_video_snd_complete">Video lähetetty</string> <string name="icon_descr_waiting_for_video">Odottaa videota</string> <string name="this_string_is_not_a_connection_link">Tämä merkkijono ei ole yhteyslinkki!</string> - <string name="settings_section_title_you">SINÄ</string> + <string name="settings_section_title_you">Sinä</string> <string name="upgrade_and_open_chat">Päivitä ja avaa keskustelu</string> <string name="v4_5_private_filenames_descr">Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä.</string> <string name="v5_0_large_files_support">Videot ja tiedostot 1 Gt asti</string> @@ -1228,7 +1227,7 @@ <string name="custom_time_unit_weeks">viikkoa</string> <string name="shutdown_alert_desc">Ilmoitukset lakkaavat toimimasta, kunnes käynnistät sovelluksen uudelleen</string> <string name="settings_shutdown">Sulje</string> - <string name="settings_section_title_app">SOVELLUS</string> + <string name="settings_section_title_app">Sovellus</string> <string name="settings_restart_app">Käynnistä uudelleen</string> <string name="shutdown_alert_question">Sulje\?</string> <string name="la_mode_off">Pois</string> @@ -1305,7 +1304,7 @@ <string name="receipts_contacts_title_enable">Salli kuittaukset\?</string> <string name="receipts_contacts_override_disabled">Kuittauksien lähettäminen on pois käytöstä %d kontakteilta</string> <string name="receipts_contacts_override_enabled">Kuittauksien lähettäminen on käytössä %d kontakteille</string> - <string name="settings_section_title_delivery_receipts">LÄHETÄ TOIMITUSKUITTAUKSET VASTAANOTTAJALLE</string> + <string name="settings_section_title_delivery_receipts">Lähetä toimituskuittaukset vastaanottajalle</string> <string name="rcv_conn_event_verification_code_reset">turvakoodi on muuttunut</string> <string name="conn_event_ratchet_sync_started">hyväksyy salausta…</string> <string name="snd_conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu %s:lle</string> @@ -1465,7 +1464,7 @@ <string name="permissions_camera">Kamera</string> <string name="permissions_open_settings">Avaa asetukset</string> <string name="protect_ip_address">Suojaa IP-osoite</string> - <string name="settings_section_title_files">TIEDOSTOT</string> + <string name="settings_section_title_files">Tiedostot</string> <string name="settings_section_title_profile_images">Profiilikuvat</string> <string name="group_member_status_unknown_short">tuntematon</string> <string name="remove_member_button">Poista jäsen</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index d95f8ad500..845fe28404 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -61,9 +61,8 @@ <string name="connection_timeout">Délai de connexion</string> <string name="error_sending_message">Erreur lors de l\'envoi du message</string> <string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à %1$s.</string> - <string name="connection_error_auth">Erreur de connexion (AUTH)</string> - <string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler. -\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string> + <string name="connection_error_auth">Erreur de connexion</string> + <string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler. \nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string> <string name="error_accepting_contact_request">Erreur de validation de la demande de contact</string> <string name="error_deleting_group">Erreur lors de la suppression du groupe</string> <string name="error_deleting_contact_request">Erreur lors de la suppression du contact</string> @@ -453,7 +452,7 @@ <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consomme davantage de batterie </b> L\'app fonctionne toujours en arrière-plan - les notifications s\'affichent instantanément.]]></string> <string name="integrity_msg_skipped">%1$d message(s) manqué(s)</string> <string name="integrity_msg_bad_id">ID du message incorrect</string> - <string name="settings_section_title_settings">PARAMÈTRES</string> + <string name="settings_section_title_settings">Paramètres</string> <string name="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand : \n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours. \n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données. @@ -487,12 +486,12 @@ <string name="icon_descr_call_progress">Appel en cours</string> <string name="icon_descr_call_ended">Appel terminé</string> <string name="your_privacy">Votre vie privée</string> - <string name="settings_section_title_device">APPAREIL</string> - <string name="settings_section_title_chats">DISCUSSIONS</string> + <string name="settings_section_title_device">Appareil</string> + <string name="settings_section_title_chats">Discussions</string> <string name="settings_developer_tools">Outils du développeur</string> - <string name="settings_section_title_icon">ICONE DE L\'APP</string> + <string name="settings_section_title_icon">Icone de l\'app</string> <string name="your_chat_database">Votre base de données de chat</string> - <string name="run_chat_section">LANCER LE CHAT</string> + <string name="run_chat_section">Lancer le chat</string> <string name="stop_chat_question">Arrêter le chat \?</string> <string name="restart_the_app_to_use_imported_chat_database">Redémarrez l\'application pour utiliser la base de données de chat importée.</string> <string name="chat_item_ttl_day">1 jour</string> @@ -522,10 +521,10 @@ <string name="settings_audio_video_calls">Appels audio et vidéo</string> <string name="status_e2e_encrypted">chiffré de bout en bout</string> <string name="settings_experimental_features">Fonctionnalités expérimentales</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> - <string name="settings_section_title_themes">THEMES</string> - <string name="settings_section_title_messages">MESSAGES ET FICHIERS</string> - <string name="settings_section_title_calls">APPELS</string> + <string name="settings_section_title_socks">SOCKS proxy</string> + <string name="settings_section_title_themes">Thèmes</string> + <string name="settings_section_title_messages">Messages et fichiers</string> + <string name="settings_section_title_calls">Appels</string> <string name="import_database">Importer la base de données</string> <string name="new_database_archive">Nouvelle archive de base de données</string> <string name="old_database_archive">Archives de l\'ancienne base de données</string> @@ -601,13 +600,13 @@ <string name="protect_app_screen">Protéger l\'écran de l\'app</string> <string name="auto_accept_images">Acceptation automatique des images</string> <string name="full_backup">Sauvegarde des données de l\'app</string> - <string name="settings_section_title_you">VOUS</string> - <string name="settings_section_title_help">AIDE</string> - <string name="settings_section_title_support">SOUTENEZ SIMPLEX CHAT</string> + <string name="settings_section_title_you">Vous</string> + <string name="settings_section_title_help">Aide</string> + <string name="settings_section_title_support">Soutenez SimpleX Chat</string> <string name="settings_section_title_incognito">Mode Incognito</string> <string name="chat_is_running">Le chat est en cours d\'exécution</string> <string name="chat_is_stopped">Le chat est arrêté</string> - <string name="chat_database_section">BASE DE DONNÉES DU CHAT</string> + <string name="chat_database_section">Base de données du chat</string> <string name="database_passphrase">Phrase secrète de la base de données</string> <string name="export_database">Exporter la base de données</string> <string name="stop_chat_confirmation">Arrêter</string> @@ -694,7 +693,7 @@ <string name="button_create_group_link">Créer un lien</string> <string name="button_edit_group_profile">Modifier le profil du groupe</string> <string name="remove_member_confirmation">Supprimer</string> - <string name="member_info_section_title_member">MEMBRE</string> + <string name="member_info_section_title_member">Membre</string> <string name="live_message">Message dynamique !</string> <string name="send_live_message">Envoyer un message dynamique</string> <string name="send_live_message_desc">Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez</string> @@ -708,8 +707,8 @@ <string name="error_deleting_link_for_group">Erreur lors de la suppression du lien du groupe</string> <string name="error_creating_link_for_group">Erreur lors de la création du lien du groupe</string> <string name="only_group_owners_can_change_prefs">Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</string> - <string name="section_title_for_console">POUR TERMINAL</string> - <string name="change_member_role_question">Changer le rôle du groupe \?</string> + <string name="section_title_for_console">Pour terminal</string> + <string name="change_member_role_question">Changer le rôle du groupe ?</string> <string name="member_role_will_be_changed_with_notification">Son rôle est désormais %s. Tous les membres du groupe en seront informés.</string> <string name="icon_descr_contact_checked">Contact vérifié⸱e</string> <string name="clear_contacts_selection_button">Effacer</string> @@ -747,7 +746,7 @@ <string name="direct_messages">Messages directs</string> <string name="full_deletion">Supprimer pour tous</string> <string name="only_you_can_delete_messages">Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé). (24 heures)</string> - <string name="conn_stats_section_title_servers">SERVEURS</string> + <string name="conn_stats_section_title_servers">Serveurs</string> <string name="receiving_via">Réception via</string> <string name="theme_system">Système</string> <string name="allow_direct_messages">Autoriser l\'envoi de messages directs aux membres.</string> @@ -996,7 +995,7 @@ <string name="show_developer_options">Afficher les options pour les développeurs</string> <string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string> <string name="developer_options">IDs de base de données et option d\'isolement du transport.</string> - <string name="settings_section_title_experimenta">EXPÉRIMENTALE</string> + <string name="settings_section_title_experimenta">Expérimentale</string> <string name="hide_dev_options">Cacher :</string> <string name="unhide_chat_profile">Dévoiler le profil de chat</string> <string name="unhide_profile">Dévoiler le profil</string> @@ -1102,7 +1101,7 @@ <string name="you_wont_lose_your_contacts_if_delete_address">Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement.</string> <string name="simplex_address">Adresse SimpleX</string> <string name="you_can_accept_or_reject_connection">Vous pouvez accepter ou refuser les demandes de contacts.</string> - <string name="theme_colors_section_title">COULEURS DE L\'INTERFACE</string> + <string name="theme_colors_section_title">Couleurs de l\'interface</string> <string name="your_contacts_will_remain_connected">Vos contacts resteront connectés.</string> <string name="share_address_with_contacts_question">Partager l\'adresse avec vos contacts ?</string> <string name="share_with_contacts">Partager avec vos contacts</string> @@ -1232,7 +1231,7 @@ <string name="shutdown_alert_question">Arrêt \?</string> <string name="settings_shutdown">Mise à l\'arrêt</string> <string name="settings_restart_app">Redémarrer</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="abort_switch_receiving_address_confirm">Abandonner</string> <string name="error_aborting_address_change">Erreur lors de l\'annulation du changement d\'adresse</string> <string name="abort_switch_receiving_address_question">Abandonner le changement d\'adresse \?</string> @@ -1251,7 +1250,7 @@ <string name="group_members_can_send_files">Les membres peuvent envoyer des fichiers et des médias.</string> <string name="files_are_prohibited_in_group">Les fichiers et les médias sont interdits.</string> <string name="fix_connection_not_supported_by_group_member">Correction non prise en charge par un membre du groupe</string> - <string name="settings_section_title_delivery_receipts">ENVOYER DES ACCUSÉS DE RÉCEPTION AUX</string> + <string name="settings_section_title_delivery_receipts">Envoyer des accusés de réception aux</string> <string name="sync_connection_force_desc">Le chiffrement fonctionne et le nouvel accord de chiffrement n\'est pas nécessaire. Cela peut provoquer des erreurs de connexion !</string> <string name="v5_2_more_things">Encore quelques points</string> <string name="delivery_receipts_title">Justificatifs de réception!</string> @@ -1776,8 +1775,8 @@ <string name="update_network_smp_proxy_fallback_question">Rabattement du routage des messages</string> <string name="private_routing_show_message_status">Afficher le statut du message</string> <string name="protect_ip_address">Protection de l\'adresse IP</string> - <string name="settings_section_title_files">FICHIERS</string> - <string name="settings_section_title_private_message_routing">ROUTAGE PRIVÉ DES MESSAGES</string> + <string name="settings_section_title_files">Fichiers</string> + <string name="settings_section_title_private_message_routing">Routage privé des messages</string> <string name="snd_error_relay">Erreur au niveau du serveur de destination : %1$s</string> <string name="ci_status_other_error">Erreur : %1$s</string> <string name="snd_error_quota">Capacité dépassée - le destinataire n\'a pas pu recevoir les messages envoyés précédemment.</string> @@ -2091,7 +2090,7 @@ <string name="network_proxy_random_credentials">Utiliser des identifiants aléatoires</string> <string name="network_proxy_username">Nom d\'utilisateur</string> <string name="delete_messages_cannot_be_undone_warning">Les messages seront supprimés - il n\'est pas possible de revenir en arrière !</string> - <string name="settings_section_title_chat_database">BASE DE DONNÉES DU CHAT</string> + <string name="settings_section_title_chat_database">Base de données du chat</string> <string name="system_mode_toast">Mode système</string> <string name="network_session_mode_server">Serveur</string> <string name="network_session_mode_server_description">De nouveaux identifiants SOCKS seront utilisées pour chaque serveur.</string> @@ -2348,7 +2347,7 @@ <string name="connect_plan_open_new_group">Ouvrir un nouveau groupe</string> <string name="simplex_link_channel">Lien pour la voie SimpleX</string> <string name="private_routing_no_session">Pas de session de routage privé</string> - <string name="error_accepting_member">Membre acceptant des erreurs</string> + <string name="error_accepting_member">Erreur lors de l\'acceptation du membre</string> <string name="error_deleting_member_support_chat">Erreur effaçant le chat avec le membre</string> <string name="unsupported_connection_link">Lien de connexion pas soutenu</string> <string name="link_requires_newer_app_version_please_upgrade">Ce lien requiert une version de l\'application plus récente. Veuillez s\'il-vous-plait actualiser l\'application ou demander à votre contact de vous envoyer un lien compatible.</string> @@ -2374,7 +2373,7 @@ <string name="compose_view_connect">Se connecter</string> <string name="relay_test_step_connect">Se connecter</string> <string name="relay_conn_status_connected">connecté</string> - <string name="info_row_connection_failed">CONNEXION ÉCHOUÉE</string> + <string name="info_row_connection_failed">Connexion échouée</string> <string name="cant_send_message_contact_deleted">contact supprimé</string> <string name="cant_send_message_contact_disabled">contact désactivé</string> <string name="contact_should_accept">le contact devrait accepter…</string> @@ -2410,7 +2409,7 @@ <string name="group_member_status_rejected">rejeté</string> <string name="reject_pending_member_alert_title">Rejeter le membre?</string> <string name="group_member_role_relay">relais</string> - <string name="member_info_section_title_relay">RELAIS</string> + <string name="member_info_section_title_relay">Relais</string> <string name="info_row_relay_address">Adresse de relais</string> <string name="relay_address_alert_title">Adresse de relais</string> <string name="relay_connection_failed">Échec de la connexion au relais</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml index e6f02f34f2..f3b1df5a40 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -243,7 +243,7 @@ <string name="ttl_min">%d मिनट</string> <string name="settings_section_title_device">उपकरण</string> <string name="group_member_status_creator">रचनाकार</string> - <string name="change_member_role_question">समूह भूमिका बदलें\?</string> + <string name="change_member_role_question">समूह भूमिका बदलें?</string> <string name="ttl_week">%d सप्ताह</string> <string name="v4_2_auto_accept_contact_requests">संपर्क अनुरोधों को स्वत: स्वीकार करें</string> <string name="integrity_msg_bad_hash">खराब संदेश हैश</string> @@ -285,4 +285,21 @@ <string name="group_member_role_member">सदस्य</string> <string name="search_verb">खोजें</string> <string name="la_mode_off">बंद है</string> + <string name="connect_via_contact_link">संपर्क पते के माध्यम से कनेक्ट करें?</string> + <string name="connect_via_invitation_link">एक-बार के लिंक के माध्यम से कनेक्ट करें?</string> + <string name="connect_via_group_link">समूह में शामिल?</string> + <string name="connect_use_current_profile">वर्तमान प्रोफ़ाइल का उपयोग करें</string> + <string name="connect_use_new_incognito_profile">नई गुप्त प्रोफ़ाइल का उपयोग करें</string> + <string name="connect_use_incognito_profile">गुप्त प्रोफ़ाइल का उपयोग करें</string> + <string name="profile_will_be_sent_to_contact_sending_link">आपकी प्रोफ़ाइल उस संपर्क को भेजी जाएगी जिससे आपको यह लिंक प्राप्त हुआ है।</string> + <string name="you_will_join_group">आप सभी ग्रुप मेंबर्स से कनेक्ट होंगे।</string> + <string name="connect_via_link_incognito">इनकॉग्निटो कनेक्ट करें</string> + <string name="connect_plan_open_chat">चैट खोलें</string> + <string name="connect_plan_open_new_group">नया ग्रुप खोलें</string> + <string name="error_parsing_uri_title">यह लिंक मान्य नहीं है</string> + <string name="error_parsing_uri_desc">कृपया जांच लें कि SimpleX लिंक सही है।</string> + <string name="opening_database">डेटाबेस खुल रहा है…</string> + <string name="database_migration_in_progress">डेटाबेस माइग्रेशन जारी है।\nइसमें कुछ मिनट लग सकते हैं।</string> + <string name="non_content_uri_alert_title">अमान्य फ़ाइल पथ</string> + <string name="non_content_uri_alert_text">आपने एक अमान्य फ़ाइल पथ साझा किया है। कृपया इस समस्या की सूचना ऐप डेवलपर्स को दें।</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml index 84e806dda0..2d29984da5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hr/strings.xml @@ -29,7 +29,7 @@ <string name="callstatus_accepted">prihvati poziv</string> <string name="permissions_required">Dodeliti dozvolu</string> <string name="audio_device_wired_headphones">Slušalice</string> - <string name="settings_section_title_help">POMOĆ</string> + <string name="settings_section_title_help">Pomoć</string> <string name="delete_group_for_self_cannot_undo_warning">Grupa će biti obrisana za Vas – ovo ne može da se poništi!</string> <string name="color_primary">Akcenat</string> <string name="v4_2_group_links">Grupni linkovi</string> @@ -120,15 +120,15 @@ <string name="servers_info_modal_error_title">Greška</string> <string name="create_1_time_link">Napravi jednokratnu poveznicu</string> <string name="paste_button">Nalepiti</string> - <string name="settings_section_title_settings">PODEŠAVANJE</string> + <string name="settings_section_title_settings">Podešavanje</string> <string name="settings_section_title_profile_images">Profilne slike</string> <string name="acknowledged">Razumeo</string> <string name="deleted">Odstranjeno</string> <string name="deleted_description">odstranjeno</string> <string name="create_profile_button">Napraviti</string> - <string name="settings_section_title_messages">PORUKE I DATOTEKE</string> + <string name="settings_section_title_messages">Poruke i datoteke</string> <string name="compose_message_placeholder">Poruka</string> - <string name="conn_stats_section_title_servers">SERVERI</string> + <string name="conn_stats_section_title_servers">Serveri</string> <string name="delete_chat_profile">Odstraniti profil razgovora</string> <string name="feature_roles_admins">administratori</string> <string name="random_port">Nasumično</string> @@ -240,7 +240,7 @@ <string name="forward_files_not_accepted_receive_files">Preuzimanje</string> <string name="network_settings_title">Napredna podešavanja</string> <string name="icon_descr_call_progress">Poziv u toku</string> - <string name="settings_section_title_calls">POZIVI</string> + <string name="settings_section_title_calls">Pozivi</string> <string name="v5_4_block_group_members">Blokiraj članove grupe</string> <string name="file_not_approved_title">Nepoznati serveri!</string> <string name="icon_descr_file">Datoteka</string> @@ -253,7 +253,7 @@ <string name="blocked_items_description">%d poruka blokirano</string> <string name="server_connecting">povezivanje</string> <string name="connected_mobile">Povezan telefon</string> - <string name="settings_section_title_you">VI</string> + <string name="settings_section_title_you">Vi</string> <string name="v6_0_privacy_blur">Zamućeno za bolju privatnost.</string> <string name="ttl_months">%d meseca(i)</string> <string name="icon_descr_call_ended">Poziv završen</string> @@ -295,7 +295,7 @@ <string name="la_minutes">%d minut(a)</string> <string name="app_check_for_updates">Proveri ažuriranje</string> <string name="app_check_for_updates_stable">Stabilno</string> - <string name="settings_section_title_files">DATOTEKE</string> + <string name="settings_section_title_files">Datoteke</string> <string name="migrate_from_device_bytes_uploaded">%s otpremljeno</string> <string name="disable_notifications_button">Onemogućiti obavještenja</string> <string name="is_not_verified">%s nije verifikovan</string> @@ -312,7 +312,7 @@ <string name="scan_QR_code">Skenirati QR kod</string> <string name="network_session_mode_server">Server</string> <string name="no_call_on_lock_screen">Onemogućiti</string> - <string name="settings_section_title_chat_database">BAZA PODATAKA CHATA</string> + <string name="settings_section_title_chat_database">Baza podataka chata</string> <string name="send_receipts_disabled">onemogućeno</string> <string name="import_theme_error">Greška pri uvoženju teme</string> <string name="files_are_prohibited_in_group">Datoteke i medijski sadržaji su zabranjeni.</string> @@ -328,7 +328,7 @@ <string name="or_scan_qr_code">Ili skenirati QR kod</string> <string name="app_check_for_updates_disabled">Onemogućeno</string> <string name="settings_section_title_app">Aplikacija</string> - <string name="settings_section_title_chats">RAZGOVORI</string> + <string name="settings_section_title_chats">Razgovori</string> <string name="files_and_media_prohibited">Datoteke i medijski sadržaji su zabranjeni!</string> <string name="disappearing_prohibited_in_this_chat">Poruke koje nestaju su zabranjene u ovom razgovoru.</string> <string name="chat_is_stopped_indication">Chat je zaustavljen</string> @@ -370,7 +370,7 @@ <string name="image_descr_qr_code">QR kod</string> <string name="chat_is_running">Chat je pokrenut</string> <string name="import_database">Uvesti bazu podataka</string> - <string name="chat_database_section">BAZA PODATAKA CHATA</string> + <string name="chat_database_section">Baza podataka chata</string> <string name="chat_is_stopped">Chat je zaustavljen</string> <string name="rcv_group_event_n_members_connected">%s, %s i %d ostali članovi povezani</string> <string name="migrate_to_device_import_failed">Uvoz neuspešan</string> @@ -428,7 +428,7 @@ <string name="simplex_address">SimpleX adresa</string> <string name="image_descr_simplex_logo">SimpleX Logo</string> <string name="show_dev_options">Prikazati:</string> - <string name="settings_section_title_device">UREĐAJ</string> + <string name="settings_section_title_device">Uređaj</string> <string name="new_message">Nova poruka</string> <string name="color_secondary">Sekundarni</string> <string name="receipts_section_contacts">Kontakti</string> @@ -474,7 +474,7 @@ <string name="favorite_chat">Omiljen</string> <string name="network_smp_proxy_mode_never">Nikada</string> <string name="network_session_mode_entity">Veza</string> - <string name="settings_section_title_themes">TEME</string> + <string name="settings_section_title_themes">Teme</string> <string name="audio_video_calls">Audio/video pozivi</string> <string name="chat_preferences_no">ne</string> <string name="conn_event_ratchet_sync_ok">šifrovanje ok</string> @@ -491,7 +491,7 @@ <string name="icon_descr_address">SimpleX Adresa</string> <string name="save_servers_button">Sačuvati</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> - <string name="settings_section_title_experimenta">EKSPERIMENTALNO</string> + <string name="settings_section_title_experimenta">Eksperimentalno</string> <string name="chat_item_ttl_none">nikada</string> <string name="clear_contacts_selection_button">Očistiti</string> <string name="v4_6_chinese_spanish_interface_descr">Zahvaljujući korisnicima – doprinesi pomoću Weblate!</string> @@ -694,7 +694,7 @@ <string name="select_chat_profile">Izabrati profil razgovora</string> <string name="smp_servers_scan_qr">Skenirati QR kod servera</string> <string name="network_settings">Napredna mrežna podešavanja</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> + <string name="settings_section_title_socks">SOCKS proxy</string> <string name="settings_section_title_incognito">Anonimni režim</string> <string name="network_option_ping_count">broj PING</string> <string name="servers_info_reset_stats_alert_title">Obnoviti statistiku?</string> @@ -725,7 +725,7 @@ <string name="migrate_from_device_archiving_database">Arhiviraj bazu podataka</string> <string name="onboarding_notifications_mode_periodic">Periodično</string> <string name="remove_member_confirmation">Ukloniti</string> - <string name="member_info_section_title_member">ČLAN</string> + <string name="member_info_section_title_member">Član</string> <string name="joining_group">Pristupanje grupi</string> <string name="smp_server">SMP server</string> <string name="invite_to_chat_button">Pozvati u razgovor</string> @@ -986,7 +986,7 @@ <string name="smp_servers_new_server">Novi server</string> <string name="subscription_percentage">Prikazati procente</string> <string name="exit_without_saving">Napustiti bez čuvanja</string> - <string name="run_chat_section">POKRENUTI RAZGOVOR</string> + <string name="run_chat_section">Pokrenuti razgovor</string> <string name="unblock_for_all_question">Odblokirati člana za sve?</string> <string name="migrate_to_device_database_init">Priprema za preuzimanje</string> <string name="proxied">Proxied(posredovan)</string> @@ -1260,9 +1260,9 @@ <string name="passcode_set">Pin kod postavljen!</string> <string name="all_app_data_will_be_cleared">Svi podaci u aplikaciji su odstranjeni.</string> <string name="app_passcode_replaced_with_self_destruct">Pin kod aplikacije je zamenjen pin kodom za samouništenje.</string> - <string name="settings_section_title_support">POTPORI SIMPLEX CHAT</string> + <string name="settings_section_title_support">Potpori SimpleX Chat</string> <string name="settings_section_title_message_shape">Oblik poruke</string> - <string name="settings_section_title_icon">IKONA APLIKACIJE</string> + <string name="settings_section_title_icon">Ikona aplikacije</string> <string name="database_passphrase">Pristupna fraza baze podataka</string> <string name="set_passphrase">Odrediti pristupnu frazu</string> <string name="database_will_be_encrypted">Baza podataka će biti šifrovana.</string> @@ -1447,7 +1447,7 @@ <string name="save_and_update_group_profile">Sačuvati i ažurirati grupni profil</string> <string name="v5_8_message_delivery_descr">Uz smanjenu potrošnju baterije.</string> <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(skenirati ili nalepiti iz memorije)</string> - <string name="connection_error_auth">Greška u vezi (AUTH)</string> + <string name="connection_error_auth">Greška u vezi</string> <string name="contact_developers">Ažurirajte aplikaciju i kontaktirajte programere.</string> <string name="non_fatal_errors_occured_during_import">Tokom uvoza došlo je do nekih nefatalnih grešaka:</string> <string name="save_passphrase_in_settings">Sačuvati pristupnu frazu u podešavanjima</string> 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 31b8a89dc7..f9e627b21a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -3,7 +3,7 @@ <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nem sikerült visszafejteni %1$d üzenetet.</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d üzenet kihagyva.</string> <string name="integrity_msg_skipped">%1$d üzenet kihagyva</string> - <string name="group_info_section_title_num_members">%1$s TAG</string> + <string name="group_info_section_title_num_members">%1$s tag</string> <string name="chat_item_ttl_month">1 hónap</string> <string name="chat_item_ttl_week">1 hét</string> <string name="v5_3_new_interface_languages">6 új kezelőfelületi nyelv</string> @@ -49,7 +49,7 @@ <string name="full_backup">Alkalmazásadatok biztonsági mentése</string> <string name="database_initialization_error_title">Az adatbázis előkészítése sikertelen</string> <string name="all_your_contacts_will_remain_connected_update_sent">Az összes partnerével továbbra is kapcsolatban marad. A profilfrissítés el lesz küldve a partnerei számára.</string> - <string name="v4_5_transport_isolation_descr">A csevegési profillal (alapértelmezett), vagy a kapcsolattal (BÉTA).</string> + <string name="v4_5_transport_isolation_descr">A csevegési profillal (alapértelmezett), vagy a kapcsolattal (béta).</string> <string name="connect__a_new_random_profile_will_be_shared">Egy új, véletlenszerű profil lesz megosztva.</string> <string name="allow_voice_messages_only_if">A hangüzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> <string name="app_version_code">Alkalmazás összeállítási száma: %s</string> @@ -71,7 +71,7 @@ <string name="v5_4_better_groups">Továbbfejlesztett csoportok</string> <string name="clear_chat_warning">Az összes üzenet törölve lesz – ez a művelet nem vonható vissza! Az üzenetek CSAK az Ön számára törlődnek.</string> <string name="icon_descr_call_ended">A hívás véget ért</string> - <string name="settings_section_title_calls">HÍVÁSOK</string> + <string name="settings_section_title_calls">Hívások</string> <string name="rcv_group_and_other_events">és további %d esemény</string> <string name="address_section_title">Cím</string> <string name="connect_plan_already_joining_the_group">A csatlakozás folyamatban van a csoporthoz!</string> @@ -149,13 +149,13 @@ <string name="callstatus_in_progress">hívás folyamatban</string> <string name="auto_accept_images">Képek automatikus elfogadása</string> <string name="allow_your_contacts_to_call">A hívások kezdeményezése engedélyezve van a partnerei számára.</string> - <string name="settings_section_title_icon">ALKALMAZÁSIKON</string> + <string name="settings_section_title_icon">Alkalmazásikon</string> <string name="v4_3_improved_server_configuration_desc">Kiszolgáló hozzáadása QR-kód beolvasásával.</string> <string name="allow_to_send_disappearing">Az eltűnő üzenetek küldése engedélyezve van.</string> <string name="allow_disappearing_messages_only_if">Az eltűnő üzenetek küldése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi.</string> <string name="icon_descr_audio_off">Hang kikapcsolva</string> <string name="allow_direct_messages">A közvetlen üzenetek küldése a tagok között engedélyezve van.</string> - <string name="settings_section_title_app">ALKALMAZÁS</string> + <string name="settings_section_title_app">Alkalmazás</string> <string name="icon_descr_call_progress">Hívás folyamatban</string> <string name="both_you_and_your_contact_can_add_message_reactions">Mindkét fél hozzáadhat az üzenetekhez reakciókat.</string> <string name="both_you_and_your_contact_can_make_calls">Mindkét fél tud hívásokat kezdeményezni.</string> @@ -201,7 +201,7 @@ <string name="contacts_can_mark_messages_for_deletion">A partnerei törlésre jelölhetnek üzeneteket; Ön majd meg tudja nézni azokat.</string> <string name="connect_via_invitation_link">Kapcsolódik az egyszer használható meghívón keresztül?</string> <string name="connect_via_link_or_qr">Kapcsolódás egy hivatkozáson vagy QR-kódon keresztül</string> - <string name="connection_error_auth">Kapcsolódási hiba (AUTH)</string> + <string name="connection_error_auth">A kapcsolódási hivatkozás el lett távolítva</string> <string name="notification_preview_mode_contact">Csak név</string> <string name="connect_via_contact_link">Kapcsolódik a kapcsolattartási címen keresztül?</string> <string name="create_address">Cím létrehozása</string> @@ -233,7 +233,7 @@ <string name="receipts_section_contacts">Partnerek</string> <string name="connection_error">Kapcsolódási hiba</string> <string name="alert_title_contact_connection_pending">A partnere még nem kapcsolódott!</string> - <string name="v5_3_discover_join_groups_descr">- kapcsolódás a könyvtárszolgáltatáshoz (BÉTA)!\n- kézbesítési jelentések (legfeljebb 20 tagig).\n- gyorsabb és stabilabb.</string> + <string name="v5_3_discover_join_groups_descr">- kapcsolódás a könyvtárszolgáltatáshoz (béta)!\n- kézbesítési jelentések (legfeljebb 20 tagig).\n- gyorsabb és stabilabb.</string> <string name="contribute">Közreműködés</string> <string name="group_member_status_intro_invitation">kapcsolódás (bemutatkozó meghívó)</string> <string name="create_simplex_address">SimpleX-cím létrehozása</string> @@ -300,7 +300,7 @@ <string name="icon_descr_call_connecting">Hívás kapcsolása</string> <string name="delete_files_and_media_question">Törli a fájlokat és a médiatartalmakat?</string> <string name="group_member_status_complete">kész</string> - <string name="chat_database_section">CSEVEGÉSI ADATBÁZIS</string> + <string name="chat_database_section">Csevegési adatbázis</string> <string name="change_self_destruct_passcode">Önmegsemmisítő jelkód módosítása</string> <string name="smp_server_test_create_queue">Várólista létrehozása</string> <string name="colored_text">színezett</string> @@ -313,7 +313,7 @@ <string name="server_connecting">kapcsolódás</string> <string name="send_disappearing_message_custom_time">Egyéni időköz</string> <string name="connect_via_link_incognito">Kapcsolódás inkognitóban</string> - <string name="settings_section_title_chats">CSEVEGÉSEK</string> + <string name="settings_section_title_chats">Csevegések</string> <string name="v5_3_new_desktop_app_descr">Új profil létrehozása a számítógépes alkalmazásban. 💻</string> <string name="group_member_status_announced">kapcsolódás (bejelentve)</string> <string name="contact_connection_pending">kapcsolódás…</string> @@ -393,7 +393,7 @@ <string name="dont_show_again">Ne jelenjen meg újra</string> <string name="auth_disable_simplex_lock">SimpleX-zár kikapcsolása</string> <string name="status_e2e_encrypted">végpontok között titkosított</string> - <string name="settings_section_title_device">ESZKÖZ</string> + <string name="settings_section_title_device">Eszköz</string> <string name="encrypted_video_call">végpontok között titkosított videóhívás</string> <string name="conn_level_desc_direct">közvetlen</string> <string name="desktop_device">Számítógép</string> @@ -455,7 +455,7 @@ <string name="ttl_d">%dnap</string> <string name="receipts_contacts_enable_for_all">Engedélyezés az összes tag számára</string> <string name="delivery_receipts_are_disabled">A kézbesítési jelentések le vannak tiltva!</string> - <string name="expand_verb">Kibontás</string> + <string name="expand_verb">Felfedés</string> <string name="error_sending_message">Hiba történt az üzenet elküldésekor</string> <string name="la_enter_app_passcode">Adja meg a jelkódot</string> <string name="for_everybody">Mindenkinél</string> @@ -522,7 +522,7 @@ <string name="v5_2_disappear_one_message_descr">Akkor is, ha le van tiltva a beszélgetésben.</string> <string name="v5_4_better_groups_descr">Gyorsabb csatlakozás és megbízhatóbb üzenetkézbesítés.</string> <string name="enable_lock">Zárolás engedélyezése</string> - <string name="settings_section_title_help">SÚGÓ</string> + <string name="settings_section_title_help">Súgó</string> <string name="group_is_decentralized">Teljesen decentralizált – csak a tagok számára látható.</string> <string name="file_with_path">Fájl: %s</string> <string name="icon_descr_hang_up">Hívás befejezése</string> @@ -530,7 +530,7 @@ <string name="file_saved">Fájl mentve</string> <string name="fix_connection_question">Kapcsolat javítása?</string> <string name="files_and_media">Fájlok és médiatartalmak</string> - <string name="section_title_for_console">KONZOLHOZ</string> + <string name="section_title_for_console">Konzolhoz</string> <string name="alert_text_encryption_renegotiation_failed">Nem sikerült a titkosítást újraegyeztetni.</string> <string name="error_deleting_user">Hiba történt a felhasználói profil törlésekor</string> <string name="fix_connection_not_supported_by_group_member">Csoporttag általi javítás nem támogatott</string> @@ -579,7 +579,7 @@ <string name="group_full_name_field">A csoport teljes neve:</string> <string name="icon_descr_help">súgó</string> <string name="enabled_self_destruct_passcode">Önmegsemmisítő jelkód engedélyezése</string> - <string name="settings_section_title_experimenta">KÍSÉRLETI</string> + <string name="settings_section_title_experimenta">Kísérleti</string> <string name="error_aborting_address_change">Hiba történt a cím módosításának megszakításakor</string> <string name="error_receiving_file">Hiba történt a fájl fogadásakor</string> <string name="conn_event_ratchet_sync_ok">titkosítása rendben van</string> @@ -631,7 +631,7 @@ <string name="onboarding_notifications_mode_service">Azonnali</string> <string name="v5_4_incognito_groups">Inkognitócsoportok</string> <string name="how_to">Útmutató</string> - <string name="hide_verb">Összecsukás</string> + <string name="hide_verb">Elrejtés</string> <string name="gallery_image_button">Kép</string> <string name="v4_3_improved_privacy_and_security">Továbbfejlesztett adatvédelem és biztonság</string> <string name="ignore">Mellőzés</string> @@ -677,7 +677,7 @@ <string name="disconnect_remote_hosts">Hordozható eszközök leválasztása</string> <string name="v4_5_multiple_chat_profiles_descr">Különböző nevek, profilképek és átvitelelkülönítés.</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Elutasítás esetén a kérés küldője NEM kap értesítést.</string> - <string name="icon_descr_expand_role">Szerepkörválasztó kibontása</string> + <string name="icon_descr_expand_role">Szerepkörválasztó felfedése</string> <string name="image_will_be_received_when_contact_is_online">A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!</string> <string name="group_member_status_invited">meghíva</string> <string name="invalid_connection_link">Érvénytelen kapcsolattartási hivatkozás</string> @@ -722,7 +722,7 @@ <string name="message_reactions_are_prohibited">A reakciók hozzáadása az üzenetekhez le van tiltva.</string> <string name="network_use_onion_hosts_no">Nem</string> <string name="item_info_no_text">nincs szöveg</string> - <string name="member_info_section_title_member">TAG</string> + <string name="member_info_section_title_member">Tag</string> <string name="onboarding_notifications_mode_subtitle">Hogyan befolyásolja az akkumulátort</string> <string name="new_member_role">Új tag szerepköre</string> <string name="la_mode_off">Kikapcsolva</string> @@ -842,7 +842,7 @@ <string name="notification_preview_mode_message">Név és üzenet</string> <string name="notifications_will_be_hidden">Az értesítések csak az alkalmazás bezárásáig érkeznek!</string> <string name="info_menu">Információ</string> - <string name="settings_section_title_messages">ÜZENETEK ÉS FÁJLOK</string> + <string name="settings_section_title_messages">Üzenetek és fájlok</string> <string name="group_member_role_member">tag</string> <string name="make_private_connection">Privát kapcsolat létrehozása</string> <string name="moderated_item_description">%s moderálta ezt az üzenetet</string> @@ -918,7 +918,7 @@ <string name="button_welcome_message">Üdvözlőüzenet</string> <string name="rcv_group_event_n_members_connected">%s, %s és további %d tag kapcsolódott</string> <string name="only_your_contact_can_make_calls">Csak a partnere kezdeményezhet hívásokat.</string> - <string name="settings_section_title_themes">TÉMÁK</string> + <string name="settings_section_title_themes">Témák</string> <string name="videos_limit_title">Túl sok videó!</string> <string name="welcome">Üdvözöljük!</string> <string name="v5_1_self_destruct_passcode">Önmegsemmisítő jelkód</string> @@ -963,7 +963,7 @@ <string name="you_accepted_connection">Ön elfogadta a kapcsolatot</string> <string name="reject_contact_button">Elutasítás</string> <string name="notification_preview_mode_message_desc">Partner nevének és az üzenet tartalmának megjelenítése</string> - <string name="settings_section_title_settings">BEÁLLÍTÁSOK</string> + <string name="settings_section_title_settings">Beállítások</string> <string name="save_profile_password">Profiljelszó mentése</string> <string name="stop_snd_file__title">Megállítja a fájlküldést?</string> <string name="unlink_desktop_question">Leválasztja a számítógépet?</string> @@ -1007,7 +1007,7 @@ <string name="scan_QR_code">QR-kód beolvasása</string> <string name="smp_servers_test_server">Kiszolgáló tesztelése</string> <string name="send_us_an_email">Küldjön nekünk e-mailt</string> - <string name="conn_stats_section_title_servers">KISZOLGÁLÓK</string> + <string name="conn_stats_section_title_servers">Kiszolgálók</string> <string name="smp_servers_test_servers">Kiszolgálók tesztelése</string> <string name="la_lock_mode_passcode">Jelkód bevitele</string> <string name="la_mode_system">Rendszer</string> @@ -1018,12 +1018,12 @@ <string name="prohibit_message_reactions">A reakciók hozzáadása az üzenethez le van tiltva.</string> <string name="use_random_passphrase">Véletlenszerű jelmondat használata</string> <string name="call_connection_peer_to_peer">egyenrangú</string> - <string name="run_chat_section">CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA</string> + <string name="run_chat_section">Csevegési szolgáltatás indítása</string> <string name="paste_the_link_you_received">Kapott hivatkozás beillesztése</string> <string name="smp_save_servers_question">Menti a kiszolgálókat?</string> <string name="v4_2_security_assessment_desc">A SimpleX Chat biztonsága a Trail of Bits által lett auditálva.</string> <string name="rcv_group_event_updated_group_profile">frissítette a csoport profilját</string> - <string name="settings_section_title_support">SIMPLEX CHAT TÁMOGATÁSA</string> + <string name="settings_section_title_support">SimpleX Chat támogatása</string> <string name="simplex_service_notification_title">SimpleX Chat szolgáltatás</string> <string name="observer_cant_send_message_title">Ön megfigyelő</string> <string name="is_verified">%s ellenőrizve</string> @@ -1072,10 +1072,10 @@ <string name="simplex_link_invitation">Egyszer használható SimpleX meghívó</string> <string name="your_calls">Hívások</string> <string name="icon_descr_sent_msg_status_send_failed">nem sikerült elküldeni</string> - <string name="theme_colors_section_title">KEZELŐFELÜLET SZÍNEI</string> + <string name="theme_colors_section_title">Kezelőfelület színei</string> <string name="restore_database_alert_desc">Adja meg a korábbi jelszót az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza.</string> <string name="color_secondary">Másodlagos szín</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> + <string name="settings_section_title_socks">SOCKS proxy</string> <string name="save_servers_button">Mentés</string> <string name="settings_restart_app">Újraindítás</string> <string name="smp_servers">SMP-kiszolgálók</string> @@ -1095,7 +1095,7 @@ <string name="set_database_passphrase">Adatbázis-jelmondat beállítása</string> <string name="view_security_code">Biztonsági kód megtekintése</string> <string name="unblock_member_question">Feloldja a tag letiltását?</string> - <string name="sender_may_have_deleted_the_connection_request">A kérés küldője törölhette a kapcsolódási kérést.</string> + <string name="sender_may_have_deleted_the_connection_request">A kérés küldője törölte a kapcsolódási kérést.</string> <string name="wrong_passphrase">Érvénytelen adatbázis-jelmondat</string> <string name="your_SMP_servers">Saját SMP-kiszolgálók</string> <string name="send_receipts_disabled_alert_title">A kézbesítési jelentések le vannak tiltva</string> @@ -1106,10 +1106,10 @@ <string name="chat_preferences_yes">igen</string> <string name="voice_message">Hangüzenet</string> <string name="settings_section_title_use_from_desktop">Társítás számítógéppel</string> - <string name="settings_section_title_you">PROFIL</string> + <string name="settings_section_title_you">Profil</string> <string name="network_proxy_port">%d-s port</string> <string name="to_connect_via_link_title">Kapcsolódás egy hivatkozáson keresztül</string> - <string name="share_address">Cím megosztása</string> + <string name="share_address">Cím megosztása…</string> <string name="smp_servers_scan_qr">Kiszolgáló QR-kódjának beolvasása</string> <string name="stop_chat_confirmation">Megállítás</string> <string name="stop_sharing_address">Megállítja a címmegosztást?</string> @@ -1278,7 +1278,7 @@ <string name="receiving_files_not_yet_supported">fájlok fogadása egyelőre még nem támogatott</string> <string name="save_group_profile">Csoportprofil mentése</string> <string name="network_options_reset_to_defaults">Visszaállítás alapértelmezettre</string> - <string name="connection_error_auth_desc">Hacsak a partnere nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt egyszer, lehet hogy ez egy hiba – jelentse a problémát.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcsolattartási hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e.</string> + <string name="connection_error_auth_desc">A partnere eltávolította ezt a hivatkozást, vagy egy egyszer használható meghívó volt, amit már felhasználtak.\nA kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy új hivatkozást.</string> <string name="video_call_no_encryption">videóhívás (végpontok között NEM titkosított)</string> <string name="smp_servers_use_server_for_new_conn">Használat új kapcsolatokhoz</string> <string name="periodic_notifications_desc">Az új üzeneteket az alkalmazás időszakosan lekéri – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ leküldéses értesítéseket – az eszközről származó adatok nem lesznek elküldve a kiszolgálóknak.</string> @@ -1370,7 +1370,7 @@ <string name="network_disable_socks">Közvetlen internetkapcsolat használata?</string> <string name="you_will_still_receive_calls_and_ntfs">Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak.</string> <string name="group_main_profile_sent">A fő csevegési profilja el lesz küldve a csoporttagok számára</string> - <string name="you_can_enable_delivery_receipts_later_alert">Később engedélyezheti őket az „Adatvédelem és biztonság” menüben.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Később is engedélyezheti őket az „Adatvédelem” menüben.</string> <string name="to_reveal_profile_enter_password">Rejtett profilja felfedéséhez adja meg a teljes jelszót a keresőmezőben, a „Csevegési profilok” menüben.</string> <string name="upgrade_and_open_chat">Fejlesztés és a csevegés megnyitása</string> <string name="you_need_to_allow_to_send_voice">Engedélyeznie kell a hangüzenetek küldését a partnere számára, hogy hangüzeneteket küldhessenek egymásnak.</string> @@ -1457,7 +1457,7 @@ <string name="receipts_section_groups">Kis csoportok (legfeljebb 20 tag)</string> <string name="connection_you_accepted_will_be_cancelled">Az Ön által elfogadott kapcsolat vissza lesz vonva!</string> <string name="send_live_message_desc">É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</string> - <string name="settings_section_title_delivery_receipts">A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI</string> + <string name="settings_section_title_delivery_receipts">A kézbesítési jelentéseket a következő címre kell küldeni</string> <string name="alert_text_msg_bad_id">A következő üzenet azonosítója érvénytelen (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő.</string> <string name="this_device_name_shared_with_mobile">Az eszköz neve meg lesz osztva a társított hordozható eszközön használt alkalmazással.</string> <string name="v4_4_live_messages_desc">A címzettek a beírás közben látják a szövegváltozásokat.</string> @@ -1471,13 +1471,13 @@ <string name="sync_connection_force_desc">A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">Ez a művelet nem vonható vissza – profiljai, partnerei, üzenetei és fájljai véglegesen törölve lesznek.</string> <string name="info_row_updated_at">Bejegyzés frissítve</string> - <string name="read_more_in_user_guide_with_link"><![CDATA[További információ a <font color="#0088ff">Használati útmutatóban</font> olvasható.]]></string> + <string name="read_more_in_user_guide_with_link"><![CDATA[További információkat a <font color="#0088ff">használati útmutatóban</font> talál.]]></string> <string name="settings_is_storing_in_clear_text">A jelmondat a beállításokban egyszerű szövegként van tárolva.</string> <string name="terminal_always_visible">Konzol megjelenítése új ablakban</string> <string name="alert_text_msg_bad_hash">Az előző üzenet kivonata különbözik.</string> <string name="receipts_section_description">Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak</string> <string name="loading_remote_file_desc">Várjon, amíg a fájl betöltődik a társított hordozható eszközről</string> - <string name="read_more_in_github_with_link"><![CDATA[További információ a <font color="#0088ff">GitHub-tárolónkban</font>.]]></string> + <string name="read_more_in_github_with_link"><![CDATA[További információkat a <font color="#0088ff">GitHub-tárolónkban</font> talál.]]></string> <string name="error_showing_content">Hiba történt a tartalom megjelenítésekor</string> <string name="error_showing_message">Hiba történt az üzenet megjelenítésekor</string> <string name="you_can_make_address_visible_via_settings">Láthatóvá teheti a SimpleXbeli partnerei számára a beállításokban.</string> @@ -1607,7 +1607,7 @@ <string name="migrate_to_device_download_failed">Sikertelen letöltés</string> <string name="migrate_to_device_downloading_archive">Archívum letöltése</string> <string name="migrate_to_device_downloading_details">Letöltési hivatkozás részletei</string> - <string name="v5_6_quantum_resistant_encryption_descr">Engedélyezés a közvetlen csevegésekben (BÉTA)!</string> + <string name="v5_6_quantum_resistant_encryption_descr">Engedélyezés a közvetlen csevegésekben (béta)!</string> <string name="migrate_to_device_enter_passphrase">Adja meg a jelmondatot</string> <string name="migrate_from_device_error_saving_settings">Hiba történt a beállítások mentésekor</string> <string name="migrate_to_device_error_downloading_archive">Hiba történt az archívum letöltésekor</string> @@ -1747,11 +1747,11 @@ <string name="network_smp_proxy_fallback_allow_description">Közvetlen üzenetküldés, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string> <string name="private_routing_explanation">Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez.</string> <string name="update_network_smp_proxy_fallback_question">Üzenet-útválasztási tartalék</string> - <string name="settings_section_title_private_message_routing">PRIVÁT ÜZENET-ÚTVÁLASZTÁS</string> + <string name="settings_section_title_private_message_routing">Privát üzenet-útválasztás</string> <string name="network_smp_proxy_mode_unprotected_description">Privát útválasztás használata az ismeretlen kiszolgálókkal, ha az IP-cím nem védett.</string> <string name="network_smp_proxy_fallback_prohibit_description">NE küldjön üzeneteket közvetlenül, még akkor sem, ha a saját kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást.</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára.</string> - <string name="settings_section_title_files">FÁJLOK</string> + <string name="settings_section_title_files">Fájlok</string> <string name="protect_ip_address">IP-cím védelme</string> <string name="app_will_ask_to_confirm_unknown_file_servers">Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van).</string> <string name="file_not_approved_title">Ismeretlen kiszolgálók!</string> @@ -1798,10 +1798,10 @@ <string name="v5_8_message_delivery_descr">Csökkentett akkumulátor-használattal.</string> <string name="error_initializing_web_view">Hiba történt a WebView előkészítésekor. Frissítse rendszerét az új verzióra. Lépjen kapcsolatba a fejlesztőkkel.\nHiba: %s</string> <string name="chat_theme_reset_to_user_theme">Felhasználó által létrehozott téma visszaállítása</string> - <string name="message_queue_info">Üzenet várólista információi</string> + <string name="message_queue_info">Üzenet várólista-információi</string> <string name="message_queue_info_none">nincs</string> <string name="info_row_debug_delivery">Kézbesítési hibák felderítése</string> - <string name="message_queue_info_server_info">a kiszolgáló várólista információi: %1$s\n\nutoljára fogadott üzenet: %2$s</string> + <string name="message_queue_info_server_info">kiszolgáló várólista-információi: %1$s\n\nutoljára fogadott üzenet: %2$s</string> <string name="file_error_auth">Érvénytelen kulcs vagy ismeretlen fájltöredékcím – valószínűleg a fájl törlődött.</string> <string name="temporary_file_error">Ideiglenes fájlhiba</string> <string name="info_row_message_status">Üzenet állapota</string> @@ -2019,7 +2019,7 @@ <string name="delete_messages_cannot_be_undone_warning">Az üzenetek törölve lesznek – ez a művelet nem vonható vissza!</string> <string name="migrate_from_device_remove_archive_question">Eltávolítja az archívumot?</string> <string name="migrate_from_device_uploaded_archive_will_be_removed">A feltöltött adatbázis-archívum véglegesen el lesz távolítva a kiszolgálókról.</string> - <string name="settings_section_title_chat_database">CSEVEGÉSI ADATBÁZIS</string> + <string name="settings_section_title_chat_database">Csevegési adatbázis</string> <string name="new_chat_share_profile">Profil megosztása</string> <string name="system_mode_toast">Rendszerbeállítások használata</string> <string name="select_chat_profile">Csevegési profil kiválasztása</string> @@ -2058,7 +2058,7 @@ <string name="network_session_mode_session_description">Minden alkalommal, amikor elindítja az alkalmazást, új SOCKS-hitelesítési adatok lesznek használva.</string> <string name="network_session_mode_session">Alkalmazás munkamenete</string> <string name="network_session_mode_server_description">Az összes kiszolgálóhoz új, SOCKS-hitelesítési adatok lesznek használva.</string> - <string name="call_desktop_permission_denied_chrome">Kattintson a címmező melletti információ gombra a mikrofon használatának engedélyezéséhez.</string> + <string name="call_desktop_permission_denied_chrome">Kattintson a címmező melletti információgombra a mikrofon használatának engedélyezéséhez.</string> <string name="call_desktop_permission_denied_safari">Nyissa meg a Safari / Beállítások / Weboldalak / Mikrofon menüt, majd válassza a helyi kiszolgálók engedélyezése beállítást.</string> <string name="call_desktop_permission_denied_title">Hívások kezdeményezéséhez engedélyezze a mikrofon használatát. Fejezze be a hívást, és próbálja meg a hívást újra.</string> <string name="v6_1_better_calls">Továbbfejlesztett hívásélmény</string> @@ -2139,11 +2139,11 @@ <string name="operator_conditions_of_use">Használati feltételek</string> <string name="operator_in_order_to_use_accept_conditions"><![CDATA[A(z) <b>%s</b> kiszolgálóinak használatához fogadja el a használati feltételeket.]]></string> <string name="operator_use_for_messages">Használat az üzenetekhez</string> - <string name="operator_use_for_messages_receiving">A fogadáshoz</string> - <string name="operator_use_for_messages_private_routing">A privát útválasztáshoz</string> + <string name="operator_use_for_messages_receiving">Üzenetek fogadásához</string> + <string name="operator_use_for_messages_private_routing">Privát útválasztáshoz</string> <string name="operator_added_message_servers">Hozzáadott üzenetkiszolgálók</string> <string name="operator_use_for_files">Használat a fájlokhoz</string> - <string name="operator_use_for_sending">A küldéshez</string> + <string name="operator_use_for_sending">Üzenetek küldéséhez</string> <string name="operator_added_xftp_servers">Hozzáadott fájl- és médiakiszolgálók</string> <string name="operator_open_conditions">Feltételek megnyitása</string> <string name="operator_open_changes">Módosítások megtekintése</string> @@ -2167,7 +2167,7 @@ <string name="chat_archive">Vagy archívumfájl importálása</string> <string name="remote_hosts_section">Távoli hordozható eszközök</string> <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi eszközök</b>: engedélyezze az automatikus indítást a rendszerbeállításokban, hogy az értesítések működjenek.]]></string> - <string name="maximum_message_size_reached_forwarding">A küldéshez másolhatja és csökkentheti az üzenet méretét.</string> + <string name="maximum_message_size_reached_forwarding">Másolhatja és csökkentheti az üzenet méretét a küldéshez.</string> <string name="add_your_team_members_to_conversations">Adja hozzá a munkatársait a beszélgetésekhez.</string> <string name="business_address">Üzleti cím</string> <string name="all_message_and_files_e2e_encrypted"><![CDATA[Az összes üzenet és fájl <b>végpontok közötti titkosítással</b>, a közvetlen üzenetek továbbá kvantumbiztos titkosítással is rendelkeznek.]]></string> @@ -2476,7 +2476,7 @@ <string name="share_group_profile_via_link_alert_text">A hivatkozás rövid lesz és a csoportprofil meg lesz osztva a hivatkozáson keresztül.</string> <string name="share_old_address_alert_button">Régi cím megosztása</string> <string name="share_old_link_alert_button">Régi (hosszú) hivatkozás megosztása</string> - <string name="settings_section_title_contact_requests_from_groups">PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL</string> + <string name="settings_section_title_contact_requests_from_groups">Partneri kapcsolatkérések a csoportokból</string> <string name="member_is_deleted_cant_accept_request">A tag törölve lett – nem lehet elfogadni a kérést</string> <string name="rcv_direct_event_group_inv_link_received">a(z) %1$s nevű csoportból partneri kapcsolatot kért</string> <string name="this_setting_is_for_your_current_profile">Ez a beállítás a jelenlegi profiljára vonatkozik</string> @@ -2519,7 +2519,7 @@ <string name="placeholder_search_voice_messages">Hangüzenetek keresése</string> <string name="content_filter_videos">Videók</string> <string name="content_filter_voice_messages">Hangüzenetek</string> - <string name="info_row_connection_failed">NEM SIKERÜLT LÉTREHOZNI A KAPCSOLATOT</string> + <string name="info_row_connection_failed">Nem sikerült létrehozni a kapcsolatot</string> <string name="member_info_member_failed">sikertelen</string> <string name="down_migration_warning_chat_relays">Ha csatornákat hozott létre vagy csak csatlakozott hozzájuk, akkor azok véglegesen le fognak állni.</string> <string name="relay_status_active">aktív</string> @@ -2535,7 +2535,6 @@ <string name="relay_conn_status_connecting">kapcsolódás</string> <string name="create_channel_title">Nyilvános csatorna létrehozása</string> <string name="create_channel_button">Nyilvános csatorna létrehozása</string> - <string name="create_channel_beta_button">Nyilvános csatorna létrehozása (BÉTA)</string> <string name="creating_channel">Csatorna létrehozása</string> <string name="relay_conn_status_deleted">törölve</string> <string name="relay_conn_status_failed">sikertelen</string> @@ -2545,8 +2544,8 @@ <string name="relay_status_invited">meghíva</string> <string name="connect_plan_open_channel">Csatorna megnyitása</string> <string name="connect_plan_open_new_channel">Új csatorna megnyitása</string> - <string name="member_info_section_title_owner">TULAJDONOS</string> - <string name="channel_members_section_owners">Tulajdonosok</string> + <string name="member_info_section_title_owner">Tulajdonos</string> + <string name="channel_members_section_owners">Tulajdonosok és közreműködők</string> <string name="button_leave_channel">Csatorna elhagyása</string> <string name="leave_channel_question">Elhagyja a csatornát?</string> <string name="relay_test_step_verify">Ellenőrzés</string> @@ -2555,7 +2554,7 @@ <string name="channel_member_you">Ön</string> <string name="chat_banner_your_channel">Saját csatorna</string> <string name="connect_plan_this_is_your_link_for_channel">Saját csatorna</string> - <string name="member_info_section_title_subscriber">FELIRATKOZÓ</string> + <string name="member_info_section_title_subscriber">Feliratkozó</string> <string name="channel_members_title_subscribers">Feliratkozók</string> <string name="channel_subscriber_count_singular">%1$d feliratkozó</string> <string name="channel_subscriber_count_plural">%1$d feliratkozó</string> @@ -2607,7 +2606,7 @@ <string name="relay_bar_active">%1$d/%2$d átjátszó aktív</string> <string name="relay_bar_connected_with_errors">%1$d/%2$d átjátszó kapcsolódott, %3$d hiba</string> <string name="relay_bar_connected">%1$d/%2$d átjátszó kapcsolódott</string> - <string name="member_info_section_title_relay">ÁTJÁTSZÓ</string> + <string name="member_info_section_title_relay">Átjátszó</string> <string name="info_row_relay_link">Átjátszóhivatkozás</string> <string name="info_row_relay_address">Átjátszó címe</string> <string name="via_relay_hostname">a következőn keresztül: %1$s</string> @@ -2671,7 +2670,6 @@ <string name="share_channel">Csatorna megosztása…</string> <string name="share_via_chat">Megosztás egy csevegésen keresztül</string> <string name="owner_verification_failed">⚠️ Nem sikerült ellenőrizni az aláírást: %s.</string> - <string name="chat_link_signed">(aláírva)</string> <string name="tap_to_open">Koppintson ide a megnyitáshoz</string> <string name="link_previews_alert_disable">Letiltás</string> <string name="link_previews_alert_enable">Engedélyezés</string> @@ -2766,7 +2764,7 @@ <string name="one_hand_ui_bottom_bar">Alsó sáv</string> <string name="link_previews_alert_desc_socks">A hivatkozások előnézetét SOCKS proxyn keresztül kéri le a kliens. A DNS-lekérdezés viszont továbbra is történhet helyi szinten, a saját DNS-kiszolgálón keresztül.</string> <string name="one_hand_ui_top_bar">Felső sáv</string> - <string name="cancel_channel_alert_msg">Az új %1$s nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódik.\nHa visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja.</string> + <string name="cancel_channel_alert_msg">Az új %1$s nevű csatornája %3$d átjátszóból %2$d átjátszóhoz kapcsolódott.\nHa visszavonja, akkor a csatorna törlődni fog – de később újra létrehozhatja.</string> <string name="button_cancel_and_delete_channel">Visszavonás és a csatorna törlése</string> <string name="add_button">Hozzáadás</string> <string name="add_relay_button">Átjátszó hozzáadása</string> @@ -2785,14 +2783,14 @@ <string name="last_active_relay_warning">Ez az utolsó aktív átjátszó. Ha eltávolítja, akkor azzal megakadályozza az üzenetek eljuttatását a feliratkozóknak.</string> <string name="close_behavior_dialog_close">Alkalmazás bezárása</string> <string name="close_behavior_dialog_text">Ha a bezárás mellett dönt, az üzenetek nem fognak megérkezni.\nEzt később a „Megjelenés” beállításaiban módosíthatja.</string> - <string name="appearance_minimize_to_tray_desc">Hagyja a SimpleX-et a háttérben futni az üzenetek fogadásához.</string> + <string name="appearance_minimize_to_tray_desc">Az üzenetek fogadásához a háttérben fut</string> <string name="tray_quit">Kilépés a SimpleXből</string> <string name="tray_show">SimpleX megjelenítése</string> <string name="tray_tooltip">SimpleX</string> <string name="tray_tooltip_unread">SimpleX – %d olvasatlan üzenet</string> <string name="close_behavior_dialog_minimize">Kicsinyítés az értesítési területre</string> <string name="close_behavior_dialog_title">Biztosan kicsinyíteni szeretné az értesítési területre?</string> - <string name="appearance_minimize_to_tray">Kicsinyítés az értesítési területre az ablak bezárásakor</string> + <string name="appearance_minimize_to_tray">Kicsinyítés az értesítési területre</string> <string name="error_deleting_message">Hiba történt az üzenet törlésekor</string> <string name="from_history">Az előzményekből</string> <string name="member_info_status">Állapot</string> @@ -2800,4 +2798,105 @@ <string name="member_info_relay_status_rejected_by_operator">az átjátszó üzemeltetője elutasította</string> <string name="another_instance_title">Az alkalmazás már fut</string> <string name="another_instance_not_responding">Lehet, hogy egy másik alkalmazáspéldány fut, vagy nem zárult be megfelelően. Így is elindítja?</string> + <string name="unsupported_channel_name">Nem támogatott csatornanév</string> + <string name="unsupported_contact_name">Nem támogatott partnernév</string> + <string name="channel_name_requires_newer_app_version">A csatorna nevén keresztüli kapcsolódáshoz újabb alkalmazásverzió szükséges.</string> + <string name="contact_name_requires_newer_app_version">A partner nevén keresztüli kapcsolódáshoz újabb alkalmazásverzió szükséges.</string> + <string name="please_upgrade_the_app">Frissítse az alkalmazást.</string> + <string name="app_update_required">Alkalmazásfrissítés szükséges</string> + <string name="group_link_requires_newer_version">Ehhez a csoporthoz az alkalmazás újabb verziója szükséges. A csatlakozáshoz frissítse az alkalmazást.</string> + <string name="settings_section_title_about">Névjegy</string> + <string name="settings_section_title_contact">Kapcsolat</string> + <string name="settings_section_title_support_project">A projekt támogatása</string> + <string name="chat_data">Csevegési adatok</string> + <string name="help_and_support">Súgó és támogatás</string> + <string name="more_privacy">További adatvédelem</string> + <string name="advanced_settings">Speciális beállítások</string> + <string name="group_member_role_observer_channel">feliratkozó</string> + <string name="group_member_role_member_channel">közreműködő</string> + <string name="channel_webpage">Csatorna weboldala</string> + <string name="group_webpage">Csoport weboldala</string> + <string name="advanced_options">Speciális beállítások</string> + <string name="web_page_url_placeholder">https://</string> + <string name="allow_anyone_to_embed">Beágyazás engedélyezése bárki számára</string> + <string name="enter_webpage_url">Adja meg az oldal webcímét</string> + <string name="webpage_url_footer">Meg fog jelenni a feliratkozóknak, és az előnézet betöltésének engedélyezésére szolgál.</string> + <string name="webpage_code">Weboldalba ágyazható kód</string> + <string name="webpage_code_footer">Adja hozzá ezt a kódot a weboldalához. Meg fogja jeleníteni a csatornája / csoportja előnézetét.</string> + <string name="copy_code">Kód másolása</string> + <string name="webpage_info">Hozzon létre egy weboldalt a csatorna előnézetének megjelenítéséhez a látogatók számára, mielőtt feliratkoznának. Üzemeltesse saját maga, vagy használjon tetszőleges statikus tárhelyet.</string> + <string name="relays_no_web_support">Az Ön által használt csevegési átjátszók nem támogatják a weboldalakat.</string> + <string name="embed_any_webpage_can_show">Bármelyik weboldal megjelenítheti az előnézetet.</string> + <string name="embed_only_your_page">Csak az Ön fenti oldala jelenítheti meg az előnézetet.</string> + <string name="member_role_will_be_changed_with_notification_channel">A szerepkör a következőre fog módosulni: %s. A csatorna összes feliratkozója értesítést fog kapni.</string> + <string name="channel_owner_count_singular">%1$d tulajdonos</string> + <string name="channel_owner_count_plural">%1$d tulajdonos</string> + <string name="channel_owners_contributors_count">%1$d tulajdonos és közreműködő</string> + <string name="relay_status_acknowledged_roster">visszaigazolt névsor</string> + <string name="badge_supports_simplex">%s támogatja a SimpleX Chatet.</string> + <string name="badge_supported_simplex">%1$s támogatta a SimpleX Chatet. A kitűző lejárt ekkor: %2$s.</string> + <string name="badge_support_from_v7">A SimpleXet az alkalmazás v7-es verziójától kezdve támogathatja.</string> + <string name="badge_invested">%s befektetett a SimpleX Chat közösségi finanszírozásába.</string> + <string name="badge_unverified_title">Ellenőrizetlen kitűző</string> + <string name="badge_unverified_desc">Nem sikerült ellenőrizni ezt a kitűzőt, és lehet, hogy nem eredeti.</string> + <string name="badge_unknown_key_title">Nem lehetett ellenőrizni a kitűzőt</string> + <string name="badge_unknown_key_desc">A kitűző egy olyan kulccsal van aláírva, amelyet az alkalmazás ezen verziója nem ismer fel. Frissítse az alkalmazást a kitűző ellenőrzéséhez.</string> + <string name="no_names_servers_enabled">Nincsenek kiszolgálók a nevek feloldásához.</string> + <string name="simplex_name_error">Hibás SimpleX-név</string> + <string name="simplex_name_no_servers_desc">Egyik saját kiszolgálója sincs beállítva a SimpleX-nevek feloldásához. Állítsa be a kiszolgálókat, vagy használjon egy kapcsolattartási hivatkozást.</string> + <string name="simplex_name_server_no_resolver_desc">A(z) %1$s kiszolgáló nem támogatja a névfeloldást. Állítsa be a kiszolgálókat, vagy használjon egy kapcsolattartási hivatkozást.</string> + <string name="simplex_name_not_found">Nem található a név</string> + <string name="simplex_name_not_found_desc">Ez a SimpleX-név nincs regisztrálva. Ellenőrizze a nevet.</string> + <string name="simplex_name_resolver_error_desc">Feloldási hiba: %1$s</string> + <string name="simplex_name_no_valid_link">Nincs érvényes hivatkozás</string> + <string name="simplex_name_no_valid_link_desc">A(z) %1$s SimpleX-név regisztrálva van, de nem rendelkezik érvényes hivatkozással.</string> + <string name="simplex_name_unconfirmed">Megerősítetlen név</string> + <string name="simplex_name_unconfirmed_desc">A(z) %1$s SimpleX-név regisztrálva van, de nincs hozzáadva a profilhoz. Adja hozzá a címéhez vagy a csatornaprofiljához, amennyiben Ön a tulajdonosa.</string> + <string name="verify_simplex_name_action">Név ellenőrzése</string> + <string name="verify_simplex_names">SimpleX-nevek ellenőrzése</string> + <string name="simplex_name_not_verified">Nincs ellenőrizve a SimpleX-név</string> + <string name="simplex_name">SimpleX-név</string> + <string name="your_simplex_name">Saját SimpleX-név</string> + <string name="set_simplex_name">SimpleX-név beállítása</string> + <string name="error_saving_simplex_name">Hiba történt a név mentésekor</string> + <string name="simplex_name_owner_no_channel_link">A(z) %1$s SimpleX-név csatornahivatkozás nélkül lett regisztrálva. A regisztrációs oldalon adjon hozzá a névhez egy csatornahivatkozást.</string> + <string name="simplex_name_owner_no_address">A(z) %1$s SimpleX-név SimpleX-cím nélkül lett regisztrálva. A regisztrációs oldalon adja hozzá a névhez a saját SimpleX-címét.</string> + <string name="set_user_simplex_name_footer">Tegye lehetővé mások számára a kapcsolódást a saját SimpleX-címével regisztrált néven keresztül.</string> + <string name="set_channel_simplex_name_footer">Tegye lehetővé mások számára a csatlakozást az ezzel a csatornahivatkozással regisztrált néven keresztül.</string> + <string name="operator_use_for_names">Nevek feloldásához</string> + <string name="connect_plan_connect_to_name">Kapcsolódás hozzá: %s</string> + <string name="connect_plan_join_name">Csatlakozás a(z) %s nevű csatornához</string> + <string name="sign_message">Üzenet aláírása</string> + <string name="sign_message_desc">Az aláírás igazolja, hogy Ön írta ezt az üzenetet, és azt később már nem lehet letagadni.</string> + <string name="info_row_signed">Aláírva</string> + <string name="info_row_signed_verified">Aláírva és ellenőrizve</string> + <string name="sign_messages">Üzenetek aláírása</string> + <string name="require_message_signatures">Üzenetek aláírásának kötelezővé tétele.</string> + <string name="do_not_require_message_signatures">Üzenetek aláírásának mellőzése.</string> + <string name="message_signatures_are_required">Kötelező aláírni az üzeneteket.</string> + <string name="message_signatures_are_not_required">Nem kötelező aláírni az üzeneteket.</string> + <string name="show_signature">Aláírás megjelenítése</string> + <string name="show_encryption">Titkosítás megjelenítése</string> + <string name="signature_missing_alert_title">Hiányzik az aláírás</string> + <string name="signature_missing_alert_desc">A csatorna megköveteli az üzenet aláírását, de az hiányzik.</string> + <string name="channel_simplex_name">Csatorna SimpleX-neve</string> + <string name="get_simplex_name_beta">SimpleX-név beszerzése (béta)</string> + <string name="register_test_name">Egy név regisztrálása tesztelési céllal</string> + <string name="remove_name">Név eltávolítása</string> + <string name="save_simplex_name_question">Menti a SimpleX-nevet?</string> + <string name="to_verify_channel_member_key">A kulcsok ellenőrzéséhez ezzel a feliratkozóval hasonlítsa össze (vagy olvassa be) az eszközökön található kódot.</string> + <string name="error_sharing_address">Hiba történt a cím megosztásakor</string> + <string name="profile_description__field">Leírás</string> + <string name="add_description">Leírás hozzáadása</string> + <string name="edit_description">Leírás szerkesztése</string> + <string name="enter_description_optional">Adja meg a leírást (nem kötelező)</string> + <string name="v7_0_channels_contributors">Közreműködők hozzáadása.</string> + <string name="v7_0_channels">Továbbfejlesztett csatornák 📢</string> + <string name="v7_0_channels_previews">Előnézet készítése a weboldalakhoz.</string> + <string name="v7_0_channels_wider_messages">Könnyebb olvashatóság.</string> + <string name="v7_0_channels_relays">Saját átjátszók kezelése.</string> + <string name="v7_0_simplex_names_descr">Nyilvános nevek a csatornákhoz vagy az üzleti profilokhoz.</string> + <string name="v7_0_simplex_names">Nyilvános SimpleX-nevek (béta)</string> + <string name="info_row_file_servers">Fájlkiszolgálók</string> + <string name="share_text_file_servers">Fájlkiszolgálók: %s</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified.svg new file mode 100644 index 0000000000..96e0c66254 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m437.5-432-73-76.5q-9-9.5-21.5-9.75T321-510q-10 10-10 22.5t10 22l96.5 95.5q8.5 9 20.05 9 11.54 0 20.45-9l181.4-180.4q8.6-8.6 8.6-21.1 0-12.5-9.5-21.5-9-7.5-20.75-7T598-591.5L437.5-432Zm-103 343L273-194l-123.5-25q-10.48-2.07-17.74-11.36-7.26-9.29-5.26-20.14l14-119.14-78-91.36q-7.5-7.5-7.5-19.05t7.5-19.45l78-90.15-14-118.85q-2-10.88 5.25-20.19t17.75-11.81L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.33-4.5 20.92-1.25Q619.5-882 625.5-872.5L688-765l122.5 24.5q10.5 2.5 17.75 11.81t5.25 20.19l-14 118.85L898-499.5q7 7.9 7 19.45 0 11.55-7.14 19.15l-78.36 91.32 14 119.08q2 10.85-5.26 20.14-7.26 9.29-17.74 11.36L688-194 625.5-88q-6 10-16.58 13.25Q598.33-71.5 588-76l-108-51-108 51q-10.33 4.5-20.92.75Q340.5-79 334.5-89Zm39.5-52.5L480-186l109 44.5 67-99 115.95-30.18-12.09-117.75L840.5-480l-80.64-93.58 12.09-117.76L656-719.5l-69-99L480-774l-109-44.5-67 99-115.95 28.16 12.09 117.76L119.5-480l80.64 91.57-12.09 119.77L304-240.5l70 99ZM480-480Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_filled.svg new file mode 100644 index 0000000000..e46361c721 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_filled.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m437.5-432-73-76.5q-9-9.5-21.5-9.75T321-510q-10 10-10 22.5t10 22l96.5 95.5q8.5 9 20 9t20.5-9l181.5-180.5q8.5-8.5 8.5-21t-9.5-21.5q-9-7.5-20.75-7T598-591.5L437.5-432Zm-103 343L273-194l-123.5-25q-10.5-2-17.75-11.25t-5.25-20.25l14-119-78-91.5q-7.5-7.5-7.5-19t7.5-19.5l78-90-14-119q-2-11 5.25-20.25t17.75-11.75L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.5-4.5 21-1.25t16.5 12.75L688-765l122.5 24.5q10.5 2.5 17.75 11.75t5.25 20.25l-14 119 78.5 90q7 8 7 19.5t-7 19l-78.5 91.5 14 119q2 11-5.25 20.25T810.5-219L688-194 625.5-88q-6 10-16.5 13.25T588-76l-108-51-108 51q-10.5 4.5-21 .75T334.5-89Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_missing.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_missing.svg new file mode 100644 index 0000000000..87397c3d36 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_missing.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M334.5-89L273-194l-123.5-25q-10.48-2.07-17.74-11.36-7.26-9.29-5.26-20.14l14-119.14-78-91.36q-7.5-7.5-7.5-19.05t7.5-19.45l78-90.15-14-118.85q-2-10.88 5.25-20.19t17.75-11.81L273-765l61.5-106.5q6-9.5 16.5-13.5t21 1l108 51 108-51q10.33-4.5 20.92-1.25Q619.5-882 625.5-872.5L688-765l122.5 24.5q10.5 2.5 17.75 11.81t5.25 20.19l-14 118.85L898-499.5q7 7.9 7 19.45 0 11.55-7.14 19.15l-78.36 91.32 14 119.08q2 10.85-5.26 20.14-7.26 9.29-17.74 11.36L688-194 625.5-88q-6 10-16.58 13.25Q598.33-71.5 588-76l-108-51-108 51q-10.33 4.5-20.92.75Q340.5-79 334.5-89Zm39.5-52.5L480-186l109 44.5 67-99 115.95-30.18-12.09-117.75L840.5-480l-80.64-93.58 12.09-117.76L656-719.5l-69-99L480-774l-109-44.5-67 99-115.95 28.16 12.09 117.76L119.5-480l80.64 91.57-12.09 119.77L304-240.5l70 99Z"/><path d="M557.782 -600.208L359.792 -402.218C348.076 -390.503 348.076 -371.508 359.792 -359.792C371.508 -348.076 390.503 -348.076 402.218 -359.792L600.208 -557.782C611.924 -569.497 611.924 -588.492 600.208 -600.208C588.492 -611.924 569.497 -611.924 557.782 -600.208ZM359.792 -557.782L557.782 -359.792C569.497 -348.076 588.492 -348.076 600.208 -359.792C611.924 -371.508 611.924 -390.503 600.208 -402.218L402.218 -600.208C390.503 -611.924 371.508 -611.924 359.792 -600.208C348.076 -588.492 348.076 -569.497 359.792 -557.782Z"/></svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml index 60ed7db384..bd3f6e2b22 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="group_info_section_title_num_members">%1$s ANGGOTA</string> + <string name="group_info_section_title_num_members">%1$s anggota</string> <string name="address_section_title">Alamat</string> <string name="moderated_items_description">%1$d pesan dimoderasi oleh %2$s</string> <string name="send_disappearing_message_1_minute">1 menit</string> @@ -44,7 +44,7 @@ <string name="smp_servers_add_to_another_device">Tambahkan ke perangkat lain</string> <string name="turn_off_battery_optimization_button">Boleh</string> <string name="network_smp_proxy_mode_always">Selalu</string> - <string name="settings_section_title_app">APLIKASI</string> + <string name="settings_section_title_app">Aplikasi</string> <string name="appearance_settings">Tampilan</string> <string name="about_simplex_chat">Tentang SimpleX Chat</string> <string name="accept">Terima</string> @@ -293,7 +293,7 @@ <string name="images_limit_title">Terlalu banyak gambar!</string> <string name="info_view_search_button">cari</string> <string name="info_view_call_button">panggilan</string> - <string name="settings_section_title_settings">PENGATURAN</string> + <string name="settings_section_title_settings">Pengaturan</string> <string name="for_everybody">Untuk semua orang</string> <string name="stop_file__action">Hentikan berkas</string> <string name="revoke_file__action">Cabut berkas</string> @@ -643,9 +643,9 @@ <string name="self_destruct_passcode">Kode sandi hapus otomatis</string> <string name="enable_self_destruct">Aktifkan hapus otomatis</string> <string name="set_passcode">Pasang kode sandi</string> - <string name="settings_section_title_help">BANTUAN</string> - <string name="settings_section_title_support">DUKUNG SIMPLEX CHAT</string> - <string name="settings_section_title_calls">PANGGILAN</string> + <string name="settings_section_title_help">Bantuan</string> + <string name="settings_section_title_support">Dukung SimpleX Chat</string> + <string name="settings_section_title_calls">Panggilan</string> <string name="restart_the_app_to_create_a_new_chat_profile">Mulai ulang aplikasi untuk buat profil obrolan baru.</string> <string name="delete_messages">Hapus pesan</string> <string name="rcv_group_event_member_left">keluar</string> @@ -729,9 +729,9 @@ <string name="privacy_media_blur_radius_medium">Sedang</string> <string name="privacy_media_blur_radius">Buram media</string> <string name="privacy_media_blur_radius_strong">Kuat</string> - <string name="settings_section_title_you">ANDA</string> + <string name="settings_section_title_you">Anda</string> <string name="privacy_media_blur_radius_soft">Lunak</string> - <string name="settings_section_title_chat_database">BASIS DATA OBROLAN</string> + <string name="settings_section_title_chat_database">Basis data obrolan</string> <string name="set_password_to_export">Setel frasa sandi untuk diekspor</string> <string name="open_database_folder">Buka folder basis data</string> <string name="rcv_group_event_user_deleted">menghapus anda</string> @@ -951,7 +951,7 @@ <string name="app_check_for_updates_notice_title">Periksa pembaruan</string> <string name="please_try_later">Silakan coba lagi nanti.</string> <string name="private_routing_error">Kesalahan perutean pribadi</string> - <string name="add_address_to_your_profile">Tambah alamat ke profil Anda, sehingga kontak dapat membagikannya dengan orang lain. Pembaruan profil akan dikirim ke kontak Anda.</string> + <string name="add_address_to_your_profile">Tambahkan alamat ke profil Anda, sehingga kontak SimpleX dapat membagikannya dengan orang lain. Pembaruan profil akan dikirim ke kontak SimpleX Anda.</string> <string name="keychain_allows_to_receive_ntfs">Android Keystore digunakan untuk simpan frasa sandi dengan aman setelah Anda memulai ulang aplikasi atau ubah frasa sandi - ini mungkin dapat menerima notifikasi.</string> <string name="allow_accepting_calls_from_lock_screen">Aktifkan panggilan dari layar kunci melalui Pengaturan.</string> <string name="alert_text_skipped_messages_it_can_happen_when">Hal ini dapat terjadi ketika:\n1. Pesan kedaluwarsa di klien pengirim setelah 2 hari atau di server setelah 30 hari.\n2. Dekripsi pesan gagal, karena Anda atau kontak Anda menggunakan cadangan basis data lama.\n3. Koneksi terganggu.</string> @@ -968,7 +968,7 @@ <string name="app_version_code">Build aplikasi: %s</string> <string name="core_version">Versi inti: v%s</string> <string name="network_smp_proxy_fallback_allow_protected">Ketika IP disembunyikan</string> - <string name="theme_colors_section_title">WARNA ANTARMUKA</string> + <string name="theme_colors_section_title">Warna antarmuka</string> <string name="update_network_smp_proxy_fallback_question">Fallback perutean pesan</string> <string name="update_network_smp_proxy_mode_question">Mode routing pesan</string> <string name="network_smp_proxy_mode_private_routing">Routing pribadi</string> @@ -1003,7 +1003,7 @@ <string name="your_ice_servers">Server ICE Anda</string> <string name="webrtc_ice_servers">Server ICE WebRTC</string> <string name="if_you_enter_self_destruct_code">Jika Anda memasukkan kode sandi hapus otomatis saat membuka aplikasi:</string> - <string name="settings_section_title_icon">IKON APLIKASI</string> + <string name="settings_section_title_icon">Ikon aplikasi</string> <string name="app_will_ask_to_confirm_unknown_file_servers">Aplikasi akan meminta untuk mengonfirmasi unduhan dari server berkas yang tidak dikenal (kecuali .onion atau saat proxy SOCKS diaktifkan).</string> <string name="message_reactions_prohibited_in_this_chat">Reaksi pesan dilarang dalam obrolan ini.</string> <string name="migrate_from_device_to_another_device">Pindah ke perangkat lain</string> @@ -1020,8 +1020,8 @@ <string name="icon_descr_call_missed">Panggilan tak terjawab</string> <string name="icon_descr_call_rejected">Panggilan ditolak</string> <string name="alert_text_msg_bad_id">ID pesan berikutnya salah (kurang atau sama dengan yang sebelumnya).\nHal ini dapat terjadi karena beberapa bug atau ketika koneksi terganggu.</string> - <string name="settings_section_title_themes">TEMA</string> - <string name="settings_section_title_delivery_receipts">KIRIM TANDA TERIMA KIRIMAN KE</string> + <string name="settings_section_title_themes">Tema</string> + <string name="settings_section_title_delivery_receipts">Kirim tanda terima kiriman ke</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Hal ini dapat terjadi ketika Anda atau koneksi Anda menggunakan cadangan basis data lama.</string> <string name="keychain_is_storing_securely">Android Keystore digunakan untuk menyimpan frasa sandi dengan aman - memungkinkan layanan notifikasi berfungsi.</string> <string name="remove_passphrase">Hapus</string> @@ -1134,9 +1134,9 @@ <string name="acknowledged">Dikenal</string> <string name="waiting_for_image">Menunggu gambar</string> <string name="waiting_for_video">Menunggu video</string> - <string name="settings_section_title_device">PERANGKAT</string> - <string name="settings_section_title_chats">OBROLAN</string> - <string name="settings_section_title_files">BERKAS</string> + <string name="settings_section_title_device">Perangkat</string> + <string name="settings_section_title_chats">Obrolan</string> + <string name="settings_section_title_files">Berkas</string> <string name="reset_all_hints">Reset semua petunjuk</string> <string name="error_adding_members">Gagal menambah anggota</string> <string name="error_joining_group">Gagal gabung ke grup</string> @@ -1239,7 +1239,7 @@ <string name="la_could_not_be_verified">Anda tidak dapat diverifikasi; silakan coba lagi.</string> <string name="smp_proxy_error_broker_version">Versi server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s.</string> <string name="proxy_destination_error_broker_version">Versi server tujuan %1$s tidak kompatibel dengan server penerusan %2$s.</string> - <string name="connection_error_auth">Kesalahan koneksi (AUTH)</string> + <string name="connection_error_auth">Kesalahan koneksi</string> <string name="smp_proxy_error_connecting">Gagal menghubungkan ke server penerusan %1$s. Coba lagi nanti.</string> <string name="smp_proxy_error_broker_host">Alamat server penerusan tidak kompatibel dengan pengaturan jaringan: %1$s.</string> <string name="smp_server_test_upload_file">Unggah berkas</string> @@ -1281,7 +1281,7 @@ <string name="unblock_for_all_question">Buka blokir anggota untuk semua?</string> <string name="unblock_for_all">Buka untuk semua</string> <string name="member_blocked_by_admin">Diblokir oleh admin</string> - <string name="member_info_section_title_member">ANGGOTA</string> + <string name="member_info_section_title_member">Anggota</string> <string name="remove_member_button">Hapus anggota</string> <string name="share_text_message_status">Status pesan: %s</string> <string name="share_text_file_status">Status berkas: %s</string> @@ -1326,7 +1326,7 @@ <string name="fix_connection_not_supported_by_contact">Perbaikan tidak didukung oleh kontak</string> <string name="info_row_chat">Obrolan</string> <string name="accept_conditions">Terima kondisi</string> - <string name="conn_stats_section_title_servers">SERVER</string> + <string name="conn_stats_section_title_servers">Server</string> <string name="create_group_button">Buat grup</string> <string name="group_full_name_field">Nama lengkap grup:</string> <string name="save_group_profile">Simpan profil grup</string> @@ -1401,7 +1401,7 @@ <string name="clear_chat_question">Hapus obrolan?</string> <string name="network_proxy_auth_mode_no_auth">Jangan gunakan kredensial dengan proxy.</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Atur <i>Gunakan host .onion</i> ke Tidak jika proxy SOCKS tidak mendukung.]]></string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Harap diperhatikan</b>: relay pesan dan berkas terhubung melalui proxy SOCKS. Panggilan dan pengiriman pratinjau tautan menggunakan koneksi langsung.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Harap diperhatikan</b>: relay pesan dan berkas terhubung melalui proxy SOCKS. Panggilan menggunakan koneksi langsung.]]></string> <string name="network_smp_proxy_fallback_prohibit_description">JANGAN mengirim pesan secara langsung, meskipun server Anda atau server tujuan tidak mendukung routing pribadi.</string> <string name="display_name_cannot_contain_whitespace">Nama tampilan tidak boleh terdapat spasi.</string> <string name="you_can_change_it_later">Frasa sandi acak disimpan dalam pengaturan sebagai teks biasa.\nAnda dapat mengubahnya nanti.</string> @@ -1466,7 +1466,7 @@ <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Terbaik untuk baterai</b>. Anda akan menerima notifikasi saat aplikasi sedang berjalan (TANPA layanan latar belakang).]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Baik untuk baterai</b>. Aplikasi memeriksa pesan setiap 10 menit. Anda mungkin melewatkan panggilan atau pesan penting.]]></string> <string name="settings_section_title_chat_theme">Tema obrolan</string> - <string name="chat_database_section">BASIS DATA OBROLAN</string> + <string name="chat_database_section">Basis data obrolan</string> <string name="set_password_to_export_desc">Basis data dienkripsi menggunakan frasa sandi acak. Harap ubah frasa sandi sebelum mengekspor.</string> <string name="chat_database_exported_title">Basis data obrolan diekspor</string> <string name="current_passphrase">Frasa sandi saat ini…</string> @@ -1685,7 +1685,7 @@ <string name="files_and_media_section">Berkas dan media</string> <string name="encrypt_database_question">Enkripsi basis data?</string> <string name="incompatible_database_version">Versi basis data tidak kompatibel</string> - <string name="section_title_for_console">UNTUK KONSOL</string> + <string name="section_title_for_console">Untuk konsol</string> <string name="connect_plan_group_already_exists">Grup sudah ada!</string> <string name="migrate_to_device_enter_passphrase">Masukkan frasa sandi</string> <string name="enable_automatic_deletion_question">Aktifkan hapus pesan otomatis?</string> @@ -1703,7 +1703,7 @@ <string name="migrate_from_device_error_verifying_passphrase">Gagal verifikasi frasa sandi:</string> <string name="servers_info_reconnect_server_error">Gagal hubungkan ulang server</string> <string name="servers_info_reconnect_servers_error">Gagal hubungkan ulang server</string> - <string name="settings_section_title_experimenta">EKSPERIMENTAL</string> + <string name="settings_section_title_experimenta">Eksperimental</string> <string name="export_database">Ekspor basis data</string> <string name="import_database">Impor basis data</string> <string name="error_stopping_chat">Gagal hentikan obrolan</string> @@ -1830,7 +1830,7 @@ <string name="migrate_to_device_bytes_downloaded">%s diunduh</string> <string name="servers_info_messages_received">Pesan diterima</string> <string name="info_row_updated_at">Catatan diperbarui pada</string> - <string name="settings_section_title_messages">PESAN DAN BERKAS</string> + <string name="settings_section_title_messages">Pesan dan berkas</string> <string name="settings_section_title_user_theme">Tema profil</string> <string name="settings_section_title_profile_images">Gambar profil</string> <string name="enter_correct_current_passphrase">Harap masukkan frasa sandi saat ini yang benar.</string> @@ -1908,7 +1908,7 @@ <string name="network_options_save">Simpan</string> <string name="make_profile_private">Jadikan profil pribadi!</string> <string name="remote_hosts_section">Ponsel jarak jauh</string> - <string name="run_chat_section">JALANKAN OBROLAN</string> + <string name="run_chat_section">Jalankan obrolan</string> <string name="store_passphrase_securely_without_recover">Harap simpan frasa sandi dengan aman, Anda TIDAK akan dapat mengakses obrolan jika hilang.</string> <string name="rcv_group_event_member_deleted">dihapus %1$s</string> <string name="share_text_sent_at">Dikirim pada: %s</string> @@ -1979,7 +1979,7 @@ <string name="smp_servers_new_server">Server baru</string> <string name="message_queue_info">Info antrian pesan</string> <string name="settings_section_title_message_shape">Bentuk pesan</string> - <string name="settings_section_title_private_message_routing">ROUTING PESAN PRIBADI</string> + <string name="settings_section_title_private_message_routing">Routing pesan pribadi</string> <string name="message_queue_info_server_info">info antrean server: %1$s\n\npesan terakhir diterima: %2$s</string> <string name="users_delete_data_only">Hanya data profil lokal</string> <string name="operator_open_changes">Buka perubahan</string> @@ -1997,7 +1997,7 @@ <string name="remote_ctrl_connection_stopped_desc">Harap periksa apakah perangkat seluler dan desktop terhubung ke jaringan lokal yang sama, dan firewall desktop mengizinkan koneksi.\nHarap sampaikan masalah lain kepada pengembang.</string> <string name="sync_connection_desc">Koneksi memerlukan negosiasi ulang enkripsi.</string> <string name="encryption_renegotiation_in_progress">Negosiasi ulang enkripsi sedang berlangsung.</string> - <string name="connection_error_quota">Pesan yang tidak terkirim</string> + <string name="connection_error_quota">Pesan belum terkirim</string> <string name="message_deleted_or_not_received_error_desc">Pesan ini telah dihapus atau belum diterima.</string> <string name="to_connect_via_link_title">Untuk terhubung via tautan</string> <string name="app_check_for_updates_notice_desc">Untuk mendapatkan pemberitahuan tentang rilis baru, aktifkan pemeriksaan berkala untuk versi Stabil atau Beta.</string> @@ -2024,7 +2024,7 @@ <string name="your_profile_is_stored_on_your_device">Profil, kontak, dan pesan terkirim Anda disimpan di perangkat Anda.</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platform perpesanan dan aplikasi yang melindungi privasi dan keamanan Anda.</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">Untuk melindungi privasi Anda, SimpleX gunakan ID terpisah untuk setiap kontak.</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> <string name="upgrade_and_open_chat">Tingkatkan dan buka obrolan</string> <string name="group_invitation_tap_to_join_incognito">Ketuk untuk gabung ke samaran</string> <string name="snd_group_event_member_blocked">Anda memblokir %s</string> @@ -2423,7 +2423,7 @@ <string name="v6_4_review_members_descr">Chat dengan anggota sebelum mereka bergabung.</string> <string name="compose_view_connect">Hubungkan</string> <string name="v6_4_connect_faster">Terhubung lebih cepat! 🚀</string> - <string name="settings_section_title_contact_requests_from_groups">PERMINTAAN KONTAK DARI GRUP</string> + <string name="settings_section_title_contact_requests_from_groups">Permintaan kontak dari grup</string> <string name="contact_should_accept">kontak harus menerima…</string> <string name="v6_4_1_short_address_create">Buat alamat Anda</string> <string name="deprecated_options_section">Opsi tidak berlaku</string> @@ -2511,4 +2511,222 @@ <string name="delete_member_messages_confirmation">Hapus pesan</string> <string name="member_messages_will_be_deleted_cannot_be_undone">Pesan anggota akan dihapus - ini tidak dapat dibatalkan!</string> <string name="remove_member_delete_messages_confirmation">Hapus pesan</string> + <string name="relay_bar_active">%1$d/%2$d relay aktif</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d relay aktif, %3$d error</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d relay aktif, %3$d gagal</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d relay aktif, %3$d dihapus</string> + <string name="relay_bar_connected">%1$d/%2$d relay terhubung</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d relay terhubung, %3$d error</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d relay terhubung, %3$d gagal</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d relay terhubung, %3$d dihapus</string> + <string name="channel_owner_count_singular">%1$d pemilik</string> + <string name="channel_owner_count_plural">%1$d pemilik</string> + <string name="channel_owners_contributors_count">%1$d pemilik & kontributor</string> + <string name="relay_bar_relays_failed">%1$d relay gagal</string> + <string name="relay_bar_relays_not_active">%1$d relay tidak aktif</string> + <string name="relay_bar_relays_removed">%1$d relay dihapus</string> + <string name="channel_subscriber_count_singular">%1$d pelanggan</string> + <string name="channel_subscriber_count_plural">%1$d pelanggan</string> + <string name="badge_supported_simplex">%1$s mendukung SimpleX Chat. Lencana berakhir pada %2$s.</string> + <string name="settings_section_title_about">Tentang</string> + <string name="relay_status_accepted">diterima</string> + <string name="relay_status_acknowledged_roster">daftar yang di akui</string> + <string name="relay_status_active">aktif</string> + <string name="add_button">Tambahkan</string> + <string name="add_relay_button">Tambah relay</string> + <string name="add_relays_title">Tambah relay</string> + <string name="relay_bar_owner_no_delivery">Tambahkan relay untuk memulihkan pengiriman pesan.</string> + <string name="webpage_code_footer">Tambahkan kode ini ke halaman web Anda. Kode ini akan menampilkan pratinjau channel/grup Anda.</string> + <string name="advanced_options">Opsi lanjutan</string> + <string name="advanced_settings">Setelan lanjutan</string> + <string name="a_link_for_one_person">Link untuk satu orang terhubung</string> + <string name="content_filter_all_messages">Semua pesan</string> + <string name="allow_anyone_to_embed">Izinkan semua orang menyematkan</string> + <string name="allow_chat_with_admins">Izinkan anggota mengobrol dengan admin.</string> + <string name="allow_direct_messages_channel">Izinkan mengirim pesan langsung ke pelanggan.</string> + <string name="allow_chat_with_admins_channel">Izinkan pelanggan mengobrol dengan admin.</string> + <string name="relay_bar_all_relays_failed">Semua relay gagal</string> + <string name="relay_bar_all_relays_removed">Semua relay dihapus</string> + <string name="another_instance_not_responding">Mungkin ada instance aplikasi lain yang sedang berjalan atau tidak keluar dengan benar. Tetap mulai?</string> + <string name="embed_any_webpage_can_show">Halaman web mana pun dapat menampilkan pratinjau.</string> + <string name="another_instance_title">Aplikasi sudah berjalan</string> + <string name="app_update_required">Pembaruan aplikasi diperlukan</string> + <string name="badge_unknown_key_title">Lencana tidak dapat diverifikasi</string> + <string name="why_built_p7">Karena kami menghancurkan kemampuan untuk mengetahui siapa Anda. Agar kekuatan Anda tidak akan pernah bisa direnggut.</string> + <string name="onboarding_be_free">Bebas\ndi jaringan Anda</string> + <string name="why_built_tagline">Bebas di jaringan Anda.</string> + <string name="block_subscriber_for_all_question">Blokir pelanggan untuk semua?</string> + <string name="one_hand_ui_bottom_bar">Bilah bawah</string> + <string name="compose_view_broadcast">Siaran</string> + <string name="test_relay_to_retrieve_name"><![CDATA[<b>Uji relay</b> untuk mengambil namanya.]]></string> + <string name="chat_link_business_address">Alamat Bisnis</string> + <string name="button_cancel_and_delete_channel">Batalkan dan hapus saluran</string> + <string name="cancel_creating_channel_question">Batalkan pembuatan saluran?</string> + <string name="cant_broadcast_message">Gagal menyiarkan</string> + <string name="channel_role_label">Saluran</string> + <string name="chat_link_channel">Tautan saluran</string> + <string name="button_channel_members">Anggota saluran</string> + <string name="channel_display_name_field">Nama saluran</string> + <string name="channel_preferences">Preferensi saluran</string> + <string name="channel_profile_is_stored_on_subscribers_devices">Profil saluran disimpan di perangkat pelanggan dan di relai obrolan.</string> + <string name="snd_channel_event_channel_profile_updated">Profil saluran diperbarui</string> + <string name="chat_list_channels">Saluran</string> + <string name="channel_temporarily_unavailable">Saluran untuk sementara tidak tersedia</string> + <string name="channel_webpage">Halaman web saluran</string> + <string name="delete_channel_for_all_subscribers_cannot_undo_warning">Saluran akan dihapus untuk semua pelanggan - ini tidak dapat dibatalkan!</string> + <string name="delete_channel_for_self_cannot_undo_warning">Saluran akan dihapus untuk Anda - ini tidak dapat dibatalkan!</string> + <string name="channel_will_start_with_relays">Saluran akan mulai berfungsi dengan %1$d dari %2$d relai. Lanjutkan?</string> + <string name="chat_data">Data obrolan</string> + <string name="chat_relay">Relai obrolan</string> + <string name="button_channel_relays">Relai saluran</string> + <string name="chat_relays">Relai obrolan</string> + <string name="channel_relays_title">Relai saluran</string> + <string name="chat_relays_forward_messages_in_channels">Relai obrolan meneruskan pesan di saluran yang Anda buat.</string> + <string name="chat_relays_forward_messages">Relai obrolan meneruskan pesan ke pelanggan saluran.</string> + <string name="chat_with_admins_is_prohibited">Obrolan dengan admin dilarang.</string> + <string name="chat_with_admins_relay_note">Obrolan dengan admin di saluran publik tidak memiliki enkripsi ujung-ke-ujung (E2E) - gunakan hanya dengan relai obrolan tepercaya.</string> + <string name="support_chats_disabled">Mengobrol dengan anggota dimatikan</string> + <string name="chat_with_admins">Obrolan dengan admin</string> + <string name="check_relay_address">Periksa alamat relai dan coba lagi.</string> + <string name="check_relay_name">Periksa nama relai dan coba lagi.</string> + <string name="close_behavior_dialog_close">Tutup aplikasi</string> + <string name="appearance_minimize_to_tray">Tutup ke baki</string> + <string name="configure_relays">Konfigurasi relai</string> + <string name="relay_test_step_connect">Hubungkan</string> + <string name="relay_conn_status_connected">terhubung</string> + <string name="relay_conn_status_connecting">menghubungkan</string> + <string name="channel_name_requires_newer_app_version">Menghubungkan melalui nama saluran memerlukan versi aplikasi yang lebih baru.</string> + <string name="contact_name_requires_newer_app_version">Menghubungkan melalui nama kontak memerlukan versi aplikasi yang lebih baru.</string> + <string name="info_row_connection_failed">Koneksi gagal</string> + <string name="connect_via_link_or_qr_code">Hubungkan melalui tautan atau kode QR</string> + <string name="settings_section_title_contact">Kontak</string> + <string name="chat_link_contact_address">Alamat kontak</string> + <string name="group_member_role_member_channel">kontributor</string> + <string name="copy_code">Salin kode</string> + <string name="webpage_info">Buat halaman web untuk menampilkan pratinjau saluran Anda kepada pengunjung sebelum mereka berlangganan. Anda dapat menghostingnya sendiri atau menggunakan layanan hosting statis apa pun.</string> + <string name="create_channel_title">Buat saluran publik</string> + <string name="create_channel_button">Buat saluran publik</string> + <string name="connect_with_someone">Buat tautan Anda</string> + <string name="create_your_public_address">Buat alamat publik Anda</string> + <string name="creating_channel">Sedang membuat saluran</string> + <string name="rcv_channel_events_count">%d acara saluran</string> + <string name="relay_test_step_decode_link">Dekode tautan</string> + <string name="button_delete_channel">Hapus saluran</string> + <string name="delete_channel_question">Hapus saluran?</string> + <string name="relay_conn_status_deleted">dihapus</string> + <string name="rcv_channel_event_channel_deleted">saluran dihapus</string> + <string name="chat_banner_channel">Saluran</string> + <string name="info_row_channel">Saluran</string> + <string name="channel_full_name_field">Nama lengkap saluran:</string> + <string name="channel_no_active_relays_try_later">Saluran tidak memiliki relai aktif. Silakan coba bergabung nanti.</string> + <string name="channel_link">Tautan saluran</string> + <string name="delete_relay">Hapus relai</string> + <string name="direct_messages_are_prohibited_channel">Pesan pribadi antar pelanggan dilarang.</string> + <string name="link_previews_alert_disable">Matikan</string> + <string name="disable_sending_recent_history_channel">Jangan kirim riwayat ke pelanggan baru.</string> + <string name="num_relays_selected">%d relai dipilih</string> + <string name="rcv_msg_error_dropped">dijatuhkan (%1$d upaya)</string> + <string name="v6_5_invite_friends">Lebih mudah untuk mengundang teman Anda 👋</string> + <string name="button_edit_channel_profile">Edit profil saluran</string> + <string name="link_previews_alert_enable">Aktifkan</string> + <string name="enable_chats_with_admins">Aktifkan</string> + <string name="enable_at_least_one_chat_relay">Aktifkan setidaknya satu relai obrolan untuk membuat saluran.</string> + <string name="enable_chats_with_admins_question">Aktifkan obrolan dengan admin?</string> + <string name="link_previews_alert_title">Aktifkan pratinjau tautan?</string> + <string name="enter_profile_name">Masukkan nama profil…</string> + <string name="enter_relay_name">Masukkan nama relai…</string> + <string name="enter_webpage_url">Masukkan URL halaman web</string> + <string name="error_prefix">Galat</string> + <string name="error_adding_relay">Galat saat menambahkan relai</string> + <string name="error_adding_relays">Galat saat menambahkan relai</string> + <string name="error_creating_channel">Galat saat membuat saluran</string> + <string name="error_deleting_message">Gagal menghapus pesan</string> + <string name="error_opening_channel">Galat saat membuka saluran</string> + <string name="rcv_msg_error_parse">galat: %s</string> + <string name="error_saving_channel_profile">Galat saat menyimpan profil saluran</string> + <string name="error_sharing_channel">Galat saat membagikan saluran</string> + <string name="member_info_member_failed">gagal</string> + <string name="relay_conn_status_failed">gagal</string> + <string name="relay_status_failed">gagal</string> + <string name="content_filter_files">Berkas</string> + <string name="content_filter_menu_item">Filter</string> + <string name="for_anyone_to_reach_you">Agar siapa pun dapat menghubungi Anda</string> + <string name="from_history">Dari riwayat</string> + <string name="chat_link_from_owner">(dari pemilik)</string> + <string name="relay_test_step_get_link">Dapatkan tautan</string> + <string name="get_started">Mulai</string> + <string name="chat_link_group">Tautan grup</string> + <string name="group_webpage">Halaman web grup</string> + <string name="help_and_support">Bantuan & dukungan</string> + <string name="recent_history_is_not_sent_to_new_members_channel">Riwayat tidak dikirim ke pelanggan baru.</string> + <string name="web_page_url_placeholder">https://</string> + <string name="close_behavior_dialog_text">Jika Anda memilih Tutup, pesan tidak akan diterima.\nAnda dapat mengubahnya nanti di pengaturan Tampilan.</string> + <string name="down_migration_warning_chat_relays">Jika Anda bergabung atau membuat saluran, saluran tersebut akan berhenti bekerja secara permanen.</string> + <string name="content_filter_images">Gambar</string> + <string name="relay_status_inactive">tidak aktif</string> + <string name="invalid_relay_address">Alamat relai tidak valid!</string> + <string name="invalid_relay_name">Nama relai tidak valid!</string> + <string name="relay_status_invited">diundang</string> + <string name="invite_someone_privately">Undang seseorang secara pribadi</string> + <string name="webpage_url_footer">Ini akan ditampilkan kepada pelanggan dan digunakan untuk memungkinkan pemuatan pratinjau.</string> + <string name="compose_view_join_channel">Gabung saluran</string> + <string name="button_leave_channel">Keluar saluran</string> + <string name="leave_channel_question">Keluar dari saluran?</string> + <string name="let_someone_connect_to_you">Izinkan seseorang terhubung dengan Anda</string> + <string name="action_button_channel_link">Tautan</string> + <string name="link_previews_alert_desc_socks">Pratinjau tautan akan diminta melalui proksi SOCKS. Pencarian DNS mungkin masih terjadi secara lokal melalui penyelesai DNS Anda.</string> + <string name="content_filter_links">Tautan</string> + <string name="owner_verification_passed">Tanda tangan tautan diverifikasi.</string> + <string name="members_can_chat_with_admins">Anggota dapat mengobrol dengan admin.</string> + <string name="alert_title_msg_error">Galat pesan</string> + <string name="e2ee_info_no_e2ee"><![CDATA[Pesan di saluran ini <b>tidak dienkripsi end-to-end</b>. Relai obrolan dapat melihat pesan ini.]]></string> + <string name="migrate">Pindahkan</string> + <string name="close_behavior_dialog_minimize">Minimalkan ke baki</string> + <string name="close_behavior_dialog_title">Minimalkan ke baki?</string> + <string name="more_privacy">Privasi lebih lanjut</string> + <string name="onboarding_network_commitments">Komitmen jaringan</string> + <string name="network_error">Galat jaringan</string> + <string name="onboarding_network_routers_cannot_know">Perute jaringan tidak dapat mengetahui\nsiapa yang berbicara dengan siapa</string> + <string name="relay_status_new">baru</string> + <string name="new_1_time_link">Tautan 1-kali baru</string> + <string name="new_chat_relay">Relai obrolan baru</string> + <string name="onboarding_no_account">Tanpa akun. Tanpa telepon. Tanpa email. Tanpa ID.\nEnkripsi paling aman.</string> + <string name="relay_bar_no_active_relays">Tidak ada relay aktif</string> + <string name="no_available_relays">Tidak ada relai yang tersedia</string> + <string name="why_built_p1">Tidak ada yang melacak percakapan Anda. Tidak ada yang menggambar peta ke mana pun Anda pergi. Privasi tidak pernah menjadi fitur — itu adalah cara hidup.</string> + <string name="no_chat_relays_enabled">Tidak ada fitur relay obrolan yang diaktifkan.</string> + <string name="relay_bar_no_relays">Tidak ada relai</string> + <string name="no_relays_selected">Tidak ada relay yang dipilih</string> + <string name="voice_recording_not_supported">Perekaman suara tidak didukung di platform Anda.</string> + <string name="unsupported_channel_name">Nama saluran tidak didukung</string> + <string name="unsupported_contact_name">Nama kontak tidak didukung</string> + <string name="please_upgrade_the_app">Silakan perbarui aplikasinya.</string> + <string name="group_link_requires_newer_version">Grup ini membutuhkan versi aplikasi yang lebih baru. Silakan perbarui aplikasi untuk bergabung.</string> + <string name="placeholder_search_images">Cari gambar</string> + <string name="placeholder_search_videos">Cari video</string> + <string name="placeholder_search_voice_messages">Cari pesan suara</string> + <string name="server_warning">Peringatan server</string> + <string name="placeholder_search_files">Cari berkas</string> + <string name="placeholder_search_links">Cari tautan</string> + <string name="content_filter_videos">Video</string> + <string name="content_filter_voice_messages">Pesan suara</string> + <string name="talk_to_someone">Bicara dengan seseorang</string> + <string name="your_public_address">Alamat publik Anda</string> + <string name="chat_banner_join_channel">Ketuk Gabung saluran</string> + <string name="chat_banner_your_channel">Saluran Anda</string> + <string name="share_channel">Bagikan saluran…</string> + <string name="share_via_chat">Bagikan via obrolan</string> + <string name="tap_to_open">Ketuk untuk buka</string> + <string name="chat_link_one_time">Tautan 1-kali</string> + <string name="owner_verification_failed">⚠️ Verifikasi tanda tangan gagal: %s.</string> + <string name="you_are_subscriber">anda adalah pelanggan</string> + <string name="onboarding_send_1_time_link">Kirim tautannya via aplikasi pesan apa pun - aman. Minta untuk tempel ke SimpleX.</string> + <string name="onboarding_or_show_qr_code">Atau perlihatkan kode QR secara langsung atau melalui panggilan video.</string> + <string name="onboarding_post_address">Gunakan alamat ini di profil media sosial, situs web, atau tanda tangan email Anda.</string> + <string name="connect_plan_join_name">Gabung saluran %s</string> + <string name="connect_plan_connect_to_name">Hubungkan ke %s</string> + <string name="no_names_servers_enabled">Tidak ada server untuk meresolusi nama.</string> + <string name="simplex_name_error">Nama SimpleX bermasalah</string> + <string name="simplex_name_no_servers_desc">Tidak ada server Anda yang diatur untuk meresolusi nama SimpleX. Konfigurasikan server, atau gunakan tautan koneksi.</string> + <string name="simplex_name_unconfirmed">Nama belum dikonfirmasi</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 6642553f2e..9fad882019 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -64,9 +64,9 @@ <string name="contact_already_exists">Il contatto esiste già</string> <string name="invalid_connection_link">Link di connessione non valido</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro.</string> - <string name="connection_error_auth">Errore di connessione (AUTH)</string> + <string name="connection_error_auth">Link di connessione rimosso</string> <string name="error_accepting_contact_request">Errore di accettazione della richiesta del contatto</string> - <string name="sender_may_have_deleted_the_connection_request">Il mittente potrebbe aver eliminato la richiesta di connessione.</string> + <string name="sender_may_have_deleted_the_connection_request">Il mittente ha eliminato la richiesta di connessione.</string> <string name="error_deleting_contact">Errore di eliminazione del contatto</string> <string name="error_deleting_group">Errore di eliminazione del gruppo</string> <string name="error_deleting_contact_request">Errore di eliminazione della richiesta di contatto</string> @@ -208,8 +208,7 @@ <string name="simplex_link_connection">via %1$s</string> <string name="simplex_link_mode_browser_warning">Aprire il link nel browser può ridurre la privacy e la sicurezza della connessione. I link SimpleX non fidati saranno in rosso.</string> <string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a %1$s.</string> - <string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo. -\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string> + <string name="connection_error_auth_desc">Il tuo contatto ha rimosso questo link, o era un link una tantum che è già stato usato.\nPer connetterti, chiedi al tuo contatto di creare un nuovo link.</string> <string name="error_smp_test_certificate">L\'impronta digitale nell\'indirizzo del server non corrisponde al certificato.</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Per migliorare la privacy, <b>SimpleX funziona in secondo piano</b> invece di usare le notifiche push.]]></string> <string name="turn_off_battery_optimization"><![CDATA[<b>Consentilo</b> nella prossima schermata per ricevere le notifiche immediatamente.]]></string> @@ -269,7 +268,7 @@ <string name="keychain_is_storing_securely">L\'archivio chiavi di Android è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.</string> <string name="allow_your_contacts_to_send_voice_messages">Permetti ai tuoi contatti di inviare messaggi vocali.</string> <string name="chat_database_deleted">Database della chat eliminato</string> - <string name="settings_section_title_icon">ICONA APP</string> + <string name="settings_section_title_icon">Icona app</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NESSUN servizio in secondo piano).]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consuma più batteria</b>! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente.]]></string> <string name="callstatus_calling">chiamata…</string> @@ -378,24 +377,24 @@ <string name="allow_accepting_calls_from_lock_screen">Attiva le chiamate dalla schermata di blocco tramite le impostazioni.</string> <string name="icon_descr_flip_camera">Fotocamera frontale/posteriore</string> <string name="icon_descr_hang_up">Riaggancia</string> - <string name="settings_section_title_calls">CHIAMATE</string> - <string name="chat_database_section">DATABASE DELLA CHAT</string> + <string name="settings_section_title_calls">Chiamate</string> + <string name="chat_database_section">Database della chat</string> <string name="chat_database_imported">Database della chat importato</string> <string name="chat_is_running">Chat in esecuzione</string> - <string name="settings_section_title_chats">CHAT</string> + <string name="settings_section_title_chats">Chat</string> <string name="set_password_to_export_desc">Il database è crittografato con una password casuale. Cambiala prima di esportare.</string> <string name="database_passphrase">Password del database</string> <string name="delete_chat_profile_question">Eliminare il profilo di chat\?</string> <string name="delete_database">Elimina database</string> <string name="settings_developer_tools">Strumenti di sviluppo</string> - <string name="settings_section_title_device">DISPOSITIVO</string> + <string name="settings_section_title_device">Dispositivo</string> <string name="error_deleting_database">Errore nell\'eliminazione del database della chat</string> <string name="error_exporting_chat_database">Errore nell\'esportazione del database della chat</string> <string name="error_starting_chat">Errore nell\'avvio della chat</string> <string name="error_stopping_chat">Errore nell\'interruzione della chat</string> <string name="settings_experimental_features">Funzionalità sperimentali</string> <string name="export_database">Esporta database</string> - <string name="settings_section_title_help">AIUTO</string> + <string name="settings_section_title_help">Aiuto</string> <string name="chat_is_stopped_indication">Chat fermata</string> <string name="database_error">Errore del database</string> <string name="passphrase_is_different">La password del database è diversa da quella salvata nell\'archivio chiavi.</string> @@ -423,7 +422,7 @@ <string name="snd_group_event_group_profile_updated">profilo del gruppo aggiornato</string> <string name="invite_prohibited">Impossibile invitare il contatto!</string> <string name="change_verb">Cambia</string> - <string name="change_member_role_question">Cambiare il ruolo del gruppo\?</string> + <string name="change_member_role_question">Cambiare il ruolo?</string> <string name="clear_contacts_selection_button">Svuota</string> <string name="group_member_status_complete">completo</string> <string name="group_member_status_connecting">in connessione</string> @@ -437,7 +436,7 @@ <string name="error_creating_link_for_group">Errore nella creazione del link del gruppo</string> <string name="error_deleting_link_for_group">Errore nell\'eliminazione del link del gruppo</string> <string name="icon_descr_expand_role">Espandi la selezione dei ruoli</string> - <string name="section_title_for_console">PER CONSOLE</string> + <string name="section_title_for_console">Per console</string> <string name="group_link">Link del gruppo</string> <string name="delete_group_for_all_members_cannot_undo_warning">Il gruppo verrà eliminato per tutti i membri. Non è reversibile!</string> <string name="delete_group_for_self_cannot_undo_warning">Il gruppo verrà eliminato per te. Non è reversibile!</string> @@ -697,23 +696,23 @@ <string name="import_database_question">Importare il database della chat\?</string> <string name="import_database">Importa database</string> <string name="settings_section_title_incognito">Modalità incognito</string> - <string name="settings_section_title_messages">MESSAGGI E FILE</string> + <string name="settings_section_title_messages">Messaggi e file</string> <string name="new_database_archive">Nuovo archivio database</string> <string name="old_database_archive">Vecchio archivio del database</string> <string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string> <string name="restart_the_app_to_use_imported_chat_database">Riavvia l\'app per usare il database della chat importato.</string> - <string name="run_chat_section">AVVIA CHAT</string> + <string name="run_chat_section">Avvia chat</string> <string name="send_link_previews">Invia le anteprime dei link</string> <string name="set_password_to_export">Imposta la password per esportare</string> - <string name="settings_section_title_settings">IMPOSTAZIONI</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="settings_section_title_settings">Impostazioni</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> <string name="stop_chat_confirmation">Ferma</string> <string name="stop_chat_question">Fermare la chat\?</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma.</string> - <string name="settings_section_title_support">SUPPORTA SIMPLEX CHAT</string> - <string name="settings_section_title_themes">TEMI</string> + <string name="settings_section_title_support">Supporta SimpleX Chat</string> + <string name="settings_section_title_themes">Temi</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string> - <string name="settings_section_title_you">TU</string> + <string name="settings_section_title_you">Tu</string> <string name="your_chat_database">Il tuo database della chat</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Il tuo attuale database di chat verrà ELIMINATO e SOSTITUITO con quello importato. \nQuesta azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string> @@ -774,7 +773,7 @@ <string name="invite_to_group_button">Invita al gruppo</string> <string name="button_leave_group">Esci dal gruppo</string> <string name="info_row_local_name">Nome locale</string> - <string name="member_info_section_title_member">MEMBRO</string> + <string name="member_info_section_title_member">Membro</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Il membro verrà rimosso dal gruppo, non è reversibile!</string> <string name="new_member_role">Nuovo ruolo del membro</string> <string name="no_contacts_selected">Nessun contatto selezionato</string> @@ -788,7 +787,7 @@ <string name="skip_inviting_button">Salta l\'invito di membri</string> <string name="switch_verb">Cambia</string> <string name="num_contacts_selected">%d contatto/i selezionato/i</string> - <string name="group_info_section_title_num_members">%1$s MEMBRI</string> + <string name="group_info_section_title_num_members">%1$s membri</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini.</string> <string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string> <string name="group_info_member_you">tu: %1$s</string> @@ -806,12 +805,12 @@ <string name="save_group_profile">Salva il profilo del gruppo</string> <string name="network_option_seconds_label">sec</string> <string name="sending_via">Invio tramite</string> - <string name="conn_stats_section_title_servers">SERVER</string> + <string name="conn_stats_section_title_servers">Server</string> <string name="switch_receiving_address">Cambia indirizzo di ricezione</string> <string name="theme_system">Sistema</string> <string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string> <string name="group_is_decentralized">Completamente decentralizzato: visibile solo ai membri.</string> - <string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica.</string> + <string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in %s. Verrà avvisato chiunque nel gruppo.</string> <string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito.</string> <string name="update_network_settings_confirmation">Aggiorna</string> <string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string> @@ -994,7 +993,7 @@ <string name="confirm_database_upgrades">Conferma aggiornamenti database</string> <string name="mtr_error_different">migrazione diversa nell\'app/nel database: %s / %s</string> <string name="invalid_migration_confirmation">Conferma di migrazione non valida</string> - <string name="settings_section_title_experimenta">SPERIMENTALE</string> + <string name="settings_section_title_experimenta">Sperimentale</string> <string name="image_will_be_received_when_contact_completes_uploading">L\'immagine verrà ricevuta quando il tuo contatto completerà l\'invio.</string> <string name="mtr_error_no_down_migration">la versione del database è più recente di quella dell\'app, ma nessuna migrazione downgrade per: %s</string> <string name="file_will_be_received_when_contact_completes_uploading">Il file verrà ricevuto quando il tuo contatto completerà l\'invio.</string> @@ -1102,7 +1101,7 @@ <string name="scan_qr_to_connect_to_contact">Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app.</string> <string name="you_can_accept_or_reject_connection">Quando le persone chiedono di connettersi, puoi accettare o rifiutare.</string> <string name="simplex_address">Indirizzo SimpleX</string> - <string name="theme_colors_section_title">COLORI DELL\'INTERFACCIA</string> + <string name="theme_colors_section_title">Colori dell\'interfaccia</string> <string name="your_contacts_will_remain_connected">I tuoi contatti resteranno connessi.</string> <string name="add_address_to_your_profile">Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti di SimpleX possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti di SimpleX.</string> <string name="create_address_and_let_people_connect">Crea un indirizzo per consentire alle persone di connettersi con te.</string> @@ -1116,7 +1115,7 @@ <string name="invite_friends">Invita amici</string> <string name="save_auto_accept_settings">Salva le impostazioni dell\'indirizzo SimpleX</string> <string name="you_can_create_it_later">Puoi crearlo più tardi</string> - <string name="share_address">Condividi indirizzo</string> + <string name="share_address">Condividi indirizzo…</string> <string name="enter_welcome_message">Inserisci il messaggio di benvenuto…</string> <string name="group_welcome_preview">Anteprima</string> <string name="you_can_share_this_address_with_your_contacts">Puoi condividere questo indirizzo con i contatti per consentire loro di connettersi con %s.</string> @@ -1230,7 +1229,7 @@ <string name="item_info_no_text">nessun testo</string> <string name="non_fatal_errors_occured_during_import">Si sono verificati alcuni errori non fatali durante l\'importazione:</string> <string name="settings_restart_app">Riavvia</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="shutdown_alert_desc">Le notifiche smetteranno di funzionare fino a quando non riavvierai l\'app</string> <string name="settings_shutdown">Spegni</string> <string name="shutdown_alert_question">Spegnere\?</string> @@ -1261,7 +1260,7 @@ <string name="sending_delivery_receipts_will_be_enabled">L\'invio delle ricevute di consegna sarà attivo per tutti i contatti.</string> <string name="error_enabling_delivery_receipts">Errore nell\'attivazione delle ricevute di consegna!</string> <string name="you_can_enable_delivery_receipts_later">Puoi attivarle più tardi nelle impostazioni</string> - <string name="settings_section_title_delivery_receipts">INVIA RICEVUTE DI CONSEGNA A</string> + <string name="settings_section_title_delivery_receipts">Invia ricevute di consegna a</string> <string name="snd_conn_event_ratchet_sync_started">concordando la crittografia per %s…</string> <string name="delivery_receipts_title">Ricevute di consegna!</string> <string name="receipts_section_contacts">Contatti</string> @@ -1276,7 +1275,7 @@ \n- e altro ancora!</string> <string name="v5_2_disappear_one_message">Fai sparire un messaggio</string> <string name="v5_2_fix_encryption">Mantieni le tue connessioni</string> - <string name="you_can_enable_delivery_receipts_later_alert">Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell\'app.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Puoi attivarle più tardi nelle impostazioni dell\'app \"La tua privacy\".</string> <string name="receipts_contacts_disable_keep_overrides">Disattiva (mantieni sostituzioni)</string> <string name="delivery_receipts_are_disabled">Le ricevute di consegna sono disattivate!</string> <string name="receipts_contacts_disable_for_all">Disattiva per tutti</string> @@ -1779,7 +1778,7 @@ <string name="network_smp_proxy_fallback_prohibit_description">NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l\'instradamento privato.</string> <string name="network_smp_proxy_mode_never_description">NON usare l\'instradamento privato.</string> <string name="network_smp_proxy_fallback_prohibit">No</string> - <string name="settings_section_title_private_message_routing">INSTRADAMENTO PRIVATO DEI MESSAGGI</string> + <string name="settings_section_title_private_message_routing">Instradamento privato dei messaggi</string> <string name="network_smp_proxy_fallback_allow_protected_description">Invia messaggi direttamente quando l\'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l\'instradamento privato.</string> <string name="private_routing_explanation">Per proteggere il tuo indirizzo IP, l\'instradamento privato usa i tuoi server SMP per consegnare i messaggi.</string> <string name="network_smp_proxy_mode_unprotected">Non protetto</string> @@ -1787,7 +1786,7 @@ <string name="protect_ip_address">Proteggi l\'indirizzo IP</string> <string name="app_will_ask_to_confirm_unknown_file_servers">L\'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion o quando il proxy SOCKS è attivo).</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file.</string> - <string name="settings_section_title_files">FILE</string> + <string name="settings_section_title_files">File</string> <string name="file_not_approved_descr">Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: \n%1$s.</string> <string name="settings_section_title_chat_theme">Tema della chat</string> @@ -2056,7 +2055,7 @@ <string name="switching_profile_error_title">Errore nel cambio di profilo</string> <string name="select_chat_profile">Seleziona il profilo di chat</string> <string name="new_chat_share_profile">Condividi il profilo</string> - <string name="settings_section_title_chat_database">DATABASE DELLA CHAT</string> + <string name="settings_section_title_chat_database">Database della chat</string> <string name="system_mode_toast">Modalità di sistema</string> <string name="migrate_from_device_remove_archive_question">Rimuovere l\'archivio?</string> <string name="delete_messages_cannot_be_undone_warning">I messaggi verranno eliminati. Non è reversibile!</string> @@ -2239,7 +2238,7 @@ <string name="only_chat_owners_can_change_prefs">Solo i proprietari della chat possono modificarne le preferenze.</string> <string name="onboarding_notifications_mode_battery">Notifiche e batteria</string> <string name="maximum_message_size_reached_forwarding">Puoi copiare e ridurre la dimensione del messaggio per inviarlo.</string> - <string name="member_role_will_be_changed_with_notification_chat">Il ruolo verrà cambiato in %s. Verrà notificato a tutti nella chat.</string> + <string name="member_role_will_be_changed_with_notification_chat">Il ruolo verrà cambiato in %s. Verrà avvisato chiunque nella chat.</string> <string name="chat_main_profile_sent">Il tuo profilo di chat verrà inviato ai membri della chat</string> <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">Non riceverai più messaggi da questa chat. La cronologia della chat verrà conservata.</string> <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Quando più di un operatore è attivato, nessuno di essi ha metadati per capire chi comunica con chi.</string> @@ -2441,7 +2440,7 @@ <string name="e2ee_info_e2ee"><![CDATA[I messaggi sono protetti da <b>crittografia end-to-end</b>.]]></string> <string name="compose_view_send_contact_request_alert_text"><![CDATA[Potrai inviare messaggi <b>solo dopo che la tua richiesta verrà accettata</b>.]]></string> <string name="compose_view_connect">Connetti</string> - <string name="contact_should_accept">il contatto dovrebbe accettare…</string> + <string name="contact_should_accept">il contatto deve accettare…</string> <string name="error_changing_user">Errore cambiando l\'profilo</string> <string name="error_preparing_contact">Errore di apertura della chat</string> <string name="error_preparing_group">Errore di apertura del gruppo</string> @@ -2512,7 +2511,7 @@ <string name="share_old_link_alert_button">Condividi il link vecchio</string> <string name="share_group_profile_via_link_alert_text">Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link.</string> <string name="upgrade_group_link">Aggiorna il link del gruppo</string> - <string name="settings_section_title_contact_requests_from_groups">RICHIESTE DI CONTATTO DAI GRUPPI</string> + <string name="settings_section_title_contact_requests_from_groups">Richieste di contatto dai gruppi</string> <string name="member_is_deleted_cant_accept_request">Il membro è eliminato - impossibile accettare la richiesta</string> <string name="rcv_direct_event_group_inv_link_received">connessione richiesta dal gruppo %1$s</string> <string name="this_setting_is_for_your_current_profile">Questa impostazione è per il tuo profilo attuale</string> @@ -2555,7 +2554,7 @@ <string name="content_filter_videos">Video</string> <string name="content_filter_voice_messages">Messaggi vocali</string> <string name="content_filter_menu_item">Filtro</string> - <string name="info_row_connection_failed">CONNESSIONE FALLITA</string> + <string name="info_row_connection_failed">Connessione fallita</string> <string name="member_info_member_failed">fallito</string> <string name="down_migration_warning_chat_relays">Se sei dentro canali o ne hai creati, essi smetteranno di funzionare definitivamente.</string> <string name="relay_bar_active">%1$d/%2$d relay attivo/i</string> @@ -2593,7 +2592,6 @@ <string name="relay_conn_status_connecting">in connessione</string> <string name="create_channel_title">Crea canale pubblico</string> <string name="create_channel_button">Crea canale pubblico</string> - <string name="create_channel_beta_button">Crea canale pubblico (BETA)</string> <string name="creating_channel">Creazione canale</string> <string name="relay_test_step_decode_link">Decodifica il link</string> <string name="button_delete_channel">Elimina canale</string> @@ -2620,12 +2618,12 @@ <string name="not_all_relays_connected">Non tutti i relay sono connessi</string> <string name="connect_plan_open_channel">Apri canale</string> <string name="connect_plan_open_new_channel">Apri il nuovo canale</string> - <string name="member_info_section_title_owner">PROPRIETARIO</string> - <string name="channel_members_section_owners">Proprietari</string> + <string name="member_info_section_title_owner">Proprietario</string> + <string name="channel_members_section_owners">Proprietari e collaboratori</string> <string name="preset_relay_address">Indirizzo relay preimpostato</string> <string name="preset_relay_name">Nome relay preimpostato</string> <string name="group_member_role_relay">relay</string> - <string name="member_info_section_title_relay">RELAY</string> + <string name="member_info_section_title_relay">Relay</string> <string name="info_row_relay_address">Indirizzo del relay</string> <string name="relay_address_alert_title">Indirizzo del relay</string> <string name="relay_connection_failed">Connessione del relay fallita</string> @@ -2636,7 +2634,7 @@ <string name="error_relay_test_server_auth">Il server richiede l\'autorizzazione per connettersi al relay, controlla la password.</string> <string name="server_warning">Avviso del server</string> <string name="share_relay_address">Condividi l\'indirizzo del relay</string> - <string name="member_info_section_title_subscriber">ISCRITTO</string> + <string name="member_info_section_title_subscriber">Iscritto</string> <string name="channel_members_title_subscribers">Iscritti</string> <string name="relay_section_footer_owner">Gli iscritti usano il link del relay per connettersi al canale.\nL\'indirizzo del relay è stato usato per impostare questo relay per il canale.</string> <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">L\'iscritto verrà rimosso dal canale, non è reversibile!</string> @@ -2706,7 +2704,6 @@ <string name="share_channel">Condividi canale…</string> <string name="share_via_chat">Condividi via chat</string> <string name="owner_verification_failed">⚠️ Verifica della firma fallita: %s.</string> - <string name="chat_link_signed">(firmato)</string> <string name="tap_to_open">Tocca per aprire</string> <string name="chat_link_one_time">Link una tantum</string> <string name="link_previews_alert_disable">Disattiva</string> @@ -2823,10 +2820,10 @@ <string name="error_deleting_message">Errore di eliminazione del messaggio</string> <string name="from_history">Dalla cronologia</string> <string name="close_behavior_dialog_text">Se scegli Chiudi, i messaggi non verranno ricevuti.\nPuoi cambiarlo più tardi nelle impostazioni di Aspetto.</string> - <string name="appearance_minimize_to_tray_desc">Tieni SimpleX attivo in secondo piano per ricevere i messaggi.</string> + <string name="appearance_minimize_to_tray_desc">Resta in secondo piano per ricevere i messaggi</string> <string name="close_behavior_dialog_minimize">Riduci nell\'area delle notifiche</string> <string name="close_behavior_dialog_title">Ridurre nell\'area delle notifiche?</string> - <string name="appearance_minimize_to_tray">Riduci nell\'area delle notifiche alla chiusura della finestra</string> + <string name="appearance_minimize_to_tray">Chiudi nell\'area delle notifiche</string> <string name="tray_quit">Esci da SimpleX</string> <string name="tray_show">Mostra SimpleX</string> <string name="tray_tooltip">SimpleX</string> @@ -2836,4 +2833,105 @@ <string name="relay_status_rejected">rifiutato</string> <string name="member_info_relay_status_rejected_by_operator">rifiutato dall\'operatore del relay</string> <string name="member_info_status">Stato</string> + <string name="channel_owner_count_singular">%1$d proprietario</string> + <string name="channel_owner_count_plural">%1$d proprietari</string> + <string name="channel_owners_contributors_count">%1$d proprietari e collaboratori</string> + <string name="badge_supported_simplex">%1$s ha sostenuto SimpleX Chat. La targhetta è scaduta il %2$s.</string> + <string name="settings_section_title_about">Informazioni</string> + <string name="relay_status_acknowledged_roster">lista riconosciuta</string> + <string name="webpage_code_footer">Aggiungi questo codice alla tua pagina web. Mostrerà l\'anteprima del tuo canale / gruppo.</string> + <string name="advanced_options">Opzioni avanzate</string> + <string name="advanced_settings">Impostazioni avanzate</string> + <string name="allow_anyone_to_embed">Consenti a chiunque di incorporare</string> + <string name="embed_any_webpage_can_show">Qualsiasi pagina web può mostrare l\'anteprima.</string> + <string name="app_update_required">Aggiornamento dell\'app necessario</string> + <string name="badge_unknown_key_title">La targhetta non può essere verificata</string> + <string name="channel_webpage">Pagina web del canale</string> + <string name="chat_data">Dati della chat</string> + <string name="channel_name_requires_newer_app_version">La connessione tramite nome del canale richiede una versione dell\'app più recente.</string> + <string name="contact_name_requires_newer_app_version">La connessione tramite nome del contatto richiede una versione dell\'app più recente.</string> + <string name="settings_section_title_contact">Contatto</string> + <string name="group_member_role_member_channel">collaboratore</string> + <string name="copy_code">Copia codice</string> + <string name="webpage_info">Crea una pagina web per mostrare l\'anteprima del tuo canale ai visitatori prima che si iscrivano. Ospitala da solo o usa un qualsiasi hosting statico.</string> + <string name="enter_webpage_url">Inserisci URL della pagina</string> + <string name="group_webpage">Pagina web del gruppo</string> + <string name="help_and_support">Aiuto e supporto</string> + <string name="web_page_url_placeholder">https://</string> + <string name="webpage_url_footer">Verrà mostrato agli iscritti e usato per permettere il caricamento dell\'anteprima.</string> + <string name="more_privacy">Più privacy</string> + <string name="embed_only_your_page">Solo la tua pagina soprastante può mostrare l\'anteprima.</string> + <string name="please_upgrade_the_app">Aggiorna l\'app.</string> + <string name="badge_invested">%s ha investito nella raccolta fondi di SimpleX Chat.</string> + <string name="badge_supports_simplex">%s sostiene SimpleX Chat.</string> + <string name="group_member_role_observer_channel">iscritto</string> + <string name="settings_section_title_support_project">Sostieni il progetto</string> + <string name="badge_unknown_key_desc">La targhetta è firmata con una chiave che questa versione dell\'app non riconosce. Aggiorna l\'app per verificare questa targhetta.</string> + <string name="member_role_will_be_changed_with_notification_channel">Il ruolo verrà cambiato in "%s". Verrà avvisato chiunque nel canale.</string> + <string name="badge_unverified_desc">Non è stato possibile verificare questa targhetta e potrebbe non essere autentica.</string> + <string name="group_link_requires_newer_version">Questo gruppo richiede una versione dell\'app più recente. Aggiorna l\'app per entrare.</string> + <string name="unsupported_channel_name">Nome del canale non supportato</string> + <string name="unsupported_contact_name">Nome del contatto non supportato</string> + <string name="badge_unverified_title">Targhetta non verificata</string> + <string name="relays_no_web_support">I relay di chat usati non supportano le pagine web.</string> + <string name="webpage_code">Codice pagina web</string> + <string name="badge_support_from_v7">Puoi sostenere SimpleX dalla versione 7 dell\'app.</string> + <string name="error_saving_simplex_name">Errore di salvataggio del nome</string> + <string name="set_user_simplex_name_footer">Consenti alle persone di collegarsi tramite il nome registrato con il tuo indirizzo SimpleX.</string> + <string name="set_channel_simplex_name_footer">Consenti alle persone di entrare attraverso il nome registrato con questo link del canale.</string> + <string name="simplex_name_not_found">Nome non trovato</string> + <string name="simplex_name_no_servers_desc">Nessuno dei tuoi server è impostato per risolvere i nomi SimpleX. Configura i server o usa un link di connessione.</string> + <string name="no_names_servers_enabled">Nessun server per risolvere i nomi.</string> + <string name="simplex_name_no_valid_link">Nessun link valido</string> + <string name="simplex_name_resolver_error_desc">Errore del risolutore: %1$s</string> + <string name="simplex_name_server_no_resolver_desc">Il server %1$s non supporta la risoluzione dei nomi. Configura i server o usa un link di connessione.</string> + <string name="set_simplex_name">Imposta nome SimpleX</string> + <string name="simplex_name">Nome SimpleX</string> + <string name="simplex_name_error">Errore del nome SimpleX</string> + <string name="simplex_name_not_verified">Nome SimpleX non verificato</string> + <string name="simplex_name_no_valid_link_desc">Il nome SimpleX %1$s è registrato, ma non ha alcun link valido.</string> + <string name="simplex_name_unconfirmed_desc">Il nome SimpleX %1$s è registrato, ma non aggiunto al profilo. Aggiungilo al profilo del tuo indirizzo o canale, se sei il proprietario.</string> + <string name="simplex_name_owner_no_channel_link">Il nome SimpleX %1$s è registrato senza link del canale. Aggiungi il link del canale al nome tramite la pagina di registrazione.</string> + <string name="simplex_name_owner_no_address">Il nome SimpleX %1$s è registrato senza indirizzo SimpleX. Aggiungi il tuo indirizzo SimpleX al nome tramite la pagina di registrazione.</string> + <string name="simplex_name_not_found_desc">Questo nome SimpleX non è registrato. Controlla il nome.</string> + <string name="operator_use_for_names">Per risolvere nomi</string> + <string name="simplex_name_unconfirmed">Nome non confermato</string> + <string name="verify_simplex_name_action">Verifica nome</string> + <string name="verify_simplex_names">Verifica nomi SimpleX</string> + <string name="your_simplex_name">Il tuo nome SimpleX</string> + <string name="connect_plan_connect_to_name">Connetti a %s</string> + <string name="connect_plan_join_name">Entra nel canale %s</string> + <string name="do_not_require_message_signatures">Non richiedere la firma dei messaggi.</string> + <string name="message_signatures_are_not_required">La firma dei messaggi non è richiesta.</string> + <string name="message_signatures_are_required">La firma dei messaggi è richiesta.</string> + <string name="require_message_signatures">Richiedi la firma dei messaggi.</string> + <string name="show_encryption">Mostra la crittografia</string> + <string name="show_signature">Mostra la firma</string> + <string name="signature_missing_alert_title">Firma mancante</string> + <string name="info_row_signed">Firmato</string> + <string name="info_row_signed_verified">Firmato e verificato</string> + <string name="sign_message_desc">La firma dimostra che hai scritto questo messaggio e non può essere negato più tardi.</string> + <string name="sign_message">Firma il messaggio</string> + <string name="sign_messages">Firma i messaggi</string> + <string name="signature_missing_alert_desc">Il canale ha richiesto di firmare questo messaggio, ma la firma non è presente.</string> + <string name="channel_simplex_name">Nome SimpleX per il canale</string> + <string name="get_simplex_name_beta">Ottieni nome SimpleX (BETA)</string> + <string name="register_test_name">Registra un nome di prova</string> + <string name="remove_name">Rimuovi nome</string> + <string name="save_simplex_name_question">Salvare il nome SimpleX?</string> + <string name="to_verify_channel_member_key">Per verificare le chiavi con questo iscritto, confrontate (o scansionate) il codice sui vostri dispositivi.</string> + <string name="add_description">Aggiungi descrizione</string> + <string name="profile_description__field">Descrizione</string> + <string name="edit_description">Modifica descrizione</string> + <string name="enter_description_optional">Inserisci la descrizione (facoltativa)</string> + <string name="error_sharing_address">Errore di condivisione dell\'indirizzo</string> + <string name="v7_0_channels">Canali migliorati 📢</string> + <string name="v7_0_channels_previews">Crea un\'anteprima web.</string> + <string name="v7_0_channels_wider_messages">Lettura più facile.</string> + <string name="v7_0_channels_relays">Gestisci i tuoi relay.</string> + <string name="v7_0_simplex_names_descr">Nomi pubblici per il tuo canale o per il lavoro.</string> + <string name="v7_0_simplex_names">Nomi pubblici SimpleX (BETA)</string> + <string name="v7_0_channels_contributors">Aggiungi collaboratori.</string> + <string name="info_row_file_servers">Server di file</string> + <string name="share_text_file_servers">Server di file: %s</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index faf69dfd03..430acfa6c1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -182,7 +182,7 @@ <string name="group_member_status_complete">חיבור הושלם</string> <string name="group_member_status_connecting">מתחבר</string> <string name="change_verb">שנה</string> - <string name="change_member_role_question">לשנות תפקיד בקבוצה\?</string> + <string name="change_member_role_question">לשנות תפקיד בקבוצה?</string> <string name="change_role">שנה תפקיד</string> <string name="info_row_connection">חיבור</string> <string name="chat_preferences">העדפות צ׳אט</string> @@ -973,7 +973,7 @@ <string name="icon_descr_speaker_off">רמקול כבוי</string> <string name="icon_descr_speaker_on">רמקול פעיל</string> <string name="settings_section_title_settings">הגדרות</string> - <string name="settings_section_title_support">תמיכה ב־SIMPLEX CHAT</string> + <string name="settings_section_title_support">תמיכה ב־SimpleX Chat</string> <string name="stop_chat_question">לעצור צ׳אט\?</string> <string name="stop_chat_to_export_import_or_delete_chat_database">עיצרו את הצ׳אט כדי לייצא, לייבא או למחוק את מסד הנתונים. לא תוכלו לקבל ולשלוח הודעות בזמן שהצ׳אט מופסק.</string> <string name="stop_chat_confirmation">עצור</string> @@ -1039,8 +1039,7 @@ <string name="to_protect_privacy_simplex_has_ids_for_queues">כדי לשמור על הפרטיות, במקום מזהי משתמש הקיימים בכל הפלטפורמות האחרות, ל־SimpleX יש מזהים לתורי הודעות, נפרדים עבור כל אחד מאנשי הקשר שלך.</string> <string name="using_simplex_chat_servers">משתמש בשרתי SimpleX Chatז</string> <string name="trying_to_connect_to_server_to_receive_messages">מנסה להתחבר לשרת המשמש לקבלת הודעות מאיש קשר זה.</string> - <string name="connection_error_auth_desc">אלא אם איש הקשר שלכם מחק את החיבור או שהקישור הזה כבר היה בשימוש, זה עשוי להיות באג - אנא דווחו על כך. -\nכדי להתחבר, אנא בקשו מאיש הקשר שלכם ליצור קישור חיבור נוסף ובידקו שיש לכם חיבור יציב לרשת.</string> + <string name="connection_error_auth_desc">אלא אם איש הקשר שלכם מחק את החיבור או שהקישור הזה כבר היה בשימוש, זה עשוי להיות באג - אנא דווחו על כך. \nכדי להתחבר, אנא בקשו מאיש הקשר שלכם ליצור קישור חיבור נוסף ובידקו שיש לכם חיבור יציב לרשת.</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">כדי להגן על המידע שלכם, הפעילו את SimpleX Lock. \nתתבקשו להשלים את האימות לפני שתכונה זו תופעל.</string> <string name="smp_servers_use_server">השתמש בשרת</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 5c17946c24..e61e4e4372 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -200,7 +200,7 @@ <string name="connect_via_link">リンク経由で繋がる。</string> <string name="connection_error">接続エラー</string> <string name="group_member_status_introduced">接続待ち (紹介済み)</string> - <string name="connection_error_auth">接続エラー (AUTH)</string> + <string name="connection_error_auth">接続エラー</string> <string name="connection_timeout">接続タイムアウト</string> <string name="connection_request_sent">接続リクエストを送信しました!</string> <string name="connection_local_display_name">接続 %1$d</string> @@ -880,15 +880,14 @@ <string name="send_us_an_email">メールを送る</string> <string name="share_image">メディア共有…</string> <string name="simplex_link_mode">SimpleXリンク</string> - <string name="settings_section_title_support">SIMPLEX CHATを支援</string> + <string name="settings_section_title_support">SimpleX Chatを支援</string> <string name="smp_servers_test_servers">テストサーバ</string> <string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string> <string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに公開されます。</string> <string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string> <string name="error_connecting_to_server_to_receive_messages">このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: %1$s)。</string> - <string name="connection_error_auth_desc">使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。 -\n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。</string> + <string name="connection_error_auth_desc">使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。 \n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">接続を完了するには、連絡相手がオンラインになる必要があります。 \nこの接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。</string> <string name="verify_security_code">セキュリティコードを確認</string> @@ -2058,4 +2057,786 @@ <string name="appearance_bars_blur_radius">ぼかし</string> <string name="chat_list_contacts">連絡先</string> <string name="chat_list_favorites">お気に入り</string> + <string name="connect_use_incognito_profile">シークレットプロフィールを使用</string> + <string name="another_instance_title">アプリはすでに実行中です</string> + <string name="another_instance_not_responding">別のアプリインスタンスが実行中か、正常に終了しなかった可能性があります。それでも起動しますか?</string> + <string name="server_no_sub">購読なし</string> + <string name="not_connected_to_server_to_receive_messages_no_sub">この接続のメッセージを受信するためのサーバに接続されていません(購読なし)。</string> + <string name="voice_recording_not_supported">お使いのプラットフォームでは音声録音はサポートされていません</string> + <string name="e2ee_info_no_e2ee"><![CDATA[このチャンネルのメッセージは<b>エンドツーエンド暗号化されていません</b>。チャットリレーはこれらのメッセージを見ることができます。]]></string> + <string name="simplex_link_relay">SimpleXリレーアドレス</string> + <string name="no_media_servers_configured_for_private_routing">ファイルを受信するためのサーバがありません。</string> + <string name="for_chat_profile">チャットプロフィール %s の場合:</string> + <string name="errors_in_servers_configuration">サーバ設定にエラーがあります。</string> + <string name="no_chat_relays_enabled">有効なチャットリレーがありません。</string> + <string name="server_warning">サーバの警告</string> + <string name="error_accepting_operator_conditions">条件の同意エラー</string> + <string name="blocking_reason_content">コンテンツが利用条件に違反しています</string> + <string name="network_error_unknown_ca">サーバアドレスのフィンガープリントが証明書と一致しません:%1$s。</string> + <string name="network_error_broker_host_desc">サーバアドレスがネットワーク設定と互換性がありません:%1$s。</string> + <string name="network_error_broker_version_desc">サーバのバージョンがお使いのアプリと互換性がありません:%1$s。</string> + <string name="private_routing_timeout">プライベートルーティングのタイムアウト</string> + <string name="private_routing_error">プライベートルーティングのエラー</string> + <string name="private_routing_no_session">プライベートルーティングのセッションがありません</string> + <string name="smp_proxy_error_unknown_ca">転送サーバアドレスのフィンガープリントが証明書と一致しません:%1$s。</string> + <string name="smp_proxy_error_broker_host">転送サーバアドレスがネットワーク設定と互換性がありません:%1$s。</string> + <string name="smp_proxy_error_broker_version">転送サーバのバージョンがネットワーク設定と互換性がありません:%1$s。</string> + <string name="proxy_destination_error_unknown_ca">宛先サーバアドレスのフィンガープリントが証明書と一致しません:%1$s。</string> + <string name="proxy_destination_error_failed_to_connect">転送サーバ %1$s が宛先サーバ %2$s への接続に失敗しました。後でもう一度お試しください。</string> + <string name="please_try_later">後でもう一度お試しください。</string> + <string name="error_creating_report">報告の作成エラー</string> + <string name="error_accepting_member">メンバーの承諾エラー</string> + <string name="error_marking_member_support_chat_read">既読にする際のエラー</string> + <string name="error_deleting_member_support_chat">チャットの削除エラー</string> + <string name="file_not_approved_descr">Tor または VPN を使用しない場合、あなたのIPアドレスはこれらのXFTPリレーに表示されます:\n%1$s。</string> + <string name="unsupported_connection_link">サポートされていない接続リンク</string> + <string name="link_requires_newer_app_version_please_upgrade">このリンクには新しいバージョンのアプリが必要です。アプリをアップグレードするか、互換性のあるリンクを送るよう連絡先に依頼してください。</string> + <string name="unsupported_channel_name">サポートされていないチャンネル名</string> + <string name="unsupported_contact_name">サポートされていない連絡先名</string> + <string name="channel_name_requires_newer_app_version">チャンネル名による接続には新しいバージョンのアプリが必要です。</string> + <string name="contact_name_requires_newer_app_version">連絡先名による接続には新しいバージョンのアプリが必要です。</string> + <string name="please_upgrade_the_app">アプリをアップグレードしてください。</string> + <string name="channel_temporarily_unavailable">チャンネルは一時的に利用できません</string> + <string name="channel_no_active_relays_try_later">チャンネルにアクティブなリレーがありません。後でもう一度参加をお試しください。</string> + <string name="app_update_required">アプリの更新が必要です</string> + <string name="group_link_requires_newer_version">このグループには新しいバージョンのアプリが必要です。参加するにはアプリをアップデートしてください。</string> + <string name="connection_error_blocked">接続がブロックされました</string> + <string name="connection_error_blocked_desc">接続がサーバ運営者によってブロックされています:\n%1$s。</string> + <string name="connection_error_quota">未配信のメッセージ</string> + <string name="connection_error_quota_desc">接続が未配信メッセージの上限に達しました。連絡先がオフラインの可能性があります。</string> + <string name="error_rejecting_contact_request">連絡先リクエストの拒否エラー</string> + <string name="error_deleting_message">メッセージの削除エラー</string> + <string name="error_updating_chat_tags">チャットリストの更新エラー</string> + <string name="error_creating_chat_tags">チャットリストの作成エラー</string> + <string name="error_loading_chat_tags">チャットリストの読み込みエラー</string> + <string name="error_preparing_contact">チャットを開く際のエラー</string> + <string name="error_preparing_group">グループを開く際のエラー</string> + <string name="error_changing_user">プロフィールの変更エラー</string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomiデバイス</b>:通知を機能させるには、システム設定で自動起動を有効にしてください。]]></string> + <string name="message_delivery_warning_title">メッセージ配信の警告</string> + <string name="message_deleted_or_not_received_error_title">メッセージなし</string> + <string name="message_deleted_or_not_received_error_desc">このメッセージは削除されたか、まだ受信されていません。</string> + <string name="report_reason_alert_title">報告の理由は?</string> + <string name="report_archive_alert_title">報告をアーカイブしますか?</string> + <string name="report_archive_alert_title_nth">%d件の報告をアーカイブしますか?</string> + <string name="report_archive_alert_title_all">すべての報告をアーカイブしますか?</string> + <string name="report_archive_alert_desc">報告はあなたのためにアーカイブされます。</string> + <string name="report_archive_alert_desc_all">すべての報告があなたのためにアーカイブされます。</string> + <string name="report_archive_for_me">自分のみ</string> + <string name="report_archive_for_all_moderators">すべてのモデレーター向け</string> + <string name="snd_error_auth">鍵が間違っているか、不明な接続です。おそらくこの接続は削除されています。</string> + <string name="snd_error_proxy">転送サーバ:%1$s\nエラー:%2$s</string> + <string name="snd_error_proxy_relay">転送サーバ:%1$s\n宛先サーバのエラー:%2$s</string> + <string name="srv_error_host">サーバアドレスがネットワーク設定と互換性がありません。</string> + <string name="srv_error_version">サーバのバージョンがネットワーク設定と互換性がありません。</string> + <string name="file_error_auth">鍵が間違っているか、不明なファイルチャンクアドレスです。おそらくファイルは削除されています。</string> + <string name="file_error_blocked">ファイルがサーバ運営者によってブロックされています:\n%1$s。</string> + <string name="placeholder_search_images">画像を検索</string> + <string name="placeholder_search_videos">動画を検索</string> + <string name="placeholder_search_voice_messages">ボイスメッセージを検索</string> + <string name="placeholder_search_files">ファイルを検索</string> + <string name="placeholder_search_links">リンクを検索</string> + <string name="archive_report">報告をアーカイブ</string> + <string name="archive_reports">報告をアーカイブ</string> + <string name="delete_report">報告を削除</string> + <string name="report_verb">報告</string> + <string name="delete_messages_cannot_be_undone_warning">メッセージが削除されます - これは元に戻せません!</string> + <string name="delete_messages_mark_deleted_warning">メッセージは削除対象としてマークされます。受信者はこれらのメッセージを表示できます。</string> + <string name="moderate_messages_will_be_deleted_warning">メッセージはすべてのメンバーに対して削除されます。</string> + <string name="moderate_messages_will_be_marked_warning">メッセージはすべてのメンバーに対して検閲済みとしてマークされます。</string> + <string name="from_history">履歴から</string> + <string name="list_menu">リスト</string> + <string name="message_forwarded_title">メッセージを転送しました</string> + <string name="message_forwarded_desc">まだ直接接続がないため、メッセージは管理者によって転送されます。</string> + <string name="member_inactive_title">メンバーが非アクティブ</string> + <string name="member_inactive_desc">メンバーがアクティブになると、メッセージは後で配信される場合があります。</string> + <string name="group_preview_open_to_join">開いて参加</string> + <string name="group_preview_rejected">拒否されました</string> + <string name="talk_to_someone">誰かと話す</string> + <string name="let_someone_connect_to_you">誰かに接続してもらう</string> + <string name="connect_via_link_or_qr_code">リンクまたはQRコードで接続</string> + <string name="connect_with_someone">リンクを作成</string> + <string name="invite_someone_privately">誰かをプライベートに招待</string> + <string name="a_link_for_one_person">1人が接続するためのリンク</string> + <string name="create_your_public_address">公開アドレスを作成</string> + <string name="your_public_address">あなたの公開アドレス</string> + <string name="for_anyone_to_reach_you">誰でもあなたに連絡できるように</string> + <string name="no_chats_in_list">リスト %s にチャットがありません。</string> + <string name="no_unread_chats">未読のチャットはありません</string> + <string name="no_chats">チャットがありません</string> + <string name="no_chats_found">チャットが見つかりません</string> + <string name="open_to_connect">開いて接続</string> + <string name="open_to_use_bot">開いてボットを使用</string> + <string name="open_to_accept">開いて承諾</string> + <string name="contact_should_accept">連絡先が承諾する必要があります…</string> + <string name="selected_chat_items_nothing_selected">何も選択されていません</string> + <string name="forward_alert_title_nothing_to_forward">転送するものがありません!</string> + <string name="forward_alert_forward_messages_without_files">ファイルなしでメッセージを転送しますか?</string> + <string name="forward_files_messages_deleted_after_selection_desc">メッセージは選択後に削除されました。</string> + <string name="chat_list_groups">グループ</string> + <string name="chat_list_channels">チャンネル</string> + <string name="chat_list_businesses">ビジネス</string> + <string name="chat_list_notes">ノート</string> + <string name="chat_list_group_reports">報告</string> + <string name="notification_group_report">報告:%s</string> + <string name="group_reports_active">%d件の報告</string> + <string name="group_reports_member_reports">メンバーからの報告</string> + <string name="group_new_support_messages">%d件のメッセージ</string> + <string name="group_new_support_chats">メンバーとの%d件のチャット</string> + <string name="group_new_support_chat_one">メンバーとの1件のチャット</string> + <string name="group_new_support_chats_short">%d件のチャット</string> + <string name="chat_banner_connect_to_chat">「接続」をタップしてチャット</string> + <string name="chat_banner_send_request_to_connect">「接続」をタップしてリクエストを送信</string> + <string name="chat_banner_connect_to_use_bot">「接続」をタップしてボットを使用</string> + <string name="chat_banner_accept_contact_request">連絡先リクエストを承諾</string> + <string name="chat_banner_your_contact">あなたの連絡先</string> + <string name="chat_banner_bot">ボット</string> + <string name="chat_banner_join_group">「グループに参加」をタップ</string> + <string name="chat_banner_join_channel">「チャンネルに参加」をタップ</string> + <string name="chat_banner_your_group">あなたのグループ</string> + <string name="chat_banner_your_channel">あなたのチャンネル</string> + <string name="chat_banner_group">グループ</string> + <string name="chat_banner_channel">チャンネル</string> + <string name="chat_banner_business_connection">ビジネス接続</string> + <string name="chat_banner_your_business_contact">あなたのビジネス連絡先</string> + <string name="forward_multiple">メッセージを転送…</string> + <string name="share_channel">チャンネルを共有…</string> + <string name="cannot_share_message_alert_text">選択したチャットの設定によりこのメッセージは禁止されています。</string> + <string name="share_via_chat">チャットで共有</string> + <string name="tap_to_open">タップして開く</string> + <string name="chat_link_channel">チャンネルのリンク</string> + <string name="chat_link_group">グループのリンク</string> + <string name="chat_link_business_address">ビジネスアドレス</string> + <string name="chat_link_contact_address">連絡先アドレス</string> + <string name="chat_link_one_time">ワンタイムリンク</string> + <string name="chat_link_from_owner">(オーナーから)</string> + <string name="error_sharing_channel">チャンネルの共有エラー</string> + <string name="owner_verification_passed">リンクの署名が検証されました。</string> + <string name="owner_verification_failed">⚠️ 署名の検証に失敗しました:%s。</string> + <string name="compose_save_messages_n">%1$s件のメッセージを保存中</string> + <string name="maximum_message_size_title">メッセージが大きすぎます!</string> + <string name="maximum_message_size_reached_text">メッセージのサイズを小さくして、もう一度送信してください。</string> + <string name="maximum_message_size_reached_non_text">メッセージのサイズを小さくするかメディアを削除して、もう一度送信してください。</string> + <string name="maximum_message_size_reached_forwarding">メッセージをコピーしてサイズを小さくすれば送信できます。</string> + <string name="report_compose_reason_header_spam">スパムを報告:グループのモデレーターのみが見ることができます。</string> + <string name="report_compose_reason_header_profile">メンバーのプロフィールを報告:グループのモデレーターのみが見ることができます。</string> + <string name="report_compose_reason_header_community">違反を報告:グループのモデレーターのみが見ることができます。</string> + <string name="report_compose_reason_header_illegal">コンテンツを報告:グループのモデレーターのみが見ることができます。</string> + <string name="report_compose_reason_header_other">その他を報告:グループのモデレーターのみが見ることができます。</string> + <string name="report_sent_alert_title">モデレーターに報告を送信しました</string> + <string name="report_sent_alert_msg_view_in_support_chat">報告は「管理者とのチャット」で確認できます。</string> + <string name="compose_view_join_group">グループに参加</string> + <string name="compose_view_join_channel">チャンネルに参加</string> + <string name="compose_view_broadcast">ブロードキャスト</string> + <string name="compose_view_add_message">メッセージを追加</string> + <string name="compose_view_connect">接続</string> + <string name="compose_view_send_contact_request_alert_question">連絡先リクエストを送信しますか?</string> + <string name="compose_view_send_contact_request_alert_text"><![CDATA[メッセージを送信できるのは<b>リクエストが承諾された後のみ</b>です。]]></string> + <string name="compose_view_send_request_without_message">メッセージなしでリクエストを送信</string> + <string name="compose_view_send_request">リクエストを送信</string> + <string name="cant_send_message_alert_title">メッセージを送信できません!</string> + <string name="cant_send_message_contact_not_ready">連絡先の準備ができていません</string> + <string name="cant_send_message_request_is_sent">リクエストを送信済み</string> + <string name="cant_send_message_contact_deleted">連絡先が削除されました</string> + <string name="cant_send_message_contact_not_synchronized">同期されていません</string> + <string name="cant_send_message_contact_disabled">連絡先が無効です</string> + <string name="you_are_subscriber">あなたは購読者です</string> + <string name="channel_role_label">チャンネル</string> + <string name="cant_send_message_rejected">参加リクエストが拒否されました</string> + <string name="cant_send_message_group_deleted">グループが削除されました</string> + <string name="cant_send_message_mem_removed">グループから削除されました</string> + <string name="cant_send_message_you_left">退出しました</string> + <string name="cant_send_message_generic">メッセージを送信できません</string> + <string name="cant_broadcast_message">ブロードキャストできません</string> + <string name="reviewed_by_admins">管理者によりレビュー済み</string> + <string name="cant_send_message_member_has_old_version">メンバーが古いバージョンを使用しています</string> + <string name="cant_send_commands_alert_text">コマンドを送信するには接続している必要があります。</string> + <string name="temporary_file_error">一時的なファイルエラー</string> + <string name="open_with_app">%s で開く</string> + <string name="disable_automatic_deletion_question">自動メッセージ削除を無効にしますか?</string> + <string name="change_automatic_deletion_question">自動メッセージ削除を変更しますか?</string> + <string name="disable_automatic_deletion_message">このチャットのメッセージは削除されません。</string> + <string name="change_automatic_chat_deletion_message">この操作は元に戻せません - このチャットで選択した時点より前に送受信したメッセージは削除されます。</string> + <string name="disable_automatic_deletion">メッセージ削除を無効にする</string> + <string name="chat_ttl_options_footer">デバイスからチャットメッセージを削除します。</string> + <string name="info_view_open_button">開く</string> + <string name="info_view_search_button">検索</string> + <string name="keep_conversation">会話を保持</string> + <string name="only_delete_conversation">会話のみ削除</string> + <string name="you_can_still_send_messages_to_contact">アーカイブ済みの連絡先から %1$s にメッセージを送信できます。</string> + <string name="you_can_still_view_conversation_with_contact">チャット一覧で %1$s との会話を引き続き表示できます。</string> + <string name="text_field_set_chat_placeholder">チャット名を設定…</string> + <string name="sync_connection_question">接続を修復しますか?</string> + <string name="sync_connection_desc">接続には暗号化の再ネゴシエーションが必要です。</string> + <string name="sync_connection_confirm">修復</string> + <string name="encryption_renegotiation_in_progress">暗号化の再ネゴシエーションを実行中です。</string> + <string name="accept_contact_request">連絡先リクエストを承諾</string> + <string name="reject_contact_request">連絡先リクエストを拒否</string> + <string name="the_sender_will_not_be_notified">送信者には通知されません。</string> + <string name="member_is_deleted_cant_accept_request">メンバーが削除されています - リクエストを承諾できません</string> + <string name="mute_all_chat">すべてミュート</string> + <string name="unread_mentions">未読のメンション</string> + <string name="change_list">リストを変更</string> + <string name="duplicated_list_error">リスト名と絵文字はすべてのリストで異なる必要があります。</string> + <string name="change_order_chat_list_menu_action">順序を変更</string> + <string name="share_address_publicly">アドレスを公開で共有</string> + <string name="share_simplex_address_on_social_media">SimpleXアドレスをSNSで共有します。</string> + <string name="share_1_time_link_with_a_friend">ワンタイムリンクを友達と共有</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[ワンタイムリンクは<i>1人の連絡先とのみ</i>使用できます - 対面または任意のメッセンジャーで共有してください。]]></string> + <string name="you_can_set_connection_name_to_remember">リンクを誰と共有したか覚えておくために、接続名を設定できます。</string> + <string name="connection_security">接続のセキュリティ</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleXアドレスとワンタイムリンクは任意のメッセンジャーで安全に共有できます。</string> + <string name="to_protect_against_your_link_replaced_compare_codes">リンクが置き換えられるのを防ぐため、連絡先のセキュリティコードを比較できます。</string> + <string name="full_link_button_text">完全なリンク</string> + <string name="short_link_button_text">短縮リンク</string> + <string name="new_chat_share_profile">プロフィールを共有</string> + <string name="select_chat_profile">チャットプロフィールを選択</string> + <string name="switching_profile_error_message">接続は %s に移動されましたが、プロフィールの切り替え時にエラーが発生しました。</string> + <string name="loading_profile">プロフィールを読み込み中…</string> + <string name="no_filtered_contacts">フィルタされた連絡先はありません</string> + <string name="context_user_picker_your_profile">あなたのプロフィール</string> + <string name="context_user_picker_cant_change_profile_alert_title">プロフィールを変更できません</string> + <string name="context_user_picker_cant_change_profile_alert_message">接続試行後に別のプロフィールを使用するには、チャットを削除してリンクをもう一度使用してください。</string> + <string name="smp_servers_other">その他のSMPサーバ</string> + <string name="smp_servers_new_server">新しいサーバ</string> + <string name="xftp_servers_other">その他のXFTPサーバ</string> + <string name="network_proxy_auth">プロキシ認証</string> + <string name="network_proxy_random_credentials">ランダムな認証情報を使用</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">プロフィールごとに異なるプロキシ認証情報を使用します。</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">接続ごとに異なるプロキシ認証情報を使用します。</string> + <string name="network_proxy_auth_mode_username_password">認証情報は暗号化されずに送信される場合があります。</string> + <string name="network_proxy_username">ユーザー名</string> + <string name="network_proxy_incorrect_config_desc">プロキシ設定が正しいことを確認してください。</string> + <string name="network_session_mode_server">サーバ</string> + <string name="network_session_mode_session_description">アプリを起動するたびに新しいSOCKS認証情報が使用されます。</string> + <string name="network_session_mode_server_description">サーバごとに新しいSOCKS認証情報が使用されます。</string> + <string name="network_smp_proxy_mode_unknown_description">不明なサーバでプライベートルーティングを使用します。</string> + <string name="network_smp_proxy_mode_unprotected_description">IPアドレスが保護されていない場合に、不明なサーバでプライベートルーティングを使用します。</string> + <string name="network_smp_proxy_fallback_allow_protected">IPが隠されている場合</string> + <string name="network_smp_proxy_fallback_allow_description">あなたまたは宛先のサーバがプライベートルーティングをサポートしていない場合、メッセージを直接送信します。</string> + <string name="network_smp_proxy_fallback_allow_protected_description">IPアドレスが保護されており、かつあなたまたは宛先のサーバがプライベートルーティングをサポートしていない場合、メッセージを直接送信します。</string> + <string name="update_network_smp_proxy_fallback_question">メッセージルーティングのフォールバック</string> + <string name="private_routing_explanation">IPアドレスを保護するため、プライベートルーティングはあなたのSMPサーバを使用してメッセージを配信します。</string> + <string name="network_smp_web_port_section_title">メッセージング用のTCPポート</string> + <string name="network_smp_web_port_toggle">Webポートを使用</string> + <string name="network_smp_web_port_footer">ポートが指定されていない場合、TCPポート %1$s を使用します。</string> + <string name="network_smp_web_port_preset_footer">プリセットサーバのみTCPポート443を使用します。</string> + <string name="network_smp_web_port_all">すべてのサーバ</string> + <string name="network_smp_web_port_preset">プリセットサーバ</string> + <string name="network_smp_web_port_off">オフ</string> + <string name="app_check_for_updates_stable">安定版</string> + <string name="app_check_for_updates_update_available">アップデートあり:%s</string> + <string name="app_check_for_updates_button_skip">このバージョンをスキップ</string> + <string name="app_check_for_updates_button_open">ファイルの場所を開く</string> + <string name="app_check_for_updates_button_install">アップデートをインストール</string> + <string name="app_check_for_updates_installed_successfully_title">インストールに成功しました</string> + <string name="app_check_for_updates_installed_successfully_desc">アプリを再起動してください。</string> + <string name="app_check_for_updates_canceled">アップデートのダウンロードをキャンセルしました</string> + <string name="app_check_for_updates_button_remind_later">後で通知</string> + <string name="app_check_for_updates_notice_desc">新しいリリースの通知を受け取るには、安定版またはベータ版の定期チェックをオンにしてください。</string> + <string name="deprecated_options_section">非推奨のオプション</string> + <string name="prefs_error_saving_settings">設定の保存エラー</string> + <string name="sent_to_your_contact_after_connection">接続後に連絡先に送信されます。</string> + <string name="address_welcome_message">ウェルカムメッセージ</string> + <string name="or_to_share_privately">またはプライベートに共有する場合</string> + <string name="simplex_address_or_1_time_link">SimpleXアドレスまたはワンタイムリンク?</string> + <string name="new_1_time_link">新しいワンタイムリンク</string> + <string name="onboarding_send_1_time_link">任意のメッセンジャーでリンクを送信してください - 安全です。SimpleXに貼り付けるよう依頼してください。</string> + <string name="onboarding_or_show_qr_code">または対面やビデオ通話でQRを表示してください。</string> + <string name="onboarding_post_address">このアドレスをSNSのプロフィール、ウェブサイト、またはメールの署名で使用してください。</string> + <string name="onboarding_or_use_qr_code">またはこのQRを使用してください - 印刷するかオンラインで表示します。</string> + <string name="business_address">ビジネスアドレス</string> + <string name="add_short_link">アドレスをアップグレード</string> + <string name="share_profile_via_link">アドレスをアップグレードしますか?</string> + <string name="share_profile_via_link_alert_text">アドレスが短くなり、あなたのプロフィールがそのアドレスを通じて共有されます。</string> + <string name="share_profile_via_link_alert_confirm">アップグレード</string> + <string name="upgrade_group_link">グループリンクをアップグレード</string> + <string name="share_group_profile_via_link">グループリンクをアップグレードしますか?</string> + <string name="share_group_profile_via_link_alert_text">リンクが短くなり、グループのプロフィールがそのリンクを通じて共有されます。</string> + <string name="share_old_address_alert_button">古いアドレスを共有</string> + <string name="share_old_link_alert_button">古いリンクを共有</string> + <string name="save_admission_question">参加承認の設定を保存しますか?</string> + <string name="save_and_notify_channel_subscribers">保存してチャンネルの購読者に通知</string> + <string name="short_descr">あなたの自己紹介:</string> + <string name="onboarding_be_free">あなたのネットワークで\n自由に</string> + <string name="onboarding_private_and_secure">プライベートで安全なメッセージング。</string> + <string name="onboarding_first_network">連絡先とグループを\nあなた自身が所有する初めてのネットワーク。</string> + <string name="get_started">始める</string> + <string name="why_simplex_is_built">SimpleXが作られた理由。</string> + <string name="onboarding_your_profile">あなたのプロフィール</string> + <string name="onboarding_on_your_phone">サーバではなく、あなたのスマートフォンに。</string> + <string name="onboarding_no_account">アカウント不要。電話番号不要。メール不要。ID不要。\n最も安全な暗号化。</string> + <string name="enter_profile_name">プロフィール名を入力…</string> + <string name="migrate">移行</string> + <string name="why_built_heading">あなたはアカウントなしで生まれた。</string> + <string name="why_built_p1">誰もあなたの会話を追跡しなかった。あなたがどこにいたかの地図を描く者もいなかった。プライバシーは機能などではなく、生き方そのものだった。</string> + <string name="why_built_p2">やがて私たちはオンラインに移り、あらゆるプラットフォームがあなたの一部を求めた — 名前、電話番号、友人。他者と話す代償として、誰と話しているかを誰かに知られることを、私たちは受け入れてしまった。電話、メール、メッセンジャー、ソーシャルメディア — 世代を超えて、人もテクノロジーもそうであり続けた。それが唯一の方法に思えた。</string> + <string name="why_built_p3">別の方法がある。電話番号のないネットワーク。ユーザー名もない。アカウントもない。いかなる種類のユーザー識別子もない。誰が接続しているかを知ることなく、人々をつなぎ、暗号化されたメッセージを運ぶネットワーク。</string> + <string name="why_built_p4">他人のドアに付いた、より良い錠ではない。あなたのプライバシーを尊重しつつも、すべての訪問者の記録を残し続ける、より親切な家主でもない。あなたは客ではない。あなたは我が家にいる。どんな王もそこには入れない — あなたは主権者だ。</string> + <string name="why_built_p5">あなたの会話はあなたのものだ。インターネット以前は常にそうだったように。ネットワークはあなたが訪れる場所ではない。あなたが作り、所有する場所だ。そしてそれをプライベートにしようと公開にしようと、誰もあなたから奪うことはできない。</string> + <string name="why_built_p6">監視されることなく他者と話すという、人類最古の自由を — それを裏切ることのできないインフラの上に築く。</string> + <string name="why_built_p7">私たちは、あなたが誰であるかを知る力を破壊したからだ。あなたの力が決して奪われないように。</string> + <string name="why_built_tagline">あなたのネットワークで自由に。</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[すべてのメッセージとファイルは<b>エンドツーエンドで暗号化</b>されて送信され、ダイレクトメッセージではポスト量子暗号で保護されます。]]></string> + <string name="onboarding_notifications_mode_battery">通知とバッテリー</string> + <string name="onboarding_network_operators">ネットワーク運営者</string> + <string name="onboarding_network_operators_app_will_use_different_operators">アプリは会話ごとに異なる運営者を使用することで、あなたのプライバシーを保護します。</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">複数の運営者が有効な場合、どの運営者も誰が誰と通信しているかを知るためのメタデータを持ちません。</string> + <string name="onboarding_network_operators_app_will_use_for_routing">例えば、あなたの連絡先がSimpleX Chatのサーバ経由でメッセージを受信する場合、あなたのアプリはFluxのサーバ経由でそれらを配信します。</string> + <string name="onboarding_select_network_operators_to_use">使用するネットワーク運営者を選択してください。</string> + <string name="how_it_helps_privacy">プライバシーにどう役立つか</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">有効な運営者の条件は30日後に承諾されます。</string> + <string name="onboarding_network_operators_conditions_you_can_configure">運営者は「ネットワークとサーバ」設定で構成できます。</string> + <string name="onboarding_network_operators_review_later">後で確認</string> + <string name="onboarding_network_operators_update">更新</string> + <string name="onboarding_your_network">あなたのネットワーク</string> + <string name="onboarding_network_routers_cannot_know">ネットワークのルーターは\n誰が誰と話しているかを知ることができません</string> + <string name="onboarding_configure_routers">ルーターを設定</string> + <string name="onboarding_configure_notifications">通知を設定</string> + <string name="onboarding_network_commitments">ネットワークの取り組み</string> + <string name="call_desktop_permission_denied_title">通話するにはマイクの使用を許可してください。通話を終了して、もう一度かけ直してください。</string> + <string name="call_desktop_permission_denied_safari">Safariの「設定」/「Webサイト」/「マイク」を開き、localhost に対して「許可」を選択してください。</string> + <string name="open_external_link_title">外部リンクを開きますか?</string> + <string name="icon_descr_sound_muted">サウンドをミュート中</string> + <string name="rcv_msg_error_dropped">破棄されました(%1$d回試行)</string> + <string name="rcv_msg_error_parse">エラー:%s</string> + <string name="alert_title_msg_error">メッセージエラー</string> + <string name="alert_text_msg_reception_error">アプリは %1$d 回の受信試行の後、このメッセージを削除しました。</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">アプリは不明なファイルサーバからのダウンロードの確認を求めます(.onion の場合、またはSOCKSプロキシが有効な場合を除く)。</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Tor または VPN を使用しない場合、あなたのIPアドレスはファイルサーバに表示されます。</string> + <string name="sanitize_links_toggle">リンクのトラッキングを削除</string> + <string name="this_setting_is_for_your_current_profile">この設定は現在のプロフィールに適用されます</string> + <string name="privacy_chat_list_open_links">チャット一覧からリンクを開く</string> + <string name="privacy_chat_list_open_links_yes">はい</string> + <string name="privacy_chat_list_open_links_no">いいえ</string> + <string name="privacy_chat_list_open_links_ask">確認する</string> + <string name="privacy_chat_list_open_web_link_question">Webリンクを開きますか?</string> + <string name="privacy_chat_list_open_web_link">リンクを開く</string> + <string name="privacy_chat_list_open_full_web_link">完全なリンクを開く</string> + <string name="privacy_chat_list_open_clean_web_link">クリーンなリンクを開く</string> + <string name="settings_section_title_contact_requests_from_groups">グループからの連絡先リクエスト</string> + <string name="remote_hosts_section">リモートのモバイル端末</string> + <string name="chat_item_ttl_default">デフォルト(%s)</string> + <string name="chat_database_exported_save">エクスポートしたアーカイブを保存できます。</string> + <string name="chat_database_exported_migrate">エクスポートしたデータベースを移行できます。</string> + <string name="chat_database_exported_not_all_files">一部のファイルはエクスポートされませんでした</string> + <string name="error_saving_database">データベースの保存エラー</string> + <string name="error_reading_passphrase">データベースのパスフレーズの読み取りエラー</string> + <string name="restore_passphrase_can_not_be_read_desc">キーストア内のパスフレーズを読み取れません。アプリと互換性のないシステム更新の後に発生した可能性があります。そうでない場合は、開発者にお問い合わせください。</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">キーストア内のパスフレーズを読み取れません。手動で入力してください。アプリと互換性のないシステム更新の後に発生した可能性があります。そうでない場合は、開発者にお問い合わせください。</string> + <string name="chat_bottom_bar">手の届くチャットツールバー</string> + <string name="one_hand_ui_bottom_bar">下部のバー</string> + <string name="one_hand_ui_top_bar">上部のバー</string> + <string name="chat_list_always_visible">新しいウィンドウでチャット一覧を表示</string> + <string name="down_migration_warning_chat_relays">チャンネルに参加または作成した場合、それらは恒久的に機能しなくなります。</string> + <string name="leave_channel_question">チャンネルから退出しますか?</string> + <string name="leave_chat_question">チャットから退出しますか?</string> + <string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">このチャンネルからのメッセージを受信しなくなります。チャット履歴は保持されます。</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">このチャットからのメッセージを受信しなくなります。チャット履歴は保持されます。</string> + <string name="rcv_direct_event_group_inv_link_received">グループ %1$s からの接続をリクエストしました</string> + <string name="rcv_group_event_member_accepted">%1$s を承諾しました</string> + <string name="rcv_group_event_user_accepted">あなたを承諾しました</string> + <string name="rcv_channel_event_channel_deleted">チャンネルを削除しました</string> + <string name="rcv_channel_event_updated_channel_profile">チャンネルのプロフィールを更新しました</string> + <string name="rcv_group_event_new_member_pending_review">新しいメンバーがグループへの参加を希望しています。</string> + <string name="snd_channel_event_channel_profile_updated">チャンネルのプロフィールを更新しました</string> + <string name="snd_group_event_member_accepted">このメンバーを承諾しました</string> + <string name="snd_group_event_user_pending_review">グループのモデレーターがグループへの参加リクエストを確認するまでお待ちください。</string> + <string name="rcv_channel_events_count">%d件のチャンネルイベント</string> + <string name="group_member_role_moderator">モデレーター</string> + <string name="group_member_role_relay">リレー</string> + <string name="group_member_status_rejected">拒否されました</string> + <string name="group_member_status_pending_approval">承認待ち</string> + <string name="group_member_status_pending_approval_short">保留中</string> + <string name="group_member_status_pending_review">審査待ち</string> + <string name="group_member_status_pending_review_short">審査</string> + <string name="invite_to_chat_button">チャットに招待</string> + <string name="button_delete_channel">チャンネルを削除</string> + <string name="button_cancel_and_delete_channel">キャンセルしてチャンネルを削除</string> + <string name="button_delete_chat">チャットを削除</string> + <string name="delete_channel_question">チャンネルを削除しますか?</string> + <string name="delete_chat_question">チャットを削除しますか?</string> + <string name="delete_channel_for_all_subscribers_cannot_undo_warning">チャンネルはすべての購読者に対して削除されます - これは元に戻せません!</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">チャットはすべてのメンバーに対して削除されます - これは元に戻せません!</string> + <string name="delete_channel_for_self_cannot_undo_warning">チャンネルはあなたに対して削除されます - これは元に戻せません!</string> + <string name="delete_chat_for_self_cannot_undo_warning">チャットはあなたに対して削除されます - これは元に戻せません!</string> + <string name="button_leave_channel">チャンネルから退出</string> + <string name="button_leave_chat">チャットから退出</string> + <string name="button_edit_channel_profile">チャンネルのプロフィールを編集</string> + <string name="channel_link">チャンネルのリンク</string> + <string name="you_can_share_channel_link_anybody_will_be_able_to_connect">リンクまたはQRコードを共有できます - 誰でもチャンネルに参加できます。</string> + <string name="only_channel_owners_can_change_prefs">チャンネルの設定を変更できるのはチャンネルのオーナーだけです。</string> + <string name="only_chat_owners_can_change_prefs">設定を変更できるのはチャットのオーナーだけです。</string> + <string name="action_button_channel_link">リンク</string> + <string name="button_support_chat">管理者とのチャット</string> + <string name="button_channel_members">チャンネルのメンバー</string> + <string name="button_channel_relays">チャットリレー</string> + <string name="button_remove_subscriber_question">購読者を削除しますか?</string> + <string name="button_remove_members_question">メンバーを削除しますか?</string> + <string name="button_delete_member_messages_question">メンバーのメッセージを削除しますか?</string> + <string name="button_delete_member_messages">メンバーのメッセージを削除</string> + <string name="button_support_chat_member">メンバーとのチャット</string> + <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">購読者はチャンネルから削除されます - これは元に戻せません!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">メンバーはグループから削除されます - これは元に戻せません!</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">メンバーはチャットから削除されます - これは元に戻せません!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">メンバーはチャットから削除されます - これは元に戻せません!</string> + <string name="member_messages_will_be_deleted_cannot_be_undone">メンバーのメッセージは削除されます - これは元に戻せません!</string> + <string name="remove_member_delete_messages_confirmation">メンバーを削除してメッセージも削除</string> + <string name="delete_member_messages_confirmation">メッセージを削除</string> + <string name="block_members_for_all_question">全員に対してメンバーをブロックしますか?</string> + <string name="unblock_members_for_all_question">全員に対してメンバーのブロックを解除しますか?</string> + <string name="unblock_members_desc">これらのメンバーからのメッセージが表示されます!</string> + <string name="member_info_member_failed">失敗</string> + <string name="member_role_will_be_changed_with_notification_chat">役割が「%s」に変更されます。チャットの全員に通知されます。</string> + <string name="info_row_chat">チャット</string> + <string name="info_row_connection_failed">接続に失敗しました</string> + <string name="message_queue_info">メッセージキュー情報</string> + <string name="message_queue_info_none">なし</string> + <string name="message_queue_info_server_info">サーバキュー情報:%1$s\n\n最後に受信したメッセージ:%2$s</string> + <string name="you_need_to_allow_calls">相手に発信できるようにするには、連絡先からの通話を許可する必要があります。</string> + <string name="calls_prohibited_ask_to_enable_calls_alert_text">連絡先に通話を有効にするよう依頼してください。</string> + <string name="cant_call_member_send_message_alert_text">通話を有効にするにはメッセージを送信してください。</string> + <string name="connection_not_ready">接続の準備ができていません。</string> + <string name="channel_full_name_field">チャンネルのフルネーム:</string> + <string name="group_short_descr_field">短い説明:</string> + <string name="group_descr_too_large">説明が長すぎます</string> + <string name="chat_main_profile_sent">あなたのチャットプロフィールはチャットのメンバーに送信されます</string> + <string name="channel_profile_is_stored_on_subscribers_devices">チャンネルのプロフィールは購読者のデバイスとチャットリレーに保存されます。</string> + <string name="save_channel_profile">チャンネルのプロフィールを保存</string> + <string name="error_saving_channel_profile">チャンネルのプロフィールの保存エラー</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">有効な運営者の条件は次の日に自動的に同意されます:%s。</string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[次の運営者の条件に同意します:<b>%s</b>。]]></string> + <string name="operator">運営者</string> + <string name="operator_servers_title">%s のサーバ</string> + <string name="operator_info_title">ネットワーク運営者</string> + <string name="operator_conditions_accepted_on">条件に同意した日:%s。</string> + <string name="operator_conditions_will_be_accepted_on">条件に同意する日:%s。</string> + <string name="use_servers_of_operator_x">%s を使用</string> + <string name="operator_conditions_failed_to_load">現在の条件のテキストを読み込めませんでした。このリンクから条件を確認できます:</string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[同じ条件が運営者 <b>%s</b> に適用されます。]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[同じ条件が次の運営者に適用されます:<b>%s</b>。]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[これらの条件は次にも適用されます:<b>%s</b>。]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[次の運営者の条件に同意します:<b>%s</b>。]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[これらの条件は次にも適用されます:<b>%s</b>。]]></string> + <string name="view_conditions">条件を表示</string> + <string name="operator_updated_conditions">更新された条件</string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[<b>%s</b> のサーバを使用するには、利用条件に同意してください。]]></string> + <string name="operator_use_for_messages">メッセージに使用</string> + <string name="operator_use_for_messages_receiving">受信用</string> + <string name="operator_use_for_messages_private_routing">プライベートルーティング用</string> + <string name="operator_use_for_files">ファイルに使用</string> + <string name="operator_use_for_sending">送信用</string> + <string name="xftp_servers_per_user">現在のチャットプロフィールの新しいファイル用のサーバ</string> + <string name="operator_added_xftp_servers">追加されたメディア・ファイルサーバ</string> + <string name="error_updating_server_title">サーバの更新エラー</string> + <string name="error_server_protocol_changed">サーバのプロトコルが変更されました。</string> + <string name="error_server_operator_changed">サーバの運営者が変更されました。</string> + <string name="operator_server_alert_title">運営者のサーバ</string> + <string name="server_added_to_operator__name">サーバが運営者 %s に追加されました。</string> + <string name="error_adding_server">サーバの追加エラー</string> + <string name="network_option_tcp_connection">TCP接続</string> + <string name="network_option_tcp_connection_timeout_background">TCP接続のバックグラウンドタイムアウト</string> + <string name="network_option_protocol_timeout_background">プロトコルのバックグラウンドタイムアウト</string> + <string name="appearance_zoom">ズーム</string> + <string name="system_mode_toast">システムモード</string> + <string name="wallpaper_scale_repeat">繰り返し</string> + <string name="channel_preferences">チャンネルの設定</string> + <string name="set_member_admission">メンバーの参加承認を設定</string> + <string name="time_to_disappear_is_set_only_for_new_contacts">消えるまでの時間は新しい連絡先にのみ設定されます。</string> + <string name="allow_your_contacts_to_send_files_and_media">連絡先がファイルやメディアを送信できるようにします。</string> + <string name="allow_files_and_media_only_if">連絡先が許可している場合にのみ、ファイルやメディアを許可します。</string> + <string name="prohibit_sending_files_and_media">ファイルやメディアの送信を禁止します。</string> + <string name="both_you_and_your_contact_can_send_files">あなたと連絡先の両方がファイルやメディアを送信できます。</string> + <string name="only_you_can_send_files">あなただけがファイルやメディアを送信できます。</string> + <string name="only_your_contact_can_send_files">連絡先だけがファイルやメディアを送信できます。</string> + <string name="files_prohibited_in_this_chat">このチャットではファイルやメディアは禁止されています。</string> + <string name="disable_sending_member_reports">モデレーターへのメッセージの報告を禁止します。</string> + <string name="direct_messages_are_prohibited">メンバー間のダイレクトメッセージは禁止されています。</string> + <string name="direct_messages_are_prohibited_in_chat">このチャットではメンバー間のダイレクトメッセージは禁止されています。</string> + <string name="group_members_can_send_reports">メンバーはメッセージをモデレーターに報告できます。</string> + <string name="member_reports_are_prohibited">このグループではメッセージの報告は禁止されています。</string> + <string name="chat_with_admins">管理者とのチャット</string> + <string name="allow_chat_with_admins">メンバーが管理者とチャットできるようにします。</string> + <string name="prohibit_chat_with_admins">管理者とのチャットを禁止します。</string> + <string name="members_can_chat_with_admins">メンバーは管理者とチャットできます。</string> + <string name="chat_with_admins_is_prohibited">管理者とのチャットは禁止されています。</string> + <string name="chat_with_admins_relay_note">公開チャンネルでの管理者とのチャットにはエンドツーエンド暗号化がありません - 信頼できるチャットリレーでのみ使用してください。</string> + <string name="enable_chats_with_admins_question">管理者とのチャットを有効にしますか?</string> + <string name="enable_chats_with_admins">有効にする</string> + <string name="group_reports_subscriber_reports">購読者からの報告</string> + <string name="allow_direct_messages_channel">購読者へのダイレクトメッセージの送信を許可します。</string> + <string name="prohibit_direct_messages_channel">購読者へのダイレクトメッセージの送信を禁止します。</string> + <string name="enable_sending_recent_history_channel">新しい購読者に最新100件までのメッセージを送信します。</string> + <string name="disable_sending_recent_history_channel">新しい購読者に履歴を送信しません。</string> + <string name="group_members_can_send_disappearing_channel">購読者は消えるメッセージを送信できます。</string> + <string name="group_members_can_send_dms_channel">購読者はダイレクトメッセージを送信できます。</string> + <string name="direct_messages_are_prohibited_channel">購読者間のダイレクトメッセージは禁止されています。</string> + <string name="group_members_can_delete_channel">購読者は送信したメッセージを元に戻せない形で削除できます。(24時間)</string> + <string name="group_members_can_add_message_reactions_channel">購読者はメッセージにリアクションを追加できます。</string> + <string name="group_members_can_send_voice_channel">購読者はボイスメッセージを送信できます。</string> + <string name="group_members_can_send_files_channel">購読者はファイルやメディアを送信できます。</string> + <string name="group_members_can_send_simplex_links_channel">購読者はSimpleXリンクを送信できます。</string> + <string name="group_members_can_send_reports_channel">購読者はメッセージをモデレーターに報告できます。</string> + <string name="recent_history_is_sent_to_new_members_channel">最新100件までのメッセージが新しい購読者に送信されます。</string> + <string name="recent_history_is_not_sent_to_new_members_channel">履歴は新しい購読者に送信されません。</string> + <string name="allow_chat_with_admins_channel">購読者が管理者とチャットできるようにします。</string> + <string name="members_can_chat_with_admins_channel">購読者は管理者とチャットできます。</string> + <string name="feature_roles_moderators">モデレーター</string> + <string name="member_admission">メンバーの参加承認</string> + <string name="admission_stage_review">メンバーを審査</string> + <string name="admission_stage_review_descr">参加を承認する前にメンバーを審査します(「ノック」)。</string> + <string name="member_criteria_off">オフ</string> + <string name="member_criteria_all">すべて</string> + <string name="member_support">メンバーとのチャット</string> + <string name="no_support_chats">メンバーとのチャットはありません</string> + <string name="support_chats_disabled">メンバーとのチャットは無効です</string> + <string name="delete_member_support_chat_button">チャットを削除</string> + <string name="delete_member_support_chat_alert_title">メンバーとのチャットを削除しますか?</string> + <string name="support_chat">管理者とのチャット</string> + <string name="reject_pending_member_button">拒否</string> + <string name="reject_pending_member_alert_title">メンバーを拒否しますか?</string> + <string name="accept_pending_member_alert_title">メンバーを承諾</string> + <string name="accept_pending_member_alert_question">メンバーがグループに参加します。承諾しますか?</string> + <string name="v6_0_new_chat_experience">新しいチャット体験 🎉</string> + <string name="v6_0_new_media_options">新しいメディアオプション</string> + <string name="v6_0_private_routing_descr">IPアドレスと接続を保護します。</string> + <string name="v6_0_chat_list_media">チャット一覧から再生できます。</string> + <string name="v6_0_increase_font_size">フォントサイズを大きくできます。</string> + <string name="v6_0_upgrade_app">アプリを自動的にアップグレード</string> + <string name="v6_1_better_security_descr">SimpleXのプロトコルがTrail of Bitsによってレビューされました。</string> + <string name="v6_1_better_calls_descr">通話中に音声とビデオを切り替えられます。</string> + <string name="v6_1_switch_chat_profile_descr">ワンタイム招待ごとにチャットプロフィールを切り替えられます。</string> + <string name="v6_1_forward_many_messages_descr">一度に最大20件のメッセージを転送できます。</string> + <string name="v6_2_network_decentralization">ネットワークの分散化</string> + <string name="v6_2_network_decentralization_descr">アプリに2番目のプリセット運営者が登場!</string> + <string name="v6_2_network_decentralization_enable_flux">メタデータのプライバシー向上のため、「ネットワークとサーバ」設定でFluxを有効にしてください。</string> + <string name="v6_2_network_decentralization_enable_flux_reason">メタデータのプライバシー向上のため。</string> + <string name="v6_2_improved_chat_navigation">チャットナビゲーションの改善</string> + <string name="v6_2_improved_chat_navigation_descr">- 最初の未読メッセージでチャットを開きます。\n- 引用されたメッセージにジャンプします。</string> + <string name="v6_2_business_chats">ビジネスチャット</string> + <string name="v6_2_business_chats_descr">顧客のためのプライバシー。</string> + <string name="v6_3_mentions">メンバーをメンション 👋</string> + <string name="v6_3_mentions_descr">メンションされたときに通知を受け取れます。</string> + <string name="v6_3_reports">プライベートな報告を送信</string> + <string name="v6_3_reports_descr">管理者によるグループの検閲を支援します。</string> + <string name="v6_3_organize_chat_lists">チャットをリストに整理</string> + <string name="v6_3_organize_chat_lists_descr">重要なメッセージを見逃しません。</string> + <string name="v6_3_private_media_file_names">プライベートなメディアファイル名。</string> + <string name="v6_3_set_message_expiration_in_chats">チャットでメッセージの有効期限を設定できます。</string> + <string name="v6_3_faster_sending_messages">メッセージの送信が高速化。</string> + <string name="v6_3_faster_deletion_of_groups">グループの削除が高速化。</string> + <string name="v6_4_connect_faster">より速く接続! 🚀</string> + <string name="v6_4_connect_faster_descr">「接続」をタップすればすぐにメッセージを送れます。</string> + <string name="v6_4_review_members">グループメンバーを審査</string> + <string name="v6_4_review_members_descr">参加前にメンバーとチャットできます。</string> + <string name="v6_4_support_chat">管理者とのチャット</string> + <string name="v6_4_support_chat_descr">グループにプライベートなフィードバックを送れます。</string> + <string name="v6_4_role_moderator">新しいグループの役割:モデレーター</string> + <string name="v6_4_role_moderator_descr">メッセージを削除し、メンバーをブロックします。</string> + <string name="v6_4_message_delivery_descr">モバイルネットワークでの通信量を削減。</string> + <string name="v6_4_1_welcome_contacts">連絡先を歓迎 👋</string> + <string name="v6_4_1_welcome_contacts_descr">プロフィールの自己紹介とウェルカムメッセージを設定できます。</string> + <string name="v6_4_1_keep_chats_clean">チャットをすっきり保つ</string> + <string name="v6_4_1_keep_chats_clean_descr">消えるメッセージをデフォルトで有効にできます。</string> + <string name="v6_4_1_short_address">短いSimpleXアドレス</string> + <string name="v6_4_1_short_address_create">アドレスを作成</string> + <string name="v6_4_1_short_address_update">アドレスを更新</string> + <string name="v6_4_1_short_address_share">アドレスを共有</string> + <string name="v6_4_1_new_interface_languages">4つの新しいインターフェース言語</string> + <string name="v6_4_1_new_interface_languages_descr">カタロニア語、インドネシア語、ルーマニア語、ベトナム語 - ユーザーの皆様に感謝します!</string> + <string name="v6_5_public_channels">公開チャンネル - 自由に発言 🚀</string> + <string name="v6_5_reliability">信頼性:チャンネルごとに多数のリレー。</string> + <string name="v6_5_ownership">所有権:自分自身のリレーを運用できます。</string> + <string name="v6_5_security">セキュリティ:オーナーがチャンネルの鍵を保持します。</string> + <string name="v6_5_privacy">プライバシー:オーナーと購読者のために。</string> + <string name="v6_5_invite_friends">友達をもっと簡単に招待 👋</string> + <string name="v6_5_invite_friends_descr">新しいユーザーの接続をより簡単にしました。</string> + <string name="v6_5_safe_web_links">安全なWebリンク</string> + <string name="v6_5_safe_web_links_descr">- リンクプレビューの送信をオプトインできます。\n- 有効な場合はSOCKSプロキシを使用します。\n- ハイパーリンクのフィッシングを防ぎます。\n- リンクのトラッキングを削除します。</string> + <string name="v6_5_non_profit_governance">非営利のガバナンス</string> + <string name="v6_5_non_profit_governance_descr">SimpleX Networkを永続させるために。</string> + <string name="view_updated_conditions">更新された条件を表示</string> + <string name="remote_ctrl_connection_stopped_desc">モバイルとデスクトップが同じローカルネットワークに接続されていること、デスクトップのファイアウォールが接続を許可していることを確認してください。\nその他の問題があれば開発者にお知らせください。</string> + <string name="remote_ctrl_connection_stopped_identity_desc">このリンクは別のモバイル端末で使用されました。デスクトップで新しいリンクを作成してください。</string> + <string name="connect_plan_chat_already_exists">チャットはすでに存在します!</string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[すでに <b>%1$s</b> と接続しています。]]></string> + <string name="chat_archive">またはアーカイブファイルをインポート</string> + <string name="migrate_from_device_remove_archive_question">アーカイブを削除しますか?</string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">アップロードされたデータベースのアーカイブはサーバから完全に削除されます。</string> + <string name="servers_info_target">情報の表示対象</string> + <string name="servers_info_transport_sessions_section_header">トランスポートセッション</string> + <string name="servers_info_subscriptions_section_header">メッセージ受信</string> + <string name="servers_info_subscriptions_connections_pending">保留中</string> + <string name="servers_info_proxied_servers_section_header">プロキシ経由のサーバ</string> + <string name="servers_info_proxied_servers_section_footer">これらのサーバには接続していません。これらにメッセージを配信するためにプライベートルーティングが使用されます。</string> + <string name="servers_info_reconnect_servers_title">サーバに再接続しますか?</string> + <string name="servers_info_reconnect_servers_message">接続中のすべてのサーバに再接続してメッセージ配信を強制します。追加の通信量を使用します。</string> + <string name="servers_info_reconnect_server_title">サーバに再接続しますか?</string> + <string name="servers_info_reconnect_server_message">サーバに再接続してメッセージ配信を強制します。追加の通信量を使用します。</string> + <string name="servers_info_reconnect_all_servers_button">すべてのサーバに再接続</string> + <string name="servers_info_reset_stats_alert_confirm">リセット</string> + <string name="servers_info_uploaded">アップロード済み</string> + <string name="servers_info_detailed_statistics_sent_messages_header">送信したメッセージ</string> + <string name="servers_info_detailed_statistics_sent_messages_total">送信合計</string> + <string name="servers_info_detailed_statistics_received_messages_header">受信したメッセージ</string> + <string name="servers_info_detailed_statistics_received_total">受信合計</string> + <string name="servers_info_detailed_statistics_receive_errors">受信エラー</string> + <string name="servers_info_starting_from">%s から開始。</string> + <string name="xftp_server">XFTPサーバ</string> + <string name="sent_directly">直接送信</string> + <string name="sent_via_proxy">プロキシ経由で送信</string> + <string name="proxied">プロキシ経由</string> + <string name="other_label">その他</string> + <string name="other_errors">その他のエラー</string> + <string name="secured">保護済み</string> + <string name="subscribed">購読済み</string> + <string name="subscription_results_ignored">購読を無視</string> + <string name="subscription_errors">購読エラー</string> + <string name="uploaded_files">アップロードしたファイル</string> + <string name="size">サイズ</string> + <string name="upload_errors">アップロードエラー</string> + <string name="server_address">サーバアドレス</string> + <string name="open_server_settings_button">サーバ設定を開く</string> + <string name="max_group_mentions_per_message_reached">1つのメッセージにつき最大 %1$s 人のメンバーをメンションできます!</string> + <string name="channel_members_title_subscribers">購読者</string> + <string name="channel_members_section_owners">オーナーと貢献者</string> + <string name="channel_subscriber_count_singular">%1$d 人の購読者</string> + <string name="channel_subscriber_count_plural">%1$d 人の購読者</string> + <string name="channel_member_you">あなた</string> + <string name="chat_relay">チャットリレー</string> + <string name="new_chat_relay">新しいチャットリレー</string> + <string name="preset_relay_name">プリセットリレー名</string> + <string name="preset_relay_address">プリセットリレーアドレス</string> + <string name="your_relay_name">あなたのリレー名</string> + <string name="your_relay_address">あなたのリレーアドレス</string> + <string name="enter_relay_name">リレー名を入力…</string> + <string name="use_relay">リレーを使用</string> + <string name="test_relay">リレーをテスト</string> + <string name="use_for_new_channels">新しいチャンネルに使用</string> + <string name="delete_relay">リレーを削除</string> + <string name="test_relay_to_retrieve_name"><![CDATA[名前を取得するには<b>リレーをテスト</b>してください。]]></string> + <string name="relay_test_failed_alert">リレーのテストに失敗しました!</string> + <string name="relay_test_step_get_link">リンクを取得</string> + <string name="relay_test_step_decode_link">リンクをデコード</string> + <string name="relay_test_step_connect">接続</string> + <string name="relay_test_step_wait_response">応答を待機</string> + <string name="relay_test_step_verify">検証</string> + <string name="error_relay_test_failed_at_step">ステップ %s でテストに失敗しました。</string> + <string name="error_relay_test_server_auth">リレーに接続するにはサーバの認可が必要です。パスワードを確認してください。</string> + <string name="invalid_relay_name">リレー名が無効です!</string> + <string name="check_relay_name">リレー名を確認して、もう一度お試しください。</string> + <string name="invalid_relay_address">リレーアドレスが無効です!</string> + <string name="check_relay_address">リレーアドレスを確認して、もう一度お試しください。</string> + <string name="error_adding_relay">リレーの追加エラー</string> + <string name="chat_relays">チャットリレー</string> + <string name="chat_relays_forward_messages_in_channels">チャットリレーは、あなたが作成したチャンネルでメッセージを転送します。</string> + <string name="channel_relays_title">チャットリレー</string> + <string name="no_chat_relays">チャットリレーがありません</string> + <string name="chat_relays_forward_messages">チャットリレーはチャンネルの購読者にメッセージを転送します。</string> + <string name="relay_conn_status_connected">接続済み</string> + <string name="relay_conn_status_connecting">接続中</string> + <string name="relay_conn_status_deleted">削除済み</string> + <string name="relay_conn_status_failed">失敗</string> + <string name="relay_conn_status_removed_by_operator">運営者により削除</string> + <string name="relay_conn_status_removed">削除済み</string> + <string name="relay_status_new">新規</string> + <string name="relay_status_invited">招待済み</string> + <string name="relay_status_accepted">承諾済み</string> + <string name="relay_status_active">アクティブ</string> + <string name="relay_status_inactive">非アクティブ</string> + <string name="relay_status_rejected">拒否済み</string> + <string name="member_info_status">ステータス</string> + <string name="member_info_relay_status_rejected_by_operator">リレー運営者により拒否</string> + <string name="relay_bar_all_relays_removed">すべてのリレーが削除されました</string> + <string name="relay_bar_all_relays_failed">すべてのリレーが失敗しました</string> + <string name="relay_bar_no_active_relays">アクティブなリレーがありません</string> + <string name="relay_bar_relays_removed">%1$d 個のリレーが削除されました</string> + <string name="relay_bar_relays_failed">%1$d 個のリレーが失敗しました</string> + <string name="relay_bar_relays_not_active">%1$d 個のリレーが非アクティブです</string> + <string name="relay_bar_active_with_failures">%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が失敗</string> + <string name="relay_bar_active_with_removed">%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個が削除済み</string> + <string name="relay_bar_active_with_errors">%2$d 個中 %1$d 個のリレーがアクティブ、%3$d 個がエラー</string> + <string name="relay_bar_active">%2$d 個中 %1$d 個のリレーがアクティブ</string> + <string name="relay_bar_connected_with_errors">%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個がエラー</string> + <string name="relay_bar_connected_with_failures">%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が失敗</string> + <string name="relay_bar_connected_with_removed">%2$d 個中 %1$d 個のリレーが接続済み、%3$d 個が削除済み</string> + <string name="relay_bar_connected">%2$d 個中 %1$d 個のリレーが接続済み</string> + <string name="relay_bar_no_relays">リレーがありません</string> + <string name="relay_bar_owner_no_delivery">メッセージ配信を回復するにはリレーを追加してください。</string> + <string name="relay_bar_subscriber_waiting">チャンネルのオーナーがリレーを追加するのを待っています。</string> + <string name="member_info_section_title_relay">リレー</string> + <string name="member_info_section_title_owner">オーナー</string> + <string name="member_info_section_title_subscriber">購読者</string> + <string name="info_row_channel">チャンネル</string> + <string name="info_row_relay_link">リレーのリンク</string> + <string name="info_row_relay_address">リレーアドレス</string> + <string name="via_relay_hostname">%1$s 経由</string> + <string name="share_relay_address">リレーアドレスを共有</string> + <string name="relay_section_footer_owner">購読者はリレーのリンクを使ってチャンネルに接続します。\nリレーアドレスは、このリレーをチャンネル用に設定するために使用されました。</string> + <string name="relay_section_footer_subscriber">あなたはこのリレーのリンクを使ってチャンネルに接続しました。</string> + <string name="button_remove_subscriber">購読者を削除</string> + <string name="button_remove_relay">リレーを削除</string> + <string name="button_remove_relay_question">リレーを削除しますか?</string> + <string name="relay_will_be_removed_from_channel">リレーはチャンネルから削除されます - これは元に戻せません!</string> + <string name="last_active_relay_warning">これは最後のアクティブなリレーです。削除すると購読者へのメッセージ配信ができなくなります。</string> + <string name="block_subscriber_for_all_question">全員に対して購読者をブロックしますか?</string> + <string name="create_channel_title">公開チャンネルを作成</string> + <string name="create_channel_button">公開チャンネルを作成</string> + <string name="channel_display_name_field">チャンネル名</string> + <string name="creating_channel">チャンネルを作成中</string> + <string name="error_creating_channel">チャンネルの作成エラー</string> + <string name="relay_results">リレーの結果:</string> + <string name="connection_reached_limit_of_undelivered_messages">接続が未配信メッセージの上限に達しました</string> + <string name="network_error">ネットワークエラー</string> + <string name="error_prefix">エラー</string> + <string name="cancel_creating_channel_question">チャンネルの作成をキャンセルしますか?</string> + <string name="cancel_channel_alert_msg">新しいチャンネル %1$s は %3$d 個中 %2$d 個のリレーに接続されています。\nキャンセルすると、チャンネルは削除されます - もう一度作成できます。</string> + <string name="enable_at_least_one_chat_relay">チャンネルを作成するには、少なくとも1つのチャットリレーを有効にしてください。</string> + <string name="your_profile_shared_with_channel_relays">あなたのプロフィール %1$s はチャンネルのリレーと購読者に共有されます。\nリレーはチャンネルのメッセージにアクセスできます。</string> + <string name="configure_relays">リレーを構成</string> + <string name="relay_status_failed">失敗</string> + <string name="add_button">追加</string> + <string name="add_relay_button">リレーを追加</string> + <string name="add_relays_title">リレーを追加</string> + <string name="no_available_relays">利用可能なリレーがありません</string> + <string name="error_adding_relays">リレーの追加エラー</string> + <string name="relays_added_format">追加されたリレー:%1$s。</string> + <string name="select_relays">リレーを選択</string> + <string name="no_relays_selected">リレーが選択されていません</string> + <string name="num_relays_selected">%d 個のリレーを選択中</string> + <string name="relay_connection_failed">リレーの接続に失敗しました</string> + <string name="not_all_relays_connected">一部のリレーが接続されていません</string> + <string name="wait_verb">待機</string> + <string name="channel_will_start_with_relays">チャンネルは %2$d 個中 %1$d 個のリレーで動作を開始します。続行しますか?</string> + <string name="relay_address_alert_title">リレーアドレス</string> + <string name="relay_address_alert_message">これはチャットリレーのアドレスであり、接続には使用できません。</string> + <string name="connect_plan_open_channel">チャンネルを開く</string> + <string name="connect_plan_open_new_channel">新しいチャンネルを開く</string> + <string name="connect_plan_this_is_your_link_for_channel">あなたのチャンネル</string> + <string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[これはチャンネル <b>%1$s</b> 用のあなたのリンクです!]]></string> + <string name="error_opening_channel">チャンネルを開く際のエラー</string> + <string name="unblock_subscriber_for_all_question">全員に対して購読者のブロックを解除しますか?</string> + <string name="link_previews_alert_title">リンクプレビューを有効にしますか?</string> + <string name="link_previews_alert_desc">リンクプレビューを送信すると、あなたのIPアドレスがWebサイトに知られる可能性があります。これは後でプライバシー設定で変更できます。</string> + <string name="link_previews_alert_desc_socks">リンクプレビューはSOCKSプロキシ経由で要求されます。DNSルックアップは、あなたのDNSリゾルバーを介してローカルで行われる場合があります。</string> + <string name="link_previews_alert_enable">有効にする</string> + <string name="link_previews_alert_disable">無効にする</string> + <string name="close_behavior_dialog_title">トレイに最小化しますか?</string> + <string name="close_behavior_dialog_text">「閉じる」を選択すると、メッセージを受信できなくなります。\nこれは後で外観設定で変更できます。</string> + <string name="close_behavior_dialog_close">アプリを閉じる</string> + <string name="close_behavior_dialog_minimize">トレイに最小化</string> + <string name="tray_show">SimpleXを表示</string> + <string name="tray_quit">SimpleXを終了</string> + <string name="tray_tooltip">SimpleX</string> + <string name="tray_tooltip_unread">SimpleX — %d 件の未読</string> + <string name="appearance_minimize_to_tray">閉じる時にトレイへ</string> + <string name="appearance_minimize_to_tray_desc">メッセージを受信するためにバックグラウンドで実行します</string> + <string name="badge_supports_simplex">%s は SimpleX Chat をサポートしています。</string> + <string name="badge_supported_simplex">%1$s は SimpleX Chat をサポートしました。バッジは %2$s に期限切れになりました。</string> + <string name="badge_support_from_v7">アプリのv7から SimpleX をサポートできます。</string> + <string name="badge_invested">%s は SimpleX Chat のクラウドファンディングに出資しました。</string> + <string name="badge_unverified_title">未検証のバッジ</string> + <string name="badge_unverified_desc">このバッジは検証できず、本物ではない可能性があります。</string> + <string name="badge_unknown_key_title">バッジを検証できません</string> + <string name="badge_unknown_key_desc">このバッジは、このバージョンのアプリが認識しない鍵で署名されています。このバッジを検証するにはアプリを更新してください。</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 83f937db32..ea87347a13 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -107,7 +107,7 @@ <string name="icon_descr_contact_checked">대화 상대 확인됨</string> <string name="create_group_link">그룹 링크 생성</string> <string name="button_create_group_link">링크 생성</string> - <string name="change_member_role_question">그룹 역할을 바꾸시겠습니까\?</string> + <string name="change_member_role_question">그룹 역할을 바꾸시겠습니까?</string> <string name="info_row_connection">연결</string> <string name="users_add">프로필 추가</string> <string name="chat_preferences_always">항상</string> @@ -307,8 +307,7 @@ <string name="connected_to_server_to_receive_messages_from_contact">이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요.</string> <string name="app_name">SimpleX</string> <string name="contact_developers">앱 업데이트 후 개발자에게 연락해 주세요.</string> - <string name="connection_error_auth_desc">대화 상대가 나갔거나 초대 링크가 이미 사용된 경우가 아니면 버그일 수 있어요. 이 경우 개발자에게 알려주세요. -\n대화 상대에게 다른 초대 링크 만들도록 부탁하고 네트워크 연결이 안정적인지 확인하세요.</string> + <string name="connection_error_auth_desc">대화 상대가 나갔거나 초대 링크가 이미 사용된 경우가 아니면 버그일 수 있어요. 이 경우 개발자에게 알려주세요. \n대화 상대에게 다른 초대 링크 만들도록 부탁하고 네트워크 연결이 안정적인지 확인하세요.</string> <string name="auth_enable_simplex_lock">SimpleX 잠금 활성화</string> <string name="auth_log_in_using_credential">자격 증명으로 로그인</string> <string name="auth_open_chat_console">채팅 콘솔 열기</string> @@ -822,7 +821,7 @@ <string name="settings_section_title_settings">설정</string> <string name="send_link_previews">링크 미리보기 보내기</string> <string name="settings_section_title_socks">SOCKS 프록시</string> - <string name="settings_section_title_support">SIMPLEX CHAT 도와주기</string> + <string name="settings_section_title_support">SimpleX Chat 도와주기</string> <string name="settings_section_title_you">나</string> <string name="settings_experimental_features">실험적 기능</string> <string name="show_dev_options">표시 :</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml index 92985b15be..09c428e48b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ku/strings.xml @@ -282,13 +282,13 @@ <string name="server_address">Adresa serverê</string> <string name="address_section_title">Adres</string> <string name="srv_error_host">Adresa serverê li eyarên torê nayê.</string> - <string name="conn_stats_section_title_servers">SERVER</string> + <string name="conn_stats_section_title_servers">Server</string> <string name="servers_info">Melûmata serveran</string> <string name="smp_servers_test_failed">Ceribandina serverê bi ser neket!</string> <string name="srv_error_version">Versiyona serverê li eyarên torê nayê.</string> <string name="accept_feature_set_1_day">1 roj deyne</string> <string name="set_group_preferences">Tercihên komê diyar bike</string> - <string name="settings_section_title_settings">EYAR</string> + <string name="settings_section_title_settings">Eyar</string> <string name="share_verb">Parve bike</string> <string name="share_invitation_link">Lînka 1-carê parve bike</string> <string name="share_address">Adresê parve bike</string> @@ -337,7 +337,7 @@ <string name="strikethrough_text">xet/xêz/xîşk</string> <string name="privacy_media_blur_radius_strong">Biqewet</string> <string name="subscribed">Abonekirî</string> - <string name="settings_section_title_support">PIŞT BIDE SIMPLEX CHATÊ</string> + <string name="settings_section_title_support">Pişt bide SimpleX Chatê</string> <string name="switch_verb">Biguhere</string> <string name="la_mode_system">Sîstem</string> <string name="color_mode_system">Sîstem</string> @@ -513,7 +513,7 @@ <string name="remote_ctrl_error_inactive">Kompîter ne aktîv e</string> <string name="remote_ctrl_error_disconnected">Girêdana bi kompîterê re qut bû</string> <string name="servers_info_details">Detay</string> - <string name="settings_section_title_device">CIHAZ</string> + <string name="settings_section_title_device">Cihaz</string> <string name="total_files_count_and_size">%d dosya bi mezibnbûniya timam ya %s</string> <string name="rcv_group_events_count">%d hewadîsên komê</string> <string name="ttl_hour">%d seet</string> @@ -588,7 +588,7 @@ <string name="chat_preferences_you_allow">Tu dihêlî</string> <string name="snd_group_event_member_accepted">te ev endam qebûl kir</string> <string name="group_info_member_you">tu: %1$s</string> - <string name="settings_section_title_you">TU</string> + <string name="settings_section_title_you">Tu</string> <string name="sender_you_pronoun">tu</string> <string name="privacy_chat_list_open_links_yes">Erê</string> <string name="chat_preferences_yes">erê</string> @@ -633,11 +633,11 @@ <string name="privacy_chat_list_open_web_link">Lînkê veke</string> <string name="privacy_chat_list_open_full_web_link">Lînka timam veke</string> <string name="privacy_chat_list_open_clean_web_link">Lînka paqij veke</string> - <string name="settings_section_title_help">ARÎKARÎ</string> - <string name="settings_section_title_app">APLÎKASYON</string> - <string name="settings_section_title_files">DOSYA</string> + <string name="settings_section_title_help">Arîkarî</string> + <string name="settings_section_title_app">Aplîkasyon</string> + <string name="settings_section_title_files">Dosya</string> <string name="settings_restart_app">Ji nû ve veke</string> - <string name="settings_section_title_socks">PROKSIYA SOCKSÊ</string> + <string name="settings_section_title_socks">Proksiya SOCKSê</string> <string name="settings_section_title_profile_images">Sûretên profîlan</string> <string name="settings_section_title_network_connection">Girêdana torê</string> <string name="settings_section_title_use_from_desktop">Ji kompîterê bişuxulîne</string> @@ -687,7 +687,7 @@ <string name="member_blocked_by_admin">Ji admîn blokkirî</string> <string name="member_info_member_blocked">blokkirî</string> <string name="member_info_member_inactive">ne aktîv</string> - <string name="member_info_section_title_member">ENDAM</string> + <string name="member_info_section_title_member">Endam</string> <string name="role_in_group">Rol</string> <string name="info_row_group">Kom</string> <string name="receiving_via">Te standin bi riya</string> @@ -785,10 +785,10 @@ <string name="network_session_mode_user">Profîla siḧbetê</string> <string name="you_control_your_chat">Tu siḧbeta xwe qontrol dikî!</string> <string name="use_chat">Siḧbetê bişuxulîne</string> - <string name="settings_section_title_chats">SIḦBET</string> + <string name="settings_section_title_chats">Siḧbet</string> <string name="settings_section_title_chat_colors">Rengên siḧbetê</string> <string name="chat_is_stopped">Siḧbet sekinandî ye</string> - <string name="chat_database_section">DATABASA SIḦBETÊ</string> + <string name="chat_database_section">Databasa siḧbetê</string> <string name="stop_chat_question">Ber siḧbet were sekinandin?</string> <string name="error_stopping_chat">Xeletî di sekinandina siḧbetê de</string> <string name="delete_chat_profile_question">Ber profîla siḧbetê were jêbirin?</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index bccd49eed9..aceb11ecf8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -17,10 +17,10 @@ <string name="call_already_ended">Skambutis jau baigtas!</string> <string name="answer_call">Atsiliepti</string> <string name="icon_descr_call_ended">Skambutis baigtas</string> - <string name="settings_section_title_calls">SKAMBUČIAI</string> + <string name="settings_section_title_calls">Skambučiai</string> <string name="allow_your_contacts_irreversibly_delete">Leisti jūsų kontaktams negrįžtamai ištrinti išsiųstas žinutes. (24 valandas)</string> <string name="back">Atgal</string> - <string name="settings_section_title_icon">PROGRAMĖLĖS PIKTOGRAMA</string> + <string name="settings_section_title_icon">Programėlės piktograma</string> <string name="chat_preferences_always">visada</string> <string name="allow_your_contacts_to_send_voice_messages">Leisti jūsų kontaktams siųsti balso žinutes.</string> <string name="allow_irreversible_message_deletion_only_if">Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų kontaktas jums tai leidžia. (24 valandas)</string> @@ -78,8 +78,8 @@ <string name="icon_descr_flip_camera">Apversti kamerą</string> <string name="icon_descr_call_rejected">Atmestas skambutis</string> <string name="privacy_and_security">Privatumas ir saugumas</string> - <string name="settings_section_title_device">ĮRENGINYS</string> - <string name="settings_section_title_help">PAGALBA</string> + <string name="settings_section_title_device">Įrenginys</string> + <string name="settings_section_title_help">Pagalba</string> <string name="encrypt_database">Šifruoti</string> <string name="remove_passphrase">Šalinti</string> <string name="button_delete_group">Ištrinti grupę</string> @@ -241,7 +241,7 @@ <string name="icon_descr_speaker_off">Išjungti garsiakalbį</string> <string name="icon_descr_speaker_on">Įjungti garsiakalbį</string> <string name="alert_title_skipped_messages">Praleistos žinutės</string> - <string name="settings_section_title_settings">NUSTATYMAI</string> + <string name="settings_section_title_settings">Nustatymai</string> <string name="theme_system">Sistemos</string> <string name="unknown_message_format">nežinomas žinutės formatas</string> <string name="simplex_link_contact">SimpleX kontakto adresas</string> @@ -292,7 +292,7 @@ <string name="icon_descr_video_off">Išjungti vaizdą</string> <string name="icon_descr_video_on">Įjungti vaizdą</string> <string name="your_privacy">Jūsų privatumas</string> - <string name="settings_section_title_you">JŪS</string> + <string name="settings_section_title_you">Jūs</string> <string name="wrong_passphrase_title">Neteisinga slaptafrazė!</string> <string name="app_name">SimpleX</string> <string name="sender_you_pronoun">jūs</string> @@ -348,10 +348,10 @@ <string name="save_and_notify_group_members">Įrašyti ir pranešti grupės nariams</string> <string name="callstate_received_confirmation">gautas patvirtinimas…</string> <string name="icon_descr_call_missed">Praleistas skambutis</string> - <string name="settings_section_title_chats">POKALBIAI</string> - <string name="settings_section_title_themes">APIPAVIDALINIMAI</string> + <string name="settings_section_title_chats">Pokalbiai</string> + <string name="settings_section_title_themes">Apipavidalinimai</string> <string name="settings_section_title_incognito">Inkognito veiksena</string> - <string name="settings_section_title_messages">ŽINUTĖS IR FAILAI</string> + <string name="settings_section_title_messages">Žinutės ir failai</string> <string name="restart_the_app_to_use_imported_chat_database">Norėdami naudoti importuotą pokalbio duomenų bazę, paleiskite programėlę iš naujo.</string> <string name="button_add_members">Pakviesti narius</string> <string name="disappearing_prohibited_in_this_chat">Išnykstančios žinutės šiame pokalbyje yra uždraustos.</string> @@ -420,7 +420,7 @@ <string name="network_session_mode_user">Pokalbio profilis</string> <string name="profile_is_only_shared_with_your_contacts">Profilis yra bendrinamas tik su jūsų kontaktais.</string> <string name="read_more_in_github_with_link"><![CDATA[Išsamiau skaitykite mūsų <font color="#0088ff">„GitHub“ saugykloje</font>.]]></string> - <string name="settings_section_title_socks">SOCKS ĮGALIOTASIS SERVERIS</string> + <string name="settings_section_title_socks">SOCKS įgaliotasis serveris</string> <string name="save_passphrase_and_open_chat">Įrašyti slaptafrazę ir atverti pokalbį</string> <string name="restore_database">Atkurti atsarginę duomenų bazės kopiją</string> <string name="restore_database_alert_title">Atkurti atsarginę duomenų bazės kopiją\?</string> @@ -459,7 +459,7 @@ <string name="contact_preferences">Kontakto nuostatos</string> <string name="join_group_button">Prisijungti</string> <string name="change_verb">Keisti</string> - <string name="conn_stats_section_title_servers">SERVERIAI</string> + <string name="conn_stats_section_title_servers">Serveriai</string> <string name="clear_chat_menu_action">Išvalyti</string> <string name="unhide_profile">Nebeslėpti profilio</string> <string name="videos_limit_title">Per daug vaizdo įrašų!</string> @@ -545,7 +545,7 @@ <string name="icon_descr_audio_off">Išjungti garsą</string> <string name="all_app_data_will_be_cleared">Visi programėlės duomenys bus ištrinti.</string> <string name="empty_chat_profile_is_created">Sukuriamas tuščias pokalbių profilis nurodytu pavadinimu ir programėlė atveriama kaip įprasta.</string> - <string name="settings_section_title_app">PROGRAMĖLĖ</string> + <string name="settings_section_title_app">Programėlė</string> <string name="keychain_is_storing_securely">Saugiam slaptafrazės saugojimui yra naudojama „Android Keystore“ – tai įgalina pranešimų tarnybą veikti.</string> <string name="color_secondary_variant">Papildoma antrinė spalva</string> <string name="color_primary_variant">Papildomas akcentavimas</string> @@ -608,7 +608,7 @@ <string name="connect_via_invitation_link">Prisijungti per vienkartinę nuorodą?</string> <string name="icon_descr_close_button">Užvėrimo mygtukas</string> <string name="devices">Įrenginiai</string> - <string name="connection_error_auth">Ryšio klaida (AUTH)</string> + <string name="connection_error_auth">Ryšio klaida</string> <string name="disable_notifications_button">Išjungti pranešimus</string> <string name="continue_to_next_step">Tęsti</string> <string name="chat_database_deleted">Pokalbio duomenų bazė ištrinta</string> @@ -636,7 +636,7 @@ <string name="create_group_button_to_create_new_group"><![CDATA[<b>Sukurti grupę</b>: sukurti naują grupę.]]></string> <string name="add_contact_tab">Pridėti kontaktą</string> <string name="customize_theme_title">Tinkinti apipavidalinimą</string> - <string name="chat_database_section">POKALBIO DUOMENŲ BAZĖ</string> + <string name="chat_database_section">Pokalbio duomenų bazė</string> <string name="v4_6_chinese_spanish_interface">Naudotojo sąsaja kinų ir ispanų kalbomis</string> <string name="delivery_receipts_title">Pranešimai apie pristatymą!</string> <string name="auth_disable_simplex_lock">Išjungti SimpleX užraktą</string> @@ -855,7 +855,7 @@ <string name="connect_via_link_incognito">Prisijungti inkognito režimu</string> <string name="enter_passphrase_notification_title">Reikalinga slaptafrazė</string> <string name="prohibit_sending_voice_messages">Uždrausti siųsti balso žinutes.</string> - <string name="settings_section_title_experimenta">EKSPERIMENTINIS</string> + <string name="settings_section_title_experimenta">Eksperimentinis</string> <string name="v5_0_large_files_support_descr">Greitai ir nelaukiant kol siuntėjas prisijungs!</string> <string name="files_and_media">Failai ir medija</string> <string name="files_are_prohibited_in_group">Failai ir medija yra draudžiami šioje grupėje.</string> @@ -1000,7 +1000,7 @@ <string name="rcv_direct_event_contact_deleted">ištrintas kontaktas</string> <string name="group_member_status_invited">pakviestas</string> <string name="info_row_deleted_at">Ištrinta</string> - <string name="section_title_for_console">KONSOLEI</string> + <string name="section_title_for_console">Konsolei</string> <string name="block_member_confirmation">Blokuoti</string> <string name="network_option_protocol_timeout">Protokolui skirtas laikas</string> <string name="chat_preferences_default">numatyta (%s)</string> @@ -1106,7 +1106,7 @@ <string name="connection_you_accepted_will_be_cancelled">Prisijungimas, kurį priėmėte, bus atšauktas!</string> <string name="tap_to_paste_link">Bakstelėkite, kad įklijuoti nuorodą</string> <string name="smp_servers_test_server">Testuoti serverį</string> - <string name="theme_colors_section_title">TEMOS SPALVOS</string> + <string name="theme_colors_section_title">Temos spalvos</string> <string name="show_slow_api_calls">Rodyti lėtus API iškvietimus</string> <string name="stop_sharing_address">Nustoti bendrinti adresą?</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Žinučių siuntimo ir programų platforma, apsauganti jūsų privatumą ir saugumą.</string> @@ -1217,7 +1217,7 @@ <string name="set_database_passphrase">Nustatyti duomenų slaptafrazę</string> <string name="set_passphrase">Nustatyti slaptafrazę</string> <string name="privacy_show_last_messages">Rodyti paskutines žinutes</string> - <string name="settings_section_title_support">PALAIKYKITE SIMPLEX CHAT</string> + <string name="settings_section_title_support">Palaikykite SimpleX Chat</string> <string name="receipts_section_description_1">Jų galima nepaisyti kontaktų ir grupių nustatymuose.</string> <string name="enable_automatic_deletion_message">Šis veiksmas negali būti atšauktas - žinutės išsiųstos ir gautos anksčiau nei pasirinkta bus ištrintos. Tai gali užtrukti kelias minutes.</string> <string name="rcv_group_event_n_members_connected">%s, %s ir %d kiti nariai prisijungė</string> @@ -1516,7 +1516,7 @@ <string name="secret_text">paslaptis</string> <string name="shutdown_alert_desc">Pranešimai nustos veikti iki tol kol paleisite programėlę iš naujo</string> <string name="you_can_use_markdown_to_format_messages__prompt">Galite naudoti markdown, kad formatuoti žinutes:</string> - <string name="run_chat_section">PALEISTI POKALBIUS</string> + <string name="run_chat_section">Paleisti pokalbius</string> <string name="settings_section_title_use_from_desktop">Naudoti iš darbastalio</string> <string name="welcome_message_is_too_long">Sveikinimo žinutė yra per ilga</string> <string name="v5_1_message_reactions">Žinučių reakcijos</string> @@ -1540,8 +1540,7 @@ <string name="marked_deleted_description">pažymėta ištrinta</string> <string name="live">GYVAI</string> <string name="moderated_description">moderuota</string> - <string name="connection_error_auth_desc">Nebent jūsų kontaktas ištrynė šį prisijungimą arba nuoroda jau buvo panaudota, tai gali būti klaida - prašome ją pranešti. -\nKad prisijungti, paprašykite savo kontakto sukurti kitą prisijungimo nuorodą ir patikrinkite, kad turite stabilų interneto ryšį.</string> + <string name="connection_error_auth_desc">Nebent jūsų kontaktas ištrynė šį prisijungimą arba nuoroda jau buvo panaudota, tai gali būti klaida - prašome ją pranešti. \nKad prisijungti, paprašykite savo kontakto sukurti kitą prisijungimo nuorodą ir patikrinkite, kad turite stabilų interneto ryšį.</string> <string name="system_restricted_background_warn"><![CDATA[Kad įjungti pranešimus, prašome pasirinkti <b>Programėlės akumuliatoriaus naudojimas</b> / <b>Neapribotas</b> programėlės nustatymuose.]]></string> <string name="system_restricted_background_in_call_warn"><![CDATA[Kad daryti skambučius fone, prašome pasirinkti <b>Programėlės akumuliatoriaus naudojimas</b> / <b>Neapribotas</b> programėlės nustatymuose.]]></string> <string name="system_restricted_background_in_call_title">Nėra foninių skambučių</string> @@ -1618,7 +1617,7 @@ <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo.</string> <string name="rcv_group_event_user_deleted">pašalino jus</string> <string name="info_row_moderated_at">Moderuota</string> - <string name="member_info_section_title_member">NARYS</string> + <string name="member_info_section_title_member">Narys</string> <string name="sender_at_ts">%s %s</string> <string name="item_info_no_text">nėra teksto</string> <string name="color_surface">Meniu ir įspėjimai</string> @@ -1651,7 +1650,7 @@ <string name="use_random_passphrase">Naudoti atsiktinę slaptafrazę</string> <string name="call_connection_peer_to_peer">lygiaverčiai mazgai</string> <string name="remove_passphrase_from_settings">Pašalinti slaptafrazę iš nustatymų?</string> - <string name="settings_section_title_delivery_receipts">SIŲSTI PRISTATYMO KVITUS PAS</string> + <string name="settings_section_title_delivery_receipts">Siųsti pristatymo kvitus pas</string> <string name="receipts_groups_override_disabled">Pristatymo kvitai yra išjungti %d grupėms</string> <string name="you_must_use_the_most_recent_version_of_database">Turite naudoti pačią naujausią pokalbių duomenų bazės versiją TIK viename įrenginyje, kitaip galite nebegauti žinučių iš kai kurių kontaktų.</string> <string name="new_passphrase">Nauja slaptafrazė…</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml index a6385a5ce0..1275c31573 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nb-rNO/strings.xml @@ -124,7 +124,7 @@ <string name="report_reason_other">En annen grunn</string> <string name="answer_call">Svar anrop</string> <string name="opensource_protocol_and_code_anybody_can_run_servers">Hvem som helst kan være vert for servere.</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="onboarding_notifications_mode_service_desc_short">Appen kjører alltid i bakgrunnen</string> <string name="app_version_code">App build: %s</string> <string name="notifications_mode_off_desc">Appen kan bare motta varsler når den er åpen, ingen bakgrunnstjeneste vil bli startet.</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index cc81e5365b..307ffc78fa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -5,7 +5,7 @@ <string name="call_on_lock_screen">Oproepen op vergrendelscherm:</string> <string name="callstatus_in_progress">oproep bezig</string> <string name="icon_descr_call_progress">Gesprek bezig</string> - <string name="settings_section_title_calls">OPROEPEN</string> + <string name="settings_section_title_calls">Oproepen</string> <string name="cancel_verb">Annuleren</string> <string name="icon_descr_cancel_file_preview">Bestandsvoorbeeld annuleren</string> <string name="icon_descr_cancel_image_preview">Annuleer afbeeldingsvoorbeeld</string> @@ -23,7 +23,7 @@ <string name="allow_to_send_voice">Sta toe om spraak berichten te verzenden.</string> <string name="chat_is_running">Chat is actief</string> <string name="clear_chat_menu_action">Wissen</string> - <string name="chat_database_section">CHAT DATABASE</string> + <string name="chat_database_section">Chat database</string> <string name="chat_console">Chat console</string> <string name="chat_database_imported">Chat database geïmporteerd</string> <string name="chat_database_deleted">Chat database verwijderd</string> @@ -85,7 +85,7 @@ <string name="app_version_code">App build: %s</string> <string name="notifications_mode_off_desc">App kan alleen meldingen ontvangen wanneer deze actief is, er wordt geen achtergrondservice gestart</string> <string name="appearance_settings">Uiterlijk</string> - <string name="settings_section_title_icon">APP ICON</string> + <string name="settings_section_title_icon">App icon</string> <string name="app_version_title">App versie</string> <string name="app_version_name">App versie: v%s</string> <string name="network_session_mode_user_description"><![CDATA[Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chatprofiel dat je in de app hebt </b>.]]></string> @@ -111,7 +111,7 @@ <string name="rcv_conn_event_switch_queue_phase_changing">adres wijzigen…</string> <string name="rcv_conn_event_switch_queue_phase_completed">adres voor u gewijzigd</string> <string name="rcv_group_event_changed_member_role">veranderde rol van %s naar %s</string> - <string name="change_member_role_question">Groep rol wijzigen\?</string> + <string name="change_member_role_question">Groep rol wijzigen?</string> <string name="chat_is_stopped">Chat is gestopt</string> <string name="notifications_mode_periodic_desc">Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut</string> <string name="rcv_group_event_changed_your_role">je rol gewijzigd in %s</string> @@ -119,7 +119,7 @@ <string name="chat_is_stopped_indication">Chat is gestopt</string> <string name="chat_preferences">Chat voorkeuren</string> <string name="network_session_mode_user">Chatprofiel</string> - <string name="settings_section_title_chats">CHATS</string> + <string name="settings_section_title_chats">Chats</string> <string name="chat_with_developers">Praat met de ontwikkelaars</string> <string name="smp_servers_check_address">Controleer het server adres en probeer het opnieuw.</string> <string name="choose_file">Bestand</string> @@ -182,7 +182,7 @@ <string name="icon_descr_call_connecting">Oproep verbinden</string> <string name="button_create_group_link">Maak link</string> <string name="smp_server_test_connect">Verbind</string> - <string name="connection_error_auth">Verbindingsfout (AUTH)</string> + <string name="connection_error_auth">Verbindingsfout</string> <string name="smp_server_test_create_queue">Maak een wachtrij</string> <string name="auth_confirm_credential">Bevestig uw inloggegevens</string> <string name="contact_connection_pending">Verbinden…</string> @@ -231,7 +231,7 @@ <string name="full_deletion">Verwijderen voor iedereen</string> <string name="delete_link">Link verwijderen</string> <string name="conn_level_desc_direct">direct</string> - <string name="settings_section_title_device">APPARAAT</string> + <string name="settings_section_title_device">Apparaat</string> <string name="delete_files_and_media_all">Verwijder alle bestanden</string> <string name="delete_messages_after">Berichten verwijderen na</string> <string name="direct_messages">Directe berichten</string> @@ -309,7 +309,7 @@ <string name="encrypted_video_call">e2e versleuteld video gesprek</string> <string name="allow_accepting_calls_from_lock_screen">Schakel oproepen vanaf het vergrendelscherm in via Instellingen.</string> <string name="icon_descr_hang_up">Ophangen</string> - <string name="settings_section_title_help">HELP</string> + <string name="settings_section_title_help">Help</string> <string name="settings_experimental_features">Experimentele functies</string> <string name="error_starting_chat">Fout bij het starten van de chat</string> <string name="export_database">Database exporteren</string> @@ -358,7 +358,7 @@ <string name="error_accepting_contact_request">Fout bij het accepteren van een contactverzoek</string> <string name="group_invitation_expired">Groep uitnodiging verlopen</string> <string name="icon_descr_file">Bestand</string> - <string name="section_title_for_console">VOOR CONSOLE</string> + <string name="section_title_for_console">Voor console</string> <string name="group_profile_is_stored_on_members_devices">Groep profiel wordt opgeslagen op de apparaten van de leden, niet op de servers.</string> <string name="notification_preview_mode_hidden">Verborgen</string> <string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!</string> @@ -473,8 +473,8 @@ <string name="leave_group_button">Verlaten</string> <string name="group_member_role_member">Lid</string> <string name="image_descr_link_preview">link voorbeeld afbeelding</string> - <string name="member_info_section_title_member">LID</string> - <string name="settings_section_title_messages">BERICHTEN EN BESTANDEN</string> + <string name="member_info_section_title_member">Lid</string> + <string name="settings_section_title_messages">Berichten en bestanden</string> <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.]]></string> <string name="member_will_be_removed_from_group_cannot_be_undone">Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!</string> <string name="message_delivery_error_title">Fout bij bezorging van bericht</string> @@ -730,10 +730,10 @@ <string name="protect_app_screen">App scherm verbergen</string> <string name="your_privacy">Uw privacy</string> <string name="send_link_previews">Link voorbeelden verzenden</string> - <string name="settings_section_title_settings">INSTELLINGEN</string> - <string name="settings_section_title_support">ONDERSTEUNING SIMPLEX CHAT</string> - <string name="settings_section_title_you">JIJ</string> - <string name="run_chat_section">CHAT UITVOEREN</string> + <string name="settings_section_title_settings">Instellingen</string> + <string name="settings_section_title_support">Ondersteuning SimpleX Chat</string> + <string name="settings_section_title_you">Jij</string> + <string name="run_chat_section">Chat uitvoeren</string> <string name="your_chat_database">Uw chat database</string> <string name="set_password_to_export">Wachtwoord instellen om te exporteren</string> <string name="restart_the_app_to_create_a_new_chat_profile">Start de app opnieuw om een nieuw chatprofiel aan te maken.</string> @@ -790,7 +790,7 @@ <string name="button_send_direct_message">Direct bericht sturen</string> <string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.</string> <string name="sending_via">Verzenden via</string> - <string name="conn_stats_section_title_servers">SERVERS</string> + <string name="conn_stats_section_title_servers">Servers</string> <string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string> <string name="switch_receiving_address">Ontvangst adres wijzigen</string> <string name="network_option_protocol_timeout">Protocol timeout</string> @@ -874,11 +874,11 @@ <string name="share_message">Bericht delen…</string> <string name="la_notice_title_simplex_lock">SimpleX Vergrendelen</string> <string name="save_passphrase_in_keychain">Sla het wachtwoord op in Keychain</string> - <string name="settings_section_title_socks">SOCKS PROXY</string> + <string name="settings_section_title_socks">SOCKS proxy</string> <string name="v4_5_italian_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string> <string name="periodic_notifications_desc">De app haalt regelmatig nieuwe berichten op - het gebruikt een paar procent van de batterij per dag. De app maakt geen gebruik van push meldingen, gegevens van uw apparaat worden niet naar de servers verzonden.</string> <string name="image_decoding_exception_desc">De afbeelding kan niet worden gedecodeerd. Probeer een andere afbeelding of neem contact op met de ontwikkelaars.</string> - <string name="settings_section_title_themes">THEMA\'S</string> + <string name="settings_section_title_themes">Thema\'s</string> <string name="smp_servers_scan_qr">Scan server QR-code</string> <string name="this_string_is_not_a_connection_link">Deze string is geen verbinding link!</string> <string name="enable_automatic_deletion_message">Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</string> @@ -887,8 +887,7 @@ <string name="chat_preferences_you_allow">Jij staat toe</string> <string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>.]]></string> - <string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. -\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string> + <string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. \nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string> <string name="use_simplex_chat_servers__question">SimpleX Chat servers gebruiken\?</string> <string name="voice_messages_are_prohibited">Spraak berichten zijn niet toegestaan.</string> <string name="personal_welcome">Welkom %1$s!</string> @@ -994,7 +993,7 @@ <string name="developer_options">Database-ID\'s en Transport isolatie optie.</string> <string name="hide_dev_options">Verbergen:</string> <string name="show_developer_options">Ontwikkelaars opties tonen</string> - <string name="settings_section_title_experimenta">EXPERIMENTEEL</string> + <string name="settings_section_title_experimenta">Experimenteel</string> <string name="delete_profile">Verwijder profiel</string> <string name="profile_password">Profiel wachtwoord</string> <string name="unhide_chat_profile">Chatprofiel zichtbaar maken</string> @@ -1146,7 +1145,7 @@ <string name="import_theme_error_desc">Zorg ervoor dat het bestand de juiste YAML-syntaxis heeft. Exporteer het thema om een voorbeeld te hebben van de themabestandsstructuur.</string> <string name="opening_database">Database openen…</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Lees meer in de <font color="#0088ff">Gebruikershandleiding</font>.]]></string> - <string name="theme_colors_section_title">INTERFACE KLEUREN</string> + <string name="theme_colors_section_title">Interface kleuren</string> <string name="you_can_share_your_address">U kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken.</string> <string name="all_app_data_will_be_cleared">Alle app-gegevens worden verwijderd.</string> <string name="empty_chat_profile_is_created">Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend.</string> @@ -1226,7 +1225,7 @@ <string name="item_info_no_text">geen tekst</string> <string name="non_fatal_errors_occured_during_import">Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:</string> <string name="shutdown_alert_question">Afsluiten\?</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="settings_restart_app">Herstarten</string> <string name="settings_shutdown">Afsluiten</string> <string name="shutdown_alert_desc">Meldingen werken niet meer totdat u de app opnieuw start</string> @@ -1285,7 +1284,7 @@ <string name="receipts_contacts_enable_keep_overrides">Inschakelen (overschrijvingen behouden)</string> <string name="receipts_contacts_override_disabled">Het verzenden van ontvangst bevestiging is uitgeschakeld voor %d-contactpersonen</string> <string name="receipts_contacts_disable_for_all">Uitschakelen voor iedereen</string> - <string name="settings_section_title_delivery_receipts">STUUR ONTVANGST BEVESTIGING NAAR</string> + <string name="settings_section_title_delivery_receipts">Stuur ontvangst bevestiging naar</string> <string name="send_receipts">Ontvangst bevestiging verzenden</string> <string name="v5_2_message_delivery_receipts_descr">De tweede vink die we gemist hebben! ✅</string> <string name="v5_2_favourites_filter_descr">Filter ongelezen en favoriete chats.</string> @@ -1779,13 +1778,13 @@ <string name="private_routing_explanation">Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen.</string> <string name="network_smp_proxy_fallback_prohibit_description">Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt.</string> <string name="update_network_smp_proxy_fallback_question">Terugval op berichtroutering</string> - <string name="settings_section_title_private_message_routing">PRIVÉBERICHT ROUTING</string> + <string name="settings_section_title_private_message_routing">Privébericht routing</string> <string name="network_smp_proxy_fallback_allow_protected_description">Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt.</string> <string name="file_not_approved_title">Onbekende servers!</string> <string name="file_not_approved_descr">Zonder Tor of VPN is uw IP-adres zichtbaar voor deze XFTP-relays: \n%1$s.</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers.</string> - <string name="settings_section_title_files">BESTANDEN</string> + <string name="settings_section_title_files">Bestanden</string> <string name="protect_ip_address">Bescherm het IP-adres</string> <string name="app_will_ask_to_confirm_unknown_file_servers">De app vraagt om downloads van onbekende bestandsservers te bevestigen (behalve .onion of wanneer SOCKS-proxy is ingeschakeld).</string> <string name="error_initializing_web_view">Fout bij het initialiseren van WebView. Update uw systeem naar de nieuwe versie. Neem contact op met ontwikkelaars. @@ -2055,7 +2054,7 @@ <string name="select_chat_profile">Selecteer chatprofiel</string> <string name="new_chat_share_profile">Profiel delen</string> <string name="switching_profile_error_message">Uw verbinding is verplaatst naar %s, maar er is een onverwachte fout opgetreden tijdens het omleiden naar het profiel.</string> - <string name="settings_section_title_chat_database">CHAT DATABASE</string> + <string name="settings_section_title_chat_database">Chat database</string> <string name="system_mode_toast">Systeemmodus</string> <string name="migrate_from_device_remove_archive_question">Archief verwijderen?</string> <string name="delete_messages_cannot_be_undone_warning">Berichten worden verwijderd. Dit kan niet ongedaan worden gemaakt!</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 9cc43851d6..0cf195510b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -495,36 +495,36 @@ <string name="alert_title_skipped_messages">Pominięte wiadomości</string> <string name="your_privacy">Twoja prywatność</string> <string name="full_backup">Kopia zapasowa danych aplikacji</string> - <string name="settings_section_title_icon">IKONA APLIKACJI</string> + <string name="settings_section_title_icon">Ikona aplikacji</string> <string name="auto_accept_images">Automatyczne akceptowanie obrazów</string> - <string name="settings_section_title_calls">POŁĄCZENIA</string> - <string name="chat_database_section">BAZA DANYCH CZATU</string> + <string name="settings_section_title_calls">Połączenia</string> + <string name="chat_database_section">Baza danych czatu</string> <string name="chat_is_running">Czat jest uruchomiony</string> <string name="chat_is_stopped">Czat jest zatrzymany</string> - <string name="settings_section_title_chats">CZATY</string> + <string name="settings_section_title_chats">Czaty</string> <string name="database_passphrase">Hasło do bazy danych</string> <string name="delete_database">Usuń bazę danych</string> <string name="settings_developer_tools">Narzędzia deweloperskie</string> - <string name="settings_section_title_device">URZĄDZENIE</string> + <string name="settings_section_title_device">Urządzenie</string> <string name="error_starting_chat">Błąd uruchamiania czatu</string> - <string name="settings_section_title_experimenta">EKSPERYMENTALNE</string> + <string name="settings_section_title_experimenta">Eksperymentalne</string> <string name="settings_experimental_features">Funkcje eksperymentalne</string> <string name="export_database">Eksportuj bazę danych</string> - <string name="settings_section_title_help">POMOC</string> + <string name="settings_section_title_help">Pomoc</string> <string name="import_database">Importuj bazę danych</string> <string name="settings_section_title_incognito">Tryb incognito</string> - <string name="settings_section_title_messages">WIADOMOŚCI I PLIKI</string> + <string name="settings_section_title_messages">Wiadomości i pliki</string> <string name="new_database_archive">Nowe archiwum bazy danych</string> <string name="old_database_archive">Stare archiwum bazy danych</string> <string name="protect_app_screen">Chroń ekran aplikacji</string> - <string name="run_chat_section">URUCHOM CZAT</string> + <string name="run_chat_section">Uruchom czat</string> <string name="send_link_previews">Wyślij podgląd linku</string> - <string name="settings_section_title_settings">USTAWIENIA</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="settings_section_title_settings">Ustawienia</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> <string name="stop_chat_question">Zatrzymać czat\?</string> - <string name="settings_section_title_support">WSPIERAJ SIMPLEX CHAT</string> - <string name="settings_section_title_themes">MOTYWY</string> - <string name="settings_section_title_you">TY</string> + <string name="settings_section_title_support">Wspieraj SimpleX Chat</string> + <string name="settings_section_title_themes">Motywy</string> + <string name="settings_section_title_you">Ty</string> <string name="your_chat_database">Twoja baza danych czatu</string> <string name="set_password_to_export">Ustaw hasło do eksportu</string> <string name="stop_chat_confirmation">Zatrzymaj</string> @@ -696,7 +696,7 @@ <string name="invite_prohibited_description">Próbujesz zaprosić osobę, z którą masz wspólny profil incognito do grupy, w której używasz swojego głównego profilu</string> <string name="group_info_member_you">ty: %1$s</string> <string name="change_verb">Zmień</string> - <string name="change_member_role_question">Zmienić rolę grupy\?</string> + <string name="change_member_role_question">Zmienić rolę grupy?</string> <string name="change_role">Zmień rolę</string> <string name="info_row_connection">Połączenie</string> <string name="info_row_database_id">ID bazy danych</string> @@ -707,12 +707,12 @@ <string name="error_creating_link_for_group">Błąd tworzenia linku grupy</string> <string name="error_deleting_link_for_group">Błąd usuwania linku grupy</string> <string name="error_removing_member">Błąd usuwania członka</string> - <string name="section_title_for_console">DLA KONSOLI</string> + <string name="section_title_for_console">Dla konsoli</string> <string name="info_row_group">Grupa</string> <string name="group_display_name_field">Wprowadź nazwę grupy:</string> <string name="group_full_name_field">Pełna nazwa grupy:</string> <string name="info_row_local_name">Nazwa lokalna</string> - <string name="member_info_section_title_member">CZŁONEK</string> + <string name="member_info_section_title_member">Członek</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Członek zostanie usunięty z grupy - nie można tego cofnąć!</string> <string name="network_status">Status sieci</string> <string name="only_group_owners_can_change_prefs">Tylko właściciele grup mogą zmieniać preferencje grupy.</string> @@ -724,7 +724,7 @@ <string name="save_welcome_message_question">Zapisać wiadomość powitalną\?</string> <string name="button_send_direct_message">Wyślij wiadomość bezpośrednią</string> <string name="sending_via">Wysyłanie przez</string> - <string name="conn_stats_section_title_servers">SERWERY</string> + <string name="conn_stats_section_title_servers">Serwery</string> <string name="switch_verb">Przełącz</string> <string name="switch_receiving_address">Zmień adres odbioru</string> <string name="group_is_decentralized">W pełni zdecentralizowana – widoczna tylko dla członków.</string> @@ -988,8 +988,7 @@ <string name="to_protect_privacy_simplex_has_ids_for_queues">Aby chronić Twoją prywatność, SimpleX używa oddzielnych identyfikatorów dla każdego z Twoich kontaktów.</string> <string name="to_verify_compare">Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.</string> <string name="smp_servers_use_server_for_new_conn">Użyj dla nowych połączeń</string> - <string name="connection_error_auth_desc">O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go. -\nAby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią.</string> + <string name="connection_error_auth_desc">O ile Twój kontakt nie usunął połączenia lub ten link był już użyty, może to być błąd - zgłoś go. \nAby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połączenia i sprawdź, czy masz stabilne połączenie z siecią.</string> <string name="use_simplex_chat_servers__question">Używać serwerów SimpleX Chat\?</string> <string name="icon_descr_simplex_team">Drużyna SimpleX</string> <string name="network_use_onion_hosts_prefer">Gdy dostępny</string> @@ -1136,7 +1135,7 @@ <string name="you_can_accept_or_reject_connection">Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić.</string> <string name="you_wont_lose_your_contacts_if_delete_address">Nie stracisz kontaktów, jeśli później usuniesz swój adres.</string> <string name="customize_theme_title">Dostosuj motyw</string> - <string name="theme_colors_section_title">KOLORY INTERFEJSU</string> + <string name="theme_colors_section_title">Kolory interfejsu</string> <string name="your_contacts_will_remain_connected">Twoje kontakty pozostaną połączone.</string> <string name="add_address_to_your_profile">Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów.</string> <string name="create_address_and_let_people_connect">Utwórz adres, aby ludzie mogli się z Tobą połączyć.</string> @@ -1229,7 +1228,7 @@ <string name="item_info_no_text">brak tekstu</string> <string name="non_fatal_errors_occured_during_import">Podczas importu wystąpiły niekrytyczne błędy:</string> <string name="settings_restart_app">Restart</string> - <string name="settings_section_title_app">APLIKACJA</string> + <string name="settings_section_title_app">Aplikacja</string> <string name="shutdown_alert_desc">Powiadomienia przestaną działać do momentu ponownego uruchomienia aplikacji.</string> <string name="settings_shutdown">Wyłączenie</string> <string name="shutdown_alert_question">Wyłączyć\?</string> @@ -1262,7 +1261,7 @@ <string name="v5_2_disappear_one_message">Spraw, aby jedna wiadomość zniknęła</string> <string name="renegotiate_encryption">Renegocjuj szyfrowanie</string> <string name="rcv_conn_event_verification_code_reset">kod bezpieczeństwa zmieniony</string> - <string name="settings_section_title_delivery_receipts">WYŚLIJ POTWIERDZENIA DOSTAWY DO</string> + <string name="settings_section_title_delivery_receipts">Wyślij potwierdzenia dostawy do</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Wysyłanie potwierdzeń dostawy zostanie włączone dla wszystkich kontaktów we wszystkich widocznych profilach czatu.</string> <string name="receipts_section_contacts">Kontakty</string> <string name="receipts_contacts_title_enable">Włączyć potwierdzenia\?</string> @@ -1772,7 +1771,7 @@ <string name="network_smp_proxy_fallback_prohibit">Nie</string> <string name="network_smp_proxy_fallback_allow_protected">Gdy IP ukryty</string> <string name="private_routing_show_message_status">Pokaż status wiadomości</string> - <string name="settings_section_title_private_message_routing">TRASOWANIE PRYWATNYCH WIADOMOŚCI</string> + <string name="settings_section_title_private_message_routing">Trasowanie prywatnych wiadomości</string> <string name="network_smp_proxy_fallback_prohibit_description">NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania.</string> <string name="private_routing_explanation">Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości.</string> <string name="network_smp_proxy_mode_unknown">Nieznane serwery</string> @@ -1788,7 +1787,7 @@ <string name="protect_ip_address">Chroń adres IP</string> <string name="app_will_ask_to_confirm_unknown_file_servers">Aplikacja będzie prosić o potwierdzenie pobierań z nieznanych serwerów plików (z wyjątkiem .onion lub gdy proxy SOCKS jest włączone).</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików.</string> - <string name="settings_section_title_files">PLIKI</string> + <string name="settings_section_title_files">Pliki</string> <string name="settings_section_title_user_theme">Motyw profilu</string> <string name="chat_list_always_visible">Pokaż listę czatów w nowym oknie</string> <string name="dark_mode_colors">Kolory ciemnego trybu</string> @@ -2065,7 +2064,7 @@ <string name="forward_files_in_progress_desc">%1$d plik(ów/i) dalej są pobierane.</string> <string name="forward_files_failed_to_receive_desc">%1$d plik(ów/i) nie udało się pobrać.</string> <string name="switching_profile_error_title">Błąd zmiany profilu</string> - <string name="settings_section_title_chat_database">BAZA CZATU</string> + <string name="settings_section_title_chat_database">Baza czatu</string> <string name="n_file_errors">%1$d błędów plików:\n%2$s</string> <string name="n_other_file_errors">%1$d innych błędów plików.</string> <string name="forward_files_messages_deleted_after_selection_desc">Wiadomości zostały usunięte po wybraniu ich.</string> @@ -2233,7 +2232,7 @@ <string name="cant_send_message_contact_deleted">kontakt usunięty</string> <string name="cant_send_message_contact_disabled">kontakt wyłączony</string> <string name="cant_send_message_contact_not_ready">kontakt nie gotowy</string> - <string name="settings_section_title_contact_requests_from_groups">PROŚBY O KONTAKT OD GRUP</string> + <string name="settings_section_title_contact_requests_from_groups">Prośby o kontakt od grup</string> <string name="contact_should_accept">kontakt powinien zaakceptować…</string> <string name="v6_4_1_short_address_create">Stwórz swój adres</string> <string name="group_new_support_chats_short">%d czat(y)</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index c129d68521..cbf69db5d4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -74,16 +74,16 @@ <string name="network_session_mode_user_description"><![CDATA[Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada perfil de bate-papo que você tiver no aplicativo</b>.]]></string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Melhor para bateria</b>. Você receberá notificações apenas quando o aplicativo estiver em execução (SEM o serviço em segundo plano).]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consome mais bateria</b>! O aplicativo em segundo plano está sempre em execução - as notificações são exibidas instantaneamente.]]></string> - <string name="settings_section_title_chats">BATE-PAPOS</string> - <string name="settings_section_title_icon">ÍCONE DO APLICATIVO</string> - <string name="chat_database_section">BANCO DE DADOS DE BATE-PAPO</string> + <string name="settings_section_title_chats">Bate-papos</string> + <string name="settings_section_title_icon">Ícone do aplicativo</string> + <string name="chat_database_section">Banco de dados de bate-papo</string> <string name="chat_is_running">O bate-papo está em execução</string> <string name="chat_is_stopped">O bate-papo está parado</string> <string name="change_database_passphrase_question">Alterar senha do banco de dados\?</string> <string name="rcv_conn_event_switch_queue_phase_completed">endereço alterado para você</string> <string name="both_you_and_your_contact_can_send_disappearing">Você e seu contato podem enviar mensagens temporárias.</string> <string name="full_backup">Backup de dados do aplicativo</string> - <string name="settings_section_title_calls">CHAMADAS</string> + <string name="settings_section_title_calls">Chamadas</string> <string name="v4_2_auto_accept_contact_requests">Aceitar solicitações de contato automaticamente</string> <string name="appearance_settings">Aparência</string> <string name="notifications_mode_service_desc">O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string> @@ -210,7 +210,7 @@ <string name="callstate_connecting">conectando…</string> <string name="group_member_status_announced">conectando (anunciado)</string> <string name="network_session_mode_entity">Conexão</string> - <string name="connection_error_auth">Erro de conexão (AUTH)</string> + <string name="connection_error_auth">Erro de conexão</string> <string name="display_name_connection_established">conexão estabelecida</string> <string name="connection_local_display_name">conexão %1$d</string> <string name="maximum_supported_file_size">Atualmente, o tamanho máximo de arquivo suportado é %1$s.</string> @@ -247,7 +247,7 @@ <string name="connection_timeout">Tempo de conexão esgotado</string> <string name="delete_member_message__question">Excluir mensagem do membro\?</string> <string name="smp_server_test_delete_queue">Excluir fila</string> - <string name="settings_section_title_device">DISPOSITIVO</string> + <string name="settings_section_title_device">Dispositivo</string> <string name="settings_developer_tools">Ferramentas de desenvolvedor</string> <string name="group_member_status_introduced">conectando (introduzido)</string> <string name="color_primary">Tonalidade</string> @@ -378,7 +378,7 @@ <string name="file_saved">Arquivo salvo</string> <string name="group_members_can_send_voice">Os membros podem enviar mensagens de voz.</string> <string name="delete_group_for_all_members_cannot_undo_warning">O grupo será excluído para todos os membros - isso não pode ser desfeito!</string> - <string name="settings_section_title_help">AJUDA</string> + <string name="settings_section_title_help">Ajuda</string> <string name="notification_display_mode_hidden_desc">Ocultar contato e mensagem</string> <string name="how_to_use_simplex_chat">Como usar</string> <string name="how_to_use_markdown">Como usar markdown</string> @@ -420,7 +420,7 @@ <string name="enter_one_ICE_server_per_line">Servidores ICE (um por linha)</string> <string name="ignore">Ignorar</string> <string name="image_will_be_received_when_contact_is_online">A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!</string> - <string name="conn_stats_section_title_servers">SERVIDORES</string> + <string name="conn_stats_section_title_servers">Servidores</string> <string name="receiving_via">Recebendo via</string> <string name="network_status">Status da conexão</string> <string name="network_option_seconds_label">seg</string> @@ -495,8 +495,8 @@ <string name="network_enable_socks">Usar proxy SOCKS\?</string> <string name="icon_descr_call_rejected">Chamada rejeitada</string> <string name="restore_database">Restaurar o backup do banco de dados</string> - <string name="section_title_for_console">PARA CONSOLE</string> - <string name="run_chat_section">EXECUTAR BATE-PAPO</string> + <string name="section_title_for_console">Para console</string> + <string name="run_chat_section">Executar bate-papo</string> <string name="stop_chat_confirmation">Parar</string> <string name="set_password_to_export">Definir senha para exportar</string> <string name="restart_the_app_to_use_imported_chat_database">Reinicie o aplicativo para usar o banco de dados do chat importado.</string> @@ -571,7 +571,7 @@ <string name="snd_group_event_changed_member_role">você mudou o cargo de %s para %s</string> <string name="new_member_role">Novo cargo de membro</string> <string name="remove_member_confirmation">Remover</string> - <string name="member_info_section_title_member">MEMBRO</string> + <string name="member_info_section_title_member">Membro</string> <string name="member_will_be_removed_from_group_cannot_be_undone">O membro será removido do grupo - isso não pode ser desfeito!</string> <string name="role_in_group">Cargo</string> <string name="sending_via">Enviando via</string> @@ -663,8 +663,8 @@ <string name="v4_6_chinese_spanish_interface">Interface chinesa e espanhola</string> <string name="v4_6_reduced_battery_usage">Maior redução no uso da bateria</string> <string name="v4_6_reduced_battery_usage_descr">Mais melhorias chegarão em breve!</string> - <string name="settings_section_title_you">VOCÊ</string> - <string name="settings_section_title_messages">MENSAGENS E ARQUIVOS</string> + <string name="settings_section_title_you">Você</string> + <string name="settings_section_title_messages">Mensagens e arquivos</string> <string name="your_chat_database">Seu banco de dados de bate-papo</string> <string name="snd_group_event_member_deleted">Você removeu %1$s</string> <string name="group_member_status_removed">removido</string> @@ -800,7 +800,7 @@ <string name="hide_profile">Ocultar perfil</string> <string name="callstate_received_confirmation">confirmação recebida…</string> <string name="relay_server_protects_ip">O servidor de relay protege seu endereço IP, mas pode observar a duração da chamada.</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="snd_conn_event_switch_queue_phase_completed">você alterou o endereço</string> <string name="database_upgrade">Atualização do banco de dados</string> <string name="member_role_will_be_changed_with_invitation">O cargo será alterado para "%s". O membro receberá um novo convite.</string> @@ -819,7 +819,7 @@ <string name="only_group_owners_can_enable_voice">Somente o proprietários de grupo podem ativar mensagens de voz</string> <string name="description_you_shared_one_time_link">você compartilhou um link de uso único</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">Você será conectado quando sua solicitação de conexão for aceita, aguarde ou verifique mais tarde!</string> - <string name="settings_section_title_settings">CONFIGURAÇÕES</string> + <string name="settings_section_title_settings">Configurações</string> <string name="v4_6_group_welcome_message_descr">Defina a mensagem mostrada aos novos membros!</string> <string name="icon_descr_settings">Configurações</string> <string name="switch_receiving_address">Alternar endereço de recebimento</string> @@ -848,7 +848,7 @@ <string name="la_notice_turn_on">Ligar</string> <string name="welcome">Bem-vindo(a)!</string> <string name="next_generation_of_private_messaging">O futuro da transmissão de mensagens</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> <string name="database_backup_can_be_restored">A tentativa de alterar a senha do banco de dados não foi concluída.</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Pare o bate-papo para exportar, importar ou excluir o banco de dados do chat. Você não poderá receber e enviar mensagens enquanto o chat estiver interrompido.</string> <string name="chat_item_ttl_seconds">%s segundo(s)</string> @@ -866,8 +866,7 @@ <string name="unknown_message_format">formato de mensagem desconhecido</string> <string name="description_via_group_link">via link de grupo</string> <string name="simplex_link_connection">via %1$s</string> - <string name="connection_error_auth_desc">A menos que seu contato tenha excluído a conexão ou este link já tenha sido usado, pode ser um bug - por favor, relate-o. -\nPara se conectar, peça ao seu contato para criar outro link de conexão e verifique se você tem uma conexão de rede estável.</string> + <string name="connection_error_auth_desc">A menos que seu contato tenha excluído a conexão ou este link já tenha sido usado, pode ser um bug - por favor, relate-o. \nPara se conectar, peça ao seu contato para criar outro link de conexão e verifique se você tem uma conexão de rede estável.</string> <string name="error_smp_test_failed_at_step">O teste falhou na etapa %s.</string> <string name="notifications_mode_periodic">Inicia periodicamente</string> <string name="icon_descr_sent_msg_status_unauthorized_send">envio não autorizado</string> @@ -887,7 +886,7 @@ <string name="icon_descr_video_call">chamada de vídeo</string> <string name="show_call_on_lock_screen">Mostrar</string> <string name="webrtc_ice_servers">Servidores ICE WebRTC</string> - <string name="settings_section_title_themes">TEMAS</string> + <string name="settings_section_title_themes">Temas</string> <string name="update_database">Atualizar</string> <string name="periodic_notifications_desc">O app busca novas mensagens periodicamente – ele usa alguns por cento da bateria por dia. O aplicativo não usa notificações por push – os dados do seu dispositivo não são enviados para os servidores.</string> <string name="enter_passphrase_notification_desc">Para receber notificações, por favor, digite a senha do banco de dados</string> @@ -1001,7 +1000,7 @@ <string name="feature_off">desativado</string> <string name="downgrade_and_open_chat">Desatualizar e abrir o bate-papo</string> <string name="chat_preferences_off">desativado</string> - <string name="settings_section_title_support">APOIE SIMPLEX CHAT</string> + <string name="settings_section_title_support">Apoie SimpleX Chat</string> <string name="enable_automatic_deletion_message">Esta ação não pode ser desfeita - as mensagens enviadas e recebidas antes do selecionado serão excluídas. Pode levar vários minutos.</string> <string name="confirm_database_upgrades">Confirme as atualizações do banco de dados</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Somente o cliente dos dispositivos armazenam perfis de usuários, contatos, grupos e mensagens.</string> @@ -1127,7 +1126,7 @@ <string name="you_wont_lose_your_contacts_if_delete_address">Você não perderá seus contatos se, posteriormente, excluir seu endereço.</string> <string name="simplex_address">Endereço SimpleX</string> <string name="you_can_accept_or_reject_connection">Quando as pessoas solicitam uma conexão, você pode aceitá-la ou rejeitá-la.</string> - <string name="theme_colors_section_title">CORES DA INTERFACE</string> + <string name="theme_colors_section_title">Cores da interface</string> <string name="share_with_contacts">compartilhar com os contatos</string> <string name="profile_update_will_be_sent_to_contacts">A atualização do perfil será enviada aos seus contatos.</string> <string name="save_settings_question">Salvar configurações\?</string> @@ -1253,7 +1252,7 @@ <string name="fix_connection_not_supported_by_group_member">Correção não suportada pelo membro do grupo</string> <string name="conn_event_ratchet_sync_started">concordando com criptografia…</string> <string name="allow_to_send_files">Permitir o envio de arquivos e mídia.</string> - <string name="settings_section_title_app">APP</string> + <string name="settings_section_title_app">App</string> <string name="conn_event_ratchet_sync_ok">criptografia OK</string> <string name="conn_event_ratchet_sync_required">renegociação de criptografia necessária</string> <string name="snd_conn_event_ratchet_sync_agreed">criptografia concordada para %s</string> @@ -1284,7 +1283,7 @@ <string name="receipts_contacts_title_enable">Ativar recibos?</string> <string name="v5_2_favourites_filter">Encontrar conversas mais rápido</string> <string name="receipts_section_contacts">Contatos</string> - <string name="settings_section_title_delivery_receipts">ENVIAR RECIBOS DE ENTREGA PARA</string> + <string name="settings_section_title_delivery_receipts">Enviar recibos de entrega para</string> <string name="receipts_contacts_override_disabled">Enviar confirmações está desativado para %d contatos.</string> <string name="receipts_contacts_override_enabled">Enviar confirmações está ativado para %d contatos.</string> <string name="send_receipts">Enviar confirmações</string> @@ -1862,9 +1861,9 @@ <string name="audio_device_speaker">Alto falante</string> <string name="audio_device_wired_headphones">Headphones</string> <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Sem Tor ou VPN, seu endereço de IP ficará visível para servidores de arquivo.</string> - <string name="settings_section_title_files">ARQUIVOS</string> + <string name="settings_section_title_files">Arquivos</string> <string name="settings_section_title_profile_images">Fotos de perfil</string> - <string name="settings_section_title_private_message_routing">ROTEAMENTO DE MENSAGEM PRIVADA</string> + <string name="settings_section_title_private_message_routing">Roteamento de mensagem privada</string> <string name="conn_event_disabled_pq">criptografia padrão ponta a ponta</string> <string name="feature_roles_owners">proprietários</string> <string name="migrate_from_device_to_another_device">Migrar para outro dispositivo</string> @@ -2055,7 +2054,7 @@ <string name="one_hand_ui">Barras de ferramentas de aplicativos acessível</string> <string name="forward_files_failed_to_receive_desc">Falha no baixar de %1$d arquivo(s).</string> <string name="forward_files_messages_deleted_after_selection_title">%1$s mensagens não encaminhadas.</string> - <string name="settings_section_title_chat_database">DADOS DO BATE-PAPO</string> + <string name="settings_section_title_chat_database">Dados do bate-papo</string> <string name="network_proxy_random_credentials">Utilize credenciais aleatórias</string> <string name="migrate_from_device_uploaded_archive_will_be_removed">O arquivo de banco de dados enviado será removido permanentemente dos servidores.</string> <string name="network_proxy_auth_mode_isolate_by_auth_entity">Use credenciais diferentes de proxy para cada conexão.</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 5f12e762aa..08285dbe78 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -28,9 +28,9 @@ <string name="full_backup">Backup de dados da aplicação</string> <string name="auto_accept_images">Aceitar imagens automaticamente</string> <string name="passcode_set">Código de acesso definido!</string> - <string name="settings_section_title_you">VOCÊ</string> - <string name="settings_section_title_messages">MENSAGENS E FICHEIROS</string> - <string name="settings_section_title_icon">ÍCONE DA APLICAÇÃO</string> + <string name="settings_section_title_you">Você</string> + <string name="settings_section_title_messages">Mensagens e ficheiros</string> + <string name="settings_section_title_icon">Ícone da aplicação</string> <string name="chat_item_ttl_month">1 mês</string> <string name="messages_section_title">Mensagens</string> <string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string> @@ -137,7 +137,7 @@ <string name="delete_group_menu_action">Eliminar</string> <string name="delete_files_and_media_all">Eliminar todos os ficheiros</string> <string name="delete_database">Eliminar base de dados</string> - <string name="chat_database_section">BASE DE DADOS DE CONVERSA</string> + <string name="chat_database_section">Base de dados de conversa</string> <string name="chat_database_deleted">Base de dados de conversa eliminada</string> <string name="display_name">Nome para Exibição</string> <string name="show_dev_options">Mostrar:</string> @@ -184,7 +184,7 @@ <string name="paste_the_link_you_received">Colar ligação recebida</string> <string name="restore_passphrase_not_found_desc">Senha não encontrada na Keystore, por favor insira-a manualmente. Isto pode ter acontecido se você restaurou os dados da aplicação usando uma ferramenta de backup. Se não for o caso, entre em contato com os desenvolvedores.</string> <string name="error_smp_test_server_auth">O servidor requer autorização para criar filas, verifique a senha</string> - <string name="conn_stats_section_title_servers">SERVIDORES</string> + <string name="conn_stats_section_title_servers">Servidores</string> <string name="error_xftp_test_server_auth">O servidor requer autorização para fazer upload, verifique a senha</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Defina <i>Usar hosts .onion</i> como Não se o proxy SOCKS não o suportar.]]></string> <string name="network_use_onion_hosts">Usar hosts .onion</string> @@ -226,7 +226,7 @@ <string name="call_already_ended">Chamada já finalizada!</string> <string name="icon_descr_call_progress">Chamada em curso</string> <string name="icon_descr_call_ended">Chamada finalizada</string> - <string name="settings_section_title_calls">CHAMADAS</string> + <string name="settings_section_title_calls">Chamadas</string> <string name="v4_5_transport_isolation_descr">Por perfil de conversa (padrão) ou por ligação (BETA).</string> <string name="cannot_access_keychain">Não é possível aceder à Keystore para salvar a senha da base de dados</string> <string name="invite_prohibited">Não é possível convidar o contato!</string> @@ -251,7 +251,7 @@ <string name="error_deleting_link_for_group">Erro ao eliminar ligação de grupo</string> <string name="network_session_mode_user">Perfil de conversa</string> <string name="change_lock_mode">Alterar o modo de bloqueio</string> - <string name="settings_section_title_chats">CONVERSAS</string> + <string name="settings_section_title_chats">Conversas</string> <string name="chat_is_running">Conversa em execução</string> <string name="error_changing_message_deletion">Erro ao alterar configuração</string> <string name="change_database_passphrase_question">Alterar a senha da base de dados\?</string> @@ -263,7 +263,7 @@ <string name="clear_contacts_selection_button">Limpar</string> <string name="error_creating_link_for_group">Erro ao criar ligação de grupo</string> <string name="change_role">Alterar função</string> - <string name="change_member_role_question">Alterar a função no grupo\?</string> + <string name="change_member_role_question">Alterar a função no grupo?</string> <string name="error_changing_role">Erro ao alterar função</string> <string name="chat_preferences_contact_allows">O contacto permite</string> <string name="chat_preferences">Preferências de conversa</string> @@ -333,7 +333,7 @@ <string name="group_link">Ligação de grupo</string> <string name="display_name_connecting">conectando…</string> <string name="display_name_connection_established">conexão estabelecida</string> - <string name="connection_error_auth">Erro de conexão (AUTH)</string> + <string name="connection_error_auth">Erro de conexão</string> <string name="smp_server_test_create_file">Criar ficheiro</string> <string name="group_connection_pending">conectando…</string> <string name="icon_descr_context">Ícone de contexto</string> @@ -471,7 +471,7 @@ <string name="info_row_database_id">ID da base de dados</string> <string name="smp_server_test_delete_file">Eliminar ficheiro</string> <string name="delete_contact_question">Eliminar contacto?</string> - <string name="settings_section_title_device">DISPOSITIVO</string> + <string name="settings_section_title_device">Dispositivo</string> <string name="direct_messages">Mensagens diretas</string> <string name="decentralized">Descentralizado</string> <string name="integrity_msg_duplicate">mensagem duplicada</string> @@ -501,7 +501,7 @@ <string name="leave_group_button">Sair</string> <string name="leave_group_question">Deixar o grupo\?</string> <string name="rcv_group_event_member_left">esquerda</string> - <string name="group_info_section_title_num_members"> %1$s MEMBROS</string> + <string name="group_info_section_title_num_members">%1$s MEMBROS</string> <string name="button_leave_group">Deixar o grupo</string> <string name="chat_preferences_yes">sim</string> <string name="description_via_group_link">via ligação de grupo</string> @@ -540,7 +540,7 @@ <string name="alert_text_decryption_error_too_many_skipped">%1$d mensagens ignoradas.</string> <string name="import_database">Importar base de dados</string> <string name="your_settings">As suas definições</string> - <string name="settings_section_title_settings">DEFINIÇÕES</string> + <string name="settings_section_title_settings">Definições</string> <string name="share_verb">Partilhar</string> <string name="share_address">Partilhar endereço</string> <string name="icon_descr_settings">Definições</string> @@ -554,12 +554,12 @@ \nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos.</string> <string name="mark_unread">Marcar como não lido</string> <string name="group_member_role_member">membro</string> - <string name="member_info_section_title_member">MEMBRO</string> + <string name="member_info_section_title_member">Membro</string> <string name="v4_3_voice_messages_desc">Máximo de 40 segundos, recebido instantaneamente.</string> <string name="icon_descr_more_button">Mais</string> <string name="network_and_servers">Rede e servidores</string> <string name="network_settings_title">Configurações avançadas</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Você pode iniciar a conversa através das Definições da aplicação / Base de Dados ou reiniciando a aplicação.</string> <string name="update_network_settings_confirmation">Atualizar</string> <string name="updating_settings_will_reconnect_client_to_all_servers">A atualização das definições reconectará o cliente a todos os servidores.</string> @@ -572,10 +572,10 @@ <string name="message_delivery_error_desc">Muito provavelmente este contato eliminou a conexão consigo.</string> <string name="this_text_is_available_in_settings">Este texto está disponível nas definições</string> <string name="onboarding_notifications_mode_subtitle">Pode ser alterado mais tarde através das definições.</string> - <string name="settings_section_title_help">AJUDA</string> - <string name="settings_section_title_support">SUPORTE SIMPLEX CHAT</string> + <string name="settings_section_title_help">Ajuda</string> + <string name="settings_section_title_support">Suporte SimpleX Chat</string> <string name="settings_experimental_features">Funcionalidades experimentais</string> - <string name="settings_section_title_themes">TEMAS</string> + <string name="settings_section_title_themes">Temas</string> <string name="theme_dark">Escuro</string> <string name="dark_theme">Tema escuro</string> <string name="chat_item_ttl_none">nunca</string> 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 81cf8ed452..ae0a98b44e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -19,7 +19,7 @@ <string name="v5_3_new_interface_languages">6 noi limbi de interfață</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesaje nu au putut fi decriptate.</string> <string name="integrity_msg_skipped">%1$d mesaj(e) omis(e)</string> - <string name="group_info_section_title_num_members">%1$s MEMBRI</string> + <string name="group_info_section_title_num_members">%1$s membri</string> <string name="chat_item_ttl_day">1 zi</string> <string name="send_disappearing_message_1_minute">1 minut</string> <string name="one_time_link_short">Link unic</string> @@ -98,7 +98,7 @@ <string name="rcv_group_and_other_events">și %d alte evenimente</string> <string name="answer_call">Răspunde la apel</string> <string name="keychain_allows_to_receive_ntfs">Android Keystore va fi folosit pentru a stoca în siguranță parola după ce repornești aplicația sau schimbi parola — acest lucru va permite primirea de notificări.</string> - <string name="settings_section_title_app">APLICAȚIE</string> + <string name="settings_section_title_app">Aplicație</string> <string name="create_group_button">Creează grup</string> <string name="v4_6_audio_video_calls">Apeluri audio și video</string> <string name="migrate_from_device_archive_and_upload">Arhivează și încarcă</string> @@ -109,7 +109,7 @@ <string name="call_service_notification_audio_call">Apel audio</string> <string name="icon_descr_audio_call">apel audio</string> <string name="icon_descr_audio_off">Audio oprit</string> - <string name="settings_section_title_icon">PICTOGRAMĂ APLICAȚIE</string> + <string name="settings_section_title_icon">Pictogramă aplicație</string> <string name="la_app_passcode">Cod de acces aplicație</string> <string name="create_secret_group_title">Creează grup secret</string> <string name="smp_server_test_create_queue">Creează coadă</string> @@ -292,11 +292,11 @@ <string name="show_dev_options">Afișează:</string> <string name="show_internal_errors">Afișează erorile interne</string> <string name="secret_text">secret</string> - <string name="settings_section_title_settings">SETĂRI</string> + <string name="settings_section_title_settings">Setări</string> <string name="rcv_group_event_1_member_connected">%s conectat</string> <string name="profile_update_event_set_new_picture">setați o nouă poză de profil</string> <string name="share_text_sent_at">Trimis la: %s</string> - <string name="conn_stats_section_title_servers">SERVERE</string> + <string name="conn_stats_section_title_servers">Servere</string> <string name="send_live_message">Trimite mesaj în direct</string> <string name="migrate_to_device_bytes_downloaded">%s descărcat</string> <string name="share_address_with_contacts_question">Partajați adresa cu contactele?</string> @@ -376,7 +376,7 @@ <string name="alert_title_msg_bad_hash">Hash mesaj incorect</string> <string name="switch_receiving_address">Schimbă adresa de primire</string> <string name="chat_is_stopped_you_should_transfer_database">Conversația este oprită. Dacă ai folosit deja această bază de date pe alt dispozitiv, ar trebui să o transferi înapoi înainte de a porni conversația.</string> - <string name="settings_section_title_calls">APELURI</string> + <string name="settings_section_title_calls">Apeluri</string> <string name="snd_group_event_changed_role_for_yourself">v-ați schimbat rolul în %s</string> <string name="snd_error_quota">Capacitate depășită - destinatarul nu a primit mesajele trimise anterior.</string> <string name="change_self_destruct_passcode">Schimbă codul de acces autodistructibil</string> @@ -431,8 +431,8 @@ <string name="status_contact_has_e2e_encryption">contactul are criptare e2e</string> <string name="status_contact_has_no_e2e_encryption">contactul nu are criptare e2e</string> <string name="receipts_section_contacts">Contacte</string> - <string name="settings_section_title_chats">CONVERSAȚII</string> - <string name="chat_database_section">BAZĂ DE DATE CONVERSAȚIE</string> + <string name="settings_section_title_chats">Conversații</string> + <string name="chat_database_section">Bază de date conversație</string> <string name="chat_database_deleted">Baza de date a conversației a fost ștearsă</string> <string name="chat_is_running">Conversația rulează</string> <string name="your_chat_database">Baza de date a conversațiilor tale</string> @@ -549,7 +549,7 @@ <string name="connecting_to_desktop">Se conectează la desktop</string> <string name="icon_descr_asked_to_receive">S-a solicitat primirea imaginii</string> <string name="connection_request_sent">Cerere de conexiune trimisă!</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>De reținut</b>: releele de mesaje și fișiere sunt conectate prin proxy SOCKS. Apelurile și trimiterea de previzualizări ale adreselor web utilizează conexiunea directă.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>De reținut</b>: releele de mesaje și fișiere sunt conectate prin proxy SOCKS. Apelurile utilizează conexiunea directă.]]></string> <string name="callstate_connected">conectat</string> <string name="confirm_passcode">Confirmați codul de access</string> <string name="confirm_database_upgrades">Confirmare actualizare bază de date</string> @@ -684,7 +684,7 @@ <string name="app_check_for_updates_notice_title">Verifică pentru actualizări</string> <string name="create_address_button">Creează</string> <string name="privacy_media_blur_radius">Estompează media</string> - <string name="settings_section_title_chat_database">BAZĂ DE DATE CONVERSAȚIE</string> + <string name="settings_section_title_chat_database">Bază de date conversație</string> <string name="v6_0_connect_faster_descr">Conectează-te cu prietenii mai ușor.</string> <string name="attempts_label">încercări</string> <string name="completed">Finalizat</string> @@ -719,11 +719,11 @@ <string name="your_settings">Setări</string> <string name="encrypted_audio_call">apel audio criptat e2e</string> <string name="encrypted_video_call">apel video criptat e2e</string> - <string name="settings_section_title_device">DISPOZITIV</string> - <string name="settings_section_title_experimenta">EXPERIMENTAL</string> + <string name="settings_section_title_device">Dispozitiv</string> + <string name="settings_section_title_experimenta">Experimental</string> <string name="encrypt_database">Criptează</string> <string name="decryption_errors">erori de decriptare</string> - <string name="settings_section_title_you">TU</string> + <string name="settings_section_title_you">Tu</string> <string name="status_no_e2e_encryption">nicio criptare e2e</string> <string name="status_e2e_encrypted">criptat e2e</string> <string name="incoming_video_call">Apel video primit</string> @@ -825,7 +825,7 @@ <string name="onboarding_notifications_mode_battery">Notificări și baterie</string> <string name="open_verb">Deschide</string> <string name="receipts_groups_title_enable">Activați confirmarea de primire pentru grupuri?</string> - <string name="section_title_for_console">PENTRU CONSOLĂ</string> + <string name="section_title_for_console">Pentru consolă</string> <string name="info_row_moderated_at">Moderat la</string> <string name="fix_connection">Remediați conexiunea</string> <string name="v4_5_multiple_chat_profiles">Profiluri de conversație multiple</string> @@ -915,7 +915,7 @@ <string name="no_chats_in_list">Nicio conversație în lista %s.</string> <string name="selected_chat_items_nothing_selected">Nimic selectat</string> <string name="info_view_open_button">deschis</string> - <string name="settings_section_title_help">AJUTOR</string> + <string name="settings_section_title_help">Ajutor</string> <string name="only_your_contact_can_send_disappearing">Doar contactul tău poate trimite mesaje care dispar.</string> <string name="migrate_to_device_importing_archive">Se importă arhiva</string> <string name="migrate_from_device_title">Migrare dispozitiv</string> @@ -1010,7 +1010,7 @@ <string name="theme_light">Luminos</string> <string name="privacy_chat_list_open_links_no">Nu</string> <string name="privacy_chat_list_open_web_link_question">Deschizi linkul web?</string> - <string name="settings_section_title_messages">MESAJE ȘI FIȘIERE</string> + <string name="settings_section_title_messages">Mesaje și fișiere</string> <string name="group_member_role_moderator">moderator</string> <string name="initial_member_role">Rol inițial</string> <string name="only_group_owners_can_change_prefs">Doar proprietarii grupului pot modifica preferințele grupului.</string> @@ -1043,7 +1043,7 @@ <string name="onboarding_notifications_mode_subtitle">Cum afectează bateria</string> <string name="receipts_contacts_enable_keep_overrides">Activare (păstrați suprascrierile)</string> <string name="enabled_self_destruct_passcode">Activează codul de autodistrugere</string> - <string name="member_info_section_title_member">MEMBRU</string> + <string name="member_info_section_title_member">Membru</string> <string name="operator_info_title">Operator de rețea</string> <string name="linked_desktops">Desktop-uri conectate</string> <string name="error_accepting_member">Eroare la acceptarea membrului</string> @@ -1148,7 +1148,7 @@ <string name="receipts_contacts_title_enable">Activați confirmarea de primire?</string> <string name="self_destruct_new_display_name">Nume nou afișat:</string> <string name="settings_developer_tools">Instrumente pentru dezvoltatori</string> - <string name="settings_section_title_files">FIȘIERE</string> + <string name="settings_section_title_files">Fișiere</string> <string name="privacy_chat_list_open_links">Deschide linkurile din lista de conversații</string> <string name="settings_section_title_message_shape">Forma mesajului</string> <string name="import_database">Importați baza de date</string> @@ -1393,7 +1393,7 @@ <string name="delete_chat_list_menu_action">Șterge</string> <string name="install_simplex_chat_for_terminal">Instalați SimpleX Chat pentru terminal</string> <string name="error_saving_ICE_servers">Eroare la salvarea serverelor ICE</string> - <string name="theme_colors_section_title">CULORILE INTERFEȚEI</string> + <string name="theme_colors_section_title">Culorile interfeței</string> <string name="total_files_count_and_size">%d fișier(e) cu dimensiunea totală de %s</string> <string name="files_and_media_section">Fișiere și media</string> <string name="rcv_group_event_member_added">invitat %1$s</string> @@ -1636,7 +1636,7 @@ <string name="users_delete_with_connections">Conexiuni de profil și server</string> <string name="callstate_received_answer">răspuns primit…</string> <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Politica de confidențialitate și condițiile de utilizare.</string> - <string name="settings_section_title_private_message_routing">RUTAREA MESAJELOR PRIVATE</string> + <string name="settings_section_title_private_message_routing">Rutarea mesajelor private</string> <string name="store_passphrase_securely">Te rugăm să stochezi parola în siguranță, altfel NU o vei putea schimba dacă o pierzi.</string> <string name="restore_passphrase_not_found_desc">Parola nu a fost găsită în Keystore. Te rugăm să o introduci manual. Acest lucru s-ar putea întâmpla dacă ai restaurat datele aplicației folosind un instrument de backup. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii.</string> <string name="restore_passphrase_can_not_be_read_desc">Parola stocată în Keystore nu poate fi citită. Acest lucru se poate întâmpla după o actualizare a sistemului incompatibilă cu aplicația. Dacă nu este cazul, te rugăm să contactezi dezvoltatorii.</string> @@ -1767,7 +1767,7 @@ <string name="reviewed_by_admins">revizuit de administratori</string> <string name="onboarding_choose_server_operators">Operatori de server</string> <string name="relay_server_protects_ip">Serverul de retransmisie protejează adresa IP, dar poate observa durata apelului.</string> - <string name="run_chat_section">PORNIȚI CHATUL</string> + <string name="run_chat_section">Porniți chatul</string> <string name="rcv_group_event_user_deleted">te-a eliminat</string> <string name="sender_at_ts">%s la %s</string> <string name="error_server_protocol_changed">Protocolul serverului a fost modificat.</string> @@ -1800,7 +1800,7 @@ <string name="select_chat_profile">Selectează profilul de conversație</string> <string name="save_auto_accept_settings">Salvează setările adresei SimpleX</string> <string name="save_list">Salvează lista</string> - <string name="settings_section_title_delivery_receipts">TRIMITE CONFIRMĂRI DE LIVRARE LA</string> + <string name="settings_section_title_delivery_receipts">Trimite confirmări de livrare la</string> <string name="self_destruct_passcode_changed">Parola de autodistrugere a fost schimbată!</string> <string name="info_row_updated_at">Înregistrare actualizată la</string> <string name="onboarding_select_network_operators_to_use">Selectează operatorii de rețea de utilizat.</string> @@ -1973,9 +1973,9 @@ <string name="app_will_ask_to_confirm_unknown_file_servers">Aplicația va cere să confirmați descărcările de pe servere de fișiere necunoscute (cu excepția celor .onion sau când proxy-ul SOCKS este activat).</string> <string name="la_mode_system">Sistem</string> <string name="receipts_section_description_1">Acestea pot fi ignorate în setările de contact și de grup.</string> - <string name="settings_section_title_support">SUPORT SIMPLEX CHAT</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> - <string name="settings_section_title_themes">TEME</string> + <string name="settings_section_title_support">Suport SimpleX Chat</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> + <string name="settings_section_title_themes">Teme</string> <string name="non_fatal_errors_occured_during_import">În timpul importului au apărut câteva erori non-fatale:</string> <string name="group_invitation_tap_to_join">Atingeți pentru a vă alătura</string> <string name="database_downgrade_warning">Atenție: este posibil să pierdeți unele date!</string> @@ -2430,7 +2430,7 @@ <string name="sent_to_your_contact_after_connection">Trimis contactului tău după conectare.</string> <string name="share_profile_via_link">Actualizezi la o adresă permanentă?</string> <string name="address_welcome_message">Mesaj de bun venit</string> - <string name="settings_section_title_contact_requests_from_groups">SOLICITĂRI DE CONTACT DE LA GRUPURI</string> + <string name="settings_section_title_contact_requests_from_groups">Solicitări de contact de la grupuri</string> <string name="this_setting_is_for_your_current_profile">Această setare este pentru profilul tău actual</string> <string name="share_old_address_alert_button">Partajează adresa veche</string> <string name="share_old_link_alert_button">Partajează linkul vechi</string> @@ -2494,4 +2494,39 @@ <string name="simplex_link_relay">Link de releu SimpleX</string> <string name="chat_banner_group">Grup</string> <string name="error_marking_member_support_chat_read">Eroare la marcarea conversației cu membrul ca fiind citită</string> + <string name="relay_bar_active">%1$d/%2$d relee active</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d relee active, %3$d erori</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d relee active, %3$d eșuate</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d relee active, %3$d șterse</string> + <string name="relay_bar_connected">%1$d/%2$d relee conectate</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d relee conectate, %3$d erori</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d relee conectate, %3$d eșuate</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d relee conectate, %3$d șterse</string> + <string name="channel_owner_count_singular">%1$d deținător</string> + <string name="channel_owner_count_plural">%1$d deținători</string> + <string name="channel_owners_contributors_count">%1$d deținători & contribuitori</string> + <string name="relay_bar_relays_failed">%1$d relee esuate</string> + <string name="relay_bar_relays_not_active">%1$d relee inactive</string> + <string name="relay_bar_relays_removed">%1$d relee sterse</string> + <string name="channel_subscriber_count_singular">%1$d abonat</string> + <string name="channel_subscriber_count_plural">%1$d abonați</string> + <string name="badge_supported_simplex">%1$s a susținut SimpleX Chat. Insigna a expirat pe %2$s.</string> + <string name="settings_section_title_about">Despre</string> + <string name="relay_status_accepted">acceptată</string> + <string name="app_update_required">Aplicația necesita actualizare</string> + <string name="badge_unknown_key_title">Eticheta nu poate fi verificată</string> + <string name="why_built_p7">Deoarece am distrus puterea să știm cine ești. Așa că puterea ta nu poate fi luată niciodată.</string> + <string name="onboarding_be_free">Fii liber\nîn rețeaua ta</string> + <string name="why_built_tagline">Fii liber în rețeaua ta.</string> + <string name="block_subscriber_for_all_question">Blochezi abonatul pentru toți?</string> + <string name="chat_banner_bot">Robot</string> + <string name="one_hand_ui_bottom_bar">Bară de jos</string> + <string name="channel_role_label">canal</string> + <string name="chat_banner_channel">Canal</string> + <string name="info_row_channel">Canal</string> + <string name="channel_full_name_field">Numele complet al canalului:</string> + <string name="channel_no_active_relays_try_later">Canalul nu are relee active. Te rugăm încearcă mai târziu.</string> + <string name="snd_channel_event_channel_profile_updated">profilul canalului a fost actualizat</string> + <string name="channel_temporarily_unavailable">Canalul este momentan inactiv</string> + <string name="channel_will_start_with_relays">Canalul va porni cu %1$d din %2$d relee. Continui?</string> </resources> 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 b1d505e644..dae2a494dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -66,7 +66,7 @@ <string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с контактом %1$s.</string> <string name="invalid_connection_link">Ошибка в ссылке контакта</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Пожалуйста, проверьте, что Вы использовали правильную ссылку, или попросите Ваш контакт отправить Вам новую.</string> - <string name="connection_error_auth">Ошибка соединения (AUTH)</string> + <string name="connection_error_auth">Ошибка соединения</string> <string name="connection_error_auth_desc">Возможно, Ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите Ваш контакт создать ещё одну ссылку и проверьте Ваше соединение с сетью.</string> <string name="error_accepting_contact_request">Ошибка при принятии запроса на соединение</string> <string name="sender_may_have_deleted_the_connection_request">Отправитель мог удалить запрос на соединение.</string> @@ -548,26 +548,26 @@ <string name="send_link_previews">Отправлять картинки ссылок</string> <string name="full_backup">Резервная копия данных</string> <!-- Settings sections --> - <string name="settings_section_title_you">ВЫ</string> - <string name="settings_section_title_settings">НАСТРОЙКИ</string> - <string name="settings_section_title_help">ПОМОЩЬ</string> - <string name="settings_section_title_support">ПОДДЕРЖАТЬ SIMPLEX CHAT</string> - <string name="settings_section_title_device">УСТРОЙСТВО</string> - <string name="settings_section_title_chats">ЧАТЫ</string> + <string name="settings_section_title_you">Вы</string> + <string name="settings_section_title_settings">Настройки</string> + <string name="settings_section_title_help">Помощь</string> + <string name="settings_section_title_support">Поддержать SimpleX Chat</string> + <string name="settings_section_title_device">Устройство</string> + <string name="settings_section_title_chats">Чаты</string> <string name="settings_developer_tools">Инструменты разработчика</string> <string name="settings_experimental_features">Экспериментальные функции</string> - <string name="settings_section_title_socks">SOCKS-ПРОКСИ</string> - <string name="settings_section_title_icon">ЗНАЧОК</string> - <string name="settings_section_title_themes">ТЕМЫ</string> - <string name="settings_section_title_messages">СООБЩЕНИЯ И ФАЙЛЫ</string> - <string name="settings_section_title_calls">ЗВОНКИ</string> + <string name="settings_section_title_socks">SOCKS-прокси</string> + <string name="settings_section_title_icon">Значок</string> + <string name="settings_section_title_themes">Темы</string> + <string name="settings_section_title_messages">Сообщения и файлы</string> + <string name="settings_section_title_calls">Звонки</string> <string name="settings_section_title_incognito">Режим Инкогнито</string> <!-- DatabaseView.kt --> <string name="your_chat_database">База данных</string> - <string name="run_chat_section">ЗАПУСТИТЬ ЧАТ</string> + <string name="run_chat_section">Запустить чат</string> <string name="chat_is_running">Чат запущен</string> <string name="chat_is_stopped">Чат остановлен</string> - <string name="chat_database_section">БАЗА ДАННЫХ</string> + <string name="chat_database_section">База данных</string> <string name="database_passphrase">Пароль базы данных</string> <string name="export_database">Экспорт архива чата</string> <string name="import_database">Импорт архива чата</string> @@ -749,7 +749,7 @@ <string name="invite_prohibited_description">Вы пытаетесь пригласить контакт, который знает Ваш профиль инкогнито, в группу, где Вы используете основной профиль</string> <!-- GroupChatInfoView.kt --> <string name="button_add_members">Пригласить в группу</string> - <string name="group_info_section_title_num_members">%1$s ЧЛЕНОВ ГРУППЫ</string> + <string name="group_info_section_title_num_members">%1$s Членов группы</string> <string name="group_info_member_you">Вы: %1$s</string> <string name="button_delete_group">Удалить группу</string> <string name="delete_group_question">Удалить группу?</string> @@ -767,7 +767,7 @@ <string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string> <string name="only_group_owners_can_change_prefs">Только владельцы группы могут изменять предпочтения группы.</string> <!-- For Console chat info section --> - <string name="section_title_for_console">ДЛЯ КОНСОЛИ</string> + <string name="section_title_for_console">Для консоли</string> <string name="info_row_local_name">Локальное имя</string> <string name="info_row_database_id">ID базы данных</string> <!-- GroupMemberInfoView.kt --> @@ -775,12 +775,12 @@ <string name="button_send_direct_message">Отправить сообщение</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Член группы будет удалён - это действие нельзя отменить!</string> <string name="remove_member_confirmation">Удалить</string> - <string name="member_info_section_title_member">ЧЛЕН ГРУППЫ</string> + <string name="member_info_section_title_member">Член группы</string> <string name="role_in_group">Роль</string> <string name="change_role">Поменять роль</string> <string name="change_verb">Поменять</string> <string name="switch_verb">Переключить</string> - <string name="change_member_role_question">Поменять роль в группе?</string> + <string name="change_member_role_question">Изменить роль?</string> <string name="member_role_will_be_changed_with_notification">Роль будет изменена на "%s". Все в группе получат сообщение.</string> <string name="member_role_will_be_changed_with_invitation">Роль будет изменена на "%s". Будет отправлено новое приглашение.</string> <string name="error_removing_member">Ошибка при удалении члена группы</string> @@ -790,7 +790,7 @@ <string name="conn_level_desc_direct">прямое</string> <string name="conn_level_desc_indirect">непрямое (%1$s)</string> <!-- ConnectionStats --> - <string name="conn_stats_section_title_servers">СЕРВЕРЫ</string> + <string name="conn_stats_section_title_servers">Серверы</string> <string name="receiving_via">Получение через</string> <string name="sending_via">Отправка через</string> <string name="network_status">Состояние сети</string> @@ -1084,7 +1084,7 @@ <string name="waiting_for_video">Ожидание видео</string> <string name="video_will_be_received_when_contact_completes_uploading">Видео будет получено когда Ваш контакт загрузит его.</string> <string name="hide_dev_options">Скрыть:</string> - <string name="settings_section_title_experimenta">ЭКСПЕРИМЕНТАЛЬНЫЕ</string> + <string name="settings_section_title_experimenta">Экспериментальные</string> <string name="videos_limit_desc">Только 10 видео могут быть отправлены одновременно</string> <string name="unhide_profile">Раскрыть профиль</string> <string name="video_will_be_received_when_contact_is_online">Видео будет получено, когда Ваш контакт будет онлайн, пожалуйста, подождите или проверьте позже!</string> @@ -1226,7 +1226,7 @@ <string name="prohibit_message_reactions">Запретить реакции на сообщения.</string> <string name="prohibit_message_reactions_group">Запретить реакции на сообщения.</string> <string name="custom_time_unit_seconds">секунд</string> - <string name="theme_colors_section_title">ЦВЕТА ИНТЕРФЕЙСА</string> + <string name="theme_colors_section_title">Цвета интерфейса</string> <string name="share_address_with_contacts_question">Поделиться адресом с контактами SimpleX?</string> <string name="profile_update_will_be_sent_to_contacts">Обновление профиля будет отправлено Вашим SimpleX контактам.</string> <string name="learn_more_about_address">Об адресе SimpleX</string> @@ -1347,15 +1347,15 @@ <string name="only_owners_can_enable_files_and_media">Только владельцы группы могут разрешить файлы и медиа.</string> <string name="files_and_media">Файлы и медиа</string> <string name="shutdown_alert_question">Выключить\?</string> - <string name="settings_section_title_app">ПРИЛОЖЕНИЕ</string> + <string name="settings_section_title_app">Приложение</string> <string name="settings_restart_app">Перезапустить</string> <string name="settings_shutdown">Выключить</string> <string name="receipts_contacts_disable_for_all">Выключить для всех</string> <string name="receipts_contacts_enable_for_all">Включить для всех</string> <string name="receipts_contacts_enable_keep_overrides">Включить (кроме исключений)</string> <string name="receipts_contacts_title_enable">Выключить отчёты о доставке\?</string> - <string name="settings_section_title_delivery_receipts">ОТПРАВКА ОТЧЁТОВ О ДОСТАВКЕ</string> - <string name="settings_section_title_contact_requests_from_groups">ЗАПРОСЫ НА СОЕДИНЕНИЕ ИЗ ГРУПП</string> + <string name="settings_section_title_delivery_receipts">Отправка отчётов о доставке</string> + <string name="settings_section_title_contact_requests_from_groups">Запросы на соединение из групп</string> <string name="conn_event_ratchet_sync_agreed">шифрование согласовано</string> <string name="snd_conn_event_ratchet_sync_agreed">шифрование согласовано для %s</string> <string name="conn_event_ratchet_sync_ok">шифрование работает</string> @@ -1670,7 +1670,6 @@ <string name="agent_internal_error_title">Внутренняя ошибка</string> <string name="clear_note_folder_question">Очистить личные заметки?</string> <string name="new_chat">Новый чат</string> - <string name="new_chat">Новое сообщение</string> <string name="or_scan_qr_code">Или отсканируйте QR-код</string> <string name="you_can_view_invitation_link_again">Вы можете увидеть ссылку-приглашение снова открыв соединение.</string> <string name="show_slow_api_calls">Показывать медленные вызовы API</string> @@ -1829,7 +1828,7 @@ <string name="v5_7_shape_profile_images">Форма картинок профилей</string> <string name="v5_7_shape_profile_images_descr">Квадрат, круг и все, что между ними.</string> <string name="v5_7_quantum_resistant_encryption_descr">Будет включено в прямых разговорах!</string> - <string name="settings_section_title_files">ФАЙЛЫ</string> + <string name="settings_section_title_files">Файлы</string> <string name="v5_8_chat_themes">Новые темы чатов</string> <string name="message_queue_info_none">нет</string> <string name="color_mode_light">Светлая</string> @@ -1898,7 +1897,7 @@ <string name="private_routing_show_message_status">Показать статус сообщения</string> <string name="update_network_smp_proxy_fallback_question">Прямая доставка сообщений</string> <string name="update_network_smp_proxy_mode_question">Режим доставки сообщений</string> - <string name="settings_section_title_private_message_routing">КОНФИДЕНЦИАЛЬНАЯ ДОСТАВКА СООБЩЕНИЙ</string> + <string name="settings_section_title_private_message_routing">Конфиденциальная доставка сообщений</string> <string name="settings_section_title_chat_colors">Цвета чата</string> <string name="settings_section_title_chat_theme">Тема чата</string> <string name="settings_section_title_user_theme">Тема профиля</string> @@ -2151,7 +2150,7 @@ <string name="forward_multiple">Переслать сообщения…</string> <string name="error_parsing_uri_desc">Проверьте правильность ссылки SimpleX.</string> <string name="error_parsing_uri_title">Ошибка ссылки</string> - <string name="settings_section_title_chat_database">БАЗА ДАННЫХ</string> + <string name="settings_section_title_chat_database">База данных</string> <string name="error_initializing_web_view_wrong_arch">Ошибка инициализации WebView. Убедитесь, что у вас установлен WebView и его поддерживаемая архитектура - arm64.\nОшибка: %s</string> <string name="icon_descr_sound_muted">Звук отключен</string> <string name="delete_messages_cannot_be_undone_warning">Сообщения будут удалены - это нельзя отменить!</string> @@ -2286,7 +2285,7 @@ <string name="info_row_chat">Разговор</string> <string name="member_will_be_removed_from_chat_cannot_be_undone">Член будет удалён из разговора - это действие нельзя отменить!</string> <string name="network_preset_servers_title">Серверы по умолчанию</string> - <string name="member_role_will_be_changed_with_notification_chat">Роль будет изменена на %s. Все участники разговора получат уведомление.</string> + <string name="member_role_will_be_changed_with_notification_chat">Роль будет изменена на "%s". Все участники разговора получат уведомление.</string> <string name="chat_main_profile_sent">Ваш профиль будет отправлен участникам разговора</string> <string name="operator_same_conditions_will_be_applied"><![CDATA[Те же условия будут действовать для оператора <b>%s</b>.]]></string> <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[Те же условия будут действовать для операторов: <b>%s</b>.]]></string> @@ -2648,7 +2647,6 @@ <string name="relay_conn_status_connecting">соединяется</string> <string name="create_channel_title">Создать публичный канал</string> <string name="create_channel_button">Создать публичный канал</string> - <string name="create_channel_beta_button">Создать публичный канал (БЕТА)</string> <string name="creating_channel">Создание канала</string> <string name="rcv_channel_events_count">%d событий канала</string> <string name="button_delete_channel">Удалить канал</string> @@ -2719,9 +2717,9 @@ <string name="connect_plan_open_channel">Открыть канал</string> <string name="connect_plan_open_new_channel">Открыть новый канал</string> <string name="channel_members_section_owners">Владельцы</string> - <string name="member_info_section_title_owner">ВЛАДЕЛЕЦ</string> + <string name="member_info_section_title_owner">Владелец</string> <string name="group_member_role_relay">релей</string> - <string name="member_info_section_title_relay">РЕЛЕЙ</string> + <string name="member_info_section_title_relay">Релей</string> <string name="info_row_relay_address">Адрес релея</string> <string name="relay_address_alert_title">Адрес релея</string> <string name="relay_connection_failed">Ошибка подключения релея</string> @@ -2740,7 +2738,7 @@ <string name="share_relay_address">Поделиться адресом релея</string> <string name="share_via_chat">Поделиться в чате</string> <string name="owner_verification_failed">⚠️ Ошибка проверки подписи: %s.</string> - <string name="member_info_section_title_subscriber">ПОДПИСЧИК</string> + <string name="member_info_section_title_subscriber">Подписчик</string> <string name="channel_members_title_subscribers">Подписчики</string> <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">Подписчик будет удалён из канала - это нельзя отменить!</string> <string name="talk_to_someone">Начните разговор</string> @@ -2771,7 +2769,6 @@ <string name="chat_banner_your_channel">Ваш канал</string> <string name="chat_link_group">Ссылка группы</string> <string name="chat_link_from_owner">(от владельца)</string> - <string name="chat_link_signed">(с подписью)</string> <string name="error_sharing_channel">Ошибка при публикации канала</string> <string name="you_are_subscriber">Вы подписчик</string> <string name="new_1_time_link">Новая одноразовая ссылка</string> @@ -2801,7 +2798,7 @@ <string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">Вы перестанете получать сообщения из этого канала. История чата сохранится.</string> <string name="rcv_channel_event_updated_channel_profile">обновил профиль канала</string> <string name="member_info_member_failed">ошибка</string> - <string name="info_row_connection_failed">ОШИБКА СОЕДИНЕНИЯ</string> + <string name="info_row_connection_failed">Ошибка соединения</string> <string name="chat_with_admins">Чат с админами</string> <string name="allow_chat_with_admins">Разрешить членам группы общаться с админами.</string> <string name="prohibit_chat_with_admins">Запретить чаты с админами.</string> @@ -2858,7 +2855,7 @@ <string name="relay_conn_status_failed">ошибка</string> <string name="relay_status_new">новый</string> <string name="relay_bar_all_relays_failed">Все релеи недоступны</string> - <string name="relay_bar_owner_no_delivery">Добавить релеи чтобы восстановить доставку сообщений.</string> + <string name="relay_bar_owner_no_delivery">Добавить релеи для восстановления доставки сообщений.</string> <string name="relay_bar_subscriber_waiting">Ожидает, когда владелец канала добавит релеи.</string> <string name="via_relay_hostname">через %1$s</string> <string name="relay_section_footer_owner">Подписчики используют ссылку релея для подключения к каналу.\nАдрес релея был использован для настройки этого релея для канала.</string> @@ -2879,4 +2876,134 @@ <string name="add_relays_title">Добавить релеи</string> <string name="button_cancel_and_delete_channel">Отменить и удалить канал</string> <string name="close_behavior_dialog_close">Закрыть приложение</string> + <string name="channel_owner_count_singular">%1$d владелец</string> + <string name="channel_owner_count_plural">%1$d владельцев</string> + <string name="channel_owners_contributors_count">%1$d владельцев и участников канала</string> + <string name="badge_supported_simplex">%1$s поддерживал SimpleX Chat. Срок действия бейджа истек %2$s.</string> + <string name="settings_section_title_about">О приложении</string> + <string name="advanced_settings">Продвинутые настройки</string> + <string name="another_instance_title">Приложение уже запущено</string> + <string name="app_update_required">Необходимо обновление приложения</string> + <string name="relay_status_acknowledged_roster">подтвержденный список</string> + <string name="webpage_code_footer">Добавьте этот код на свой веб-сайт. Он отобразит предпросмотр вашего канала или группы.</string> + <string name="allow_anyone_to_embed">Разрешить всем встраивать</string> + <string name="embed_any_webpage_can_show">Предпросмотр можно отобразить на любой веб-странице.</string> + <string name="badge_unknown_key_title">Не удалось проверить подлинность значка.</string> + <string name="badge_unknown_key_desc">Этот значок подписан ключом, который неизвестен текущей версии приложения. Обновите приложение, чтобы проверить его подлинность.</string> + <string name="badge_unverified_desc">Не удалось проверить подлинность этого значка. Возможно, он не является подлинным.</string> + <string name="badge_unverified_title">Неподтвержденный значок</string> + <string name="badge_invested">%s инвестировал(а) в краудфандинг SimpleX Chat.</string> + <string name="badge_support_from_v7">Вы можете поддержать SimpleX начиная с версии приложения v7.</string> + <string name="badge_supports_simplex">%s поддерживает SimpleX Chat.</string> + <string name="appearance_minimize_to_tray_desc">Работает в фоновом режиме для получения сообщений</string> + <string name="no_names_servers_enabled">Нет серверов для разрешения имён.</string> + <string name="advanced_options">Продвинутые настройки</string> + <string name="another_instance_not_responding">Другой экземпляр приложения уже запущен или был завершён некорректно. Продолжить запуск?</string> + <string name="channel_webpage">Веб-страница канала</string> + <string name="num_relays_selected">Выбрано %d релеев</string> + <string name="chat_data">Данные чата</string> + <string name="channel_name_requires_newer_app_version">Для подключения по имени канала требуется более новая версия приложения.</string> + <string name="contact_name_requires_newer_app_version">Для подключения по имени контакта требуется более новая версия приложения.</string> + <string name="settings_section_title_contact">Контакт</string> + <string name="group_member_role_member_channel">соавтор</string> + <string name="group_member_role_observer_channel">подписчик</string> + <string name="button_remove_relay">Удалить релей</string> + <string name="button_remove_relay_question">Удалить релей?</string> + <string name="appearance_minimize_to_tray">Свернуть в трей</string> + <string name="copy_code">Скопировать код</string> + <string name="relay_bar_no_relays">Релеи отсутствуют</string> + <string name="no_relays_selected">Релеи не выбраны</string> + <string name="tray_tooltip_unread">SimpleX - %d непрочитанных сообщений</string> + <string name="v7_0_channels_contributors">Добавить соавторов.</string> + <string name="add_description">Добавить описание</string> + <string name="v7_0_channels">Улучшенные каналы 📢</string> + <string name="channel_simplex_name">SimpleX имя канала</string> + <string name="connect_plan_connect_to_name">Соединиться с %s</string> + <string name="webpage_info">Создайте веб-страницу, чтобы показывать предпросмотр Вашего канала посетителям до подписки. Хостите её сами или используйте любой статический хостинг.</string> + <string name="v7_0_channels_previews">Создать веб-предпросмотр.</string> + <string name="profile_description__field">Описание</string> + <string name="do_not_require_message_signatures">Не требовать подпись сообщений.</string> + <string name="v7_0_channels_wider_messages">Легче для чтения.</string> + <string name="edit_description">Редактировать описание</string> + <string name="enter_description_optional">Введите описание (необязательно)</string> + <string name="enter_webpage_url">Введите адрес страницы</string> + <string name="error_adding_relays">Ошибка добавления релеев</string> + <string name="error_deleting_message">Ошибка удаления сообщения</string> + <string name="error_saving_simplex_name">Ошибка сохранения имени</string> + <string name="error_sharing_address">Ошибка отправки адреса</string> + <string name="info_row_file_servers">Серверы файлов</string> + <string name="share_text_file_servers">Серверы файлов: %s</string> + <string name="from_history">Из истории</string> + <string name="get_simplex_name_beta">Зарегистрировать SimpleX имя (BETA)</string> + <string name="group_webpage">Веб-страница группы</string> + <string name="help_and_support">Помощь и поддержка</string> + <string name="register_test_name">Как зарегистрировать тестовое имя</string> + <string name="web_page_url_placeholder">https://</string> + <string name="close_behavior_dialog_text">Если Вы выберете Закрыть, сообщения не будут доставляться.\nВы можете изменить это позже в настройках Интерфейса.</string> + <string name="webpage_url_footer">Адрес будет показан подписчикам и разрешит загрузку предпросмотра.</string> + <string name="connect_plan_join_name">Вступить в канал %s</string> + <string name="unsupported_channel_name">Неподдерживаемое имя канала</string> + <string name="unsupported_contact_name">Неподдерживаемое имя контакта</string> + <string name="please_upgrade_the_app">Пожалуйста, обновите приложение.</string> + <string name="simplex_name_error">Ошибка SimpleX имени</string> + <string name="simplex_name_no_servers_desc">Ни один из Ваших серверов не настроен для разрешения SimpleX имён. Настройте серверы или используйте ссылку для соединения.</string> + <string name="simplex_name_server_no_resolver_desc">Сервер %1$s не поддерживает разрешение имён. Настройте серверы или используйте ссылку для соединения.</string> + <string name="simplex_name_not_found">Имя не найдено</string> + <string name="simplex_name_not_found_desc">Это SimpleX имя не зарегистрировано. Пожалуйста, проверьте имя.</string> + <string name="simplex_name_resolver_error_desc">Ошибка разрешения имени: %1$s</string> + <string name="simplex_name_no_valid_link">Нет действительной ссылки</string> + <string name="simplex_name_no_valid_link_desc">SimpleX имя %1$s зарегистрировано, но не имеет действительной ссылки.</string> + <string name="simplex_name_unconfirmed">Неподтверждённое имя</string> + <string name="simplex_name_unconfirmed_desc">SimpleX имя %1$s зарегистрировано, но не добавлено в профиль. Пожалуйста, добавьте его в профиль Вашего адреса или канала, если Вы владелец.</string> + <string name="group_link_requires_newer_version">Эта группа требует более новой версии приложения. Пожалуйста, обновите приложение, чтобы вступить.</string> + <string name="save_simplex_name_question">Сохранить SimpleX имя?</string> + <string name="remove_name">Удалить имя</string> + <string name="sign_message">Подписать сообщение</string> + <string name="sign_message_desc">Подпись доказывает, что Вы — автор этого сообщения, и это нельзя будет отрицать.</string> + <string name="info_row_signed">Подписано</string> + <string name="info_row_signed_verified">Подписано и проверено</string> + <string name="sign_messages">Подпись сообщений</string> + <string name="require_message_signatures">Требовать подпись сообщений.</string> + <string name="message_signatures_are_required">Подпись сообщений обязательна.</string> + <string name="message_signatures_are_not_required">Подпись сообщений не обязательна.</string> + <string name="show_signature">Показывать подпись</string> + <string name="show_encryption">Показывать шифрование</string> + <string name="signature_missing_alert_title">Подпись отсутствует</string> + <string name="signature_missing_alert_desc">Канал требует подпись сообщений, но у этого сообщения подпись отсутствует.</string> + <string name="verify_simplex_name_action">Проверить имя</string> + <string name="verify_simplex_names">Проверять SimpleX имена</string> + <string name="simplex_name_not_verified">SimpleX имя не проверено</string> + <string name="simplex_name">SimpleX имя</string> + <string name="your_simplex_name">Ваше SimpleX имя</string> + <string name="set_simplex_name">Установить SimpleX имя</string> + <string name="simplex_name_owner_no_channel_link">SimpleX имя %1$s зарегистрировано без ссылки канала. Добавьте ссылку канала к имени на странице регистрации.</string> + <string name="simplex_name_owner_no_address">SimpleX имя %1$s зарегистрировано без SimpleX адреса. Добавьте Ваш SimpleX адрес к имени на странице регистрации.</string> + <string name="set_user_simplex_name_footer">Позвольте людям соединяться с Вами через имя, зарегистрированное для Вашего SimpleX адреса.</string> + <string name="set_channel_simplex_name_footer">Позвольте людям вступать через имя, зарегистрированное для ссылки этого канала.</string> + <string name="to_verify_channel_member_key">Чтобы подтвердить ключи с этим подписчиком, сравните (или сканируйте) код на ваших устройствах.</string> + <string name="settings_section_title_support_project">Поддержать проект</string> + <string name="more_privacy">Больше конфиденциальности</string> + <string name="webpage_code">Код веб-страницы</string> + <string name="relays_no_web_support">Используемые чат-релеи не поддерживают веб-страницы.</string> + <string name="embed_only_your_page">Предпросмотр можно отобразить только на Вашей странице, указанной выше.</string> + <string name="member_role_will_be_changed_with_notification_channel">Роль будет изменена на "%s". Все в канале получат сообщение.</string> + <string name="operator_use_for_names">Для разрешения имён</string> + <string name="v7_0_simplex_names">Публичные SimpleX имена (BETA)</string> + <string name="v7_0_simplex_names_descr">Публичные имена для Вашего канала или бизнеса.</string> + <string name="v7_0_channels_relays">Управлять своими релеями.</string> + <string name="relay_conn_status_removed">удалён</string> + <string name="relay_status_rejected">отклонён</string> + <string name="member_info_status">Статус</string> + <string name="member_info_relay_status_rejected_by_operator">отклонён оператором релея</string> + <string name="relay_will_be_removed_from_channel">Релей будет удалён из канала - это нельзя отменить!</string> + <string name="last_active_relay_warning">Это последний активный релей. После его удаления доставка сообщений подписчикам будет невозможна.</string> + <string name="cancel_channel_alert_msg">Ваш новый канал %1$s подключен к %2$d из %3$d релеев.\nЕсли Вы отмените, канал будет удалён - Вы сможете создать его снова.</string> + <string name="no_available_relays">Нет доступных релеев</string> + <string name="relays_added_format">Добавлены релеи: %1$s.</string> + <string name="select_relays">Выберите релеи</string> + <string name="close_behavior_dialog_title">Свернуть в трей?</string> + <string name="close_behavior_dialog_minimize">Свернуть в трей</string> + <string name="tray_show">Показать SimpleX</string> + <string name="tray_quit">Выйти из SimpleX</string> + <string name="tray_tooltip">SimpleX</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index c355d8d9fb..c7313e76a3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -139,7 +139,7 @@ <string name="rcv_conn_event_switch_queue_phase_completed">เปลี่ยนที่อยู่สําหรับคุณแล้ว</string> <string name="invite_prohibited">ไม่สามารถเชิญผู้ติดต่อได้!</string> <string name="change_role">เปลี่ยนบทบาท</string> - <string name="change_member_role_question">เปลี่ยนบทบาทกลุ่ม\?</string> + <string name="change_member_role_question">เปลี่ยนบทบาทกลุ่ม?</string> <string name="icon_descr_cancel_live_message">ยกเลิกข้อความสด</string> <string name="feature_cancelled_item">ยกเลิกเรียบร้อยแล้ว %s</string> <string name="alert_title_cant_invite_contacts">ไม่สามารถเชิญผู้ติดต่อได้!</string> @@ -172,7 +172,7 @@ <string name="display_name_connection_established">สร้างการเชื่อมต่อแล้ว</string> <string name="connection_local_display_name">การเชื่อมต่อ %1$d</string> <string name="contact_already_exists">ผู้ติดต่อรายนี้มีอยู่แล้ว</string> - <string name="connection_error_auth">การเชื่อมต่อผิดพลาด (AUTH)</string> + <string name="connection_error_auth">การเชื่อมต่อผิดพลาด</string> <string name="smp_server_test_compare_file">เปรียบเทียบไฟล์</string> <string name="smp_server_test_connect">เชื่อมต่อ</string> <string name="smp_server_test_create_file">สร้างไฟล์</string> @@ -996,7 +996,7 @@ <string name="alert_title_skipped_messages">ข้อความที่ข้ามไป</string> <string name="submit_passcode">ส่ง</string> <string name="la_mode_system">ระบบ</string> - <string name="settings_section_title_support">สนับสนุน SIMPLEX แชท</string> + <string name="settings_section_title_support">สนับสนุน SimpleX Chat</string> <string name="settings_section_title_socks">พร็อกซี SOCKS</string> <string name="stop_chat_confirmation">หยุด</string> <string name="stop_chat_question">หยุดแชท\?</string> @@ -1058,8 +1058,7 @@ <string name="simplex_link_connection">ผ่าน %1$s</string> <string name="failed_to_create_user_duplicate_desc">คุณมีโปรไฟล์แชทที่ใช้ชื่อแสดงเดียวกันอยู่แล้ว กรุณาเลือกชื่ออื่น</string> <string name="you_are_already_connected_to_vName_via_this_link">คุณเชื่อมต่อกับ %1$s แล้ว</string> - <string name="connection_error_auth_desc">เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน -\nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</string> + <string name="connection_error_auth_desc">เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน \nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</string> <string name="smp_server_test_upload_file">อัปโหลดไฟล์</string> <string name="enter_passphrase_notification_desc">หากต้องการรับการแจ้งเตือน โปรดป้อนรหัสผ่านของฐานข้อมูล</string> <string name="simplex_service_notification_title">บริการ SimpleX Chat </string> @@ -1327,4 +1326,4 @@ <string name="in_reply_to">ในการตอบกลับถึง</string> <string name="no_history">ไม่มีประวัติ</string> <string name="conn_event_ratchet_sync_ok">encryptionใช้ได้</string> -</resources> \ No newline at end of file +</resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 0e9c54fb87..a1ead8c55e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -50,8 +50,8 @@ <string name="answer_call">Aramayı cevapla</string> <string name="full_backup">Uygulama veri yedekleme</string> <string name="all_app_data_will_be_cleared">Tüm uygulama verileri silinir.</string> - <string name="settings_section_title_app">UYGULAMA</string> - <string name="settings_section_title_icon">UYGULAMA SİMGESİ</string> + <string name="settings_section_title_app">Uygulama</string> + <string name="settings_section_title_icon">Uygulama simgesi</string> <string name="chat_item_ttl_week">1 hafta</string> <string name="conn_event_ratchet_sync_started">şifreleme kabul ediliyor…</string> <string name="group_member_role_admin">yönetici</string> @@ -64,7 +64,7 @@ <string name="v4_6_audio_video_calls">Sesli ve görüntülü aramalar</string> <string name="chat_item_ttl_day">1 gün</string> <string name="chat_item_ttl_month">1 ay</string> - <string name="add_address_to_your_profile">Profilinize adres ekleyin, böylece kişileriniz adresinizi diğer insanlarla paylaşabilir. Profil güncellemesi kişilerinize gönderilecektir.</string> + <string name="add_address_to_your_profile">Adresinizi profilinize ekleyin, böylece SimpleX kişileriniz bunu başkalarıyla paylaşabilir. Profil güncellemeniz SimpleX kişilerinizle paylaşılacaktır.</string> <string name="address_section_title">Adres</string> <string name="v4_2_group_links_desc">Yöneticiler, gruplara katılım bağlantısı oluşturabilirler.</string> <string name="all_group_members_will_remain_connected">Konuşma üyelerinin tümü bağlı kalacaktır.</string> @@ -86,7 +86,7 @@ <string name="scan_code_from_contacts_app">Konuştuğunuz kişinin uygulamasından güvenlik kodunu okut.</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde.</string> <string name="save_servers_button">Kaydet</string> - <string name="theme_colors_section_title">ARAYÜZ RENKLERİ</string> + <string name="theme_colors_section_title">Arayüz renkleri</string> <string name="save_auto_accept_settings">SimpleX adres ayarlarını kaydet</string> <string name="save_settings_question">Ayarlar kaydedilsin mi?</string> <string name="save_and_notify_contacts">Kaydet ve konuştuğun kişilere bildir</string> @@ -97,7 +97,7 @@ <string name="icon_descr_audio_off">Ses kapalı</string> <string name="authentication_cancelled">Doğrulama iptal edildi</string> <string name="settings_restart_app">Yeniden başlat</string> - <string name="settings_section_title_themes">TEMALAR</string> + <string name="settings_section_title_themes">Temalar</string> <string name="restart_the_app_to_use_imported_chat_database">İçe aktarılan konuşma veri tabanını kullanmak için uygulamayı yeniden başlat.</string> <string name="restart_the_app_to_create_a_new_chat_profile">Yeni bir konuşma profili oluşturmak için uygulamayı yeniden başlatın.</string> <string name="restore_database_alert_confirm">Geri Yükle</string> @@ -184,7 +184,7 @@ <string name="deleted_description">silindi</string> <string name="receiving_files_not_yet_supported">dosya alma henüz desteklenmiyor</string> <string name="sender_you_pronoun">sen</string> - <string name="invalid_chat">geçersi̇z sohbet</string> + <string name="invalid_chat">geçersiz sohbet</string> <string name="connection_local_display_name">bağlantı %1$d</string> <string name="simplex_link_mode_browser">Tarayıcı ile</string> <string name="simplex_link_connection">%1$s tarafından</string> @@ -266,7 +266,7 @@ <string name="allow_your_contacts_to_send_disappearing_messages">Kişilerinin sana, kendiğinden yok olan mesajlar göndermesine izin ver.</string> <string name="disappearing_messages_are_prohibited">Kendiliğinden yok olan mesajlara izin verilmiyor.</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesajlar deşifrelenemedi.</string> - <string name="group_info_section_title_num_members">%1$s ÜYELER</string> + <string name="group_info_section_title_num_members">%1$s üyeler</string> <string name="integrity_msg_skipped">%1$d atlanılmış mesaj(lar)</string> <string name="allow_calls_only_if">Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin.</string> <string name="allow_your_contacts_adding_message_reactions">Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver.</string> @@ -287,10 +287,10 @@ <string name="color_secondary_variant">Ek ikincil renk</string> <string name="button_remove_member">Üyeyi çıkar</string> <string name="remove_member_confirmation">Kaldır</string> - <string name="settings_section_title_calls">ARAMALAR</string> - <string name="settings_section_title_chats">SOHBETLER</string> - <string name="settings_section_title_you">SEN</string> - <string name="chat_database_section">SOHBET VERİTABANI</string> + <string name="settings_section_title_calls">Aramalar</string> + <string name="settings_section_title_chats">Sohbetler</string> + <string name="settings_section_title_you">Sen</string> + <string name="chat_database_section">Sohbet veritabanı</string> <string name="remove_passphrase">Kaldır</string> <string name="wrong_passphrase_title">Yanlış parola!</string> <string name="confirm_database_upgrades">Veritabanı yükseltmelerini onayla</string> @@ -300,7 +300,7 @@ <string name="group_member_status_announced">bağlanılıyor (duyuruldu)</string> <string name="group_info_member_you">sen: %1$s</string> <string name="group_member_status_removed">kaldırıldı</string> - <string name="member_info_section_title_member">ÜYE</string> + <string name="member_info_section_title_member">Üye</string> <string name="group_members_can_send_disappearing">Üyeler kendiliğinden yok olan mesajlar gönderebilir.</string> <string name="prohibit_sending_disappearing">Kendiliğinden yok olan mesaj gönderimini engelle.</string> <string name="allow_voice_messages_only_if">Yalnızca kişiniz sesli mesaj göndermeye izin veriyorsa sen de ver.</string> @@ -422,7 +422,7 @@ <string name="if_you_enter_self_destruct_code">Eğer uygulamayı açarken tüm verileri yok eden erişim kodunu girersen:</string> <string name="if_you_enter_passcode_data_removed">Eğer uygulamayı açarken bu erişim kodunu kullanırsan uygulama içi tüm veriler kalıcı olarak silinecektir!</string> <string name="set_passcode">Erişim kodu belirle</string> - <string name="settings_section_title_device">AYGIT</string> + <string name="settings_section_title_device">Aygit</string> <string name="database_passphrase">Veri tabanı parolası</string> <string name="set_password_to_export_desc">Veri tabanı, rastgele bir parola ile şifrelendi. Dışa aktarmadan önce lütfen değiştir.</string> <string name="delete_files_and_media_question">Dosyaları ve medyayı sil\?</string> @@ -438,7 +438,7 @@ <string name="info_row_deleted_at">Şu tarihte silindi</string> <string name="share_text_deleted_at">Şu tarihte silindi: %s</string> <string name="item_info_current">(güncel)</string> - <string name="change_member_role_question">Grup yetkisini değiştir\?</string> + <string name="change_member_role_question">Rolü değiştir?</string> <string name="chat_preferences_default">varsayılan (%s)</string> <string name="v5_1_custom_themes_descr">Renk temalarını kişiselleştir ve paylaş</string> <string name="v5_1_custom_themes">Kişiselleştirilmiş temalar</string> @@ -600,7 +600,7 @@ <string name="error_saving_ICE_servers">ICE sonucuları kaydedilirken hata oluştu</string> <string name="error_updating_user_privacy">Kullanıcı gizliliği güncellenirken hata oluştu</string> <string name="favorite_chat">Gözde</string> - <string name="settings_section_title_experimenta">DENEYSEL</string> + <string name="settings_section_title_experimenta">Deneysel</string> <string name="revoke_file__message">Dosya, sunuculardan silinecektir.</string> <string name="v5_2_fix_encryption_descr">Yedekleri geri yükledikten sonra şifrelemeyi onar.</string> <string name="v4_4_french_interface">Fransız arayüzü</string> @@ -622,7 +622,7 @@ <string name="error_starting_chat">Konuşma başlatılırken hata oluştu</string> <string name="settings_experimental_features">Deneysel özellikler</string> <string name="export_database">Veri tabanını dışa aktar</string> - <string name="settings_section_title_help">YARDIM</string> + <string name="settings_section_title_help">Yardim</string> <string name="import_database">Veri tabanını içe aktar</string> <string name="error_stopping_chat">Konuşma durdulurken hata oluştu</string> <string name="import_database_confirmation">İçe aktar</string> @@ -634,7 +634,7 @@ <string name="snd_group_event_group_profile_updated">grup profili güncellendi</string> <string name="group_member_status_group_deleted">grup silindi</string> <string name="error_updating_link_for_group">Toplu konuşma bağlantısı güncellenirken hata oluştu</string> - <string name="section_title_for_console">UÇBİRİM İÇİN</string> + <string name="section_title_for_console">Uçbirim için</string> <string name="group_link">Grup bağlantısı</string> <string name="info_row_group">Grup</string> <string name="conn_level_desc_indirect">dolaylı (%1$s)</string> @@ -801,7 +801,7 @@ <string name="callstatus_ended">arama sona erdi %1$s</string> <string name="call_on_lock_screen">Kilit ekranında aramalar:</string> <string name="alert_title_msg_bad_id">Kötü mesaj kimliği</string> - <string name="settings_section_title_messages">MESAJLAR VE DOSYALAR</string> + <string name="settings_section_title_messages">Mesajlar ve dosyalar</string> <string name="change_database_passphrase_question">Veri tabanı parolasını değiştir\?</string> <string name="restore_passphrase_not_found_desc">Parola Keystore\'da bulunamadı, lütfen manuel olarak girin. Bu, uygulamanın verilerini bir yedekleme aracı kullanarak geri yüklediyseniz olabilir. Eğer durum böyle değilse, lütfen geliştiricilerle iletişime geçin.</string> <string name="leave_group_button">Ayrıl</string> @@ -880,7 +880,7 @@ <string name="you_can_share_your_address">Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir.</string> <string name="snd_conn_event_switch_queue_phase_completed">bağlantı değiştirdiniz</string> <string name="you_can_enable_delivery_receipts_later">Daha sonra Ayarlardan etkinleştirebilirsin</string> - <string name="you_can_enable_delivery_receipts_later_alert">Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Bunları daha sonra uygulamanın Gizlilik ayarlarından etkinleştirebilirsiniz.</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir. \nBu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz).</string> <string name="contact_sent_large_file">Kişiniz desteklenen maksimum boyuttan (%1$s) daha büyük bir dosya gönderdi.</string> @@ -1002,7 +1002,7 @@ <string name="v5_0_polish_interface">Arayüz geliştirildi</string> <string name="unfavorite_chat">Favorilerden çıkar</string> <string name="make_profile_private">Sohbeti gizli yap!</string> - <string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi kişilerinize gönderilecektir.</string> + <string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi SimpleX kişilerinize gönderilecektir.</string> <string name="read_more_in_github_with_link"><![CDATA[<font color="#0088ff">GitHub repomuzda</font> daha fazlasını okuyun.]]></string> <string name="alert_text_fragment_please_report_to_developers">Lütfen geliştiricilere bildirin.</string> <string name="users_delete_with_connections">Profil ve sunucu bağlantıları</string> @@ -1037,7 +1037,7 @@ <string name="auth_stop_chat">Sohbeti durdur</string> <string name="connect_use_current_profile">Mevcut profili kullan</string> <string name="la_mode_system">Sistem</string> - <string name="settings_section_title_support">SIMPLEX CHAT\'İ DESTEKLE</string> + <string name="settings_section_title_support">SimpleX Chat\'i destekle</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Sohbet veri tabanını dışa aktarmak, içe aktarmak veya silmek için sohbeti durdur. Sohbet durdurulduğunda mesaj alamaz ve gönderemezsiniz.</string> <string name="desktop_device">Masaüstü</string> <string name="contact_tap_to_connect">Bağlanmak için dokun</string> @@ -1102,7 +1102,7 @@ <string name="show_developer_options">Geliştirici seçeneklerini göster</string> <string name="rcv_group_event_1_member_connected">%s bağlandı</string> <string name="network_disable_socks_info">Onaylarsanız, mesajlaşma sunucuları IP adresinizi ve sağlayıcınızı - hangi sunuculara bağlandığınızı - görebilecektir.</string> - <string name="share_with_contacts">Kişilerle paylaş</string> + <string name="share_with_contacts">SimpleX kişileriyle paylaşın</string> <string name="chat_item_ttl_seconds">%s saniye (sn)</string> <string name="recipient_colon_delivery_status">%s: %s</string> <string name="system_restricted_background_desc">SimpleX arka planda çalışamaz. Bildirimleri sadece uygulama çalışırken alırsınız.</string> @@ -1111,7 +1111,7 @@ <string name="connect_plan_already_joining_the_group">Zaten gruba bağlanılıyor!</string> <string name="group_members_n">%s, %s ve %d üye</string> <string name="this_device">Bu cihaz</string> - <string name="share_address_with_contacts_question">Adresi kişilerle paylaş?</string> + <string name="share_address_with_contacts_question">SimpleX kişileriyle adres paylaşılsın mı?</string> <string name="system_restricted_background_in_call_desc">Uygulama arka planda 1 dakika kaldıktan sonra kapatılabilir.</string> <string name="text_field_set_contact_placeholder">Kişi ismini ayarla…</string> <string name="send_us_an_email">Bize e-posta gönder</string> @@ -1134,7 +1134,7 @@ <string name="share_link">Bağlantı paylaş</string> <string name="icon_descr_simplex_team">SimpleX Ekibi</string> <string name="rcv_group_event_3_members_connected">%s, %s ve %s bağlandı</string> - <string name="settings_section_title_socks">SOCKS VEKİLİ</string> + <string name="settings_section_title_socks">SOCKS vekili</string> <string name="desktop_devices">Masaüstür cihazlar</string> <string name="smp_servers">SMP sunucuları</string> <string name="not_compatible">Uyumlu değil!</string> @@ -1217,7 +1217,7 @@ <string name="rcv_conn_event_verification_code_reset">güvenlik kodu değiştirildi</string> <string name="v4_6_audio_video_calls_descr">Bluetooth desteği ve diğer iyileştirmeler.</string> <string name="icon_descr_settings">Ayarlar</string> - <string name="settings_section_title_settings">AYARLAR</string> + <string name="settings_section_title_settings">Ayarlar</string> <string name="compose_send_direct_message_to_connect">Bağlanmak için doğrudan mesaj gönderin</string> <string name="security_code">Güvenlik kodu</string> <string name="v5_4_better_groups_descr">Daha hızlı gruplara katılma ve daha güvenilir mesajlar.</string> @@ -1261,7 +1261,7 @@ <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Veritabanı şifrelenecek ve parola ayarlarda depolanacak.</string> <string name="v5_4_block_group_members">Grup üyelerini engelle</string> <string name="feature_received_prohibited">alınmış, yasaklanmış</string> - <string name="error_xftp_test_server_auth">Sunucunun yükleme yapması için yetkilendirilmesi gerekli, şifreyi kontrol et</string> + <string name="error_xftp_test_server_auth">Sunucu yükleme için yetki gerektiriyor, şifreyi kontrol edin.</string> <string name="encryption_renegotiation_error">Şifreleme yeniden aşma hatası</string> <string name="smp_servers_preset_server">Ön ayarlı sunucu</string> <string name="feature_cancelled_item">%s iptal edildi</string> @@ -1279,7 +1279,7 @@ <string name="sending_delivery_receipts_will_be_enabled">Tüm kişiler için iletim bilgisi gönderme özelliği etkinleştirilecek</string> <string name="ensure_xftp_server_address_are_correct_format_and_unique">XFTP sunucu adreslerinin doğru formatta olduğundan, satırın ayrılmış ve kopyalanmamış olduğundan emin olun.</string> <string name="refresh_qr_code">Yenile</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Lütfen unutmayın</b>: mesaj ve dosya yönlendiricileri SOCKS vekili tarafından bağlandı. Aramalar ve bağlantı ön gösterimleri doğrudan bağlantı kullanıyor.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Lütfen dikkat</b>: Mesaj ve dosya aktarımları SOCKS proxy üzerinden bağlanır. Aramalar doğrudan bağlantı kullanır.]]></string> <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱mobil: <b>Telefon uygulamasında aç</b> seçeneğine tıkla, sonra uygulama içinden <b>Bağlan</b> seçeneğine tıkla.]]></string> <string name="smp_server_test_secure_queue">Gizli sıra</string> <string name="connected_desktop">Masaüstü bağlandı</string> @@ -1352,7 +1352,7 @@ <string name="relay_server_if_necessary">Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir.</string> <string name="remote_host_was_disconnected_toast"><![CDATA[Telefon bağlantılı <b>%s</b> ın bağlantısı kesildi]]></string> <string name="smp_servers_test_server">Sunucuyu test et</string> - <string name="conn_stats_section_title_servers">SUNUCULAR</string> + <string name="conn_stats_section_title_servers">Sunucular</string> <string name="smp_servers_test_servers">Sunucuları test et</string> <string name="privacy_message_draft">Mesaj taslağı</string> <string name="v5_2_disappear_one_message">Bir mesajı yok edin</string> @@ -1361,13 +1361,13 @@ <string name="icon_descr_contact_checked">Kişi doğrulandı</string> <string name="use_random_passphrase">Rasgele parola kullan</string> <string name="v5_0_app_passcode_descr">Sistem yetkilendirilmesi yerine ayarla.</string> - <string name="run_chat_section">SOHBETİ ÇALIŞTIR</string> + <string name="run_chat_section">Sohbeti çalıştır</string> <string name="network_disable_socks">Direkt internet bağlantısı kullan?</string> <string name="rcv_group_event_updated_group_profile">grup profili güncellendi</string> <string name="network_use_onion_hosts_required_desc">Onion ana bilgisayarları bağlantı için gerekli olacaktır. \nLütfen unutmayın: artık .onion adresi olmayan sunuculara bağlanamayacaksınız.</string> <string name="switch_receiving_address_desc">Alınan adres başka bir sunucuda değiştirilecektir. Adres değişimi gönderen çevrimiçi olunca tamamlanacaktır.</string> - <string name="error_smp_test_server_auth">Sunucunun sıralar oluşturması için yetkilendirilmesi gerekli, şifreyi kontrol et</string> + <string name="error_smp_test_server_auth">Sunucu kuyruk oluşturmak için yetki gerektiriyor, şifreyi kontrol edin.</string> <string name="system_restricted_background_in_call_title">Arkaplan araması yok</string> <string name="group_is_decentralized">Tamamiyle merkezi olmayan - sadece üyelere görünür.</string> <string name="error_showing_content">içerik gösterilirken hata</string> @@ -1381,7 +1381,7 @@ <string name="code_you_scanned_is_not_simplex_link_qr_code">Tarattığın kod SimpleX QR kodu bağlantısı değil.</string> <string name="send_receipts_disabled_alert_msg">Bu grupta %1$d den fazla kişi var,çoklu gönderim yapılamıyor.</string> <string name="connected_to_desktop">Masaüstüne bağlandı</string> - <string name="error_smp_test_certificate">Muhtemelen, sunucu adresindeki sertifika parmak izi doğru değil</string> + <string name="error_smp_test_certificate">Sunucu adresindeki parmak izi sertifika ile eşleşmiyor.</string> <string name="v5_2_message_delivery_receipts_descr">✅ özlediğimiz ikinci tik!</string> <string name="clear_verification">Doğrulamayı temizle</string> <string name="setup_database_passphrase">Veritabanı parolası ayarla</string> @@ -1446,7 +1446,7 @@ <string name="new_chat">Yeni sohbet</string> <string name="send_live_message_desc">Bir canlı mesaj gönder - bu yazdıklarını anlık olarak alıcıya(lara) güncelleyen bir mesajdır</string> <string name="remove_passphrase_from_keychain">Şifre Yöneticisindeki parola silinsin mi?</string> - <string name="settings_section_title_delivery_receipts">LERE GÖNDER</string> + <string name="settings_section_title_delivery_receipts">Lere gönder</string> <string name="connect_via_link_incognito">Takma adla bağlan</string> <string name="always_use_relay">Her zaman yönlendirici kullan.</string> <string name="auth_unlock">Kilidini aç</string> @@ -1471,8 +1471,7 @@ <string name="send_receipts">Alıcılara gönder</string> <string name="loading_chats">Sohbetler yükleniyor…</string> <string name="chat_help_tap_button">Butona bas</string> - <string name="connection_error_auth_desc">Kişinin bağlantısını silmesi veya bağlantının çoktan kullanılması gibi bir durum yoksa, bu bir hata olabilir - lütfen bize bildirin. -\nBağlanmak için, lütfen kişiye başka bir bağlanma bağlantısı göndermesini isteyin ve stabil bir internet bağlantınız olduğunu kontrol edin.</string> + <string name="connection_error_auth_desc">Kişinin bağlantısını silmesi veya bağlantının çoktan kullanılması gibi bir durum yoksa, bu bir hata olabilir - lütfen bize bildirin. \nBağlanmak için, lütfen kişiye başka bir bağlanma bağlantısı göndermesini isteyin ve stabil bir internet bağlantınız olduğunu kontrol edin.</string> <string name="image_decoding_exception_desc">Bu fotoğraf deşifre edilemedi. Lütfen,başka bir fotoğraf deneyin veya geliştiricilerle iletişime geçin.</string> <string name="disconnect_remote_hosts">Telefonların bağlantısını kes</string> <string name="receiving_via">Aracılığıyla alınıyor</string> @@ -1534,7 +1533,7 @@ <string name="info_row_received_at">Şuradan alındı</string> <string name="accept_feature_set_1_day">1 güne ayarla</string> <string name="color_received_message">Alınmış mesaj</string> - <string name="rcv_group_event_member_created_contact">doğrudan bağlandı</string> + <string name="rcv_group_event_member_created_contact">talep edilen bağlantı</string> <string name="blocked_item_description">engellendi</string> <string name="v5_5_private_notes">Gizli notlar</string> <string name="v5_5_message_delivery_descr">Azaltılmış pil kullanımı ile birlikte.</string> @@ -1556,7 +1555,7 @@ <string name="group_member_status_unknown">bilinmeyen durum</string> <string name="snd_group_event_member_blocked">engelledin %s</string> <string name="snd_group_event_member_unblocked">engeli kaldırdın %s</string> - <string name="past_member_vName">Geçmiş üye %1$s</string> + <string name="past_member_vName">Üye %1$s</string> <string name="member_blocked_by_admin">Yönetici tarafından engellendi</string> <string name="block_for_all">Herkes için engelle</string> <string name="info_row_created_at">Şurada oluşturuldu</string> @@ -1778,7 +1777,7 @@ <string name="network_smp_proxy_fallback_prohibit_description">Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN.</string> <string name="network_smp_proxy_fallback_allow_protected_description">IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</string> <string name="network_smp_proxy_fallback_allow_description">Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin.</string> - <string name="settings_section_title_private_message_routing">GİZLİ MESAJ YÖNLENDİRME</string> + <string name="settings_section_title_private_message_routing">Gizli mesaj yönlendirme</string> <string name="private_routing_show_message_status">Mesaj durumunu göster</string> <string name="private_routing_explanation">IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır.</string> <string name="network_smp_proxy_mode_unprotected">Korumasız</string> @@ -1805,7 +1804,7 @@ <string name="color_mode_dark">Karanlık</string> <string name="chat_theme_apply_to_light_mode">Aydınlık mod</string> <string name="protect_ip_address">IP adresini koru</string> - <string name="settings_section_title_files">DOSYALAR</string> + <string name="settings_section_title_files">Dosyalar</string> <string name="settings_section_title_chat_colors">Sohbet renkleri</string> <string name="wallpaper_scale_fit">Sığdır</string> <string name="color_received_quote">Alınan cevap</string> @@ -1844,7 +1843,7 @@ \nson alınan msj: %2$s</string> <string name="smp_proxy_error_connecting">Yönlendirme sunucusuna (%1$s) bağlantı sırasında hata oluştu. Lütfen daha sonra tekrar deneyin.</string> <string name="file_error_relay">Dosya sunucusu hatası. %1$s</string> - <string name="scan_paste_link">Tara / Bağlantı yapıştır</string> + <string name="scan_paste_link">Bağlantıyı yapıştır / Tara</string> <string name="app_check_for_updates">Güncellemeleri kontrol et</string> <string name="app_check_for_updates_disabled">Devre dışı</string> <string name="app_check_for_updates_download_completed_title">Uygulama Güncellemesi indirildi</string> @@ -1889,7 +1888,7 @@ <string name="invite_friends_short">Davet</string> <string name="create_address_button">Yarat</string> <string name="privacy_media_blur_radius_off">Kapalı</string> - <string name="settings_section_title_chat_database">Mesajlaşma Veritabanı</string> + <string name="settings_section_title_chat_database">Sohbet veritabanı</string> <string name="chat_database_exported_continue">Devam et</string> <string name="share_text_message_status">Mesaj durumu: %s</string> <string name="appearance_font_size">Yazı tipi boyutu</string> @@ -2267,7 +2266,7 @@ <string name="member_support">Üyelerle sohbetler</string> <string name="onboarding_notifications_mode_off_desc_short">Arkaplan servisi yok</string> <string name="onboarding_conditions_accept">Kabul Et</string> - <string name="onboarding_conditions_by_using_you_agree">SimpleX Chat\'i kullanarak şunları kabul etmiş olursunuz:\n- genel gruplarda sadece yasal içerik göndermeyi.\n- diğer kullanıcılara saygı göstermeyi - spam yapmamayı.</string> + <string name="onboarding_conditions_by_using_you_agree">SimpleX Chat\'i kullanarak şunları kabul etmiş olursunuz:\n- Herkese açık gruplara sadece yasal içerik göndermeyi.\n- Diğer kullanıcılara saygı göstermeyi - spam yapmamayı.</string> <string name="leave_chat_question">Sohbetten çıkılsın mı?</string> <string name="group_member_role_moderator">yönetici</string> <string name="network_smp_web_port_all">Bütün sunucular</string> @@ -2280,7 +2279,7 @@ <string name="delete_member_support_chat_alert_title">Üye ile birlikte sohbet silinsin mi?</string> <string name="no_support_chats">Üyeli sohbetler yok</string> <string name="accept_pending_member_alert_confirmation_as_member">Üye olarak kabul et</string> - <string name="error_deleting_member_support_chat">Üye ve sohbet silinirken hata oluştu</string> + <string name="error_deleting_member_support_chat">Sohbet silinirken hata oluştu</string> <string name="no_chats">Sohbetler yok</string> <string name="no_chats_found">Sohbetler bulunamadı</string> <string name="group_new_support_messages">%d mesajlar</string> @@ -2347,7 +2346,7 @@ <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX adresi ve tek kullanımlık bağlantılar herhangi bir mesajlaşma uygulaması üzerinden güvenle paylaşılabilir.</string> <string name="short_link_button_text">Kısa bağlantı</string> <string name="save_admission_question">Giriş ayarlarını kaydetmek ister misiniz?</string> - <string name="onboarding_conditions_private_chats_not_accessible">Özel sohbetler, gruplar ve kişileriniz sunucu operatörleri tarafından erişilemez.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Operatörler şunları taahhüt eder:\n- Bağımsız olun\n- Meta veri kullanımını en aza indirin\n- Doğrulanmış açık kaynak kodunu çalıştırın</string> <string name="onboarding_network_operators_cant_see_who_talks_to_whom">Birden fazla operatör etkinleştirildiğinde, hiçbirinin kimin kiminle iletişim kurduğunu öğrenmek için meta verisi yoktur.</string> <string name="onboarding_select_network_operators_to_use">Kullanılacak ağ operatörlerini seçin.</string> <string name="onboarding_network_operators_update">Güncelleme</string> @@ -2360,7 +2359,7 @@ <string name="view_conditions">Koşulları görüntüle</string> <string name="set_member_admission">Üye kabulü</string> <string name="v6_2_network_decentralization_descr">Uygulamadaki ikinci önceden ayarlanmış operatör!</string> - <string name="add_short_link">Kısa bağlantı ekle</string> + <string name="add_short_link">Adresi güncelle</string> <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Gizlilik politikası ve kullanım koşulları.</string> <string name="onboarding_network_operators_review_later">Daha sonra incele</string> <string name="onboarding_choose_server_operators">Sunucu operatörleri</string> @@ -2464,7 +2463,7 @@ <string name="open_to_connect">Bağlanmak için açık</string> <string name="group_preview_open_to_join">Katılmak için açık</string> <string name="private_routing_timeout">Özel yönlendirme zaman aşımı</string> - <string name="share_profile_via_link_alert_text">Profil, adres aracılığıyla paylaşılacaktır.</string> + <string name="share_profile_via_link_alert_text">Adres kısa olacak ve profiliniz adres üzerinden paylaşılacaktır.</string> <string name="network_option_protocol_timeout_background">Protokol arka plan zaman aşımı</string> <string name="reject_contact_request">İletişim isteğini reddet</string> <string name="v6_4_role_moderator_descr">Mesajları siler ve üyeleri engeller.</string> @@ -2475,9 +2474,9 @@ <string name="compose_view_send_request_without_message">Mesaj olmadan istek gönder</string> <string name="v6_4_support_chat_descr">Özel geri bildirimlerinizi gruplara gönderin.</string> <string name="sent_to_your_contact_after_connection">Bağlantı kurulduktan sonra kişinize gönderilir.</string> - <string name="share_group_profile_via_link">Grup profilini bağlantı yoluyla paylaş</string> - <string name="share_profile_via_link_alert_confirm">Profil paylaş</string> - <string name="share_profile_via_link">Profilinizi adres yoluyla paylaşın</string> + <string name="share_group_profile_via_link">Grup linki güncellensin mi?</string> + <string name="share_profile_via_link_alert_confirm">Güncelle</string> + <string name="share_profile_via_link">Adres güncellensin mi?</string> <string name="network_option_tcp_connection_timeout_background">TCP bağlantısı arka plan zaman aşımı</string> <string name="the_sender_will_not_be_notified">Gönderen bilgilendirilmeyecektir.</string> <string name="context_user_picker_cant_change_profile_alert_message">Bağlantı denemesinden sonra başka bir profil kullanmak için sohbeti silin ve bağlantıyı tekrar kullanın.</string> @@ -2505,8 +2504,402 @@ <string name="chat_banner_bot">Bot</string> <string name="cant_send_commands_alert_text">Komutlar gönderebilmek için bağlanmanış olmanız gereklidir.</string> <string name="member_is_deleted_cant_accept_request">Üye silinmiş - isteği kabul edemeyecek</string> - <string name="upgrade_group_link">Grup linkini güncelle</string> + <string name="upgrade_group_link">Grup bağlantısını güncelle</string> <string name="server_no_sub">Abonelik yok</string> <string name="not_connected_to_server_to_receive_messages_no_sub">Bu bağlantıdan mesaj almak için kullanılan sunucuya bağlı değilsiniz (abonelik yok).</string> - <string name="simplex_link_relay">SimpleX Relay Linki</string> + <string name="simplex_link_relay">SimpleX aktarıcı adresi</string> + <string name="relay_bar_active">%1$d/%2$d aktarıcı aktif</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d aktarıcı aktif, %3$d hata</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d aktarıcı aktif, %3$d başarısız</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d aktarıcı aktif, %3$d kaldırıldı</string> + <string name="relay_bar_connected">%1$d/%2$d aktarıcıya bağlanıldı</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d aktarıcı bağlı, %3$d hata</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d aktarıcı bağlı, %3$d başarısız</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d röle bağlandı, %3$d kaldırıldı</string> + <string name="channel_owner_count_singular">%1$d sahip</string> + <string name="channel_owner_count_plural">%1$d sahipler</string> + <string name="channel_owners_contributors_count">%1$d sahipler & katkıda bulunanlar</string> + <string name="relay_bar_relays_failed">%1$d röle başarısız</string> + <string name="relay_bar_relays_not_active">%1$d röle aktif değil</string> + <string name="relay_bar_relays_removed">%1$d röle kaldırıldı</string> + <string name="channel_subscriber_count_singular">%1$d abone</string> + <string name="channel_subscriber_count_plural">%1$d aboneler</string> + <string name="badge_supported_simplex">%1$s SimpleX Chat’i destekledi. Rozet %2$s tarihinde geçerliliğini yitirdi.</string> + <string name="v6_4_1_new_interface_languages">4 yeni arayüz dili</string> + <string name="settings_section_title_about">Hakkında</string> + <string name="relay_status_accepted">kabul edildi</string> + <string name="relay_status_acknowledged_roster">onaylanan kadro</string> + <string name="relay_status_active">aktif</string> + <string name="add_button">Ekle</string> + <string name="add_relay_button">Röle ekle</string> + <string name="add_relays_title">Röleler ekle</string> + <string name="relay_bar_owner_no_delivery">Mesaj iletimini geri yüklemek için röle ekleyin.</string> + <string name="webpage_code_footer">Bu kodu web sayfanıza ekleyin. Kanal veya grubunuzun önizlemesini görüntüler.</string> + <string name="advanced_options">Gelişmiş seçenekler</string> + <string name="advanced_settings">Gelişmiş ayarlar</string> + <string name="a_link_for_one_person">Bir kişi için bağlantı</string> + <string name="content_filter_all_messages">Tüm mesajlar</string> + <string name="allow_anyone_to_embed">Herkese gömme izni ver</string> + <string name="allow_files_and_media_only_if">Dosya ve medya paylaşımına yalnızca kişiniz izin verirse izin verin.</string> + <string name="allow_chat_with_admins">Üyelerin yöneticilerle sohbet etmesine izin ver.</string> + <string name="allow_direct_messages_channel">Abonelere doğrudan mesaj gönderilmesine izin ver.</string> + <string name="allow_chat_with_admins_channel">Abonelerin yöneticilerle sohbet etmesine izin ver.</string> + <string name="allow_your_contacts_to_send_files_and_media">Kişilerinizin dosya ve medya göndermesine izin verin.</string> + <string name="relay_bar_all_relays_failed">Tüm röleler başarısız oldu</string> + <string name="relay_bar_all_relays_removed">Tüm röleler kaldırıldı</string> + <string name="another_instance_not_responding">Uygulamanın başka bir oturumu açık olabilir veya düzgün kapatılmamış. Yine de devam edilsin mi?</string> + <string name="embed_any_webpage_can_show">Herhangi bir web sayfası önizleme gösterebilir.</string> + <string name="another_instance_title">Uygulama zaten çalışıyor.</string> + <string name="app_update_required">Uygulama güncelleme gerektiriyor.</string> + <string name="badge_unknown_key_title">Rozet doğrulanamadı.</string> + <string name="why_built_p7">Kim olduğunuzu bilme gücünü yok ettik. Böylece gücünüz asla elinizden alınamasın diye.</string> + <string name="onboarding_be_free">Kendi ağında\nözgürleş</string> + <string name="why_built_tagline">Kendi ağında özgürleş.</string> + <string name="block_subscriber_for_all_question">Abone herkes için engellensin mi?</string> + <string name="both_you_and_your_contact_can_send_files">Siz ve karşı taraf dosya ve medya gönderebilirsiniz.</string> + <string name="one_hand_ui_bottom_bar">Alt çubuk</string> + <string name="compose_view_broadcast">Yayın</string> + <string name="test_relay_to_retrieve_name"><![CDATA[Röle ismini almak için <b>test et</b>.]]></string> + <string name="chat_link_business_address">İş adresi</string> + <string name="button_cancel_and_delete_channel">İptal et ve kanalı sil</string> + <string name="cancel_creating_channel_question">Kanalı oluşturmaktan vazgeç?</string> + <string name="cant_broadcast_message">yayın yapılamıyor</string> + <string name="v6_4_1_new_interface_languages_descr">Kullanıcılarımızın katkılarıyla: Katalanca, Endonezce, Rumence ve Vietnamca!</string> + <string name="channel_role_label">kanal</string> + <string name="chat_banner_channel">Kanal</string> + <string name="info_row_channel">Kanal</string> + <string name="channel_full_name_field">Kanalın tam adı:</string> + <string name="channel_no_active_relays_try_later">Kanalda aktif röle bulunmuyor. Lütfen daha sonra katılmayı deneyin.</string> + <string name="chat_link_channel">Kanal bağlantısı</string> + <string name="channel_link">Kanal bağlantısı</string> + <string name="button_channel_members">Kanal üyeleri</string> + <string name="channel_display_name_field">Kanal adı</string> + <string name="channel_preferences">Kanal tercihleri</string> + <string name="channel_profile_is_stored_on_subscribers_devices">Kanal profili abonelerin cihazlarında ve sohbet rölelerinde saklanır.</string> + <string name="snd_channel_event_channel_profile_updated">kanal profili güncellendi</string> + <string name="chat_list_channels">Kanallar</string> + <string name="channel_temporarily_unavailable">Kanal geçici olarak kullanılamıyor</string> + <string name="channel_webpage">Kanal web sayfası</string> + <string name="delete_channel_for_all_subscribers_cannot_undo_warning">Kanal tüm aboneler için silinecektir - bu geri alınamaz!</string> + <string name="delete_channel_for_self_cannot_undo_warning">Kanal sizin için silinecektir - bu geri alınamaz!</string> + <string name="channel_will_start_with_relays">Kanal %2$d rölenin %1$d\'si ile çalışmaya başlayacaktır. Devam edecek mi?</string> + <string name="chat_data">Sohbet verileri</string> + <string name="chat_relay">Sohbet rölesi</string> + <string name="button_channel_relays">Sohbet röleleri</string> + <string name="chat_relays">Sohbet röleleri</string> + <string name="channel_relays_title">Sohbet röleleri</string> + <string name="chat_relays_forward_messages_in_channels">Sohbet aktarıcıları, oluşturduğunuz kanallardaki mesajları iletir.</string> + <string name="chat_relays_forward_messages">Sohbet aktarıcıları mesajları kanal abonelerine iletir.</string> + <string name="chat_with_admins_is_prohibited">Yöneticilerle sohbet etmek yasaktır.</string> + <string name="chat_with_admins_relay_note">Genel kanallarda yöneticilerle yapılan sohbetlerde E2E şifreleme yoktur - yalnızca güvenilir sohbet aktarıcılarıyla kullanın.</string> + <string name="support_chats_disabled">Üyelerle sohbetler devre dışı bırakıldı</string> + <string name="chat_with_admins">Yöneticilerle sohbet edin</string> + <string name="check_relay_address">Röle adresini kontrol edin ve tekrar deneyin.</string> + <string name="check_relay_name">Röle adını kontrol edin ve tekrar deneyin.</string> + <string name="close_behavior_dialog_close">Uygulamayı kapatın</string> + <string name="appearance_minimize_to_tray">Sistem tepsisine kapat</string> + <string name="configure_relays">Röleleri yapılandırma</string> + <string name="relay_test_step_connect">Bağlan</string> + <string name="relay_conn_status_connected">bağlı</string> + <string name="relay_conn_status_connecting">bağlanılıyor</string> + <string name="channel_name_requires_newer_app_version">Kanal adı üzerinden bağlanmak için daha yeni bir uygulama sürümü gerekir.</string> + <string name="contact_name_requires_newer_app_version">Kişi adı ile bağlanmak için daha yeni bir uygulama sürümü gerekir.</string> + <string name="info_row_connection_failed">Bağlantı başarısız</string> + <string name="connect_via_link_or_qr_code">Bağlantı veya kare kod ile bağlanın</string> + <string name="settings_section_title_contact">İletişim</string> + <string name="chat_link_contact_address">İletişim adresi</string> + <string name="settings_section_title_contact_requests_from_groups">Gruplardan gelen iletişim talepleri</string> + <string name="group_member_role_member_channel">katkıda bulunan</string> + <string name="copy_code">Kodu kopyala</string> + <string name="webpage_info">Abone olmadan önce ziyaretçilere kanalınızın önizlemesini göstermek için bir web sayfası oluşturun. Kendiniz barındırın veya herhangi bir statik barındırma kullanın.</string> + <string name="create_channel_title">Herkese açık kanal oluşturun</string> + <string name="create_channel_button">Herkese açık kanal oluşturun</string> + <string name="v6_4_1_short_address_create">Adresinizi oluşturun</string> + <string name="connect_with_someone">Bağlantınızı oluşturun</string> + <string name="create_your_public_address">Herkese açık adresinizi oluşturun</string> + <string name="creating_channel">Kanal oluşturma</string> + <string name="rcv_channel_events_count">%d kanal olayı</string> + <string name="relay_test_step_decode_link">Bağlantıyı deşifre et</string> + <string name="button_delete_channel">Kanalı sil</string> + <string name="delete_channel_question">Kanalı silelim mi?</string> + <string name="relay_conn_status_deleted">silindi</string> + <string name="rcv_channel_event_channel_deleted">silinmiş kanal</string> + <string name="button_delete_member_messages">Üye mesajlarını sil</string> + <string name="button_delete_member_messages_question">Üye mesajları silinsin mi?</string> + <string name="delete_member_messages_confirmation">Mesajları sil</string> + <string name="delete_relay">Aktarıcıyı sil</string> + <string name="deprecated_options_section">Kullanımdan kaldırılan seçenekler</string> + <string name="direct_messages_are_prohibited_channel">Aboneler arasında doğrudan mesajlaşma yasaktır.</string> + <string name="link_previews_alert_disable">Devre dışı bırak</string> + <string name="disable_sending_recent_history_channel">Yeni aboneler geçmişi görüntüleyemesin.</string> + <string name="num_relays_selected">%d aktarıcı seçildi</string> + <string name="rcv_msg_error_dropped">düştü (%1$d deneme)</string> + <string name="v6_5_invite_friends">Arkadaşlarınızı davet etmek daha kolay 👋</string> + <string name="button_edit_channel_profile">Kanal profilini düzenle</string> + <string name="link_previews_alert_enable">Etkinleştir</string> + <string name="enable_chats_with_admins">Etkinleştir</string> + <string name="enable_at_least_one_chat_relay">Bir kanal oluşturmak için en az bir sohbet aktarıcısını etkinleştirin.</string> + <string name="enable_chats_with_admins_question">Yöneticilerle sohbet etkinleştirilsin mi?</string> + <string name="v6_4_1_keep_chats_clean_descr">Varsayılan olarak kaybolan mesajları etkinleştirin.</string> + <string name="link_previews_alert_title">Bağlantı önizlemelerini etkinleştirin mi?</string> + <string name="enter_profile_name">Profil adını girin…</string> + <string name="enter_relay_name">Aktarıcı adını girin…</string> + <string name="enter_webpage_url">Web sayfası URL\'sini girin</string> + <string name="error_prefix">Hata</string> + <string name="error_adding_relay">Aktarıcı ekleme hatası</string> + <string name="error_adding_relays">Aktarıcı ekleme hatası</string> + <string name="error_creating_channel">Kanal oluşturulurken hata oluştu</string> + <string name="error_deleting_message">Mesaj silinirken hata oluştu</string> + <string name="error_marking_member_support_chat_read">Okundu olarak işaretlenirken hata</string> + <string name="error_opening_channel">Kanal açılırken hata oluştu</string> + <string name="rcv_msg_error_parse">hata: %s</string> + <string name="error_saving_channel_profile">Kanal profili kaydedilirken hata oluştu</string> + <string name="error_saving_simplex_name">İsim kaydedilirken hata oluştu</string> + <string name="error_sharing_channel">Kanal paylaşılırken hata oluştu</string> + <string name="member_info_member_failed">başarısız oldu</string> + <string name="relay_conn_status_failed">başarısız oldu</string> + <string name="relay_status_failed">başarısız oldu</string> + <string name="content_filter_files">Dosyalar</string> + <string name="files_prohibited_in_this_chat">Bu sohbette dosya ve medya yasaktır.</string> + <string name="content_filter_menu_item">Filtre</string> + <string name="proxy_destination_error_unknown_ca">Hedef sunucu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string> + <string name="smp_proxy_error_unknown_ca">Yönlendirme sunucusu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string> + <string name="network_error_unknown_ca">Sunucu adresindeki parmak izi sertifika ile eşleşmiyor: %1$s.</string> + <string name="for_anyone_to_reach_you">Herkesin size ulaşabilmesi için</string> + <string name="from_history">Tarihten</string> + <string name="chat_link_from_owner">(sahibinden)</string> + <string name="relay_test_step_get_link">Bağlantı al</string> + <string name="get_started">Başlayın</string> + <string name="chat_link_group">Grup bağlantısı</string> + <string name="group_webpage">Grup web sayfası</string> + <string name="help_and_support">Yardım ve destek</string> + <string name="recent_history_is_not_sent_to_new_members_channel">Geçmiş yeni abonelere gönderilmez.</string> + <string name="web_page_url_placeholder">https://</string> + <string name="close_behavior_dialog_text">Kapat\'ı seçerseniz mesajlar alınmaz.\nBunu daha sonra Görünüm ayarlarından değiştirebilirsiniz.</string> + <string name="down_migration_warning_chat_relays">Kanallara katıldıysanız veya kanallar oluşturduysanız, bunlar kalıcı olarak çalışmayı durduracaktır.</string> + <string name="content_filter_images">Görüntüler</string> + <string name="relay_status_inactive">aktif değil</string> + <string name="invalid_relay_address">Geçersiz aktarıcı adresi!</string> + <string name="invalid_relay_name">Geçersiz aktarıcı adı!</string> + <string name="relay_status_invited">davet edildi</string> + <string name="invite_someone_privately">Birini özel olarak davet edin</string> + <string name="webpage_url_footer">Abonelere gösterilecek ve önizlemenin yüklenmesine izin vermek için kullanılacaktır.</string> + <string name="compose_view_join_channel">Kanala katılın</string> + <string name="v6_4_1_keep_chats_clean">Sohbetlerinizi temiz tutun</string> + <string name="button_leave_channel">Kanaldan ayrılın</string> + <string name="leave_channel_question">Kanaldan ayrılınsın mı?</string> + <string name="set_user_simplex_name_footer">İnsanların SimpleX adresinize kayıtlı isim üzerinden size bağlanmasına izin verin.</string> + <string name="set_channel_simplex_name_footer">İnsanların bu kanal bağlantısıyla kayıtlı isim üzerinden katılmasına izin verin.</string> + <string name="let_someone_connect_to_you">Birinin sizinle bağlantı kurmasına izin verin</string> + <string name="action_button_channel_link">Bağlantı</string> + <string name="link_previews_alert_desc_socks">Bağlantı önizlemesi SOCKS proxy aracılığıyla talep edilecektir. DNS araması hala DNS çözümleyiciniz aracılığıyla yerel olarak gerçekleşebilir.</string> + <string name="content_filter_links">Bağlantılar</string> + <string name="owner_verification_passed">Bağlantı imzası doğrulandı.</string> + <string name="member_messages_will_be_deleted_cannot_be_undone">Üye mesajları silinecektir - bu geri alınamaz!</string> + <string name="members_can_chat_with_admins">Üyeler yöneticilerle sohbet edebilir.</string> + <string name="alert_title_msg_error">Mesaj hatası</string> + <string name="e2ee_info_no_e2ee"><![CDATA[Bu kanaldaki mesajlar <b>uçtan uca şifrelenmez</b>. Sohbet aktarıcıları bu mesajları görebilir.]]></string> + <string name="migrate">Taşı</string> + <string name="close_behavior_dialog_minimize">Sistem tepsisine küçült</string> + <string name="close_behavior_dialog_title">Sistem tepsisine küçültülsün mü?</string> + <string name="more_privacy">Daha fazla gizlilik</string> + <string name="simplex_name_not_found">İsim bulunamadı</string> + <string name="onboarding_network_commitments">Ağ taahhütleri</string> + <string name="network_error">Ağ hatası</string> + <string name="onboarding_network_routers_cannot_know">Ağ yönlendiricileri şunları bilemez\nkim kiminle konuşuyor</string> + <string name="relay_status_new">yeni</string> + <string name="new_1_time_link">Yeni 1 kerelik bağlantı</string> + <string name="new_chat_relay">Yeni sohbet aktarıcısı</string> + <string name="onboarding_no_account">Hesap yok. Telefon yok. E-posta yok. Kimlik yok.\nEn güvenli şifreleme.</string> + <string name="relay_bar_no_active_relays">Aktif aktarıcı yok</string> + <string name="no_available_relays">Kullanılabilir aktarıcı yok</string> + <string name="why_built_p1">Kimse konuşmalarınızı takip etmedi. Kimse nerede olduğunuza dair bir harita çizmedi. Gizlilik hiçbir zaman bir özellik değildi - bu yaşam biçimiydi.</string> + <string name="no_chat_relays">Sohbet aktarıcısı yok</string> + <string name="no_chat_relays_enabled">Etkin sohbet aktarıcısı yok.</string> + <string name="simplex_name_no_servers_desc">Sunucularınızın hiçbiri SimpleX adlarını çözümleyecek şekilde ayarlanmamış. Sunucuları yapılandırın veya bir bağlantı adresi kullanın.</string> + <string name="v6_5_non_profit_governance">Kâr amacı gütmeyen yönetişim</string> + <string name="relay_bar_no_relays">Aktarıcı yok</string> + <string name="no_relays_selected">Seçili aktarıcı yok</string> + <string name="no_names_servers_enabled">İsimleri çözümleyecek sunucu yok.</string> + <string name="why_built_p4">Başkasının kapısında daha iyi bir kilit değil. Mahremiyetinize saygı duyan ama yine de tüm ziyaretçilerin kaydını tutan daha iyi bir ev sahibi değil. Siz misafir değilsiniz. Siz evinizsiniz. Hiçbir kral oraya giremez - egemen olan sizsiniz.</string> + <string name="not_all_relays_connected">Tüm aktarıcılar bağlı değil</string> + <string name="simplex_name_no_valid_link">Geçerli bağlantı yok</string> + <string name="chat_link_one_time">Tek seferlik bağlantı</string> + <string name="only_channel_owners_can_change_prefs">Kanal tercihlerini yalnızca kanal sahipleri değiştirebilir.</string> + <string name="only_you_can_send_files">Yalnızca siz dosya ve medya gönderebilirsiniz.</string> + <string name="only_your_contact_can_send_files">Yalnızca irtibat kişiniz dosya ve medya gönderebilir.</string> + <string name="embed_only_your_page">Yalnızca yukarıdaki sayfanız önizlemeyi gösterebilir.</string> + <string name="onboarding_on_your_phone">Telefonunuzda, sunucularda değil.</string> + <string name="connect_plan_open_channel">Açık kanal</string> + <string name="privacy_chat_list_open_clean_web_link">Temiz bağlantıyı aç</string> + <string name="open_external_link_title">Harici bağlantı açılsın mı?</string> + <string name="privacy_chat_list_open_full_web_link">Tam bağlantıyı aç</string> + <string name="connect_plan_open_new_channel">Yeni kanal açın</string> + <string name="v6_5_safe_web_links_descr">- bağlantı önizlemelerini göndermeyi tercih edin.\n- etkinleştirilmişse SOCKS proxy kullanın.\n- köprü kimlik avını önleyin.\n- bağlantı izlemeyi kaldırın.</string> + <string name="onboarding_or_show_qr_code">Ya da kare kodu şahsen veya görüntülü arama yoluyla gösterin.</string> + <string name="onboarding_or_use_qr_code">Ya da bu kare kodu kullanın - yazdırın veya çevrimiçi gösterin.</string> + <string name="member_info_section_title_owner">Sahip</string> + <string name="channel_members_section_owners">Sahipler ve katkıda bulunanlar</string> + <string name="v6_5_ownership">Sahiplik: kendi aktarıcılarınızı çalıştırabilirsiniz.</string> + <string name="please_upgrade_the_app">Lütfen uygulamayı güncelleyin.</string> + <string name="preset_relay_address">Önceden ayarlanmış aktarıcı adresi</string> + <string name="preset_relay_name">Önceden ayarlanmış aktarıcı adı</string> + <string name="v6_5_privacy">Gizlilik: sahipler ve aboneler için.</string> + <string name="onboarding_private_and_secure">Özel ve güvenli mesajlaşma.</string> + <string name="prohibit_chat_with_admins">Yöneticilerle sohbeti yasaklayın.</string> + <string name="prohibit_direct_messages_channel">Abonelere doğrudan mesaj göndermeyi yasaklayın.</string> + <string name="prohibit_sending_files_and_media">Dosya ve medya göndermeyi yasaklayın.</string> + <string name="v6_5_public_channels">Herkese açık kanallar - özgürce konuşun 🚀</string> + <string name="tray_quit">SimpleX\'ten çıkın</string> + <string name="relay_status_rejected">reddedildi</string> + <string name="member_info_relay_status_rejected_by_operator">aktarıcı operatörü tarafından reddedildi</string> + <string name="group_member_role_relay">aktarıcı</string> + <string name="member_info_section_title_relay">Aktarıcı</string> + <string name="info_row_relay_address">Aktarıcı adresi</string> + <string name="relay_address_alert_title">Aktarıcı adresi</string> + <string name="relay_connection_failed">Aktarıcı bağlantısı başarısız</string> + <string name="info_row_relay_link">Aktarıcı bağlantısı</string> + <string name="relay_results">Aktarıcı sonuçları:</string> + <string name="relays_added_format">Aktarıcı eklendi: %1$s.</string> + <string name="relay_test_failed_alert">Röle testi başarısız!</string> + <string name="relay_will_be_removed_from_channel">Röle kanaldan kaldırılacaktır - bu geri alınamaz!</string> + <string name="v6_5_reliability">Güvenilirlik: kanal başına birçok röle.</string> + <string name="remove_member_delete_messages_confirmation">Mesajları kaldır ve sil</string> + <string name="relay_conn_status_removed">kaldırıldı</string> + <string name="relay_conn_status_removed_by_operator">operatör tarafından kaldırıldı</string> + <string name="sanitize_links_toggle">Bağlantı izlemeyi kaldırın</string> + <string name="button_remove_relay">Aktarıcıyı kaldır</string> + <string name="button_remove_relay_question">Aktarıcı kaldırılsın mı?</string> + <string name="button_remove_subscriber">Üyeyi kaldır</string> + <string name="button_remove_subscriber_question">Üye kaldırılsın mı?</string> + <string name="rcv_direct_event_group_inv_link_received">%1$s grubundan bağlantı talep edildi</string> + <string name="simplex_name_resolver_error_desc">Çözümleyici hatası: %1$s</string> + <string name="appearance_minimize_to_tray_desc">Mesajları almak için arka planda çalışır</string> + <string name="v6_5_safe_web_links">Güvenli web bağlantıları</string> + <string name="save_and_notify_channel_subscribers">Kanal abonelerini kaydedin ve bilgilendirin</string> + <string name="save_channel_profile">Kanal profilini kaydet</string> + <string name="placeholder_search_files">Dosyalardan ara</string> + <string name="placeholder_search_images">Görsellerden ara</string> + <string name="placeholder_search_links">Bağlantılardan ara</string> + <string name="placeholder_search_videos">Videolardan ara</string> + <string name="placeholder_search_voice_messages">Sesli mesajlardan ara</string> + <string name="v6_5_security">Güvenlik: kanal anahtarları sahiplerindedir.</string> + <string name="select_relays">Aktarıcıları seçin</string> + <string name="link_previews_alert_desc">Bir bağlantı önizlemesi göndermek IP adresinizi web sitesine gösterebilir. Bunu daha sonra Gizlilik ayarlarından değiştirebilirsiniz.</string> + <string name="onboarding_send_1_time_link">Bağlantıyı herhangi bir mesajlaşma programı aracılığıyla gönderin - güvenlidir. SimpleX\'e yapıştırılmasını isteyin.</string> + <string name="enable_sending_recent_history_channel">Yeni abonelere 100 adede kadar son mesajları gönderin.</string> + <string name="simplex_name_server_no_resolver_desc">Sunucu %1$s ad çözümlemesini desteklemiyor. Sunucuları yapılandırın veya bir bağlantı adresini kullanın.</string> + <string name="error_relay_test_server_auth">Sunucu aktarıcıya bağlanmak için yetki gerektiriyor, parolayı kontrol edin.</string> + <string name="server_warning">Sunucu uyarısı</string> + <string name="v6_4_1_welcome_contacts_descr">Profil biyografisini ve hoş geldiniz mesajını ayarlayın.</string> + <string name="set_simplex_name">SimpleX adını ayarla</string> + <string name="onboarding_configure_notifications">Bildirimleri ayarla</string> + <string name="onboarding_configure_routers">Yönlendiricileri kurun</string> + <string name="share_channel">Kanalı paylaş…</string> + <string name="share_old_address_alert_button">Eski adresi paylaş</string> + <string name="share_old_link_alert_button">Eski bağlantıyı paylaş</string> + <string name="share_relay_address">Aktarıcı adresini paylaş</string> + <string name="share_via_chat">Sohbet yoluyla paylaşın</string> + <string name="v6_4_1_short_address_share">Adresinizi paylaşın</string> + <string name="v6_4_1_short_address">Kısa SimpleX adresi</string> + <string name="tray_show">SimpleX\'i göster</string> + <string name="owner_verification_failed">⚠️ İmza doğrulama başarısız oldu: %s.</string> + <string name="tray_tooltip">SimpleX</string> + <string name="tray_tooltip_unread">SimpleX - %d okunmamış</string> + <string name="simplex_name">SimpleX adı</string> + <string name="simplex_name_error">SimpleX ad hatası</string> + <string name="simplex_name_not_verified">SimpleX adı doğrulanmadı</string> + <string name="badge_invested">SimpleX Chat kitlesel fonlamasına yatırım yapan %s\'ler.</string> + <string name="badge_supports_simplex">%s kişi SimpleX Chat\'i destekliyor.</string> + <string name="member_info_status">Durum</string> + <string name="group_member_role_observer_channel">abone</string> + <string name="member_info_section_title_subscriber">Abone</string> + <string name="group_reports_subscriber_reports">Abone raporları</string> + <string name="channel_members_title_subscribers">Aboneler</string> + <string name="group_members_can_add_message_reactions_channel">Aboneler mesaj tepkileri ekleyebilir.</string> + <string name="members_can_chat_with_admins_channel">Aboneler yöneticilerle sohbet edebilir.</string> + <string name="group_members_can_delete_channel">Aboneler gönderilen mesajları geri alınamaz şekilde silebilir. (24 saat)</string> + <string name="group_members_can_send_reports_channel">Aboneler mesajları moderatörlere bildirebilir.</string> + <string name="group_members_can_send_dms_channel">Aboneler doğrudan mesaj gönderebilir.</string> + <string name="group_members_can_send_disappearing_channel">Aboneler kaybolan mesajlar gönderebilir.</string> + <string name="group_members_can_send_files_channel">Aboneler dosya ve medya gönderebilir.</string> + <string name="group_members_can_send_simplex_links_channel">Aboneler SimpleX bağlantıları gönderebilir.</string> + <string name="group_members_can_send_voice_channel">Aboneler sesli mesaj gönderebilir.</string> + <string name="relay_section_footer_owner">Aboneler kanala bağlanmak için röle bağlantısını kullanır.\nKanal için bu röleyi kurmak için röle adresi kullanıldı.</string> + <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">Abone kanaldan çıkarılacaktır - bu geri alınamaz!</string> + <string name="settings_section_title_support_project">Projeyi destekleyin</string> + <string name="talk_to_someone">Biriyle konuş</string> + <string name="chat_banner_join_channel">Kanala katıl\'a dokunun</string> + <string name="tap_to_open">Açmak için dokunun</string> + <string name="error_relay_test_failed_at_step">Test %s adımında başarısız oldu.</string> + <string name="test_relay">Test aktarıcısı</string> + <string name="alert_text_msg_reception_error">Uygulama bu mesajı %1$d alma denemesinden sonra kaldırdı.</string> + <string name="badge_unknown_key_desc">Rozet, uygulamanın bu sürümünün tanımadığı bir anahtarla imzalanmıştır. Bu rozeti doğrulamak için uygulamayı güncelleyin.</string> + <string name="connection_reached_limit_of_undelivered_messages">Bağlantı teslim edilmemiş mesaj sınırına ulaştı</string> + <string name="onboarding_first_network">Sahip olduğunuz ilk ağ\nkişileriniz ve gruplarınız.</string> + <string name="share_group_profile_via_link_alert_text">Bağlantı kısa olacak ve grup profili bağlantı üzerinden paylaşılacaktır.</string> + <string name="why_built_p2">Sonra internete geçtik ve her platform sizden bir parça istedi - adınız, numaranız, arkadaşlarınız. Başkalarıyla konuşmanın bedelinin, birilerinin kiminle konuştuğumuzu bilmesine izin vermek olduğunu kabul ettik. Her nesil, insanlar ve teknoloji, bunu bu şekilde yaptı - telefon, e-posta, mesajlaşma programları, sosyal medya. Mümkün olan tek yol bu gibi görünüyordu.</string> + <string name="why_built_p6">En eski insan özgürlüğü - bir başkasıyla izlenmeden konuşmak - ona ihanet edemeyecek bir altyapı üzerine inşa edilmiştir.</string> + <string name="why_built_p3">Başka bir yol daha var. Telefon numarası olmayan bir ağ. Kullanıcı adı yok. Hesaplar yok. Herhangi bir kullanıcı kimliği yok. İnsanları birbirine bağlayan ve kimin bağlı olduğunu bilmeden şifreli mesajlar taşıyan bir ağ.</string> + <string name="member_role_will_be_changed_with_notification_channel">Rol %s olarak değiştirilecektir. Kanaldaki herkes bilgilendirilecektir.</string> + <string name="simplex_name_no_valid_link_desc">SimpleX adı %1$s kayıtlı, ancak geçerli bir bağlantısı yok.</string> + <string name="simplex_name_unconfirmed_desc">SimpleX adı %1$s kayıtlı, ancak profile eklenmemiş. Eğer sahibi sizseniz, lütfen adresinize veya kanal profilinize ekleyin.</string> + <string name="simplex_name_owner_no_channel_link">SimpleX adı %1$s kanal bağlantısı olmadan kaydedildi. Kayıt sayfası üzerinden isme kanal bağlantısı ekleyin.</string> + <string name="simplex_name_owner_no_address">SimpleX adı %1$s SimpleX adresi olmadan kaydedildi. SimpleX adresinizi kayıt sayfası üzerinden isme ekleyin.</string> + <string name="badge_unverified_desc">Bu rozet doğrulanamamıştır ve orijinal olmayabilir.</string> + <string name="group_link_requires_newer_version">Bu grup, uygulamanın daha yeni bir sürümünü gerektirmektedir. Katılmak için lütfen uygulamayı güncelleyin.</string> + <string name="relay_address_alert_message">Bu bir sohbet aktarım adresidir, bağlanmak için kullanılamaz.</string> + <string name="last_active_relay_warning">Bu son aktif aktarıcıdır. Kaldırılması abonelere mesaj iletimini engelleyecektir.</string> + <string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[Bu <b>%1$s</b> kanalı için bağlantınız!]]></string> + <string name="this_setting_is_for_your_current_profile">Bu ayar mevcut profiliniz içindir</string> + <string name="simplex_name_not_found_desc">Bu SimpleX adı kayıtlı değil. Lütfen ismi kontrol edin.</string> + <string name="v6_5_non_profit_governance_descr">SimpleX Ağını kalıcı kılmak için.</string> + <string name="one_hand_ui_top_bar">Üst çubuk</string> + <string name="operator_use_for_names">İsimleri çözümlemek için</string> + <string name="unblock_subscriber_for_all_question">Herkes için abone engeli kaldırılsın mı?</string> + <string name="simplex_name_unconfirmed">Onaylanmamış isim</string> + <string name="unsupported_channel_name">Desteklenmeyen kanal adı</string> + <string name="unsupported_contact_name">Desteklenmeyen kişi adı</string> + <string name="badge_unverified_title">Doğrulanmamış rozet</string> + <string name="rcv_channel_event_updated_channel_profile">güncellenen kanal profili</string> + <string name="v6_4_1_short_address_update">Adresinizi güncelleyin</string> + <string name="recent_history_is_sent_to_new_members_channel">Yeni abonelere en fazla son 100 mesaj gönderilir.</string> + <string name="relays_no_web_support">Kullanılan sohbet aktarıcıları web sayfalarını desteklemez.</string> + <string name="use_for_new_channels">Yeni kanallar için kullanın</string> + <string name="wait_verb">Bekle</string> + <string name="relay_test_step_wait_response">Yanıt bekleniyor</string> + <string name="webpage_code">Web sayfası kodu</string> + <string name="v6_4_1_welcome_contacts">Kişilerinizi karşılayın 👋</string> + <string name="v6_5_invite_friends_descr">Yeni kullanıcılar için bağlantı kurmayı kolaylaştırdık.</string> + <string name="why_simplex_is_built">SimpleX\'in yapılış amacı.</string> + <string name="channel_member_you">sen</string> + <string name="you_are_subscriber">Abonesiniz</string> + <string name="you_can_share_channel_link_anybody_will_be_able_to_connect">Bir bağlantı veya QR kodu paylaşabilirsiniz; böylece kanala herkes katılabilir.</string> + <string name="badge_support_from_v7">Uygulamanın v7 sürümünden itibaren SimpleX\'i destekleyebilirsiniz.</string> + <string name="relay_section_footer_subscriber">Bu aktarım bağlantısı (relay link) üzerinden kanala bağlandınız.</string> + <string name="chat_banner_your_channel">Kanalınız</string> + <string name="connect_plan_this_is_your_link_for_channel">Kanalınız</string> + <string name="why_built_p5">Konuşmalarınız, internetten önce her zaman olduğu gibi, size aittir. Ağ, ziyaret ettiğiniz bir yer değildir; sizin yarattığınız ve sahip olduğunuz bir yerdir. Ve ister gizli ister herkese açık yapın, bunu hiç kimse elinizden alamaz.</string> + <string name="onboarding_your_network">Ağınız</string> + <string name="onboarding_your_profile">Profiliniz</string> + <string name="your_public_address">Genel adresiniz</string> + <string name="your_relay_address">Yönlendirici adresiniz</string> + <string name="your_relay_name">Aktarıcı adınız</string> + <string name="your_simplex_name">SimpleX Adınız</string> + <string name="why_built_heading">Bir hesap gerekmeden başladınız.</string> + <string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">Bu kanaldan mesaj almayı durduracaksınız. Sohbet geçmişi korunacaktır.</string> + <string name="content_filter_voice_messages">Sesli mesaj</string> + <string name="voice_recording_not_supported">Kullanmakta olduğunuz platform ses kaydı almayı desteklemiyor.</string> + <string name="relay_bar_subscriber_waiting">Kanal sahibinin aktarıcıları (relay\'leri) eklemesi bekleniyor.</string> + <string name="connect_plan_connect_to_name">%s\'ye bağlanın</string> + <string name="connect_plan_join_name">Kanal %s\'ye katıl</string> + <string name="use_relay">Aktarıcı kullan</string> + <string name="onboarding_post_address">Bu adresi sosyal medya profilinizde, web sitenizde veya e-posta imzanızda kullanın.</string> + <string name="relay_test_step_verify">Doğrulama</string> + <string name="verify_simplex_name_action">Adı doğrulayın</string> + <string name="verify_simplex_names">SimpleX adlarını doğrula</string> + <string name="via_relay_hostname">%1$s aracılığıyla</string> + <string name="content_filter_videos">Videolar</string> + <string name="cancel_channel_alert_msg">Yeni kanalınız %1$s, %3$d aktarıcının %2$d\'sine bağlandı.\nİptal ederseniz, kanal silinir - yeniden oluşturabilirsiniz.</string> + <string name="your_profile_shared_with_channel_relays">Profiliniz %1$s kanal aktarıcıları ve aboneleri ile paylaşılacaktır.\nAktarıcılar kanal mesajlarına erişebilir.</string> </resources> 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 4e62631dbb..9a59fec674 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -33,14 +33,14 @@ <string name="allow_to_send_disappearing">Дозволити надсилати зникаючі повідомлення.</string> <string name="callstatus_accepted">прийнятий виклик</string> <string name="always_use_relay">Завжди використовувати реле</string> - <string name="settings_section_title_app">ДОДАТОК</string> + <string name="settings_section_title_app">Додаток</string> <string name="allow_direct_messages">Дозволити надсилання приватних повідомлень учасникам.</string> <string name="allow_to_delete_messages">Дозволити безповоротно видаляти надіслані повідомлення. (24 години)</string> <string name="allow_to_send_voice">Дозволяйте надсилати голосові повідомлення.</string> <string name="allow_message_reactions">Дозволити реакції на повідомлення.</string> <string name="v5_1_self_destruct_passcode_descr">Вся інформація стирається при його введенні.</string> <string name="v5_0_app_passcode">Пароль для додатка</string> - <string name="settings_section_title_icon">ІКОНКА ДОДАТКУ</string> + <string name="settings_section_title_icon">Іконка додатку</string> <string name="allow_disappearing_messages_only_if">Дозволити зникаючі повідомлення тільки за умови, що ваш контакт дозволяє їх.</string> <string name="allow_your_contacts_adding_message_reactions">Дозвольте вашим контактам додавати реакції на повідомлення.</string> <string name="allow_message_reactions_only_if">Дозволити реакції на повідомлення тільки за умови, що ваш контакт дозволяє їх.</string> @@ -62,7 +62,7 @@ <string name="allow_your_contacts_to_send_disappearing_messages">Дозвольте вашим контактам надсилати повідомлення, які зникають.</string> <string name="clear_chat_warning">Усі повідомлення будуть видалені - цю дію неможливо скасувати! Повідомлення будуть видалені ЛИШЕ для вас.</string> <string name="app_version_title">Версія додатку</string> - <string name="add_address_to_your_profile">Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам.</string> + <string name="add_address_to_your_profile">Додайте адресу до свого профілю, щоб ваші контакти SimpleX могли поділитися нею з іншими людьми. Оновлення профілю буде відправлено вашим контактам SimpleX.</string> <string name="all_your_contacts_will_remain_connected_update_sent">Усі ваші контакти залишаться підключеними. Оновлення профілю буде відправлено вашим контактам.</string> <string name="answer_call">Відповісти на виклик</string> <string name="address_section_title">Адреса</string> @@ -123,10 +123,10 @@ <string name="server_connected">підключено</string> <string name="server_error">помилка</string> <string name="server_connecting">підключення</string> - <string name="connected_to_server_to_receive_messages_from_contact">Ви підключені до сервера для отримання повідомлень від цього контакту.</string> + <string name="connected_to_server_to_receive_messages_from_contact">Ви підключені до сервера для отримання повідомлень від цього зʼєднання.</string> <string name="error_connecting_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього контакту (помилка: %1$s).</string> <string name="deleted_description">видалено</string> - <string name="trying_to_connect_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього контакту.</string> + <string name="trying_to_connect_to_server_to_receive_messages">Спроба підключитися до сервера для отримання повідомлень від цього зʼєднання.</string> <string name="marked_deleted_description">відзначено як видалено</string> <string name="moderated_item_description">модеровано %s</string> <string name="sending_files_not_yet_supported">надсилання файлів поки що не підтримується</string> @@ -171,13 +171,13 @@ <string name="connection_error">Помилка підключення</string> <string name="network_error_desc">Будь ласка, перевірте ваше мережеве підключення з %1$s та спробуйте ще раз.</string> <string name="contact_already_exists">Контакт вже існує</string> - <string name="connection_error_auth">Помилка підключення (AUTH)</string> + <string name="connection_error_auth">Помилка підключення</string> <string name="sender_may_have_deleted_the_connection_request">Відправник, можливо, видалив запит на з\'єднання.</string> <string name="error_deleting_contact_request">Помилка видалення запиту на контакт</string> <string name="error_changing_address">Помилка зміни адреси</string> <string name="error_smp_test_failed_at_step">Тест не пройшов на кроці %s.</string> - <string name="error_smp_test_server_auth">Сервер вимагає авторизації для створення черг, перевірте пароль</string> - <string name="error_smp_test_certificate">Можливо, відбиток цифрового підпису сертифіката в адресі сервера невірний</string> + <string name="error_smp_test_server_auth">Сервер вимагає авторизації для створення черг, перевірте пароль.</string> + <string name="error_smp_test_certificate">Відбиток у адресі сервера не збігається з сертифікатом.</string> <string name="smp_server_test_create_queue">Створити чергу</string> <string name="service_notifications">Миттєві сповіщення!</string> <string name="service_notifications_disabled">Миттєві сповіщення вимкнено!</string> @@ -278,8 +278,8 @@ <string name="icon_descr_flip_camera">Повернути камеру</string> <string name="icon_descr_call_rejected">Відхилений виклик</string> <string name="integrity_msg_skipped">%1$d пропущено повідомлень</string> - <string name="settings_section_title_chats">ЧАТИ</string> - <string name="settings_section_title_socks">SOCKS-ПРОКСІ</string> + <string name="settings_section_title_chats">Чати</string> + <string name="settings_section_title_socks">SOCKS-проксі</string> <string name="error_starting_chat">Помилка при запуску чату</string> <string name="stop_chat_confirmation">Зупинити</string> <string name="import_database_confirmation">Імпортувати</string> @@ -416,9 +416,9 @@ <string name="icon_descr_call_connecting">Підключення виклику</string> <string name="privacy_and_security">Конфіденційність і безпека</string> <string name="your_privacy">Конфіденційність</string> - <string name="settings_section_title_settings">НАЛАШТУВАННЯ</string> - <string name="settings_section_title_help">ДОПОМОГА</string> - <string name="settings_section_title_support">ПІДТРИМАЙТЕ SIMPLEX CHAT</string> + <string name="settings_section_title_settings">Налаштування</string> + <string name="settings_section_title_help">Допомога</string> + <string name="settings_section_title_support">Підтримайте SimpleX Chat</string> <string name="stop_chat_to_export_import_or_delete_chat_database">Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено.</string> <string name="error_deleting_database">Помилка видалення бази даних чату</string> <string name="notifications_will_be_hidden">Сповіщення будуть доставлятися лише до зупинки додатка!</string> @@ -434,12 +434,12 @@ <string name="snd_conn_event_switch_queue_phase_completed_for_member">ви змінили адресу для %s</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">змінює адресу для %s…</string> <string name="invite_prohibited">Неможливо запросити контакт!</string> - <string name="group_info_section_title_num_members">%1$s УЧАСНИКІВ</string> + <string name="group_info_section_title_num_members">%1$s учасників</string> <string name="button_delete_group">Видалити групу</string> <string name="group_link">Посилання на групу</string> <string name="button_edit_group_profile">Редагувати профіль групи</string> <string name="create_group_link">Створити посилання на групу</string> - <string name="change_member_role_question">Змінити роль у групі\?</string> + <string name="change_member_role_question">Змінити роль?</string> <string name="error_removing_member">Помилка при вилученні учасника</string> <string name="group_main_profile_sent">Ваш профіль буде відправлений учасникам групи</string> <string name="full_deletion">Видалення для всіх</string> @@ -448,7 +448,7 @@ <string name="v4_3_voice_messages_desc">Максимум 40 секунд, надходять миттєво.</string> <string name="v5_0_app_passcode_descr">Встановіть його замість системної аутентифікації.</string> <string name="shutdown_alert_question">Вимкнути\?</string> - <string name="share_with_contacts">Поділитися з контактами</string> + <string name="share_with_contacts">Поділитися з контактами SimpleX</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профіль зберігається на вашому пристрої та ділиться лише з вашими контактами. Серверам SimpleX профіль недоступний.</string> <string name="save_and_notify_contacts">Зберегти та сповістити контакти</string> <string name="save_and_notify_group_members">Зберегти та сповістити учасників</string> @@ -464,7 +464,7 @@ <string name="settings_restart_app">Перезапустити</string> <string name="your_chat_database">База даних чату</string> <string name="chat_is_stopped">Чат зупинено</string> - <string name="chat_database_section">БАЗА ДАНИХ ЧАТУ</string> + <string name="chat_database_section">База даних чату</string> <string name="new_database_archive">Новий архів бази даних</string> <string name="stop_chat_question">Зупинити чат\?</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ваша поточна база даних чату буде ВИДАЛЕНА та ЗАМІНЕНА імпортованою. @@ -658,11 +658,11 @@ <string name="self_destruct_passcode_enabled">Пароль самознищення увімкнено!</string> <string name="self_destruct_passcode_changed">Пароль самознищення змінено!</string> <string name="your_profile_is_stored_on_your_device">Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.</string> - <string name="settings_section_title_you">ВИ</string> - <string name="settings_section_title_device">ПРИСТРІЙ</string> + <string name="settings_section_title_you">Ви</string> + <string name="settings_section_title_device">Пристрій</string> <string name="settings_shutdown">Вимкнути</string> - <string name="settings_section_title_themes">ТЕМИ</string> - <string name="settings_section_title_messages">ПОВІДОМЛЕННЯ ТА ФАЙЛИ</string> + <string name="settings_section_title_themes">Теми</string> + <string name="settings_section_title_messages">Повідомлення та файли</string> <string name="chat_is_running">Чат працює</string> <string name="import_database">Імпортувати базу даних</string> <string name="old_database_archive">Старий архів бази даних</string> @@ -782,7 +782,7 @@ <string name="custom_time_unit_months">місяці</string> <string name="you_are_already_connected_to_vName_via_this_link">Ви вже підключені до %1$s через це посилання.</string> <string name="settings_section_title_incognito">Режим інкогніто</string> - <string name="conn_stats_section_title_servers">СЕРВЕРИ</string> + <string name="conn_stats_section_title_servers">Сервери</string> <string name="save_welcome_message_question">Зберегти вітальне повідомлення?</string> <string name="receiving_via">Отримання через</string> <string name="muted_when_inactive">Приглушено, коли неактивно!</string> @@ -795,8 +795,7 @@ <string name="ttl_hours">%d години</string> <string name="error_deleting_pending_contact_connection">Помилка видалення очікуючого з\'єднання з контактом</string> <string name="error_loading_details">Помилка завантаження деталей</string> - <string name="connection_error_auth_desc">Якщо ваш контакт не видалив з\'єднання або це посилання вже використано, це може бути помилкою - будь ласка, повідомте про це. -\nДля підключення попросіть вашого контакту створити інше посилання на з\'єднання та перевірте стабільність мережевого підключення.</string> + <string name="connection_error_auth_desc">Якщо ваш контакт не видалив з\'єднання або це посилання вже використано, це може бути помилкою - будь ласка, повідомте про це. \nДля підключення попросіть вашого контакту створити інше посилання на з\'єднання та перевірте стабільність мережевого підключення.</string> <string name="error_deleting_contact">Помилка видалення контакту</string> <string name="error_deleting_group">Помилка видалення групи</string> <string name="smp_server_test_disconnect">Відключити</string> @@ -840,7 +839,7 @@ <string name="network_use_onion_hosts_no_desc">.Onion-хости не будуть використовуватися.</string> <string name="network_session_mode_transport_isolation">Ізоляція транспорту</string> <string name="customize_theme_title">Налаштування теми</string> - <string name="share_address_with_contacts_question">Поділитися адресою з контактами?</string> + <string name="share_address_with_contacts_question">Поділитися адресою з контактами SimpleX?</string> <string name="callstatus_connecting">підключення дзвінка…</string> <string name="we_do_not_store_contacts_or_messages_on_servers">Ми не зберігаємо жодні з ваших контактів чи повідомлень (після доставки) на серверах.</string> <string name="callstate_waiting_for_answer">очікування відповіді…</string> @@ -879,7 +878,7 @@ <string name="ttl_day">%d день</string> <string name="ttl_days">%d днів</string> <string name="feature_cancelled_item">скасовано %s</string> - <string name="run_chat_section">ЗАПУСК ЧАТУ</string> + <string name="run_chat_section">Запуск чату</string> <string name="database_passphrase">Пароль бази даних</string> <string name="export_database">Експортувати базу даних</string> <string name="delete_files_and_media_all">Видалити всі файли</string> @@ -930,7 +929,7 @@ <string name="snd_conn_event_switch_queue_phase_changing">змінює адресу…</string> <string name="leave_group_button">Залишити</string> <string name="group_member_role_observer">спостерігач</string> - <string name="member_info_section_title_member">УЧАСНИК</string> + <string name="member_info_section_title_member">Учасник</string> <string name="incognito_info_protects">Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту.</string> <string name="v4_5_reduced_battery_usage_descr">Більше поліпшень незабаром!</string> <string name="only_group_owners_can_enable_voice">Тільки власники груп можуть увімкнути голосові повідомлення.</string> @@ -979,7 +978,7 @@ <string name="icon_descr_contact_checked">Контакт відмічено</string> <string name="invite_prohibited_description">Ви намагаєтеся запросити контакт, з яким ви поділилися інкогніто-профілем, до групи, в якій ви використовуєте основний профіль</string> <string name="error_creating_link_for_group">Помилка при створенні посилання на групу</string> - <string name="section_title_for_console">ДЛЯ КОНСОЛІ</string> + <string name="section_title_for_console">Для консолі</string> <string name="member_will_be_removed_from_group_cannot_be_undone">Учасника буде вилучено з групи - цю дію неможливо скасувати!</string> <string name="change_role">Змінити роль</string> <string name="you_will_still_receive_calls_and_ntfs">Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні.</string> @@ -1021,11 +1020,11 @@ <string name="host_verb">Хост</string> <string name="port_verb">Порт</string> <string name="network_use_onion_hosts_required">Обов\'язково</string> - <string name="theme_colors_section_title">КОЛЬОРИ ІНТЕРФЕЙСУ</string> + <string name="theme_colors_section_title">Кольори інтерфейсу</string> <string name="create_address_and_let_people_connect">Створіть адресу, щоб дозволити людям підключатися до вас.</string> <string name="your_contacts_will_remain_connected">Контакти залишатимуться підключеними.</string> <string name="create_simplex_address">Створити SimpleX-адресу</string> - <string name="profile_update_will_be_sent_to_contacts">Оновлення профілю буде відправлено вашим контактам.</string> + <string name="profile_update_will_be_sent_to_contacts">Оновлення профілю буде відправлено вашим SimpleX контактам.</string> <string name="stop_sharing_address">Зупинити поділ адреси?</string> <string name="stop_sharing">Зупинити поділ</string> <string name="enter_welcome_message_optional">Введіть текст привітання... (необов\'язково)</string> @@ -1110,7 +1109,7 @@ <string name="from_gallery_button">Галерея</string> <string name="icon_descr_simplex_team">Команда SimpleX</string> <string name="contact_wants_to_connect_with_you">хоче підключитися до вас!</string> - <string name="settings_section_title_experimenta">ЕКСПЕРИМЕНТАЛЬНІ ФУНКЦІЇ</string> + <string name="settings_section_title_experimenta">Експериментальні функції</string> <string name="you_must_use_the_most_recent_version_of_database">Ви повинні використовувати найновішу версію бази даних чату лише на одному пристрої, інакше ви можете припинити отримання повідомлень від деяких контактів.</string> <string name="messages_section_description">Цей параметр застосовується до повідомлень у вашому поточному профілі чату</string> <string name="encrypted_database">Зашифрована база даних</string> @@ -1135,7 +1134,7 @@ <string name="ttl_d">%dд</string> <string name="v4_6_hidden_chat_profiles_descr">Захистіть свої чат-профілі паролем!</string> <string name="decryption_error">Помилка дешифрування</string> - <string name="error_xftp_test_server_auth">Сервер вимагає авторизації для завантаження, перевірте пароль</string> + <string name="error_xftp_test_server_auth">Сервер вимагає авторизації для завантаження, перевірте пароль.</string> <string name="smp_server_test_upload_file">Завантажити файл</string> <string name="smp_server_test_download_file">Завантажити файл</string> <string name="database_initialization_error_title">Не вдається ініціалізувати базу даних</string> @@ -1163,7 +1162,7 @@ <string name="opensource_protocol_and_code_anybody_can_run_servers">Кожен може хостити сервери.</string> <string name="settings_developer_tools">Інструменти розробника</string> <string name="settings_experimental_features">Експериментальні функції</string> - <string name="settings_section_title_calls">ДЗВІНКИ</string> + <string name="settings_section_title_calls">Дзвінки</string> <string name="save_passphrase_in_keychain">Зберегти ключову фразу в сховищі ключів</string> <string name="error_encrypting_database">Помилка шифрування бази даних</string> <string name="remove_passphrase_from_keychain">Вилучити ключову фразу із сховища ключів?</string> @@ -1259,7 +1258,7 @@ <string name="files_and_media_prohibited">Заборонено файли та медіа!</string> <string name="connect__your_profile_will_be_shared">Буде відправлено ваш профіль %1$s.</string> <string name="receipts_groups_disable_for_all">Вимкнути для всіх груп</string> - <string name="settings_section_title_delivery_receipts">НАДСИЛАТИ ПОВІДОМЛЕННЯ ПРО ДОСТАВКУ</string> + <string name="settings_section_title_delivery_receipts">Надсилати повідомлення про доставку</string> <string name="connect_via_member_address_alert_title">Підключитися безпосередньо?</string> <string name="recipient_colon_delivery_status">%s: %s</string> <string name="connect_via_member_address_alert_desc">Запит на підключення буде відправлено учаснику групи.</string> @@ -1335,7 +1334,7 @@ <string name="system_restricted_background_warn"><![CDATA[Щоб увімкнути сповіщення, оберіть, будь ласка, <b>Використання батареї додатком</b> / <b>Без обмежень</b> у налаштуваннях додатка.]]></string> <string name="system_restricted_background_in_call_warn"><![CDATA[Щоб здійснювати дзвінки в фоновому режимі, будь ласка, оберіть <b>Використання батареї додатком</b> / <b>Без обмежень</b> у налаштуваннях додатка.]]></string> <string name="connect_use_new_incognito_profile">Використовувати новий інкогніто-профіль</string> - <string name="you_can_enable_delivery_receipts_later_alert">Ви зможете увімкнути їх пізніше через налаштування конфіденційності та безпеки додатка.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Ви зможете увімкнути їх пізніше через ваше налаштування безпеки додатка.</string> <string name="rcv_group_event_n_members_connected">%s, %s і ще %d учасників підключилися</string> <string name="privacy_show_last_messages">Показувати останні повідомлення</string> <string name="error_synchronizing_connection">Помилка синхронізації з\'єднання</string> @@ -1356,7 +1355,7 @@ <string name="error_creating_member_contact">Помилка при створенні контакту учасника</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Ви вже приєднуєтеся до групи за цим посиланням.</string> <string name="create_group_button">Створити групу</string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Зверніть увагу</b>: ретрансляція повідомлень та файлів підключається через SOCKS-проксі. Дзвінки та відправлення переглядів посилань використовують пряме підключення.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Зверніть увагу</b>: ретрансляція повідомлень і файлів здійснюється через SOCKS-проксі. Дзвінки здійснюються через пряме з’єднання.]]></string> <string name="create_another_profile_button">Створити профіль</string> <string name="group_members_2">%s і %s</string> <string name="connect_plan_join_your_group">Приєднатися до вашої групи?</string> @@ -1428,7 +1427,7 @@ <string name="member_contact_send_direct_message">відправити для підключення</string> <string name="terminal_always_visible">Показувати консоль в новому вікні</string> <string name="block_member_desc">Усі нові повідомлення від %s будуть приховані!</string> - <string name="rcv_group_event_member_created_contact">підключив(лась) безпосередньо</string> + <string name="rcv_group_event_member_created_contact">запитано підключення</string> <string name="blocked_item_description">заблоковано</string> <string name="v5_4_block_group_members">Блокувати учасників групи</string> <string name="v5_4_incognito_groups_descr">Створіть групу, використовуючи випадковий профіль.</string> @@ -1610,7 +1609,7 @@ <string name="profile_update_event_set_new_picture">встановити новий аватар</string> <string name="profile_update_event_updated_profile">оновлений профіль</string> <string name="profile_update_event_removed_address">вилучено адресу контакту</string> - <string name="past_member_vName">Колишній учасник %1$s</string> + <string name="past_member_vName">Учасник %1$s</string> <string name="call_service_notification_end_call">Кінець дзвінка</string> <string name="call_service_notification_video_call">Відеодзвінок</string> <string name="call_service_notification_audio_call">Аудіодзвінок</string> @@ -1734,7 +1733,7 @@ <string name="v5_7_call_sounds">Звуки вхідного дзвінка</string> <string name="chat_theme_apply_to_light_mode">Світлий режим</string> <string name="update_network_smp_proxy_fallback_question">Запасний варіант маршрутизації повідомлень</string> - <string name="settings_section_title_private_message_routing">МАРШРУТИЗАЦІЯ ПРИВАТНИХ ПОВІДОМЛЕНЬ</string> + <string name="settings_section_title_private_message_routing">Маршрутизація приватних повідомлень</string> <string name="forwarded_description">переслано</string> <string name="network_type_other">Інше</string> <string name="allow_to_send_simplex_links">Дозволити надсилати посилання SimpleX.</string> @@ -1746,7 +1745,7 @@ <string name="permissions_camera_and_record_audio">Камера та мікрофон</string> <string name="permissions_grant">Надайте дозвіл(и) на здійснення дзвінків</string> <string name="permissions_open_settings">Відкрити налаштування</string> - <string name="settings_section_title_files">ФАЙЛИ</string> + <string name="settings_section_title_files">Файли</string> <string name="settings_section_title_profile_images">Зображення профілів</string> <string name="settings_section_title_network_connection">Підключення до мережі</string> <string name="feature_roles_admins">адміністратори</string> @@ -1838,7 +1837,7 @@ \nостаннє отримане повідомлення: %2$s</string> <string name="proxy_destination_error_broker_host">Адреса сервера призначення %1$s несумісна з налаштуваннями сервера переадресації %2$s.</string> <string name="file_error_no_file">Файл не знайдено — ймовірно, файл був видалений або скасований.</string> - <string name="scan_paste_link">Сканувати / Вставити посилання</string> + <string name="scan_paste_link">Вставити / Сканувати посилання</string> <string name="xftp_servers_configured">Налаштовані XFTP сервери</string> <string name="app_check_for_updates_beta">Бета</string> <string name="info_row_file_status">Статус файлу</string> @@ -2049,7 +2048,7 @@ <string name="reset_all_hints">Скинути всі підказки</string> <string name="app_check_for_updates_update_available">Доступно оновлення: %s</string> <string name="app_check_for_updates_canceled">Завантаження оновлення скасовано</string> - <string name="settings_section_title_chat_database">БАЗА ДАНИХ ЧАТУ</string> + <string name="settings_section_title_chat_database">База даних чату</string> <string name="select_chat_profile">Вибрати профіль чату</string> <string name="switching_profile_error_title">Помилка при зміні профілю</string> <string name="delete_messages_cannot_be_undone_warning">Повідомлення будуть видалені — це не можна скасувати!</string> @@ -2371,9 +2370,9 @@ <string name="members_will_be_removed_from_chat_cannot_be_undone">Учасників буде видалено з чату – це неможливо скасувати!</string> <string name="unblock_members_for_all_question">Розблокувати учасників для всіх?</string> <string name="operator_updated_conditions">Оновлені умови</string> - <string name="onboarding_conditions_private_chats_not_accessible">Приватні чати, групи та ваші контакти недоступні для операторів сервера.</string> + <string name="onboarding_conditions_private_chats_not_accessible">Оператори зобов’язуються:\n- Бути незалежними\n- Мінімізувати використання метаданих\n- Використовувати перевірений код з відкритим кодом</string> <string name="onboarding_conditions_accept">Прийняти</string> - <string name="onboarding_conditions_by_using_you_agree">Використовуючи SimpleX Chat, ви погоджуєтесь на:\n- надсилати тільки легальний контент у публічних групах.\n- поважати інших користувачів – без спаму.</string> + <string name="onboarding_conditions_by_using_you_agree">Ви зобов’язуєтеся:\n- розміщувати у публічних групах лише законний контент\n- поважати інших користувачів — не розсилати спам</string> <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">Політика конфіденційності та умови використання</string> <string name="link_requires_newer_app_version_please_upgrade">Це посилання вимагає новішої версії додатку. Будь ласка, оновіть додаток або попросіть вашого контакту надіслати сумісне посилання.</string> <string name="full_link_button_text">Повне посилання</string> @@ -2408,7 +2407,7 @@ <string name="accept_pending_member_alert_title">Прийняти учасника</string> <string name="delete_member_support_chat_button">Видалити чат</string> <string name="delete_member_support_chat_alert_title">Видалити чат з учасником?</string> - <string name="error_deleting_member_support_chat">Помилка видалення чату з учасником</string> + <string name="error_deleting_member_support_chat">Помилка видалення чату</string> <string name="set_member_admission">Встановити прийом учасників</string> <string name="save_admission_question">Зберегти налаштування прийому?</string> <string name="snd_group_event_user_pending_review">Будь ласка, зачекайте, поки модератори групи розглянуть ваш запит на приєднання до групи.</string> @@ -2513,7 +2512,7 @@ <string name="allow_your_contacts_to_send_files_and_media">Дозвольте своїм контактам надсилати файли та медіа.</string> <string name="chat_banner_bot">Бот</string> <string name="both_you_and_your_contact_can_send_files">Ви, і ваш контакт можете надсилати файли та медіа.</string> - <string name="settings_section_title_contact_requests_from_groups">ЗАПИТИ НА ЗВ’ЯЗОК ВІД ГРУП</string> + <string name="settings_section_title_contact_requests_from_groups">Запити на зв’язок від груп</string> <string name="deprecated_options_section">Застарілі опції</string> <string name="error_marking_member_support_chat_read">Помилка при відмітці як прочитане</string> <string name="files_prohibited_in_this_chat">Файли та медіа заборонені у цьому чаті.</string> @@ -2528,4 +2527,171 @@ <string name="prohibit_sending_files_and_media">Заборонити надсилання файлів і медіа.</string> <string name="sanitize_links_toggle">Видалити відстеження посилань</string> <string name="rcv_direct_event_group_inv_link_received">запит на підключення до групи %1$s</string> + <string name="relay_bar_active">%1$d/%2$d ретранслятор активний</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d ретранслятор активний, %3$d помилки</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d ретранслятор активний, %3$d невдача</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d ретранслятор активний, %3$d видалено</string> + <string name="relay_bar_connected">%1$d/%2$d ретранслятор підключений</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d ретранслятор підключений, %3$d помилки</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d ретранслятор підключений, %3$d невдача</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d ретранслятор підключений, %3$d видалено</string> + <string name="channel_owner_count_singular">%1$d власник</string> + <string name="channel_owner_count_plural">%1$d власники</string> + <string name="channel_owners_contributors_count">%1$d власники і автори</string> + <string name="relay_bar_relays_failed">%1$d перемикачі вийшли з ладу</string> + <string name="relay_bar_relays_not_active">%1$d перемикачі не активні</string> + <string name="relay_bar_relays_removed">%1$d перемикачі видалені</string> + <string name="channel_subscriber_count_singular">%1$d підписник</string> + <string name="channel_subscriber_count_plural">%1$d підписники</string> + <string name="badge_supported_simplex">%1$s підтримував SimpleX Chat. Завершення дії значка %2$s.</string> + <string name="settings_section_title_about">Про</string> + <string name="relay_status_accepted">підтверджено</string> + <string name="relay_status_acknowledged_roster">затверджений список</string> + <string name="relay_status_active">активний</string> + <string name="add_button">Додати</string> + <string name="add_relay_button">Додати перемикач</string> + <string name="add_relays_title">Додати перемикачі</string> + <string name="relay_bar_owner_no_delivery">Додайте перемикачі для відновлення доставки повідомлень.</string> + <string name="webpage_code_footer">Додайте цей код на свою веб-сторінку. Він відобразить попередній перегляд вашого каналу / групи.</string> + <string name="advanced_options">Розширені параметри</string> + <string name="advanced_settings">Розширені налаштування</string> + <string name="a_link_for_one_person">Посилання на одну особу для звʼязку</string> + <string name="content_filter_all_messages">Усі повідомлення</string> + <string name="allow_anyone_to_embed">Дозволити будь-кому вбудовувати</string> + <string name="allow_chat_with_admins">Дозволити користувачам переписуватись з адміністраторами.</string> + <string name="allow_direct_messages_channel">Дозволити надсилати прямі повідомлення підписникам.</string> + <string name="allow_chat_with_admins_channel">Дозволити підписникам переписуватись з адміністраторами.</string> + <string name="relay_bar_all_relays_failed">Усі перемикачі вийшли з ладу</string> + <string name="relay_bar_all_relays_removed">Усі перемикачі видалені</string> + <string name="another_instance_not_responding">Можливо, інший екземпляр програми вже працює або не завершив роботу належним чином. Все одно запустити?</string> + <string name="embed_any_webpage_can_show">Будь-яка веб-сторінка може відображати попередній огляд.</string> + <string name="another_instance_title">Додаток вже запущено</string> + <string name="app_update_required">Потрібно оновити додаток</string> + <string name="badge_unknown_key_title">Значок не можливо верифікувати</string> + <string name="why_built_p7">Тому що ми знищили здатність дізнатися, хто ти є. Щоб твою силу ніколи не можна було відібрати.</string> + <string name="onboarding_be_free">Будь вільним\nв твоій мережі</string> + <string name="why_built_tagline">Будь вільним у твоїй мережі.</string> + <string name="block_subscriber_for_all_question">Заблокувати підписника для усіх?</string> + <string name="one_hand_ui_bottom_bar">Нижня панель</string> + <string name="compose_view_broadcast">Трансляція</string> + <string name="test_relay_to_retrieve_name"><![CDATA[<b>Тестування перемикача</b> щоб дізнатися його назву.]]></string> + <string name="chat_link_business_address">Бізнес адреса</string> + <string name="button_cancel_and_delete_channel">Відмінити і видалити канал</string> + <string name="cancel_creating_channel_question">Відмінити створення каналу?</string> + <string name="cant_broadcast_message">не можливо транслювати</string> + <string name="channel_role_label">канал</string> + <string name="chat_banner_channel">Канал</string> + <string name="info_row_channel">Канал</string> + <string name="channel_full_name_field">Повна назва каналу:</string> + <string name="channel_no_active_relays_try_later">Канал не має активних перемикачів. Будь ласка, спробуйте підключитися пізніше.</string> + <string name="channel_link">Посилання на канал</string> + <string name="chat_link_channel">Посилання на канал</string> + <string name="button_channel_members">Учасники каналу</string> + <string name="channel_display_name_field">Імʼя каналу</string> + <string name="channel_preferences">Налаштування каналів</string> + <string name="channel_profile_is_stored_on_subscribers_devices">Профіль каналу зберігається на пристроях підписників та на ретрансляторах чату.</string> + <string name="snd_channel_event_channel_profile_updated">профіль каналу оновлено</string> + <string name="chat_list_channels">Канали</string> + <string name="channel_temporarily_unavailable">Канали тимчасово недоступні</string> + <string name="channel_webpage">Веб-сторінка каналу</string> + <string name="delete_channel_for_all_subscribers_cannot_undo_warning">Канал буде видалено для всіх підписників — цю дію неможливо скасувати!</string> + <string name="delete_channel_for_self_cannot_undo_warning">Канал буде видалено для вас — цю дію неможливо скасувати!</string> + <string name="channel_will_start_with_relays">Канал почне працювати з %1$d з %2$d реле. Продовжити?</string> + <string name="chat_data">Дані чату</string> + <string name="chat_relay">Ретрансляція чату</string> + <string name="button_channel_relays">Ретранслятори чату</string> + <string name="chat_relays">Ретранслятори чату</string> + <string name="channel_relays_title">Ретранслятори чату</string> + <string name="chat_relays_forward_messages_in_channels">Ретранслятори чату пересилають повідомлення в каналах, які ви створюєте.</string> + <string name="chat_relays_forward_messages">Ретранслятори чату пересилають повідомлення підписникам каналу.</string> + <string name="chat_with_admins_is_prohibited">Спілкування з адміністраторами заборонено.</string> + <string name="chat_with_admins_relay_note">Чати з адміністраторами у публічних каналах не мають шифрування «від кінця до кінця» (E2E) — використовуйте їх лише з надійними серверами-ретрансляторами.</string> + <string name="support_chats_disabled">Чат із учасниками вимкнено</string> + <string name="chat_with_admins">Поспілкуватися з адміністраторами</string> + <string name="check_relay_address">Перевірте адресу реле та спробуйте ще раз.</string> + <string name="check_relay_name">Перевірте назву реле та спробуйте ще раз.</string> + <string name="close_behavior_dialog_close">Закрити додаток</string> + <string name="appearance_minimize_to_tray">Закрити в трей</string> + <string name="configure_relays">Налаштувати ретранслятори</string> + <string name="relay_test_step_connect">Підключитися</string> + <string name="relay_conn_status_connected">підключено</string> + <string name="relay_conn_status_connecting">підключення</string> + <string name="channel_name_requires_newer_app_version">Для підключення за назвою каналу потрібна новіша версія додатка.</string> + <string name="contact_name_requires_newer_app_version">Для підключення за іменем контакту потрібна новіша версія додатка.</string> + <string name="info_row_connection_failed">Підключення не вдалося</string> + <string name="connect_via_link_or_qr_code">Підключитися через посилання або QR код</string> + <string name="settings_section_title_contact">Контакт</string> + <string name="chat_link_contact_address">Адреса контакту</string> + <string name="group_member_role_member_channel">автор</string> + <string name="copy_code">Скопіювати код</string> + <string name="webpage_info">Створіть веб-сторінку, на якій відвідувачам буде показано попередній перегляд вашого каналу ще до того, як вони підпишуться. Розмістіть її на власному хостингу або скористайтеся будь-яким сервісом статичного хостингу.</string> + <string name="create_channel_title">Створити публічний канал</string> + <string name="create_channel_button">Створити публічний канал</string> + <string name="connect_with_someone">Створити своє посилання</string> + <string name="create_your_public_address">Створити свою публічну адресу</string> + <string name="creating_channel">Канал створюється</string> + <string name="rcv_channel_events_count">%d подій каналу</string> + <string name="relay_test_step_decode_link">Декодувати посилання</string> + <string name="button_delete_channel">Видалити канал</string> + <string name="delete_channel_question">Видалити канал?</string> + <string name="relay_conn_status_deleted">видалено</string> + <string name="rcv_channel_event_channel_deleted">видалений канал</string> + <string name="button_delete_member_messages">Видалити повідомлення учасників</string> + <string name="button_delete_member_messages_question">Видалити повідомлення учасників?</string> + <string name="delete_member_messages_confirmation">Видалити повідомлення</string> + <string name="enter_webpage_url">Введіть URL веб-сторінки</string> + <string name="error_prefix">Помилка</string> + <string name="error_deleting_message">Помилка видалення повідомлення</string> + <string name="error_opening_channel">Помилка відкриття каналу</string> + <string name="rcv_msg_error_parse">помилка: %s</string> + <string name="error_saving_channel_profile">Помилка збереження профілю каналу</string> + <string name="delete_relay">Видалити реле</string> + <string name="direct_messages_are_prohibited_channel">Прямі повідомлення між підписниками заборонені.</string> + <string name="link_previews_alert_disable">Вимкнути</string> + <string name="disable_sending_recent_history_channel">Не надсилати історію новим підписникам.</string> + <string name="num_relays_selected">%d реле обрано</string> + <string name="rcv_msg_error_dropped">не доставлено (після %1$d спроб)</string> + <string name="v6_5_invite_friends">Простіше запросити друзів 👋</string> + <string name="button_edit_channel_profile">Редагувати профіль каналу</string> + <string name="link_previews_alert_enable">Увімкнути</string> + <string name="enable_chats_with_admins">Увімкнути</string> + <string name="enable_at_least_one_chat_relay">Увімкніть хоча б одне реле чату для створення каналу.</string> + <string name="enable_chats_with_admins_question">Увімкнути чати з адміністраторами?</string> + <string name="link_previews_alert_title">Увімкнути попередній перегляд посилань?</string> + <string name="enter_profile_name">Введіть назву профілю…</string> + <string name="enter_relay_name">Введіть назву реле…</string> + <string name="error_adding_relay">Помилка додавання реле</string> + <string name="error_adding_relays">Помилка додавання реле</string> + <string name="error_creating_channel">Помилка створення каналу</string> + <string name="error_sharing_channel">Помилка поширення каналу</string> + <string name="member_info_member_failed">помилка</string> + <string name="relay_conn_status_failed">помилка</string> + <string name="relay_status_failed">помилка</string> + <string name="content_filter_files">Файли</string> + <string name="content_filter_menu_item">Фільтр</string> + <string name="proxy_destination_error_unknown_ca">Відбиток в адресі цільового сервера не збігається із сертифікатом: %1$s.</string> + <string name="for_anyone_to_reach_you">Щоб будь-хто міг з вами зв’язатися</string> + <string name="from_history">З історії</string> + <string name="chat_link_from_owner">(від власника)</string> + <string name="relay_test_step_get_link">Отримати посилання</string> + <string name="get_started">Початок роботи</string> + <string name="chat_link_group">Посилання групи</string> + <string name="group_webpage">Вебсторінка групи</string> + <string name="help_and_support">Підтримка та допомога</string> + <string name="recent_history_is_not_sent_to_new_members_channel">Історію не буде надіслано новим підписникам.</string> + <string name="web_page_url_placeholder">https://</string> + <string name="close_behavior_dialog_text">Якщо ви оберете «Закрити», повідомлення не будуть отримані.\nВи зможете змінити це згодом у Налаштуваннях зовнішнього вигляду.</string> + <string name="down_migration_warning_chat_relays">Якщо ви доєднувалися до каналів або створювали канали, вони остаточно припинять працювати.</string> + <string name="content_filter_images">Зображення</string> + <string name="relay_status_inactive">неактивний</string> + <string name="invalid_relay_address">Невірна адреса реле!</string> + <string name="invalid_relay_name">Невірна назва реле!</string> + <string name="relay_status_invited">запрошено</string> + <string name="invite_someone_privately">Запросіть когось приватно</string> + <string name="webpage_url_footer">Воно буде показано підписникам і використано для надання дозволу на завантаження попереднього перегляду.</string> + <string name="compose_view_join_channel">Приєднатися до каналу</string> + <string name="button_leave_channel">Залишити канал</string> + <string name="leave_channel_question">Залишити канал?</string> + <string name="let_someone_connect_to_you">Дозволити іншим зв’язуватися з вами</string> + <string name="action_button_channel_link">Посилання</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index 235158585d..c285722200 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d tin nhắn không thể giải mã.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Không thể giải mã %1$d tin nhắn.</string> <string name="moderated_items_description">%1$d tin nhắn đã bị xóa bởi %2$s</string> - <string name="group_info_section_title_num_members">%1$s THÀNH VIÊN</string> + <string name="group_info_section_title_num_members">%1$s thành viên</string> <string name="learn_more_about_address">Thông tin về địa chỉ SimpleX</string> <string name="address_section_title">Địa chỉ</string> <string name="abort_switch_receiving_address_confirm">Hủy bỏ</string> @@ -18,7 +18,7 @@ <string name="accept">Chấp nhận</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d tin nhắn đã bị bỏ qua.</string> <string name="users_add">Thêm hồ sơ</string> - <string name="integrity_msg_skipped">%1$d tin nhắn bị bỏ qua</string> + <string name="integrity_msg_skipped">%1$d tin nhắn đã bị bỏ qua</string> <string name="send_disappearing_message_1_minute">1 phút</string> <string name="send_disappearing_message_5_minutes">5 phút</string> <string name="accept_feature">Chấp nhận</string> @@ -50,7 +50,7 @@ <string name="turn_off_battery_optimization_button">Cho phép</string> <string name="color_primary">Màu sơ cấp</string> <string name="network_enable_socks_info">Truy cập các máy chủ thông qua SOCKS proxy tại cổng %d? Proxy phải được khởi động trước khi bật cài đặt này.</string> - <string name="add_address_to_your_profile">Thêm địa chỉ vào hồ sơ để các liên hệ của bạn có thể dễ dàng chia sẻ với mọi người. Bản cập nhật hồ sơ cũng sẽ được gửi tới các liên hệ hiện thời.</string> + <string name="add_address_to_your_profile">Thêm địa chỉ vào hồ sơ của bạn để các liên hệ SimpleX có thể chia sẻ địa chỉ đó với người khác. Thông tin hồ sơ đã cập nhật sẽ được gửi đến các liên hệ SimpleX của bạn.</string> <string name="allow_disappearing_messages_only_if">Cho phép nhắn tin nhắn tự xóa chỉ khi liên hệ của bạn cũng cho phép</string> <string name="allow_verb">Cho phép</string> <string name="above_then_preposition_continuation">theo như ở trên, thì:</string> @@ -93,14 +93,14 @@ <string name="notifications_mode_off_desc">Ứng dụng chỉ có thể nhận thông báo khi nó đang chạy, không có dịch vụ nền nào được khởi động</string> <string name="app_version_code">Bản dựng ứng dụng: %s</string> <string name="appearance_settings">Giao diện</string> - <string name="settings_section_title_app">ỨNG DỤNG</string> + <string name="settings_section_title_app">Ứng dụng</string> <string name="v5_6_app_data_migration">Di chuyển dữ liệu ứng dụng</string> <string name="full_backup">Sao lưu dữ liệu ứng dụng</string> <string name="app_passcode_replaced_with_self_destruct">Mã truy cập ứng dụng đã được thay thế bằng mã tự hủy.</string> <string name="v5_3_encrypt_local_files_descr">Ứng dụng mã hóa các tệp cục bộ mới (trừ video).</string> <string name="migrate_to_device_apply_onion">Áp dụng</string> <string name="la_app_passcode">Mã truy cập ứng dụng</string> - <string name="settings_section_title_icon">BIỂU TƯỢNG ỨNG DỤNG</string> + <string name="settings_section_title_icon">Biểu tượng ứng dụng</string> <string name="v5_0_app_passcode">Mã truy cập</string> <string name="app_version_name">Phiên bản ứng dụng: v%s</string> <string name="app_version_title">Phiên bản ứng dụng</string> @@ -184,7 +184,7 @@ <string name="icon_descr_call_ended">Cuộc gọi kết thúc</string> <string name="callstatus_ended">cuộc gọi kết thúc %1$s</string> <string name="callstatus_error">lỗi cuộc gọi</string> - <string name="settings_section_title_calls">CUỘC GỌI</string> + <string name="settings_section_title_calls">Cuộc gọi</string> <string name="icon_descr_cancel_image_preview">Hủy xem trước ảnh</string> <string name="icon_descr_cancel_file_preview">Hủy xem trước tệp</string> <string name="cancel_verb">Hủy</string> @@ -234,11 +234,11 @@ <string name="snd_conn_event_switch_queue_phase_changing_for_member">đang thay đổi địa chỉ cho %s…</string> <string name="chat_preferences">Tùy chọn trò chuyện</string> <string name="settings_section_title_chat_colors">Màu trò chuyện</string> - <string name="chat_database_section">CƠ SỞ DỮ LIỆU TRÒ CHUYỆN</string> + <string name="chat_database_section">Cơ sở dữ liệu trò chuyện</string> <string name="chat_is_stopped">Kết nối trò chuyện đã được dừng lại</string> <string name="migrate_to_device_chat_migrated">Cơ sở dữ liệu đã được di chuyển!</string> <string name="your_chats">Các cuộc trò chuyện</string> - <string name="settings_section_title_chats">CÁC CUỘC TRÒ CHUYỆN</string> + <string name="settings_section_title_chats">Các cuộc trò chuyện</string> <string name="notifications_mode_periodic_desc">Kiểm tra tin nhắn mới mỗi 10 phút trong tối đa 1 phút</string> <string name="v4_6_chinese_spanish_interface">Giao diện Trung Quốc và Tây Ban Nha</string> <string name="chat_with_developers">Trò chuyện với nhà phát triển</string> @@ -322,7 +322,7 @@ <string name="connection_local_display_name">kết nối %1$d</string> <string name="display_name_connection_established">kết nối đã được tạo lập</string> <string name="connect_with_contact_name_question">Kết nối với %1$s?</string> - <string name="connection_error_auth">Lỗi kết nối (AUTH)</string> + <string name="connection_error_auth">Lỗi kết nối</string> <string name="network_session_mode_entity">Kết nối</string> <string name="callstatus_connecting">đang kết nối cuộc gọi…</string> <string name="connect_via_contact_link">Kết nối qua địa chỉ liên lạc?</string> @@ -463,7 +463,7 @@ <string name="smp_servers_delete_server">Xóa máy chủ</string> <string name="smp_server_test_delete_queue">Xóa hàng đợi</string> <string name="settings_developer_tools">Công cụ nhà phát triển</string> - <string name="settings_section_title_device">THIẾT BỊ</string> + <string name="settings_section_title_device">Thiết bị</string> <string name="developer_options_section">Tùy chọn cho nhà phát triển</string> <string name="auth_device_authentication_is_disabled_turning_off">Xác thực thiết bị đã bị vô hiệu hóa. Tắt Khóa SimpleX.</string> <string name="snd_error_relay">Lỗi máy chủ đích: %1$s</string> @@ -738,7 +738,7 @@ <string name="error_setting_network_config">Lỗi cập nhật cấu hình mạng</string> <string name="error_updating_user_privacy">Lỗi cập nhật quyền riêng tư người dùng</string> <string name="icon_descr_expand_role">Mở rộng chọn chức vụ</string> - <string name="settings_section_title_experimenta">THỬ NGHIỆM</string> + <string name="settings_section_title_experimenta">Thử nghiệm</string> <string name="expand_verb">Mở rộng</string> <string name="exit_without_saving">Thoát mà không lưu</string> <string name="expired_label">đã hết hạn</string> @@ -748,7 +748,7 @@ <string name="export_database">Xuất cơ sở dữ liệu</string> <string name="migrate_from_device_error_uploading_archive">Lỗi tải lên kho lưu trữ</string> <string name="migrate_from_device_exported_file_doesnt_exist">Tập tin đã xuất không tồn tại</string> - <string name="settings_section_title_files">TẬP TIN</string> + <string name="settings_section_title_files">Tập tin</string> <string name="failed_to_parse_chats_title">Không thể tải các cuộc trò chuyện</string> <string name="file_error_no_file">Không tìm thấy tệp - có thể tập tin đã bị xóa và hủy bỏ.</string> <string name="file_error">Lỗi tệp</string> @@ -776,7 +776,7 @@ <string name="file_will_be_received_when_contact_is_online">Tệp sẽ được nhận khi liên hệ của bạn hoạt động, vui lòng chờ hoặc kiểm tra lại sau!</string> <string name="share_text_file_status">Trạng thái tệp: %s</string> <string name="wallpaper_scale_fill">Lấp đầy</string> - <string name="settings_section_title_chat_database">CƠ SỞ DỮ LIỆU TRÒ CHUYỆN</string> + <string name="settings_section_title_chat_database">Cơ sở dữ liệu trò chuyện</string> <string name="switching_profile_error_title">Lỗi chuyển đổi hồ sơ</string> <string name="v5_2_favourites_filter_descr">Lọc các cuộc hội thoại chưa đọc và các cuộc hội thoại yêu thích.</string> <string name="v5_1_message_reactions_descr">Cuối cùng, chúng ta đã có chúng! 🚀</string> @@ -805,23 +805,22 @@ <string name="network_proxy_incorrect_config_title">Lỗi lưu proxy</string> <string name="icon_descr_flip_camera">Đổi máy ảnh</string> <string name="appearance_font_size">Kích thước font</string> - <string name="n_other_file_errors">%1$d lỗi tệp khác.</string> + <string name="n_other_file_errors">%1$d tệp bị lỗi khác.</string> <string name="error_forwarding_messages">Lỗi chuyển tiếp tin nhắn</string> <string name="forward_files_failed_to_receive_desc">%1$d tệp tải không thành công.</string> <string name="forward_files_missing_desc">%1$d tệp đã bị xóa.</string> - <string name="forward_files_not_accepted_desc">%1$d tệp đã không được tải xuống.</string> + <string name="forward_files_not_accepted_desc">%1$d tệp không được tải xuống.</string> <string name="forward_files_not_accepted_receive_files">Tải xuống</string> <string name="forward_alert_title_messages_to_forward">Chuyển tiếp %1$s tin nhắn?</string> <string name="forward_multiple">Chuyển tiếp tin nhắn…</string> - <string name="n_file_errors">%1$d lỗi tệp: -\n%2$s</string> + <string name="n_file_errors">%1$d tệp bị lỗi:\n%2$s</string> <string name="forward_files_in_progress_desc">%1$d tệp đang được tải xuống.</string> <string name="forward_files_messages_deleted_after_selection_title">%1$s tin nhắn không được chuyển tiếp</string> <string name="compose_forward_messages_n">Đang chuyển tiếp %1$s tin nhắn</string> <string name="proxy_destination_error_failed_to_connect">Máy chủ chuyển tiếp %1$s không thể kết nối tới máy chủ đích %2$s. Vui lòng thử lại sau.</string> <string name="smp_proxy_error_broker_host">Địa chỉ máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string> <string name="smp_proxy_error_broker_version">Phiên bản máy chủ chuyển tiếp không tương thích với cài đặt mạng: %1$s.</string> - <string name="section_title_for_console">CHO CONSOLE</string> + <string name="section_title_for_console">Cho console</string> <string name="forward_message">Chuyển tiếp tin nhắn…</string> <string name="v4_6_reduced_battery_usage">Giảm thiểu sử dụng pin hơn nữa</string> <string name="forward_alert_forward_messages_without_files">Chuyển tiếp tin nhắn mà không có tệp?</string> @@ -863,7 +862,7 @@ <string name="email_invite_body">Xin chào! \nKết nối với tôi qua SimpleX Chat: %s</string> <string name="hide_profile">Ẩn hồ sơ</string> - <string name="settings_section_title_help">TRỢ GIÚP</string> + <string name="settings_section_title_help">Trợ giúp</string> <string name="delete_group_for_all_members_cannot_undo_warning">Nhóm sẽ bị xóa cho tất cả các thành viên - điều này không thể hoàn tác!</string> <string name="delete_group_for_self_cannot_undo_warning">Nhóm sẽ bị xóa cho bạn - điều này không thể hoàn tác!</string> <string name="group_preferences">Tùy chọn nhóm</string> @@ -952,7 +951,7 @@ <string name="app_check_for_updates_button_install">Cài đặt cập nhật</string> <string name="incoming_video_call">Cuộc gọi video đến</string> <string name="desktop_incompatible_version">Phiên bản không tương thích</string> - <string name="theme_colors_section_title">MÀU SẮC GIAO DIỆN</string> + <string name="theme_colors_section_title">Màu sắc giao diện</string> <string name="group_member_status_invited">đã được mời</string> <string name="error_parsing_uri_title">Đường dẫn không hợp lệ</string> <string name="invalid_chat">cuộc trò chuyện không hợp lệ</string> @@ -1001,7 +1000,7 @@ <string name="button_add_members">Mời thành viên</string> <string name="invite_to_group_button">Mời vào nhóm</string> <string name="button_leave_group">Rời nhóm</string> - <string name="member_info_section_title_member">THÀNH VIÊN</string> + <string name="member_info_section_title_member">Thành viên</string> <string name="message_queue_info">Thông tin hàng đợi tin nhắn</string> <string name="users_delete_data_only">Chỉ dữ liệu hồ sơ cục bộ</string> <string name="v5_2_fix_encryption">Giữ lại các kết nối của bạn</string> @@ -1050,7 +1049,7 @@ <string name="member_will_be_removed_from_group_cannot_be_undone">Thành viên sẽ bị xóa khỏi nhóm - việc này không thể được hoàn tác!</string> <string name="chat_theme_apply_to_light_mode">Chế độ sáng</string> <string name="v5_7_new_interface_languages">UI tiếng Litva</string> - <string name="settings_section_title_messages">TIN NHẮN VÀ TỆP</string> + <string name="settings_section_title_messages">Tin nhắn và tệp</string> <string name="message_deletion_prohibited_in_chat">Việc xóa tin nhắn mà không thể phục hồi là bị cấm.</string> <string name="v5_5_join_group_conversation">Tham gia vào các cuộc trò chuyện nhóm</string> <string name="update_network_smp_proxy_mode_question">Chế độ định tuyến tin nhắn</string> @@ -1323,7 +1322,7 @@ <string name="profile_password">Mật khẩu hồ sơ</string> <string name="note_folder_local_display_name">Ghi chú riêng tư</string> <string name="prohibit_message_deletion">Cấm xóa tin nhắn mà không thể phục hồi.</string> - <string name="settings_section_title_private_message_routing">ĐỊNH TUYẾN TIN NHẮN RIÊNG TƯ</string> + <string name="settings_section_title_private_message_routing">Định tuyến tin nhắn riêng tư</string> <string name="display_name__field">Tên hồ sơ:</string> <string name="image_descr_profile_image">ảnh đại diện</string> <string name="users_delete_with_connections">Hồ sơ và các kết nối máy chủ</string> @@ -1581,7 +1580,7 @@ <string name="servers_info_reset_stats_alert_title">Đặt lại tất cả số liệu thống kê?</string> <string name="save_passphrase_and_open_chat">Lưu mật khẩu và mở kết nối trò chuyện</string> <string name="send_verb">Gửi</string> - <string name="run_chat_section">KHỞI CHẠY KẾT NỐI TRÒ CHUYỆN</string> + <string name="run_chat_section">Khởi chạy kết nối trò chuyện</string> <string name="save_verb">Lưu</string> <string name="scan_paste_link">Quét / Dán đường dẫn</string> <string name="smp_servers_scan_qr">Quét mã QR máy chủ</string> @@ -1623,7 +1622,7 @@ <string name="save_welcome_message_question">Lưu lời chào?</string> <string name="icon_descr_sent_msg_status_send_failed">gửi thất bại</string> <string name="scan_code_from_contacts_app">Quét mã bảo mật từ ứng dụng của liên hệ bạn.</string> - <string name="settings_section_title_delivery_receipts">GỬI CHỈ BÁO ĐÃ NHẬN TỚI</string> + <string name="settings_section_title_delivery_receipts">Gửi chỉ báo đã nhận tới</string> <string name="search_verb">Tìm kiếm</string> <string name="search_or_paste_simplex_link">Tìm kiếm hoặc dán đường dẫn SimpleX</string> <string name="save_list">Lưu danh sách</string> @@ -1685,7 +1684,7 @@ <string name="profile_update_event_set_new_picture">đặt ảnh đại diện mới</string> <string name="v4_4_disappearing_messages_desc">Các tin nhắn đã gửi sẽ bị xóa sau thời gian đã cài.</string> <string name="message_queue_info_server_info">thông tin hàng đợi máy chủ: %1$s\n\ntin nhắn được nhận cuối cùng: %2$s</string> - <string name="settings_section_title_settings">CÀI ĐẶT</string> + <string name="settings_section_title_settings">Cài đặt</string> <string name="info_row_sent_at">Đã gửi vào</string> <string name="server_address">Địa chỉ máy chủ</string> <string name="session_code">Mã phiên</string> @@ -1716,7 +1715,7 @@ <string name="set_passphrase">Đặt mật khẩu</string> <string name="toolbar_settings">Cài đặt</string> <string name="network_error_broker_host_desc">Địa chỉ máy chủ không tương thích với cài đặt mạng: %1$s.</string> - <string name="conn_stats_section_title_servers">CÁC MÁY CHỦ</string> + <string name="conn_stats_section_title_servers">Các máy chủ</string> <string name="error_xftp_test_server_auth">Máy chủ yêu cầu xác thực để tải lên, kiểm tra mật khẩu</string> <string name="network_session_mode_server">Máy chủ</string> <string name="set_passcode">Đặt mã truy cập</string> @@ -1828,7 +1827,7 @@ <string name="icon_descr_speaker_on">Loa ngoài bật</string> <string name="icon_descr_sound_muted">Âm thanh đã bị tắt</string> <string name="app_check_for_updates_stable">Ổn định</string> - <string name="settings_section_title_socks">PROXY SOCKS</string> + <string name="settings_section_title_socks">Proxy SOCKS</string> <string name="receipts_section_groups">Các nhóm nhỏ (tối đa 20 thành viên)</string> <string name="non_fatal_errors_occured_during_import">Một vài lỗi không nghiêm trọng đã xảy ra trong lúc nhập:</string> <string name="icon_descr_speaker_off">Loa ngoài tắt</string> @@ -1896,7 +1895,7 @@ <string name="v4_6_chinese_spanish_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> <string name="v5_0_polish_interface_descr">Xin gửi lời cảm ơn tới các người dùng đã góp công qua Weblate!</string> <string name="system_mode_toast">Chế độ hệ thống</string> - <string name="settings_section_title_support">HỖ TRỢ SIMPLEX CHAT</string> + <string name="settings_section_title_support">Hỗ trợ SimpleX Chat</string> <string name="temporary_file_error">Lỗi tệp tạm thời</string> <string name="chat_help_tap_button">Nhấn nút</string> <string name="network_option_tcp_connection">Kết nối TCP</string> @@ -1947,7 +1946,7 @@ <string name="alert_text_msg_bad_id">ID của tin nhắn tiếp theo là không chính xác (nhỏ hơn hoặc bằng với cái trước).\nViệc này có thể xảy ra do một vài lỗi hoặc khi kết nối bị xâm phạm.</string> <string name="database_backup_can_be_restored">Nỗ lực đổi mật khẩu cơ sở dữ liệu đã không được hoàn thành.</string> <string name="this_device_name_shared_with_mobile">Tên thiết bị sẽ được chia sẻ với thiết bị di động đã được kết nối.</string> - <string name="settings_section_title_themes">CÁC CHỦ ĐỀ</string> + <string name="settings_section_title_themes">Các chủ đề</string> <string name="failed_to_create_user_invalid_desc">Tên hiển thị này không hợp lệ. Xin vui lòng chọn một cái tên khác.</string> <string name="profile_is_only_shared_with_your_contacts">Hồ sơ chỉ được chia sẻ với các liên hệ của bạn.</string> <string name="e2ee_info_pq_short">Cuộc trò chuyện này được bảo vệ bằng mã hóa đầu cuối có kháng lượng tử.</string> @@ -2196,7 +2195,7 @@ <string name="one_hand_ui_change_instruction">Bạn có thể thay đổi nói trong cài đặt Giao diện.</string> <string name="connect_plan_you_are_already_joining_the_group_via_this_link">Bạn đang tham gia nhóm thông qua đường dẫn này.</string> <string name="you_can_enable_delivery_receipts_later">Bạn có thể bật vào lúc sau thông qua Cài đặt</string> - <string name="settings_section_title_you">BẠN</string> + <string name="settings_section_title_you">Bạn</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Bạn có thể chia sẻ một đường dẫn hoặc mã QR - bất kỳ ai cũng sẽ có thể tham gia nhóm. Bạn sẽ không mất các thành viên của nhóm nếu sau này bạn xóa nó đi.</string> <string name="migrate_to_device_try_again">Bạn có thể thử một lần nữa.</string> <string name="connected_to_server_to_receive_messages_from_contact">Bạn đã kết nối tới máy chủ dùng để nhận tin nhắn từ liên hệ này.</string> @@ -2407,4 +2406,123 @@ <string name="cant_send_message_alert_title">Bạn không thể gửi tin nhắn!</string> <string name="report_sent_alert_msg_view_in_support_chat">Bạn có thể xem các báo cáo của mình trong Cuộc trò chuyện với các quản trị viên.</string> <string name="cant_send_message_you_left">bạn đã rời đi</string> + <string name="advanced_options">Tuỳ chọn nâng cao</string> + <string name="content_filter_all_messages">Tất cả tin nhắn</string> + <string name="allow_anyone_to_embed">Cho phép bất kỳ ai nhúng</string> + <string name="allow_chat_with_admins_channel">Cho phép người đăng kí trò chuyện với quản trị viên</string> + <string name="allow_your_contacts_to_send_files_and_media">Cho phép liên hệ của bạn gửi tệp và phương tiện</string> + <string name="relay_bar_all_relays_failed">Tất cả các lượt chuyển tiếp thất bại</string> + <string name="app_update_required">Ứng dụng cần được cập nhật</string> + <string name="short_descr__field">Tiểu sử:</string> + <string name="bio_too_large">Tiểu sử quá dài</string> + <string name="block_subscriber_for_all_question">Chặn người đăng kí?</string> + <string name="chat_link_business_address">Địa chỉ doanh nghiệp</string> + <string name="cancel_creating_channel_question">Huỷ tạo kênh?</string> + <string name="cant_broadcast_message">không thể phát sóng</string> + <string name="context_user_picker_cant_change_profile_alert_title">Không thể thay đổi hồ sơ</string> + <string name="v6_4_1_new_interface_languages_descr">Tiếng Catalan, tiếng Indonesia, tiếng Romania và tiếng Việt - nhờ những người dùng của chúng tôi</string> + <string name="channel_role_label">kênh</string> + <string name="chat_banner_channel">Kênh</string> + <string name="info_row_channel">Kênh</string> + <string name="channel_full_name_field">Tên kênh đầy đủ:</string> + <string name="channel_temporarily_unavailable">Kênh tạm thời không khả dụng</string> + <string name="delete_channel_for_self_cannot_undo_warning">Kênh sẽ bị xoá ở phía bạn - hành động này không thể hoàn tác!</string> + <string name="chat_with_admins_is_prohibited">Các cuộc trò chuyện với quản trị viên bị cấm.</string> + <string name="v6_4_support_chat">Trò chuyện với các quản trị viên</string> + <string name="chat_with_admins">Trò chuyện với các quản trị viên</string> + <string name="close_behavior_dialog_close">Đóng ứng dụng</string> + <string name="compose_view_connect">Kết nối</string> + <string name="relay_test_step_connect">Kết nối</string> + <string name="relay_conn_status_connected">đã kết nối</string> + <string name="v6_4_connect_faster">Kết nối nhanh hơn 🚀</string> + <string name="relay_conn_status_connecting">đang kết nối</string> + <string name="channel_name_requires_newer_app_version">Kết nối bằng tên kênh yêu cầu phiên bản ứng dụng mới hơn</string> + <string name="contact_name_requires_newer_app_version">Kết nối bằng tên liên hệ yêu cầu phiên bản ứng dụng mới hơn.</string> + <string name="info_row_connection_failed">Kết nối thất bại</string> + <string name="connect_via_link_or_qr_code">Kết nối bằng đường dẫn hoặc mã QR</string> + <string name="settings_section_title_contact">Liên hệ</string> + <string name="chat_link_contact_address">Địa chỉ liên hệ</string> + <string name="relay_test_step_decode_link">Giải mã liên kết</string> + <string name="button_delete_channel">Xoá kênh</string> + <string name="delete_channel_question">Xoá kênh?</string> + <string name="relay_conn_status_deleted">đã xoá</string> + <string name="rcv_channel_event_channel_deleted">kênh đã xoá</string> + <string name="button_delete_member_messages">Xoá các tin nhắn của thành viên</string> + <string name="button_delete_member_messages_question">Xoá các tin nhắn của thành viên?</string> + <string name="delete_member_messages_confirmation">Xoá các tin nhắn</string> + <string name="button_edit_channel_profile">Sửa hồ sơ kênh</string> + <string name="link_previews_alert_enable">Cho phép</string> + <string name="enable_chats_with_admins">Cho phép</string> + <string name="enter_profile_name">Nhập tên hồ sơ…</string> + <string name="error_prefix">Lỗi</string> + <string name="error_changing_user">Lỗi khi thay đổi hồ sơ</string> + <string name="error_creating_channel">Lỗi tạo kênh</string> + <string name="error_deleting_message">Lỗi khi xoá tin nhắn</string> + <string name="error_marking_member_support_chat_read">Lỗi khi đánh dấu \"đã xem\"</string> + <string name="error_opening_channel">Lỗi mở kênh</string> + <string name="error_preparing_contact">Lỗi khi mở đoạn chat</string> + <string name="error_preparing_group">Lỗi khi mở nhóm</string> + <string name="error_rejecting_contact_request">Lỗi khi từ chối lời mời kết nối</string> + <string name="from_history">Từ lịch sử</string> + <string name="get_started">Bắt đầu</string> + <string name="chat_banner_group">Nhóm</string> + <string name="chat_link_group">Đường dẫn nhóm</string> + <string name="close_behavior_dialog_text">Nếu bạn chọn Đóng, tin nhắn sẽ không được nhận.\nBạn có thể thay đổi sau trong Cài đặt giao diện.</string> + <string name="relay_status_inactive">không hoạt động</string> + <string name="relay_status_invited">đã được mời</string> + <string name="compose_view_join_channel">Tham gia kênh</string> + <string name="compose_view_join_group">Tham gia nhóm</string> + <string name="let_someone_connect_to_you">Để ai đó kết nối với bạn</string> + <string name="action_button_channel_link">Đường dẫn</string> + <string name="loading_profile">Đang tải hồ sơ…</string> + <string name="settings_section_title_about">Giới thiệu</string> + <string name="accept_contact_request">Đồng ý yêu cầu kết nối</string> + <string name="chat_banner_accept_contact_request">Đồng ý yêu cầu kết nối</string> + <string name="relay_status_accepted">đã chấp nhận</string> + <string name="relay_status_acknowledged_roster">danh sách đã xác nhận</string> + <string name="relay_status_active">đang hoạt động</string> + <string name="add_button">Thêm mới</string> + <string name="compose_view_add_message">Thêm tin nhắn</string> + <string name="add_relay_button">Thêm thiết bị</string> + <string name="add_relays_title">Thêm mới thiết bị</string> + <string name="relay_bar_owner_no_delivery">Thêm thiết bị để khôi phục việc gửi tin nhắn</string> + <string name="relay_bar_active">%1$d/%2$d thiết bị hoạt động</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d thiết bị hoạt động, %3$d lỗi</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d thiết bị hoạt động, %3$d thất bại</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d thiết bị hoạt động, %3$d bị xóa</string> + <string name="relay_bar_connected">%1$d/%2$d thiết bị đã kết nối</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d thiết bị đã kết nối, %3$d lỗi</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d thiết bị đã kết nối, %3$d thất bại</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d thiết bị đã kết nối, %3$d bị xóa</string> + <string name="channel_owner_count_singular">%1$d chủ sở hữu</string> + <string name="channel_owner_count_plural">%1$d chủ sở hữu</string> + <string name="channel_owners_contributors_count">%1$d chủ sở hữu và người đóng góp</string> + <string name="relay_bar_relays_failed">%1$d thiết bị thất bại</string> + <string name="relay_bar_relays_not_active">%1$d thiết bị không hoạt động</string> + <string name="relay_bar_relays_removed">%1$d thiết bị đã bị loại bỏ</string> + <string name="channel_subscriber_count_singular">%1$d người theo dõi</string> + <string name="channel_subscriber_count_plural">%1$d người theo dõi</string> + <string name="badge_supported_simplex">%1$s đã hỗ trợ SimpleX Chat. Huy hiệu đã hết hạn vào %2$s.</string> + <string name="v6_4_1_new_interface_languages">4 ngôn ngữ giao diện mới</string> + <string name="webpage_code_footer">Hãy thêm đoạn mã này vào trang web của bạn. Nó sẽ hiển thị bản xem trước (preview) cho kênh hoặc nhóm của bạn.</string> + <string name="advanced_settings">Cài đặt nâng cao</string> + <string name="a_link_for_one_person">Một đường dẫn cho một người kết nối</string> + <string name="allow_files_and_media_only_if">Chỉ cho phép tệp và phương tiện nếu người liên hệ của bạn cho phép họ làm điều đó.</string> + <string name="allow_chat_with_admins">Cho phép thành viên trò chuyện với quản trị viên.</string> + <string name="allow_direct_messages_channel">Cho phép gửi tin nhắn trực tiếp cho người theo dõi.</string> + <string name="relay_bar_all_relays_removed">Toàn bộ thiết bị đã bị xóa</string> + <string name="another_instance_not_responding">Một phiên bản khác của ứng dụng có thể đang chạy hoặc chưa thoát đúng cách. Bạn có muốn tiếp tục không?</string> + <string name="embed_any_webpage_can_show">Bất kỳ trang web nào cũng có thể hiển thị bản xem trước.</string> + <string name="another_instance_title">Ứng dụng đang được mở</string> + <string name="badge_unknown_key_title">Không thể xác nhận huy hiệu</string> + <string name="why_built_p7">Vì chúng ta đã loại bỏ hoàn toàn khả năng nhận biết bạn là ai. Để quyền lực của bạn không bao giờ bị tước đoạt.</string> + <string name="onboarding_be_free">Hãy tự do\ntrong mạng lưới của bạn</string> + <string name="why_built_tagline">Hãy tự do trong mạng lưới của bạn.</string> + <string name="chat_banner_bot">Bot ảo</string> + <string name="both_you_and_your_contact_can_send_files">Cho phép bạn và đối tác của bạn có thể gửi tệp và hình ảnh.</string> + <string name="one_hand_ui_bottom_bar">Thanh công cụ dưới</string> + <string name="compose_view_broadcast">Phát sóng</string> + <string name="channel_no_active_relays_try_later">Kênh hiện không có máy chủ chuyển tiếp nào đang hoạt động. Vui lòng thử tham gia lại sau.</string> + <string name="member_info_section_title_subscriber">Người theo dõi</string> + <string name="group_member_role_observer_channel">người theo dõi</string> </resources> 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 0901494460..9dea7a5a0a 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 @@ -30,7 +30,7 @@ <string name="connection_error">连接错误</string> <string name="connection_timeout">连接超时</string> <string name="contact_already_exists">联系人已存在</string> - <string name="connection_error_auth">连接错误(AUTH)</string> + <string name="connection_error_auth">连接链接已删除</string> <string name="answer_call">接听来电</string> <string name="delete_chat_profile_question">删除聊天资料?</string> <string name="delete_files_and_media_all">删除所有文件</string> @@ -149,7 +149,7 @@ <string name="rcv_group_event_changed_member_role">将 %s 的角色更改为 %s</string> <string name="rcv_group_event_changed_your_role">将你的角色更改为 %s</string> <string name="change_role">改变角色</string> - <string name="change_member_role_question">更改群角色?</string> + <string name="change_member_role_question">更改角色?</string> <string name="icon_descr_cancel_link_preview">取消链接预览</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">正在为 %s 更改地址……</string> <string name="rcv_conn_event_switch_queue_phase_changing">更改地址中……</string> @@ -568,7 +568,7 @@ <string name="your_chat_profiles">你的聊天资料</string> <string name="icon_descr_call_missed">未接来电</string> <string name="icon_descr_call_pending_sent">待定来电</string> - <string name="connection_error_auth_desc">除非你的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。 \n如果要连接,请让你的联系人创建另一个连接链接,并检查你的网络连接是否稳定。</string> + <string name="connection_error_auth_desc">你的联系人删除了此链接,或者其为已使用的一次性链接。\n要进行连接,请你的联系人创建新链接。</string> <string name="you_are_already_connected_to_vName_via_this_link">你已经连接到 %1$s。</string> <string name="your_chat_profile_will_be_sent_to_your_contact">你的聊天资料将被发送 \n给你的联系人</string> <string name="users_delete_with_connections">资料和服务器连接</string> @@ -702,7 +702,7 @@ <string name="reject_contact_button">拒绝</string> <string name="reply_verb">回复</string> <string name="network_options_reset_to_defaults">重置为默认</string> - <string name="run_chat_section">运行聊天程序</string> + <string name="run_chat_section">运行聊天</string> <string name="scan_code">扫码</string> <string name="scan_code_from_contacts_app">从你联系人的应用程序中扫描安全码。</string> <string name="security_code">安全码</string> @@ -754,7 +754,7 @@ <string name="next_generation_of_private_messaging">下一代私密通讯软件</string> <string name="paste_the_link_you_received">粘贴你收到的链接</string> <string name="alert_title_skipped_messages">已跳过消息</string> - <string name="settings_section_title_support">支持 SIMPLEX CHAT</string> + <string name="settings_section_title_support">支持 SimpleX Chat</string> <string name="send_link_previews">发送链接预览</string> <string name="settings_section_title_socks">SOCKS 代理</string> <string name="stop_chat_question">停止聊天程序?</string> @@ -785,7 +785,7 @@ <string name="simplex_link_invitation">SimpleX 一次性邀请</string> <string name="simplex_link_group">SimpleX 群链接</string> <string name="simplex_link_mode">SimpleX 链接</string> - <string name="sender_may_have_deleted_the_connection_request">发送人可能已删除连接请求。</string> + <string name="sender_may_have_deleted_the_connection_request">发送人已删除连接请求。</string> <string name="smp_servers_preset_server">预设服务器</string> <string name="image_descr_qr_code">二维码</string> <string name="network_session_mode_transport_isolation">传输隔离</string> @@ -1179,7 +1179,7 @@ \n用 SimpleX Chat 与我联系:%s</string> <string name="email_invite_subject">让我们一起在 SimpleX Chat 里聊天</string> <string name="you_can_create_it_later">你可以以后创建它</string> - <string name="share_address">分享地址</string> + <string name="share_address">分享地址…</string> <string name="you_can_share_this_address_with_your_contacts">你可以与你的联系人分享该地址,让他们与 %s 联系。</string> <string name="group_welcome_preview">预览</string> <string name="import_theme">导入主题</string> @@ -1298,7 +1298,7 @@ <string name="v5_2_disappear_one_message_descr">即使在对话中禁用。</string> <string name="use_random_passphrase">使用随机密码</string> <string name="system_restricted_background_in_call_title">无后台通话</string> - <string name="you_can_enable_delivery_receipts_later_alert">你可以稍后通过应用程序隐私和安全设置启用它们。</string> + <string name="you_can_enable_delivery_receipts_later_alert">你可以稍后通过应用程序的你的隐私设置启用它们。</string> <string name="save_passphrase_in_settings">在设置中保存密码</string> <string name="enable_receipts_all">启用</string> <string name="send_receipts_disabled_alert_msg">该群成员超过 %1$d ,未发送送达回执。</string> @@ -2213,7 +2213,7 @@ <string name="button_add_team_members">添加团队成员</string> <string name="delete_chat_for_all_members_cannot_undo_warning">将为所有成员删除聊天 —— 此操作无法撤销!</string> <string name="only_chat_owners_can_change_prefs">仅聊天所有人可更改首选项。</string> - <string name="member_role_will_be_changed_with_notification_chat">角色将被更改为 %s。聊天中的每个人都会收到通知。</string> + <string name="member_role_will_be_changed_with_notification_chat">角色将被更改为 "%s"。聊天中的每个人都会收到通知。</string> <string name="direct_messages_are_prohibited">成员之间的私信被禁止。</string> <string name="direct_messages_are_prohibited_in_chat">此聊天禁止成员之间的私信。</string> <string name="v6_2_business_chats">企业聊天</string> @@ -2579,7 +2579,6 @@ <string name="relay_conn_status_connecting">正在连接</string> <string name="create_channel_title">创建公开频道</string> <string name="create_channel_button">创建公开频道</string> - <string name="create_channel_beta_button">创建公开频道(测试版)</string> <string name="creating_channel">正在创建频道</string> <string name="relay_test_step_decode_link">解码链接</string> <string name="button_delete_channel">删除频道</string> @@ -2610,7 +2609,7 @@ <string name="connect_plan_open_channel">打开频道</string> <string name="connect_plan_open_new_channel">打开新频道</string> <string name="member_info_section_title_owner">所有者</string> - <string name="channel_members_section_owners">所有者</string> + <string name="channel_members_section_owners">所有者和贡献者</string> <string name="preset_relay_address">预设中继地址</string> <string name="preset_relay_name">预设中继名</string> <string name="group_member_role_relay">中继</string> @@ -2691,7 +2690,6 @@ <string name="share_channel">分享频道…</string> <string name="share_via_chat">经聊天分享</string> <string name="owner_verification_failed">⚠️ 签名验证失败:%s。</string> - <string name="chat_link_signed">(已签名)</string> <string name="tap_to_open">轻触打开</string> <string name="link_previews_alert_desc">发送链接预览可能会将你的 IP 地址暴露给网站。你可以稍后在“隐私”设置中更改此设置。</string> <string name="connection_reached_limit_of_undelivered_messages">连接达到了未送达消息的上限</string> @@ -2803,10 +2801,10 @@ <string name="button_remove_relay_question">删除中继?</string> <string name="select_relays">选择中继</string> <string name="close_behavior_dialog_text">如果选择关闭将不会接收消息。\n可以之后在外观设置中更改。</string> - <string name="appearance_minimize_to_tray_desc">保持 SimpleX 在后台运行以接收消息。</string> + <string name="appearance_minimize_to_tray_desc">在后台运行以接收消息</string> <string name="close_behavior_dialog_minimize">最小化到托盘</string> <string name="close_behavior_dialog_title">最小化到托盘?</string> - <string name="appearance_minimize_to_tray">关闭窗口时最小化到托盘</string> + <string name="appearance_minimize_to_tray">关闭窗口到托盘</string> <string name="tray_quit">退出 SimpleX</string> <string name="tray_show">显示 SimpleX</string> <string name="tray_tooltip">SimpleX</string> @@ -2819,4 +2817,104 @@ <string name="relay_status_rejected">被拒绝</string> <string name="member_info_relay_status_rejected_by_operator">被中继运营方拒绝</string> <string name="member_info_status">状态</string> + <string name="webpage_code_footer">将此代码添加到您的网页。他会展示您频道/群组的预览。</string> + <string name="badge_unknown_key_desc">此徽章用了这个版本的 SimpleX Chat 无法识别的密钥进行签名。更新 SimpleX Chat 以验证此徽章。</string> + <string name="webpage_info">创建网页在访客订阅前向其展示你的频道预览。自行托管或使用任何静态托管服务。</string> + <string name="channel_owner_count_singular">%1$d 名所有者</string> + <string name="channel_owner_count_plural">%1$d 名所有者</string> + <string name="channel_owners_contributors_count">%1$d 名所有者和贡献者</string> + <string name="badge_supported_simplex">%1$s 支持过 SimpleX Chat。该徽章已于 %2$s 过期。</string> + <string name="settings_section_title_about">关于</string> + <string name="badge_unknown_key_title">无法验证徽章</string> + <string name="channel_webpage">频道网页</string> + <string name="chat_data">聊天数据</string> + <string name="channel_name_requires_newer_app_version">通过频道名连接需要更新的应用版本。</string> + <string name="contact_name_requires_newer_app_version">通过联系人名称连接需要更新的应用版本。</string> + <string name="settings_section_title_contact">联系人</string> + <string name="group_member_role_member_channel">贡献者</string> + <string name="copy_code">复制代码</string> + <string name="enter_webpage_url">输入网页 URL</string> + <string name="group_webpage">群组网页</string> + <string name="help_and_support">帮助和支持</string> + <string name="web_page_url_placeholder">https://</string> + <string name="webpage_url_footer">它将展示给订阅者并用来允许加载预览。</string> + <string name="more_privacy">更多隐私</string> + <string name="embed_only_your_page">只有您上面的页面可以显示预览。</string> + <string name="please_upgrade_the_app">请升级应用</string> + <string name="badge_invested">%s 给 SimpleX Chat 众筹投过钱。</string> + <string name="badge_supports_simplex">%s 支持 SimpleX Chat。</string> + <string name="group_member_role_observer_channel">订阅者</string> + <string name="settings_section_title_support_project">支持本项目</string> + <string name="member_role_will_be_changed_with_notification_channel">该角色将更改为 "%s"。频道中的每个人都会收到通知。</string> + <string name="badge_unverified_desc">此徽章无法被验证,可能不是真的。</string> + <string name="group_link_requires_newer_version">此群组需要更新版本的应用。要加入请更新应用。</string> + <string name="unsupported_channel_name">不支持的频道名</string> + <string name="unsupported_contact_name">不支持的联系人名</string> + <string name="badge_unverified_title">未验证的徽章</string> + <string name="relays_no_web_support">所用的聊天中继不支持网页。</string> + <string name="webpage_code">网页代码</string> + <string name="badge_support_from_v7">从 v7 版本起您可以支持 SimpleX。</string> + <string name="advanced_options">高级选项</string> + <string name="advanced_settings">高级设置</string> + <string name="allow_anyone_to_embed">允许任何人嵌入</string> + <string name="embed_any_webpage_can_show">任何网页均可显示预览。</string> + <string name="app_update_required">需要更新应用</string> + <string name="simplex_name_owner_no_address">SimpleX 名称 %1$s 已注册但没有 SimpleX 地址。通过注册页添加此类地址到名称。</string> + <string name="simplex_name_owner_no_channel_link">SimpleX 名称 %1$s 已注册但没有频道链接。通过注册页添加频道链接到名称。</string> + <string name="simplex_name_unconfirmed_desc">SimpleX 名称 %1$s 已注册但未添加到资料中。如果你是所有者,请将其添加到你的地址或频道资料。</string> + <string name="simplex_name_no_servers_desc">你的服务器没有一台被设定为解析 SimpleX 名称。配置服务器或使用连接链接。</string> + <string name="simplex_name_server_no_resolver_desc">%1$s 服务器不支持名称解析。配置服务器或使用连接链接。</string> + <string name="error_saving_simplex_name">保存名称出错</string> + <string name="set_user_simplex_name_footer">让别人通过用你的 SimpleX 地址注册的名称和你建立联系。</string> + <string name="set_channel_simplex_name_footer">让别人通过用这个频道链接注册的名称加入。</string> + <string name="simplex_name_not_found">未找到名称</string> + <string name="no_names_servers_enabled">没有解析名称的服务器。</string> + <string name="simplex_name_no_valid_link">无有效链接</string> + <string name="simplex_name_resolver_error_desc">解析错误:%1$s</string> + <string name="set_simplex_name">设置 SimpleX 名称</string> + <string name="simplex_name">SimpleX 名称</string> + <string name="simplex_name_error">SimpleX 名称错误</string> + <string name="simplex_name_not_verified">SimpleX 名称未验证</string> + <string name="simplex_name_no_valid_link_desc">SimpleX 名称 %1$s 已注册但没有有效链接。</string> + <string name="simplex_name_not_found_desc">SimpleX 名称未注册。请检查该名称。</string> + <string name="operator_use_for_names">用于解析名称</string> + <string name="simplex_name_unconfirmed">未确认的名称</string> + <string name="verify_simplex_name_action">验证名称</string> + <string name="verify_simplex_names">验证 SimpleX 名称</string> + <string name="your_simplex_name">你的 SimpleX 名称</string> + <string name="connect_plan_connect_to_name">连接到 %s</string> + <string name="connect_plan_join_name">加入 %s 频道</string> + <string name="channel_simplex_name">频道的 SimpleX 名称</string> + <string name="do_not_require_message_signatures">不要求消息签名。</string> + <string name="get_simplex_name_beta">获得 SimpleX 名称 (测试)</string> + <string name="message_signatures_are_not_required">不要求消息签名。</string> + <string name="message_signatures_are_required">要求消息签名。</string> + <string name="register_test_name">如何注册测试名</string> + <string name="remove_name">删除名称</string> + <string name="require_message_signatures">要求消息签名。</string> + <string name="save_simplex_name_question">保存 SimpleX 名称?</string> + <string name="show_encryption">显示加密</string> + <string name="show_signature">显示签名</string> + <string name="signature_missing_alert_title">签名缺失</string> + <string name="info_row_signed">已签名</string> + <string name="info_row_signed_verified">已签名及验证</string> + <string name="sign_message_desc">签名证明你写了这则消息,之后无法否认。</string> + <string name="sign_message">签署消息</string> + <string name="sign_messages">签署消息</string> + <string name="signature_missing_alert_desc">频道要求签署此消息,但签名缺失。</string> + <string name="to_verify_channel_member_key">要验证此订阅者的密钥,比较(或扫描)设备上的代码。</string> + <string name="add_description">添加描述</string> + <string name="profile_description__field">描述</string> + <string name="edit_description">编辑描述</string> + <string name="enter_description_optional">输入描述(可选)</string> + <string name="error_sharing_address">分享地址出错</string> + <string name="v7_0_channels_contributors">添加贡献者。</string> + <string name="v7_0_channels">更好的频道 📢</string> + <string name="v7_0_channels_previews">创建 web 预览。</string> + <string name="v7_0_channels_wider_messages">更易阅读。</string> + <string name="v7_0_channels_relays">管理你的中继。</string> + <string name="v7_0_simplex_names_descr">你的频道或企业的公开名称。</string> + <string name="v7_0_simplex_names">SimpleX 公开名称 (测试)</string> + <string name="info_row_file_servers">文件服务器</string> + <string name="share_text_file_servers">文件服务器:%s</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index 9ec116058a..c6cf22f427 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -310,9 +310,8 @@ <string name="only_group_owners_can_enable_voice">只有群組的負責人才能啟用語音訊息。</string> <string name="send_verb">傳送</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">請你確認使用的是正確的連結,或者請你的聯絡人傳送一個新的連結給你。</string> - <string name="connection_error_auth">連接錯誤 (AUTH)</string> - <string name="connection_error_auth_desc">除非你的聯絡人刪除了連結或此連結已經被使用,否則它可能是一個錯誤 - 請報告問題。 -\n要連接,請詢問你的聯絡人建立新的連結和確保你的網路是穩定的。</string> + <string name="connection_error_auth">連接錯誤</string> + <string name="connection_error_auth_desc">除非你的聯絡人刪除了連結或此連結已經被使用,否則它可能是一個錯誤 - 請報告問題。 \n要連接,請詢問你的聯絡人建立新的連結和確保你的網路是穩定的。</string> <string name="error_accepting_contact_request">接受聯絡人的連接請求時出錯</string> <string name="error_deleting_pending_contact_connection">刪除待處理的聊絡人連接時出錯</string> <string name="error_changing_address">修改聯絡地址時出錯</string> @@ -668,7 +667,7 @@ <string name="settings_section_title_device">裝置</string> <string name="settings_section_title_help">幫助</string> <string name="settings_section_title_settings">設定</string> - <string name="settings_section_title_support">幫助 SIMPLEX CHAT</string> + <string name="settings_section_title_support">幫助 SimpleX Chat</string> <string name="settings_section_title_chats">聊天</string> <string name="settings_developer_tools">開發者工具</string> <string name="settings_section_title_socks">SOCKS 代理伺服器</string> @@ -2194,4 +2193,675 @@ <string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[你<b>不能</b>在兩部裝置上使用同一資料庫。]]></string> <string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">警告:不支援在多個裝置上同時聊天,否則會導致訊息傳送失敗</string> <string name="chat_archive">或匯入封存檔案</string> + <string name="connect_plan_open_new_group">開啟新群組</string> + <string name="another_instance_title">應用程式已在執行</string> + <string name="another_instance_not_responding">另一個應用程式執行個體可能正在執行,或未正確結束。仍要啟動嗎?</string> + <string name="not_connected_to_server_to_receive_messages_no_sub">你尚未連線至用於接收此連線訊息的伺服器(沒有訂閱)。</string> + <string name="voice_recording_not_supported">你的平台不支援語音錄製</string> + <string name="e2ee_info_e2ee"><![CDATA[訊息受到<b>端對端加密</b>保護。]]></string> + <string name="e2ee_info_no_e2ee"><![CDATA[此頻道中的訊息<b>並非端對端加密</b>。聊天中繼可以看到這些訊息。]]></string> + <string name="display_name_requested_to_connect">已請求連線</string> + <string name="simplex_link_relay">SimpleX 中繼地址</string> + <string name="for_chat_profile">聊天個人檔案 %s:</string> + <string name="errors_in_servers_configuration">伺服器設定中有錯誤。</string> + <string name="no_chat_relays_enabled">未啟用聊天中繼。</string> + <string name="server_warning">伺服器警告</string> + <string name="network_error_unknown_ca">伺服器地址中的指紋與憑證不符:%1$s。</string> + <string name="network_error_broker_host_desc">伺服器地址與網路設定不相容:%1$s。</string> + <string name="network_error_broker_version_desc">伺服器版本與你的應用程式不相容:%1$s。</string> + <string name="private_routing_timeout">私密路由逾時</string> + <string name="private_routing_no_session">沒有私密路由工作階段</string> + <string name="smp_proxy_error_unknown_ca">轉發伺服器地址中的指紋與憑證不符:%1$s。</string> + <string name="proxy_destination_error_unknown_ca">目的地伺服器地址中的指紋與憑證不符:%1$s。</string> + <string name="error_creating_report">建立檢舉報告時發生錯誤</string> + <string name="error_accepting_member">接受成員時發生錯誤</string> + <string name="error_marking_member_support_chat_read">標記為已讀時發生錯誤</string> + <string name="error_deleting_member_support_chat">刪除聊天時發生錯誤</string> + <string name="unsupported_connection_link">不支援的連線連結</string> + <string name="link_requires_newer_app_version_please_upgrade">此連結需要較新的應用程式版本。請升級應用程式,或請你的聯絡人傳送相容的連結。</string> + <string name="unsupported_channel_name">不支援的頻道名稱</string> + <string name="unsupported_contact_name">不支援的聯絡人名稱</string> + <string name="channel_name_requires_newer_app_version">透過頻道名稱連線需要較新的應用程式版本。</string> + <string name="contact_name_requires_newer_app_version">透過聯絡人名稱連線需要較新的應用程式版本。</string> + <string name="please_upgrade_the_app">請升級應用程式。</string> + <string name="channel_temporarily_unavailable">頻道暫時無法使用</string> + <string name="channel_no_active_relays_try_later">頻道沒有啟用中的中繼。請稍後再嘗試加入。</string> + <string name="app_update_required">需要更新應用程式</string> + <string name="group_link_requires_newer_version">此群組需要較新的應用程式版本。請更新應用程式以加入。</string> + <string name="connection_error_blocked_desc">連線已被伺服器營運商封鎖:\n%1$s。</string> + <string name="connection_error_quota_desc">此連線已達未送達訊息上限,你的聯絡人可能離線。</string> + <string name="error_rejecting_contact_request">拒絕聯絡請求時發生錯誤</string> + <string name="error_deleting_message">刪除訊息時發生錯誤</string> + <string name="error_updating_chat_tags">更新聊天列表時發生錯誤</string> + <string name="error_creating_chat_tags">建立聊天列表時發生錯誤</string> + <string name="error_loading_chat_tags">載入聊天列表時發生錯誤</string> + <string name="error_preparing_contact">開啟聊天時發生錯誤</string> + <string name="error_preparing_group">開啟群組時發生錯誤</string> + <string name="error_changing_user">變更個人檔案時發生錯誤</string> + <string name="system_restricted_background_warn"><![CDATA[若要啟用通知,請在應用程式設定中選擇<b>應用程式電池用量</b> / <b>不受限制</b>。]]></string> + <string name="system_restricted_background_in_call_desc">應用程式在背景執行 1 分鐘後可能會被關閉。</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[若要在背景中進行通話,請在應用程式設定中選擇<b>應用程式電池用量</b> / <b>不受限制</b>。]]></string> + <string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>Xiaomi 裝置</b>:請在系統設定中啟用自動啟動,以便通知正常運作。]]></string> + <string name="message_deleted_or_not_received_error_desc">此訊息已刪除或尚未接收。</string> + <string name="report_archive_alert_desc">該檢舉報告將為你封存。</string> + <string name="report_archive_alert_desc_all">所有檢舉報告將為你封存。</string> + <string name="snd_error_auth">金鑰錯誤或未知連線——此連線很可能已刪除。</string> + <string name="srv_error_host">伺服器地址與網路設定不相容。</string> + <string name="srv_error_version">伺服器版本與網路設定不相容。</string> + <string name="file_error_auth">金鑰錯誤或未知檔案分塊地址——檔案很可能已刪除。</string> + <string name="file_error_blocked">檔案已被伺服器營運商封鎖:\n%1$s。</string> + <string name="placeholder_search_voice_messages">搜尋語音訊息</string> + <string name="delete_messages_cannot_be_undone_warning">訊息將被刪除,此操作無法復原!</string> + <string name="moderate_messages_will_be_deleted_warning">這些訊息將為所有成員刪除。</string> + <string name="moderate_messages_will_be_marked_warning">這些訊息將對所有成員標記為已審核。</string> + <string name="from_history">來自歷史記錄</string> + <string name="talk_to_someone">與某人交談</string> + <string name="let_someone_connect_to_you">讓某人與你連線</string> + <string name="connect_via_link_or_qr_code">透過連結或二維碼連線</string> + <string name="connect_with_someone">建立你的連結</string> + <string name="invite_someone_privately">私下邀請某人</string> + <string name="a_link_for_one_person">供一人連線的連結</string> + <string name="create_your_public_address">建立你的公開地址</string> + <string name="your_public_address">你的公開地址</string> + <string name="for_anyone_to_reach_you">讓任何人都能聯絡你</string> + <string name="no_chats_in_list">列表 %s 中沒有聊天。</string> + <string name="contact_should_accept">聯絡人需要接受…</string> + <string name="address_creation_instruction">稍後可點選選單中的「建立 SimpleX 地址」來建立。</string> + <string name="forward_alert_title_nothing_to_forward">沒有可轉發的內容!</string> + <string name="forward_alert_forward_messages_without_files">要轉發不含檔案的訊息嗎?</string> + <string name="forward_files_messages_deleted_after_selection_desc">這些訊息在你選取後已被刪除。</string> + <string name="chat_list_channels">頻道</string> + <string name="group_new_support_chats">%d 個與成員的聊天</string> + <string name="group_new_support_chat_one">1 個與成員的聊天</string> + <string name="chat_banner_connect_to_chat">點選「連線」開始聊天</string> + <string name="chat_banner_send_request_to_connect">點選「連線」傳送請求</string> + <string name="chat_banner_connect_to_use_bot">點選「連線」即可使用機器人</string> + <string name="chat_banner_join_group">點選「加入群組」</string> + <string name="chat_banner_join_channel">點選「加入頻道」</string> + <string name="chat_banner_your_channel">你的頻道</string> + <string name="chat_banner_channel">頻道</string> + <string name="chat_banner_your_business_contact">你的業務聯絡人</string> + <string name="share_channel">分享頻道…</string> + <string name="cannot_share_message_alert_text">所選聊天偏好設定禁止傳送此訊息。</string> + <string name="share_via_chat">透過聊天分享</string> + <string name="tap_to_open">點選開啟</string> + <string name="chat_link_channel">頻道連結</string> + <string name="chat_link_group">群組連結</string> + <string name="chat_link_business_address">業務地址</string> + <string name="chat_link_contact_address">聯絡地址</string> + <string name="chat_link_one_time">一次性連結</string> + <string name="chat_link_from_owner">(來自擁有者)</string> + <string name="error_sharing_channel">分享頻道時發生錯誤</string> + <string name="owner_verification_passed">連結簽章已驗證。</string> + <string name="owner_verification_failed">⚠️ 簽章驗證失敗:%s。</string> + <string name="video_decoding_exception_desc">無法解碼此影片。請嘗試其他影片或聯絡開發者。</string> + <string name="compose_send_direct_message_to_connect">傳送直接訊息來連線</string> + <string name="simplex_links_not_allowed">不允許 SimpleX 連結</string> + <string name="voice_messages_not_allowed">不允許語音訊息</string> + <string name="maximum_message_size_title">訊息太大!</string> + <string name="maximum_message_size_reached_text">請縮減訊息大小後再傳送。</string> + <string name="maximum_message_size_reached_non_text">請縮減訊息大小或移除媒體後再傳送。</string> + <string name="maximum_message_size_reached_forwarding">你可以複製並縮減訊息大小後傳送。</string> + <string name="report_compose_reason_header_spam">檢舉垃圾訊息:只有群組審核員會看到。</string> + <string name="report_compose_reason_header_profile">檢舉成員個人檔案:只有群組審核員會看到。</string> + <string name="report_compose_reason_header_community">檢舉違規:只有群組審核員會看到。</string> + <string name="report_compose_reason_header_illegal">檢舉內容:只有群組審核員會看到。</string> + <string name="report_compose_reason_header_other">檢舉其他原因:只有群組審核員會看到。</string> + <string name="report_sent_alert_title">檢舉報告已傳送給審核員</string> + <string name="report_sent_alert_msg_view_in_support_chat">你可以在「與管理員聊天」中查看你的檢舉報告。</string> + <string name="compose_view_join_channel">加入頻道</string> + <string name="compose_view_broadcast">廣播</string> + <string name="compose_view_send_contact_request_alert_question">要傳送聯絡請求嗎?</string> + <string name="compose_view_send_contact_request_alert_text"><![CDATA[你<b>只有在請求被接受後</b>才能傳送訊息。]]></string> + <string name="compose_view_send_request_without_message">傳送不含訊息的請求</string> + <string name="cant_send_message_request_is_sent">請求已傳送</string> + <string name="you_are_subscriber">你是訂閱者</string> + <string name="channel_role_label">頻道</string> + <string name="cant_send_message_rejected">加入請求已被拒絕</string> + <string name="cant_send_message_group_deleted">群組已刪除</string> + <string name="cant_send_message_mem_removed">已從群組移除</string> + <string name="cant_broadcast_message">無法廣播</string> + <string name="reviewed_by_admins">已由管理員審核</string> + <string name="cant_send_message_member_has_old_version">成員使用舊版本</string> + <string name="cant_send_commands_alert_text">你必須先連線才能傳送命令。</string> + <string name="disable_automatic_deletion_message">此聊天中的訊息永遠不會被刪除。</string> + <string name="change_automatic_chat_deletion_message">此操作無法復原——此聊天中早於所選時間傳送和接收的訊息將被刪除。</string> + <string name="you_can_still_send_messages_to_contact">你可以從「已封存聯絡人」向 %1$s 傳送訊息。</string> + <string name="you_can_still_view_conversation_with_contact">你仍可在聊天列表中查看與 %1$s 的對話。</string> + <string name="sync_connection_force_desc">加密正在正常運作,不需要新的加密協議。這可能會導致連線錯誤!</string> + <string name="sync_connection_desc">連線需要重新協商加密。</string> + <string name="encryption_renegotiation_in_progress">正在重新協商加密。</string> + <string name="reject_contact_request">拒絕聯絡請求</string> + <string name="the_sender_will_not_be_notified">傳送者不會收到通知。</string> + <string name="member_is_deleted_cant_accept_request">成員已刪除,無法接受請求</string> + <string name="unread_mentions">未讀提及</string> + <string name="duplicated_list_error">所有列表的名稱和 emoji 都應不同。</string> + <string name="delete_chat_list_warning">所有聊天都將從列表 %s 中移除,且該列表會被刪除</string> + <string name="share_simplex_address_on_social_media">在社交媒體上分享 SimpleX 地址。</string> + <string name="share_1_time_link_with_a_friend">與朋友分享一次性連結</string> + <string name="one_time_link_can_be_used_with_one_contact_only"><![CDATA[一次性連結<i>只能供一名聯絡人使用</i>——請當面分享,或透過任何通訊應用程式分享。]]></string> + <string name="you_can_set_connection_name_to_remember">你可以設定連線名稱,以記住此連結分享給了誰。</string> + <string name="simplex_address_and_1_time_links_are_safe_to_share">SimpleX 地址和一次性連結可安全地透過任何通訊應用程式分享。</string> + <string name="to_protect_against_your_link_replaced_compare_codes">為防止你的連結被替換,你可以比較聯絡人的安全碼。</string> + <string name="scan_paste_link">貼上連結 / 掃描</string> + <string name="switching_profile_error_title">切換個人檔案時發生錯誤</string> + <string name="switching_profile_error_message">你的連線已移至 %s,但切換個人檔案時發生錯誤。</string> + <string name="you_can_view_invitation_link_again">你可以在連線詳細資料中再次查看邀請連結。</string> + <string name="share_this_1_time_link">分享這個一次性邀請連結</string> + <string name="the_text_you_pasted_is_not_a_link">你貼上的文字不是 SimpleX 連結。</string> + <string name="tap_to_paste_link">點選貼上連結</string> + <string name="code_you_scanned_is_not_simplex_link_qr_code">你掃描的代碼不是 SimpleX 連結二維碼。</string> + <string name="context_user_picker_cant_change_profile_alert_title">無法變更個人檔案</string> + <string name="context_user_picker_cant_change_profile_alert_message">若要在嘗試連線後使用另一個個人檔案,請刪除聊天並再次使用連結。</string> + <string name="network_proxy_auth_mode_isolate_by_auth_user">為每個個人檔案使用不同的代理認證資料。</string> + <string name="network_proxy_auth_mode_isolate_by_auth_entity">為每個連線使用不同的代理認證資料。</string> + <string name="network_proxy_auth_mode_no_auth">不要對代理使用認證資料。</string> + <string name="network_proxy_auth_mode_username_password">你的認證資料可能會以未加密方式傳送。</string> + <string name="network_proxy_incorrect_config_desc">請確認代理設定正確。</string> + <string name="network_session_mode_session_description">每次啟動應用程式時都會使用新的 SOCKS 認證資料。</string> + <string name="network_session_mode_server_description">每個伺服器都會使用新的 SOCKS 認證資料。</string> + <string name="network_smp_proxy_mode_unknown_description">對未知伺服器使用私密路由。</string> + <string name="network_smp_proxy_mode_unprotected_description">當 IP 地址未受保護時,對未知伺服器使用私密路由。</string> + <string name="network_smp_proxy_fallback_allow_protected">IP 已隱藏時</string> + <string name="network_smp_proxy_fallback_allow_description">當你或目的地伺服器不支援私密路由時,直接傳送訊息。</string> + <string name="network_smp_proxy_fallback_allow_protected_description">當 IP 地址受到保護,且你或目的地伺服器不支援私密路由時,直接傳送訊息。</string> + <string name="private_routing_explanation">為保護你的 IP 地址,私密路由會使用你的 SMP 伺服器傳送訊息。</string> + <string name="network_smp_web_port_section_title">訊息傳遞的 TCP 連接埠</string> + <string name="network_smp_web_port_toggle">使用 Web 連接埠</string> + <string name="network_smp_web_port_footer">未指定連接埠時,使用 TCP 連接埠 %1$s。</string> + <string name="network_smp_web_port_preset_footer">僅對預設伺服器使用 TCP 連接埠 443。</string> + <string name="app_check_for_updates_notice_desc">若要接收新版本通知,請開啟穩定版或 Beta 版的定期檢查。</string> + <string name="show_slow_api_calls">顯示較慢的 API 呼叫</string> + <string name="prefs_error_saving_settings">儲存設定時發生錯誤</string> + <string name="sent_to_your_contact_after_connection">連線後會傳送給你的聯絡人。</string> + <string name="or_to_share_privately">或私下分享</string> + <string name="simplex_address_or_1_time_link">SimpleX 地址還是一次性連結?</string> + <string name="new_1_time_link">新的一次性連結</string> + <string name="onboarding_send_1_time_link">透過任何通訊應用程式傳送連結——這是安全的。請對方貼到 SimpleX。</string> + <string name="onboarding_or_show_qr_code">或當面顯示二維碼,也可透過視訊通話顯示。</string> + <string name="onboarding_post_address">在你的社交媒體個人檔案、網站或電子郵件簽名中使用此地址。</string> + <string name="onboarding_or_use_qr_code">或使用此二維碼——可列印或在線上顯示。</string> + <string name="add_your_team_members_to_conversations">將你的團隊成員加入對話。</string> + <string name="share_profile_via_link_alert_text">地址將會變短,且你的個人檔案會透過此地址分享。</string> + <string name="upgrade_group_link">升級群組連結</string> + <string name="share_group_profile_via_link">要升級群組連結嗎?</string> + <string name="share_group_profile_via_link_alert_text">連結將會變短,且群組檔案會透過此連結分享。</string> + <string name="share_old_address_alert_button">分享舊地址</string> + <string name="share_old_link_alert_button">分享舊連結</string> + <string name="you_can_make_address_visible_via_settings">你可以透過設定,讓你的 SimpleX 聯絡人看見它。</string> + <string name="short_descr__field">簡介:</string> + <string name="bio_too_large">簡介太長</string> + <string name="save_admission_question">要儲存加入審批設定嗎?</string> + <string name="save_and_notify_channel_subscribers">儲存並通知頻道訂閱者</string> + <string name="unable_to_open_browser_desc">通話需要預設網頁瀏覽器。請在系統中設定預設瀏覽器,並向開發者分享更多資訊。</string> + <string name="error_initializing_web_view_wrong_arch">初始化 WebView 時發生錯誤。請確認已安裝 WebView,且其支援的架構為 arm64。\n錯誤:%s</string> + <string name="onboarding_be_free">在你的網路中\n自由交流</string> + <string name="onboarding_private_and_secure">私密且安全的通訊。</string> + <string name="onboarding_first_network">第一個讓你擁有\n自己聯絡人和群組的網路。</string> + <string name="get_started">開始使用</string> + <string name="why_simplex_is_built">SimpleX 的打造初衷。</string> + <string name="onboarding_your_profile">你的個人檔案</string> + <string name="onboarding_on_your_phone">在你的手機上,不在伺服器上。</string> + <string name="onboarding_no_account">沒有帳戶。沒有電話。沒有電子郵件。沒有 ID。\n最安全的加密。</string> + <string name="enter_profile_name">輸入個人檔案名稱…</string> + <string name="migrate">遷移</string> + <string name="why_built_heading">你生來就沒有帳戶。</string> + <string name="why_built_p1">沒有人追蹤你的對話。沒有人繪製你去過哪裡的地圖。私隱從來不是一項功能——它本來就是生活方式。</string> + <string name="why_built_p2">後來我們轉到線上,每個平台都要求你交出一部分自己——你的姓名、電話號碼、朋友。我們接受了這樣的代價:與他人交談,就要讓別人知道我們在和誰交談。每一代人和技術都是如此——電話、電子郵件、通訊應用程式、社交媒體。這似乎是唯一可行的方式。</string> + <string name="why_built_p3">但還有另一種方式。一個沒有電話號碼、沒有使用者名稱、沒有帳戶、沒有任何使用者身份的網路。一個能連接人們並傳送加密訊息,卻不知道誰與誰連線的網路。</string> + <string name="why_built_p4">這不是在別人的門上加一把更好的鎖。也不是一位更尊重你私隱、卻仍記錄所有訪客的房東。你不是訪客。這裡就是你的家。沒有人能擅自進入——你擁有自主權。</string> + <string name="why_built_p5">你的對話屬於你,就像互聯網出現以前一直如此。網路不是你到訪的地方,而是你建立並擁有的地方。無論你讓它私密還是公開,沒有人能從你手中奪走它。</string> + <string name="why_built_p6">人類最古老的自由——不被監視地與另一個人交談——建立在不會背叛它的基礎設施之上。</string> + <string name="why_built_p7">因為我們摧毀了識別你身份的能力,讓你的自主權永遠不會被奪走。</string> + <string name="why_built_tagline">在你的網路中自由交流。</string> + <string name="all_message_and_files_e2e_encrypted"><![CDATA[所有訊息和檔案都以<b>端對端加密</b>傳送,直接訊息具備後量子安全性。]]></string> + <string name="onboarding_conditions_private_chats_not_accessible">營運商承諾:\n- 保持獨立\n- 盡量減少中繼資料使用\n- 執行已驗證的開源程式碼</string> + <string name="onboarding_conditions_by_using_you_agree">你承諾:\n- 只在公開群組中發佈合法內容\n- 尊重其他使用者——不發送垃圾訊息</string> + <string name="onboarding_conditions_privacy_policy_and_conditions_of_use">私隱政策與使用條件。</string> + <string name="onboarding_network_operators_simplex_flux_agreement">SimpleX Chat 與 Flux 達成協議,將 Flux 營運的伺服器納入應用程式。</string> + <string name="onboarding_network_operators_app_will_use_different_operators">應用程式會在每個對話中使用不同營運商,以保護你的私隱。</string> + <string name="onboarding_network_operators_cant_see_who_talks_to_whom">啟用多於一個營運商時,沒有任何一方擁有足以得知誰與誰通訊的中繼資料。</string> + <string name="onboarding_network_operators_app_will_use_for_routing">例如,如果你的聯絡人透過 SimpleX Chat 伺服器接收訊息,你的應用程式會透過 Flux 伺服器傳送訊息。</string> + <string name="onboarding_select_network_operators_to_use">選擇要使用的網路營運商。</string> + <string name="how_it_helps_privacy">這如何有助於私隱</string> + <string name="onboarding_network_operators_configure_via_settings">你可以透過設定配置伺服器。</string> + <string name="onboarding_network_operators_conditions_will_be_accepted">30 天後,將會接受已啟用營運商的條件。</string> + <string name="onboarding_network_operators_conditions_you_can_configure">你可以在「網路與伺服器」設定中配置營運商。</string> + <string name="onboarding_your_network">你的網路</string> + <string name="onboarding_network_routers_cannot_know">網路路由器無法知道\n誰在和誰交談</string> + <string name="onboarding_configure_routers">設定路由器</string> + <string name="onboarding_configure_notifications">設定通知</string> + <string name="onboarding_network_commitments">網路承諾</string> + <string name="call_desktop_permission_denied_title">若要通話,請允許使用麥克風。結束通話後再嘗試撥打。</string> + <string name="call_desktop_permission_denied_chrome">點按地址欄旁的資訊按鈕,以允許使用麥克風。</string> + <string name="call_desktop_permission_denied_safari">開啟 Safari 設定 / 網站 / 麥克風,然後為 localhost 選擇「允許」。</string> + <string name="open_external_link_title">要開啟外部連結嗎?</string> + <string name="rcv_msg_error_dropped">已丟棄(%1$d 次嘗試)</string> + <string name="rcv_msg_error_parse">錯誤:%s</string> + <string name="alert_title_msg_error">訊息錯誤</string> + <string name="alert_text_msg_reception_error">應用程式在嘗試接收此訊息 %1$d 次後將其移除。</string> + <string name="app_will_ask_to_confirm_unknown_file_servers">應用程式會要求確認來自未知檔案伺服器的下載(.onion 或啟用 SOCKS 代理時除外)。</string> + <string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">未使用 Tor 或 VPN 時,你的 IP 地址會對檔案伺服器可見。</string> + <string name="sanitize_links_toggle">移除連結追蹤</string> + <string name="this_setting_is_for_your_current_profile">此設定適用於你目前的個人檔案</string> + <string name="receipts_section_description">這些設定適用於你目前的個人檔案</string> + <string name="receipts_section_description_1">可在聯絡人和群組設定中覆寫這些設定。</string> + <string name="receipts_contacts_override_enabled">已為 %d 個聯絡人啟用送達回條</string> + <string name="receipts_contacts_override_disabled">已為 %d 個聯絡人停用送達回條</string> + <string name="receipts_section_groups">小型群組(最多 20 人)</string> + <string name="receipts_groups_override_enabled">已為 %d 個群組啟用送達回條</string> + <string name="receipts_groups_override_disabled">已為 %d 個群組停用送達回條</string> + <string name="privacy_chat_list_open_links">從聊天列表開啟連結</string> + <string name="privacy_chat_list_open_web_link_question">要開啟網頁連結嗎?</string> + <string name="privacy_chat_list_open_full_web_link">開啟完整連結</string> + <string name="privacy_chat_list_open_clean_web_link">開啟乾淨連結</string> + <string name="settings_section_title_delivery_receipts">傳送送達回條給</string> + <string name="settings_section_title_contact_requests_from_groups">來自群組的聯絡請求</string> + <string name="settings_section_title_about">關於</string> + <string name="settings_section_title_contact">聯絡</string> + <string name="settings_section_title_support_project">支持此專案</string> + <string name="chat_data">聊天數據</string> + <string name="help_and_support">說明與支援</string> + <string name="more_privacy">更多私隱</string> + <string name="advanced_settings">進階設定</string> + <string name="remote_hosts_section">遠端行動裝置</string> + <string name="chat_database_exported_save">你可以儲存匯出的封存檔。</string> + <string name="chat_database_exported_migrate">你可以遷移匯出的數據庫。</string> + <string name="chat_database_exported_not_all_files">部分檔案未匯出</string> + <string name="error_saving_database">儲存數據庫時發生錯誤</string> + <string name="save_passphrase_in_settings">在設定中儲存密碼短語</string> + <string name="remove_passphrase_from_settings">要從設定中移除密碼短語嗎?</string> + <string name="settings_is_storing_in_clear_text">密碼短語會以純文字形式儲存在設定中。</string> + <string name="passphrase_will_be_saved_in_settings">密碼短語會在你變更或重新啟動應用程式後以純文字形式儲存在設定中。</string> + <string name="error_reading_passphrase">讀取數據庫密碼短語時發生錯誤</string> + <string name="restore_passphrase_can_not_be_read_desc">Keystore 中的密碼短語無法讀取。這可能是在系統更新與應用程式不相容後發生。如果不是這種情況,請聯絡開發者。</string> + <string name="restore_passphrase_can_not_be_read_enter_manually_desc">Keystore 中的密碼短語無法讀取,請手動輸入。這可能是在系統更新與應用程式不相容後發生。如果不是這種情況,請聯絡開發者。</string> + <string name="one_hand_ui_bottom_bar">底部列</string> + <string name="one_hand_ui_top_bar">頂部列</string> + <string name="terminal_always_visible">在新視窗中顯示控制台</string> + <string name="chat_list_always_visible">在新視窗中顯示聊天列表</string> + <string name="down_migration_warning_chat_relays">如果你加入或建立了頻道,它們將永久停止運作。</string> + <string name="leave_channel_question">要離開頻道嗎?</string> + <string name="you_will_stop_receiving_messages_from_this_channel_chat_history_will_be_preserved">你將停止接收此頻道的訊息。聊天記錄將會保留。</string> + <string name="you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved">你將停止接收此聊天的訊息。聊天記錄將會保留。</string> + <string name="rcv_direct_event_group_inv_link_received">來自群組 %1$s 的連線請求</string> + <string name="rcv_channel_event_channel_deleted">已刪除頻道</string> + <string name="rcv_channel_event_updated_channel_profile">已更新頻道檔案</string> + <string name="rcv_group_event_new_member_pending_review">新成員想加入群組。</string> + <string name="snd_channel_event_channel_profile_updated">頻道檔案已更新</string> + <string name="snd_group_event_user_pending_review">請等待群組審核員審核你的加入請求。</string> + <string name="rcv_group_event_2_members_connected">%s 和 %s 已連線</string> + <string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已連線</string> + <string name="rcv_group_event_n_members_connected">%s、%s 和另外 %d 名成員已連線</string> + <string name="rcv_channel_events_count">%d 個頻道事件</string> + <string name="profile_update_event_set_new_picture">設定了新的個人檔案圖片</string> + <string name="profile_update_event_set_new_address">設定了新的聯絡地址</string> + <string name="group_member_role_observer_channel">訂閱者</string> + <string name="group_member_role_member_channel">貢獻者</string> + <string name="group_member_role_relay">中繼</string> + <string name="button_delete_channel">刪除頻道</string> + <string name="button_cancel_and_delete_channel">取消並刪除頻道</string> + <string name="delete_channel_question">要刪除頻道嗎?</string> + <string name="delete_channel_for_all_subscribers_cannot_undo_warning">頻道將為所有訂閱者刪除,此操作無法復原!</string> + <string name="delete_chat_for_all_members_cannot_undo_warning">聊天將為所有成員刪除,此操作無法復原!</string> + <string name="delete_channel_for_self_cannot_undo_warning">頻道將為你刪除,此操作無法復原!</string> + <string name="delete_chat_for_self_cannot_undo_warning">聊天將為你刪除,此操作無法復原!</string> + <string name="button_leave_channel">離開頻道</string> + <string name="button_edit_channel_profile">編輯頻道檔案</string> + <string name="channel_link">頻道連結</string> + <string name="channel_webpage">頻道網頁</string> + <string name="group_webpage">群組網頁</string> + <string name="advanced_options">進階選項</string> + <string name="web_page_url_placeholder">https://</string> + <string name="allow_anyone_to_embed">允許任何人嵌入</string> + <string name="enter_webpage_url">輸入網頁 URL</string> + <string name="webpage_url_footer">它將顯示給訂閱者,並用於允許載入預覽。</string> + <string name="webpage_code">網頁程式碼</string> + <string name="webpage_code_footer">將此程式碼加入你的網頁。它會顯示你的頻道 / 群組預覽。</string> + <string name="copy_code">複製程式碼</string> + <string name="webpage_info">建立一個網頁,在訪客訂閱前向他們顯示你的頻道預覽。你可以自行託管,或使用任何靜態託管服務。</string> + <string name="relays_no_web_support">使用的聊天中繼不支援網頁。</string> + <string name="embed_any_webpage_can_show">任何網頁都可以顯示預覽。</string> + <string name="embed_only_your_page">只有你上方的頁面可以顯示預覽。</string> + <string name="you_can_share_channel_link_anybody_will_be_able_to_connect">你可以分享連結或二維碼——任何人都能加入頻道。</string> + <string name="only_channel_owners_can_change_prefs">只有頻道擁有者可以變更頻道偏好設定。</string> + <string name="only_chat_owners_can_change_prefs">只有聊天擁有者可以變更偏好設定。</string> + <string name="send_receipts_disabled_alert_msg">此群組有超過 %1$d 名成員,不會傳送送達回條。</string> + <string name="action_button_channel_link">連結</string> + <string name="button_channel_members">頻道成員</string> + <string name="button_channel_relays">聊天中繼</string> + <string name="button_remove_subscriber_question">要移除訂閱者嗎?</string> + <string name="button_delete_member_messages_question">要刪除成員訊息嗎?</string> + <string name="button_delete_member_messages">刪除成員訊息</string> + <string name="subscriber_will_be_removed_from_channel_cannot_be_undone">訂閱者將從頻道移除,此操作無法復原!</string> + <string name="members_will_be_removed_from_group_cannot_be_undone">成員將從群組移除,此操作無法復原!</string> + <string name="member_will_be_removed_from_chat_cannot_be_undone">成員將從聊天移除,此操作無法復原!</string> + <string name="members_will_be_removed_from_chat_cannot_be_undone">成員將從聊天移除,此操作無法復原!</string> + <string name="member_messages_will_be_deleted_cannot_be_undone">成員訊息將被刪除,此操作無法復原!</string> + <string name="remove_member_delete_messages_confirmation">移除並刪除訊息</string> + <string name="block_members_for_all_question">要為所有人封鎖成員嗎?</string> + <string name="block_members_desc">這些成員的所有新訊息都會被隱藏!</string> + <string name="unblock_for_all_question">要為所有人解除封鎖成員嗎?</string> + <string name="unblock_members_for_all_question">要為所有人解除封鎖多名成員嗎?</string> + <string name="unblock_for_all">為所有人解除封鎖</string> + <string name="unblock_members_desc">這些成員的訊息將會顯示!</string> + <string name="member_info_member_failed">失敗</string> + <string name="member_role_will_be_changed_with_notification_chat">角色將變更為 "%s"。聊天中的所有人都會收到通知。</string> + <string name="member_role_will_be_changed_with_notification_channel">角色將變更為 "%s"。頻道中的所有人都會收到通知。</string> + <string name="info_row_connection_failed">連線失敗</string> + <string name="message_queue_info_server_info">伺服器佇列資訊:%1$s\n\n最後收到的訊息:%2$s</string> + <string name="you_need_to_allow_calls">你需要允許你的聯絡人通話,才能致電給對方。</string> + <string name="cant_call_member_send_message_alert_text">傳送訊息以啟用通話。</string> + <string name="welcome_message_is_too_long">歡迎訊息太長</string> + <string name="channel_full_name_field">頻道全名:</string> + <string name="group_descr_too_large">描述太大</string> + <string name="chat_main_profile_sent">你的聊天個人檔案將傳送給聊天成員</string> + <string name="channel_profile_is_stored_on_subscribers_devices">頻道檔案會儲存在訂閱者裝置和聊天中繼上。</string> + <string name="save_channel_profile">儲存頻道檔案</string> + <string name="error_saving_channel_profile">儲存頻道檔案時發生錯誤</string> + <string name="operator_conditions_accepted_for_enabled_operators_on">將於 %s 自動接受已啟用營運商的條件。</string> + <string name="operators_conditions_accepted_for"><![CDATA[已接受營運商的條件:<b>%s</b>。]]></string> + <string name="operators_conditions_will_be_accepted_for"><![CDATA[將接受以下營運商的條件:<b>%s</b>。]]></string> + <string name="operator_conditions_accepted_on">已於 %s 接受條件。</string> + <string name="operator_conditions_will_be_accepted_on">將於 %s 接受條件。</string> + <string name="operator_conditions_failed_to_load">無法載入目前的條件文字,你可以透過此連結查看條件:</string> + <string name="operator_conditions_accepted_for_some"><![CDATA[以下營運商的條件已接受:<b>%s</b>。]]></string> + <string name="operator_same_conditions_will_be_applied"><![CDATA[相同條件將套用於營運商 <b>%s</b>。]]></string> + <string name="operator_same_conditions_will_apply_to_operators"><![CDATA[相同條件將套用於以下營運商:<b>%s</b>。]]></string> + <string name="operator_conditions_will_be_applied"><![CDATA[這些條件也將套用於:<b>%s</b>。]]></string> + <string name="operator_conditions_will_be_accepted_for_some"><![CDATA[將接受以下營運商的條件:<b>%s</b>。]]></string> + <string name="operators_conditions_will_also_apply"><![CDATA[這些條件也將套用於:<b>%s</b>。]]></string> + <string name="operator_in_order_to_use_accept_conditions"><![CDATA[若要使用 <b>%s</b> 的伺服器,請接受使用條件。]]></string> + <string name="xftp_servers_per_user">你目前聊天個人檔案的新檔案伺服器</string> + <string name="error_server_protocol_changed">伺服器協議已變更。</string> + <string name="server_added_to_operator__name">伺服器已加入營運商 %s。</string> + <string name="network_option_tcp_connection_timeout_background">TCP 連線背景逾時</string> + <string name="network_option_protocol_timeout_background">協議背景逾時</string> + <string name="chat_theme_reset_to_app_theme">重設為應用程式主題</string> + <string name="chat_theme_reset_to_user_theme">重設為使用者主題</string> + <string name="channel_preferences">頻道偏好設定</string> + <string name="set_member_admission">設定成員加入審批</string> + <string name="time_to_disappear_is_set_only_for_new_contacts">自動銷毀時間僅為新聯絡人設定。</string> + <string name="allow_your_contacts_to_send_files_and_media">允許你的聯絡人傳送檔案和媒體。</string> + <string name="allow_files_and_media_only_if">僅在你的聯絡人允許時,才允許檔案和媒體。</string> + <string name="prohibit_sending_files_and_media">禁止傳送檔案和媒體。</string> + <string name="both_you_and_your_contact_can_send_files">你和你的聯絡人都可以傳送檔案和媒體。</string> + <string name="only_you_can_send_files">只有你可以傳送檔案和媒體。</string> + <string name="only_your_contact_can_send_files">只有你的聯絡人可以傳送檔案和媒體。</string> + <string name="files_prohibited_in_this_chat">此聊天中禁止檔案和媒體。</string> + <string name="enable_sending_recent_history">向新成員傳送最多最近 100 則訊息。</string> + <string name="disable_sending_member_reports">禁止向審核員檢舉訊息。</string> + <string name="direct_messages_are_prohibited">成員之間禁止直接訊息。</string> + <string name="direct_messages_are_prohibited_in_chat">此聊天中禁止成員之間的直接訊息。</string> + <string name="simplex_links_are_prohibited_in_group">禁止 SimpleX 連結。</string> + <string name="recent_history_is_sent_to_new_members">會向新成員傳送最多最近 100 則訊息。</string> + <string name="group_members_can_send_reports">成員可以向審核員檢舉訊息。</string> + <string name="member_reports_are_prohibited">此群組中禁止檢舉訊息。</string> + <string name="chat_with_admins">與管理員聊天</string> + <string name="allow_chat_with_admins">允許成員與管理員聊天。</string> + <string name="prohibit_chat_with_admins">禁止與管理員聊天。</string> + <string name="members_can_chat_with_admins">成員可以與管理員聊天。</string> + <string name="chat_with_admins_is_prohibited">禁止與管理員聊天。</string> + <string name="chat_with_admins_relay_note">公開頻道中與管理員的聊天沒有 E2E 加密,僅應與可信任的聊天中繼一起使用。</string> + <string name="enable_chats_with_admins_question">要啟用與管理員聊天嗎?</string> + <string name="enable_chats_with_admins">啟用</string> + <string name="group_reports_subscriber_reports">訂閱者檢舉報告</string> + <string name="allow_direct_messages_channel">允許向訂閱者傳送直接訊息。</string> + <string name="prohibit_direct_messages_channel">禁止向訂閱者傳送直接訊息。</string> + <string name="enable_sending_recent_history_channel">向新訂閱者傳送最多最近 100 則訊息。</string> + <string name="disable_sending_recent_history_channel">不向新訂閱者傳送歷史記錄。</string> + <string name="group_members_can_send_disappearing_channel">訂閱者可以傳送自動銷毀訊息。</string> + <string name="group_members_can_send_dms_channel">訂閱者可以傳送直接訊息。</string> + <string name="direct_messages_are_prohibited_channel">訂閱者之間禁止傳送直接訊息。</string> + <string name="group_members_can_delete_channel">訂閱者可以不可復原地刪除已傳送的訊息。(24 小時)</string> + <string name="group_members_can_add_message_reactions_channel">訂閱者可以加入訊息回應。</string> + <string name="group_members_can_send_voice_channel">訂閱者可以傳送語音訊息。</string> + <string name="group_members_can_send_files_channel">訂閱者可以傳送檔案和媒體。</string> + <string name="group_members_can_send_simplex_links_channel">訂閱者可以傳送 SimpleX 連結。</string> + <string name="group_members_can_send_reports_channel">訂閱者可以向審核員檢舉訊息。</string> + <string name="recent_history_is_sent_to_new_members_channel">會向新訂閱者傳送最多最近 100 則訊息。</string> + <string name="recent_history_is_not_sent_to_new_members_channel">不會向新訂閱者傳送歷史記錄。</string> + <string name="allow_chat_with_admins_channel">允許訂閱者與管理員聊天。</string> + <string name="members_can_chat_with_admins_channel">訂閱者可以與管理員聊天。</string> + <string name="member_admission">成員加入審批</string> + <string name="admission_stage_review_descr">加入前審核成員(敲門)。</string> + <string name="no_support_chats">沒有與成員的聊天</string> + <string name="support_chats_disabled">已停用與成員聊天</string> + <string name="delete_member_support_chat_alert_title">要刪除與成員的聊天嗎?</string> + <string name="accept_pending_member_alert_question">該成員將加入群組,要接受嗎?</string> + <string name="v5_2_message_delivery_receipts_descr">我們漏掉了第二個勾號!✅</string> + <string name="v5_3_simpler_incognito_mode_descr">連線時切換無痕。</string> + <string name="v5_4_link_mobile_desktop_descr">透過安全的抗量子協議。</string> + <string name="v5_4_block_group_members_descr">用來隱藏不想看到的訊息。</string> + <string name="v5_5_private_notes_descr">支援加密檔案和媒體。</string> + <string name="v5_5_simpler_connect_ui_descr">搜尋列可接受邀請連結。</string> + <string name="v5_6_picture_in_picture_calls_descr">通話時也能使用應用程式。</string> + <string name="v5_7_quantum_resistant_encryption_descr">將在直接聊天中啟用!</string> + <string name="v5_7_call_sounds_descr">連線語音和視訊通話時。</string> + <string name="v5_7_shape_profile_images">設定個人檔案圖片形狀</string> + <string name="v5_7_shape_profile_images_descr">正方形、圓形,或兩者之間的任何形狀。</string> + <string name="v6_0_reachable_chat_toolbar_descr">單手使用應用程式。</string> + <string name="v6_1_better_security_descr">SimpleX 協議已由 Trail of Bits 審查。</string> + <string name="v6_1_better_calls_descr">通話期間切換語音和視訊。</string> + <string name="v6_1_switch_chat_profile_descr">為一次性邀請切換聊天個人檔案。</string> + <string name="v6_1_forward_many_messages_descr">一次最多轉發 20 則訊息。</string> + <string name="v6_2_network_decentralization_descr">應用程式中的第二個預設營運商!</string> + <string name="v6_2_improved_chat_navigation">改進的聊天導覽</string> + <string name="v6_2_improved_chat_navigation_descr">- 在第一則未讀訊息處開啟聊天。\n- 跳至引用的訊息。</string> + <string name="v6_2_business_chats_descr">你的客戶私隱。</string> + <string name="v6_3_mentions">提及成員 👋</string> + <string name="v6_3_mentions_descr">被提及時收到通知。</string> + <string name="v6_3_reports">傳送私密檢舉報告</string> + <string name="v6_3_reports_descr">協助管理員審核其群組。</string> + <string name="v6_3_organize_chat_lists">將聊天整理到列表中</string> + <string name="v6_3_organize_chat_lists_descr">不要錯過重要訊息。</string> + <string name="v6_3_private_media_file_names">私密媒體檔案名稱。</string> + <string name="v6_3_set_message_expiration_in_chats">在聊天中設定訊息過期時間。</string> + <string name="v6_3_faster_sending_messages">更快傳送訊息。</string> + <string name="v6_3_faster_deletion_of_groups">更快刪除群組。</string> + <string name="v6_4_connect_faster">更快連線!🚀</string> + <string name="v6_4_connect_faster_descr">點選「連線」後即可即時傳訊。</string> + <string name="v6_4_review_members">審核群組成員</string> + <string name="v6_4_review_members_descr">在成員加入前與其聊天。</string> + <string name="v6_4_support_chat">與管理員聊天</string> + <string name="v6_4_role_moderator">新群組角色:審核員</string> + <string name="v6_4_role_moderator_descr">移除訊息並封鎖成員。</string> + <string name="v6_4_message_delivery_descr">手機網路流量更少。</string> + <string name="v6_4_1_welcome_contacts">歡迎你的聯絡人 👋</string> + <string name="v6_4_1_welcome_contacts_descr">設定個人檔案簡介和歡迎訊息。</string> + <string name="v6_4_1_keep_chats_clean">保持聊天整潔</string> + <string name="v6_4_1_keep_chats_clean_descr">預設啟用自動銷毀訊息。</string> + <string name="v6_4_1_short_address">短 SimpleX 地址</string> + <string name="v6_4_1_short_address_create">建立你的地址</string> + <string name="v6_4_1_short_address_update">更新你的地址</string> + <string name="v6_4_1_short_address_share">分享你的地址</string> + <string name="v6_4_1_new_interface_languages">4 種新的介面語言</string> + <string name="v6_4_1_new_interface_languages_descr">加泰羅尼亞文、印尼文、羅馬尼亞文和越南文 - 感謝我們的使用者!</string> + <string name="v6_5_public_channels">公開頻道 - 自由發言 🚀</string> + <string name="v6_5_reliability">可靠性:每個頻道可使用多個中繼。</string> + <string name="v6_5_ownership">擁有權:你可以執行自己的中繼。</string> + <string name="v6_5_security">安全性:擁有者持有頻道金鑰。</string> + <string name="v6_5_privacy">私隱:適用於擁有者和訂閱者。</string> + <string name="v6_5_invite_friends">更輕鬆邀請你的朋友 👋</string> + <string name="v6_5_invite_friends_descr">我們讓新使用者的連線流程更簡單。</string> + <string name="v6_5_safe_web_links">安全的網頁連結</string> + <string name="v6_4_support_chat_descr">向群組傳送你的私密意見回饋。</string> + <string name="v6_5_safe_web_links_descr">- 選擇是否傳送連結預覽。\n- 如已啟用,使用 SOCKS 代理。\n- 防止超連結釣魚。\n- 移除連結追蹤。</string> + <string name="v6_5_non_profit_governance">非營利治理</string> + <string name="v6_5_non_profit_governance_descr">讓 SimpleX Network 長久運作。</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">將為所有可見聊天個人檔案中的所有聯絡人啟用送達回條。</string> + <string name="sending_delivery_receipts_will_be_enabled">將為所有聯絡人啟用送達回條。</string> + <string name="you_can_enable_delivery_receipts_later">你可以稍後透過「設定」啟用</string> + <string name="you_can_enable_delivery_receipts_later_alert">你可以稍後透過應用程式的「你的私隱」設定啟用它們。</string> + <string name="scan_from_mobile">從手機掃描</string> + <string name="verify_code_on_mobile">在手機上驗證代碼</string> + <string name="this_device_name_shared_with_mobile">裝置名稱將與已連接的手機用戶端共享。</string> + <string name="remote_ctrl_connection_stopped_identity_desc">此連結已由另一部手機裝置使用,請在桌面端建立新連結。</string> + <string name="waiting_for_mobile_to_connect">正在等待手機連接:</string> + <string name="waiting_for_desktop">正在等待桌面端…</string> + <string name="verify_code_with_desktop">使用桌面端驗證代碼</string> + <string name="scan_qr_code_from_desktop">從桌面端掃描二維碼</string> + <string name="open_port_in_firewall_desc">若要允許手機應用程式連接到桌面端,若你已啟用防火牆,請在防火牆中開啟此連接埠</string> + <string name="remote_host_error_timeout"><![CDATA[連接到手機 <b>%s</b> 時逾時]]></string> + <string name="remote_ctrl_error_timeout">連接到桌面端時逾時</string> + <string name="in_developing_desc">此功能尚未支援。請試用下一個版本。</string> + <string name="connect_plan_this_is_your_own_one_time_link">這是你自己的一次性連結!</string> + <string name="connect_plan_you_are_already_connecting_to_vName"><![CDATA[你已在連接到 <b>%1$s</b>。]]></string> + <string name="connect_plan_you_are_already_connecting_via_this_one_time_link">你已在透過此一次性連結連接!</string> + <string name="connect_plan_this_is_your_own_simplex_address">這是你自己的 SimpleX 地址!</string> + <string name="connect_plan_you_have_already_requested_connection_via_this_address">你已透過此地址請求連接!</string> + <string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[這是你用於群組 <b>%1$s</b> 的連結!]]></string> + <string name="connect_plan_you_are_already_joining_the_group_vName"><![CDATA[你已在加入群組 <b>%1$s</b>。]]></string> + <string name="connect_plan_you_are_already_joining_the_group_via_this_link">你已在透過此連結加入群組。</string> + <string name="connect_plan_you_are_already_in_group_vName"><![CDATA[你已在群組 <b>%1$s</b> 中。]]></string> + <string name="connect_plan_you_are_already_connected_with_vName"><![CDATA[你已與 <b>%1$s</b> 連接。]]></string> + <string name="migrate_from_device_uploaded_archive_will_be_removed">已上載的數據庫封存將從伺服器永久移除。</string> + <string name="servers_info_target">顯示資訊:</string> + <string name="servers_info_private_data_disclaimer">從 %s 開始。\n所有資料都會在你的裝置上保持私密。</string> + <string name="servers_info_starting_from">從 %s 開始。</string> + <string name="channel_members_title_subscribers">訂閱者</string> + <string name="channel_members_section_owners">擁有者及貢獻者</string> + <string name="channel_subscriber_count_singular">%1$d 位訂閱者</string> + <string name="channel_subscriber_count_plural">%1$d 位訂閱者</string> + <string name="channel_owner_count_singular">%1$d 位擁有者</string> + <string name="channel_owner_count_plural">%1$d 位擁有者</string> + <string name="channel_owners_contributors_count">%1$d 位擁有者及貢獻者</string> + <string name="channel_member_you">你</string> + <string name="chat_relay">聊天中繼</string> + <string name="new_chat_relay">新聊天中繼</string> + <string name="preset_relay_name">預設中繼名稱</string> + <string name="preset_relay_address">預設中繼地址</string> + <string name="your_relay_name">你的中繼名稱</string> + <string name="your_relay_address">你的中繼地址</string> + <string name="enter_relay_name">輸入中繼名稱…</string> + <string name="use_relay">使用中繼</string> + <string name="test_relay">測試中繼</string> + <string name="use_for_new_channels">用於新頻道</string> + <string name="delete_relay">刪除中繼</string> + <string name="test_relay_to_retrieve_name"><![CDATA[<b>測試中繼</b>以取得其名稱。]]></string> + <string name="relay_test_failed_alert">中繼測試失敗!</string> + <string name="relay_test_step_get_link">取得連結</string> + <string name="relay_test_step_decode_link">解碼連結</string> + <string name="relay_test_step_connect">連線</string> + <string name="relay_test_step_wait_response">等待回應</string> + <string name="relay_test_step_verify">驗證</string> + <string name="error_relay_test_failed_at_step">測試在步驟 %s 失敗。</string> + <string name="error_relay_test_server_auth">伺服器需要授權才能連接到中繼,請檢查密碼。</string> + <string name="invalid_relay_name">無效的中繼名稱!</string> + <string name="check_relay_name">請檢查中繼名稱並重試。</string> + <string name="invalid_relay_address">無效的中繼地址!</string> + <string name="check_relay_address">請檢查中繼地址並重試。</string> + <string name="error_adding_relay">新增中繼時發生錯誤</string> + <string name="chat_relays">聊天中繼</string> + <string name="chat_relays_forward_messages_in_channels">聊天中繼會轉發你建立的頻道中的訊息。</string> + <string name="channel_relays_title">聊天中繼</string> + <string name="no_chat_relays">沒有聊天中繼</string> + <string name="chat_relays_forward_messages">聊天中繼會將訊息轉發給頻道訂閱者。</string> + <string name="relay_conn_status_connected">已連接</string> + <string name="relay_conn_status_connecting">連接中</string> + <string name="relay_conn_status_deleted">已刪除</string> + <string name="relay_conn_status_failed">失敗</string> + <string name="relay_conn_status_removed_by_operator">已由營運商移除</string> + <string name="relay_conn_status_removed">已移除</string> + <string name="relay_status_new">新增</string> + <string name="relay_status_invited">已邀請</string> + <string name="relay_status_accepted">已接受</string> + <string name="relay_status_acknowledged_roster">已確認名單</string> + <string name="relay_status_active">作用中</string> + <string name="relay_status_inactive">非作用中</string> + <string name="relay_status_rejected">已拒絕</string> + <string name="member_info_status">狀態</string> + <string name="member_info_relay_status_rejected_by_operator">已由中繼營運商拒絕</string> + <string name="relay_bar_all_relays_removed">所有中繼已移除</string> + <string name="relay_bar_all_relays_failed">所有中繼都失敗</string> + <string name="relay_bar_no_active_relays">沒有作用中的中繼</string> + <string name="relay_bar_relays_removed">已移除 %1$d 個中繼</string> + <string name="relay_bar_relays_failed">%1$d 個中繼失敗</string> + <string name="relay_bar_relays_not_active">%1$d 個中繼非作用中</string> + <string name="relay_bar_active_with_failures">%1$d/%2$d 個中繼作用中,%3$d 個失敗</string> + <string name="relay_bar_active_with_removed">%1$d/%2$d 個中繼作用中,%3$d 個已移除</string> + <string name="relay_bar_active_with_errors">%1$d/%2$d 個中繼作用中,%3$d 個錯誤</string> + <string name="relay_bar_active">%1$d/%2$d 個中繼作用中</string> + <string name="relay_bar_connected_with_errors">%1$d/%2$d 個中繼已連接,%3$d 個錯誤</string> + <string name="relay_bar_connected_with_failures">%1$d/%2$d 個中繼已連接,%3$d 個失敗</string> + <string name="relay_bar_connected_with_removed">%1$d/%2$d 個中繼已連接,%3$d 個已移除</string> + <string name="relay_bar_connected">%1$d/%2$d 個中繼已連接</string> + <string name="relay_bar_no_relays">沒有中繼</string> + <string name="relay_bar_owner_no_delivery">新增中繼以恢復訊息送達。</string> + <string name="relay_bar_subscriber_waiting">正在等待頻道擁有者新增中繼。</string> + <string name="member_info_section_title_relay">中繼</string> + <string name="member_info_section_title_owner">擁有者</string> + <string name="member_info_section_title_subscriber">訂閱者</string> + <string name="info_row_channel">頻道</string> + <string name="info_row_relay_link">中繼連結</string> + <string name="info_row_relay_address">中繼地址</string> + <string name="via_relay_hostname">透過 %1$s</string> + <string name="share_relay_address">分享中繼地址</string> + <string name="relay_section_footer_owner">訂閱者使用中繼連結連接到頻道。\n中繼地址用於為此頻道設定此中繼。</string> + <string name="relay_section_footer_subscriber">你已透過此中繼連結連接到頻道。</string> + <string name="button_remove_subscriber">移除訂閱者</string> + <string name="button_remove_relay">移除中繼</string> + <string name="button_remove_relay_question">要移除中繼嗎?</string> + <string name="relay_will_be_removed_from_channel">中繼將從頻道移除 - 此操作無法復原!</string> + <string name="last_active_relay_warning">這是最後一個作用中的中繼。移除它會阻止訊息送達訂閱者。</string> + <string name="block_subscriber_for_all_question">要對全部封鎖訂閱者嗎?</string> + <string name="create_channel_title">建立公開頻道</string> + <string name="create_channel_button">建立公開頻道</string> + <string name="channel_display_name_field">頻道名稱</string> + <string name="creating_channel">正在建立頻道</string> + <string name="error_creating_channel">建立頻道時發生錯誤</string> + <string name="relay_results">中繼結果:</string> + <string name="connection_reached_limit_of_undelivered_messages">連接已達未送達訊息數量上限</string> + <string name="network_error">網路錯誤</string> + <string name="error_prefix">錯誤</string> + <string name="cancel_creating_channel_question">要取消建立頻道嗎?</string> + <string name="cancel_channel_alert_msg">你的新頻道 %1$s 已連接到 %2$d/%3$d 個中繼。\n如果取消,頻道將被刪除 - 你可以再次建立。</string> + <string name="enable_at_least_one_chat_relay">啟用至少一個聊天中繼以建立頻道。</string> + <string name="your_profile_shared_with_channel_relays">你的個人檔案 %1$s 將與頻道中繼和訂閱者共享。\n中繼可以存取頻道訊息。</string> + <string name="configure_relays">設定中繼</string> + <string name="relay_status_failed">失敗</string> + <string name="add_button">新增</string> + <string name="add_relay_button">新增中繼</string> + <string name="add_relays_title">新增中繼</string> + <string name="no_available_relays">沒有可用的中繼</string> + <string name="error_adding_relays">新增中繼時發生錯誤</string> + <string name="relays_added_format">已新增中繼:%1$s。</string> + <string name="select_relays">選擇中繼</string> + <string name="no_relays_selected">未選擇中繼</string> + <string name="num_relays_selected">已選擇 %d 個中繼</string> + <string name="relay_connection_failed">中繼連接失敗</string> + <string name="not_all_relays_connected">並非所有中繼都已連接</string> + <string name="wait_verb">等待</string> + <string name="channel_will_start_with_relays">頻道將以 %1$d/%2$d 個中繼開始運作。要繼續嗎?</string> + <string name="relay_address_alert_title">中繼地址</string> + <string name="relay_address_alert_message">這是聊天中繼地址,不能用於連接。</string> + <string name="connect_plan_open_channel">開啟頻道</string> + <string name="connect_plan_open_new_channel">開啟新頻道</string> + <string name="connect_plan_this_is_your_link_for_channel">你的頻道</string> + <string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[這是你用於頻道 <b>%1$s</b> 的連結!]]></string> + <string name="error_opening_channel">開啟頻道時發生錯誤</string> + <string name="unblock_subscriber_for_all_question">要對全部解除封鎖訂閱者嗎?</string> + <string name="link_previews_alert_title">要啟用連結預覽嗎?</string> + <string name="link_previews_alert_desc">傳送連結預覽可能會向網站透露你的 IP 地址。你可以稍後在「私隱」設定中變更此設定。</string> + <string name="link_previews_alert_desc_socks">連結預覽將透過 SOCKS 代理請求。DNS 查詢仍可能透過你的 DNS 解析器在本機發生。</string> + <string name="link_previews_alert_enable">啟用</string> + <string name="link_previews_alert_disable">停用</string> + <string name="close_behavior_dialog_title">要最小化到系統匣嗎?</string> + <string name="close_behavior_dialog_text">如果選擇「關閉」,將無法接收訊息。\n你可以稍後在「外觀」設定中變更。</string> + <string name="close_behavior_dialog_close">關閉應用程式</string> + <string name="close_behavior_dialog_minimize">最小化到系統匣</string> + <string name="tray_show">顯示 SimpleX</string> + <string name="tray_quit">結束 SimpleX</string> + <string name="tray_tooltip">SimpleX</string> + <string name="tray_tooltip_unread">SimpleX — %d 則未讀</string> + <string name="appearance_minimize_to_tray">關閉到系統匣</string> + <string name="appearance_minimize_to_tray_desc">在背景執行以接收訊息</string> + <string name="badge_supports_simplex">%s 支持 SimpleX Chat。</string> + <string name="badge_supported_simplex">%1$s 曾支持 SimpleX Chat。此徽章已於 %2$s 過期。</string> + <string name="badge_support_from_v7">你可以從應用程式 v7 開始支持 SimpleX。</string> + <string name="badge_invested">%s 投資了 SimpleX Chat 眾籌。</string> + <string name="badge_unverified_title">未驗證徽章</string> + <string name="badge_unverified_desc">無法驗證此徽章,可能並非真實。</string> + <string name="badge_unknown_key_title">無法驗證徽章</string> + <string name="badge_unknown_key_desc">此徽章使用此版本應用程式無法識別的金鑰簽署。請更新應用程式以驗證此徽章。</string> </resources> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_2.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_3.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/crowdfunding_4.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake_light.svg b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake_light.svg new file mode 100644 index 0000000000..cd6f033c62 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/assets/default/MR/images/own_stake_light.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <rect width="24" height="24" fill="none"/> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js index 7c0836960c..e6828817ee 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js @@ -3,7 +3,7 @@ useWorker = typeof window.Worker !== "undefined"; isDesktop = true; // Create WebSocket connection. -const socket = new WebSocket(`ws://${location.host}`); +const socket = new WebSocket(`ws://${location.host}${location.search}`); socket.addEventListener("open", (_event) => { console.log("Opened socket"); sendMessageToNative = (msg) => { @@ -192,4 +192,4 @@ function updateCallInfoView(state, description) { document.getElementById("state").innerText = state; document.getElementById("description").innerText = description; } -//# sourceMappingURL=ui.js.map \ No newline at end of file +//# sourceMappingURL=ui.js.map diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index f7a87e3ced..58260181cf 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -10,15 +10,18 @@ import java.io.* import java.net.URI actual val dataDir: File = File(desktopPlatform.dataPath) -actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } +// No deleteOnExit() here: a transient second instance also inits this val, and its exit +// would delete the shared folder while the primary runs. Registered in Main instead. +actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex") actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") actual val appFilesDir: File = filesDir actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers").also { it.mkdirs() } actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() } +// No deleteRecursively() here (see tmpDir): a second instance would wipe this shared +// folder while the primary runs. Cleaned in Main instead. actual val preferencesTmpDir = File(desktopPlatform.configPath, "tmp") - .also { it.deleteRecursively() } actual val chatDatabaseFileName: String = "simplex_v1_chat.db" actual val agentDatabaseFileName: String = "simplex_v1_agent.db" diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 8d26f2f085..d59677b726 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -13,9 +13,14 @@ import java.io.File import java.util.* import kotlin.math.max -internal val vlcFactory: MediaPlayerFactory by lazy { MediaPlayerFactory() } +// Serialize the two factory constructions: each MediaPlayerFactory() runs VLC native discovery via +// a JDK ServiceLoader, which is not thread-safe. Building both factories concurrently (e.g. vlcFactory +// on the render thread while vlcPreviewFactory is built on the preview thread) corrupts the ServiceLoader +// enumeration and throws NoSuchElementException from CompoundEnumeration.nextElement. +private val vlcFactoryLock = Any() +internal val vlcFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory() } } // No hardware acceleration - more secure for previews -internal val vlcPreviewFactory: MediaPlayerFactory by lazy { MediaPlayerFactory("--avcodec-hw=none") } +internal val vlcPreviewFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory("--avcodec-hw=none") } } actual class RecorderNative: RecorderInterface { private var player: MediaPlayer? = null diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/StartupError.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/StartupError.kt new file mode 100644 index 0000000000..c1dc7205ed --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/StartupError.kt @@ -0,0 +1,164 @@ +package chat.simplex.common.platform + +import com.sun.jna.* +import com.sun.jna.platform.win32.* +import com.sun.jna.platform.win32.WinDef.* +import com.sun.jna.platform.win32.WinUser.* +import kotlin.system.exitProcess + +// Windows-only: shows a startup error the jpackage launcher otherwise hides behind "Failed to launch +// JVM" (#4146); on Linux/Mac the error rethrown by the caller reaches stderr. Uses a native window, +// not Swing, because broken AWT initialization can be the cause. Laid out like a message box: an error +// icon and a message at the top, two clickable report links (the GitHub issue tracker and the support +// email) above a read-only, selectable, scrolling box with the stack trace, and an OK button. +fun showStartupError(e: Throwable) { + if (!desktopPlatform.isWindows()) return + try { + // Win32 edit controls only break lines on CRLF; normalize every line ending to one CRLF so the + // trace does not render as merged lines (the JVM's own separator is CRLF on Windows, LF elsewhere) + showWindowsErrorWindow("SimpleX failed to start", e.stackTraceToString().lines().joinToString("\r\n")) + } catch (_: Throwable) { + return // dialog failed to show; let the caller rethrow so the launcher's own box is the fallback + } + // The dialog was shown and dismissed. Exit cleanly so the jpackage launcher does not also show its + // own generic "Failed to launch JVM" box on top of ours (it shows that box on any nonzero exit). + exitProcess(0) +} + +private const val ISSUES_URL = "https://github.com/simplex-chat/simplex-chat/issues" +private const val SUPPORT_EMAIL = "chat@simplex.chat" + +// Win32 constants not defined in jna-platform +private const val ES_MULTILINE = 0x0004 +private const val ES_READONLY = 0x0800 +private const val ES_AUTOVSCROLL = 0x0040 +private const val WS_EX_CLIENTEDGE = 0x0200 +private const val CW_USEDEFAULT = 0x80000000.toInt() +private const val WM_SETTEXT = 0x000C +private const val WM_SETFONT = 0x0030 +private const val WM_COMMAND = 0x0111 +private const val WM_CTLCOLORSTATIC = 0x0138 +private const val EM_SETLIMITTEXT = 0x00C5 +private const val SS_ICON = 0x0003 +private const val SS_NOTIFY = 0x0100 +private const val STM_SETICON = 0x0170 +private const val COLOR_WINDOW = 5 +private const val WHITE_BRUSH = 0 +private const val LINK_COLOR = 0x00EE0000 // COLORREF 0x00BBGGRR = link blue RGB(0,0,238) +private const val IDI_ERROR = 32513 +private const val APP_ICON_ID = 1 // the app icon jpackage embeds in the launcher exe +private const val DEFAULT_GUI_FONT = 17 +private const val OK_BUTTON_ID = 1 +private const val URL_LINK_ID = 2 +private const val EMAIL_LINK_ID = 3 + +// Shows a modal-style native error dialog: error icon and message, two clickable report-link statics, +// a read-only selectable edit control with the stack trace, and an OK button. Blocks on its own +// message loop until the window is closed. +private fun showWindowsErrorWindow(title: String, trace: String) { + val user32 = User32.INSTANCE + val user32native = NativeLibrary.getInstance("user32") + val gdi32 = NativeLibrary.getInstance("gdi32") + val shellExecute = NativeLibrary.getInstance("shell32").getFunction("ShellExecuteW") + val sendMessageW = user32native.getFunction("SendMessageW") + val hInstance = Kernel32.INSTANCE.GetModuleHandle(null) + val className = "SimpleXStartupError" + + // Paint static backgrounds white to match the window, and draw the two link statics in blue + val whiteBrush = gdi32.getFunction("GetStockObject").invokePointer(arrayOf<Any?>(Integer.valueOf(WHITE_BRUSH))) + val setTextColor = gdi32.getFunction("SetTextColor") + // The link statics' HWNDs, set once created and read back in the paint callback + var urlLinkHwnd = 0L + var emailLinkHwnd = 0L + + fun open(target: String) = shellExecute.invokePointer(arrayOf<Any?>( + Pointer.NULL, WString("open"), WString(target), Pointer.NULL, Pointer.NULL, Integer.valueOf(SW_SHOWNORMAL))) + + val wndProc = WindowProc { hwnd, uMsg, wParam, lParam -> + when { + // A click on the OK button or a link static arrives as WM_COMMAND with the control id in LOWORD + uMsg == WM_COMMAND -> { + when (wParam.toInt() and 0xFFFF) { + OK_BUTTON_ID -> user32.DestroyWindow(hwnd) + URL_LINK_ID -> open(ISSUES_URL) // open the issue tracker in a browser + EMAIL_LINK_ID -> open("mailto:$SUPPORT_EMAIL") // open the mail client + } + LRESULT(0) + } + uMsg == WM_CTLCOLORSTATIC -> { + if (lParam.toLong() == urlLinkHwnd || lParam.toLong() == emailLinkHwnd) { + setTextColor.invokeInt(arrayOf<Any?>(Pointer.createConstant(wParam.toLong()), Integer.valueOf(LINK_COLOR))) + } + LRESULT(Pointer.nativeValue(whiteBrush)) + } + uMsg == WM_DESTROY -> { user32.PostQuitMessage(0); LRESULT(0) } + else -> user32.DefWindowProc(hwnd, uMsg, wParam, lParam) + } + } + + // Prefer the app's own icon so the title bar/taskbar is not the default "unknown" icon; fall back + // to the system error icon (also shown in the window below). + val hErrorIcon = user32native.getFunction("LoadIconW").invokePointer(arrayOf<Any?>(Pointer.NULL, Pointer.createConstant(IDI_ERROR))) + val hAppIcon = user32native.getFunction("LoadIconW").invokePointer(arrayOf<Any?>(hInstance, Pointer.createConstant(APP_ICON_ID))) + val windowIcon = hAppIcon ?: hErrorIcon // invokePointer returns null when the exe has no such icon + + val windowClass = WNDCLASSEX() + windowClass.cbSize = windowClass.size() + windowClass.lpfnWndProc = wndProc + windowClass.hInstance = hInstance + windowClass.lpszClassName = className + windowClass.hbrBackground = HBRUSH(Pointer.createConstant(COLOR_WINDOW + 1)) // system window (light) color + windowClass.hIcon = HICON(windowIcon) + windowClass.hIconSm = HICON(windowIcon) + user32.RegisterClassEx(windowClass) + + // If the window cannot be created, throw rather than enter the message loop: with no window, + // GetMessage would block forever. The caller's catch then rethrows and the launcher box is shown. + val window = user32.CreateWindowEx( + 0, className, title, WS_CAPTION or WS_SYSMENU or WS_VISIBLE, + CW_USEDEFAULT, CW_USEDEFAULT, 760, 520, + null, null, hInstance, null + ) ?: throw IllegalStateException("failed to create startup error window") + val client = RECT() + user32.GetClientRect(window, client) + val cw = client.right + val ch = client.bottom + + val iconView = user32.CreateWindowEx(0, "STATIC", null, WS_CHILD or WS_VISIBLE or SS_ICON, + 18, 18, 32, 32, window, null, hInstance, null) + user32.SendMessage(iconView, STM_SETICON, WPARAM(Pointer.nativeValue(hErrorIcon)), LPARAM(0)) + + user32.CreateWindowEx(0, "STATIC", "SimpleX could not start. Copy the error below and report it via:", + WS_CHILD or WS_VISIBLE, 60, 20, cw - 80, 20, window, null, hInstance, null) + val urlLink = user32.CreateWindowEx(0, "STATIC", ISSUES_URL, WS_CHILD or WS_VISIBLE or SS_NOTIFY, + 60, 46, cw - 80, 18, window, HMENU(Pointer.createConstant(URL_LINK_ID)), hInstance, null) + urlLinkHwnd = Pointer.nativeValue(urlLink.pointer) + val emailLink = user32.CreateWindowEx(0, "STATIC", SUPPORT_EMAIL, WS_CHILD or WS_VISIBLE or SS_NOTIFY, + 60, 68, cw - 80, 18, window, HMENU(Pointer.createConstant(EMAIL_LINK_ID)), hInstance, null) + emailLinkHwnd = Pointer.nativeValue(emailLink.pointer) + + val traceView = user32.CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", null, + WS_CHILD or WS_VISIBLE or WS_VSCROLL or ES_MULTILINE or ES_READONLY or ES_AUTOVSCROLL, + 18, 96, cw - 36, ch - 96 - 54, window, null, hInstance, null) + user32.SendMessage(traceView, EM_SETLIMITTEXT, WPARAM(0), LPARAM(0)) // do not truncate long traces + sendMessageW.invokePointer(arrayOf<Any?>(traceView, Integer.valueOf(WM_SETTEXT), Pointer.NULL, WString(trace))) + + val okButton = user32.CreateWindowEx(0, "BUTTON", "OK", WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON, + cw - 98, ch - 42, 80, 30, window, HMENU(Pointer.createConstant(OK_BUTTON_ID)), hInstance, null) + + // The default control font is the dated bitmap "System" font; use the standard dialog font + val hFont = gdi32.getFunction("GetStockObject").invokePointer(arrayOf<Any?>(Integer.valueOf(DEFAULT_GUI_FONT))) + for (view in listOf(urlLink, emailLink, traceView, okButton)) { + user32.SendMessage(view, WM_SETFONT, WPARAM(Pointer.nativeValue(hFont)), LPARAM(1)) + } + + user32.ShowWindow(window, SW_SHOWNORMAL) + user32.SetForegroundWindow(window) + user32.SetFocus(traceView) // so Ctrl+A / Ctrl+C act on the trace immediately + + val msg = MSG() + while (user32.GetMessage(msg, null, 0, 0) > 0) { + user32.TranslateMessage(msg) + user32.DispatchMessage(msg) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt index c3b6dc3a4c..768d2f421d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -7,6 +7,10 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.* import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface +import uk.co.caprica.vlcj.media.Media +import uk.co.caprica.vlcj.media.MediaEventAdapter +import uk.co.caprica.vlcj.media.MediaParsedStatus +import uk.co.caprica.vlcj.media.ParseFlag import uk.co.caprica.vlcj.media.VideoOrientation import uk.co.caprica.vlcj.player.base.* import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent @@ -255,6 +259,43 @@ actual class VideoPlayer actual constructor( return@withContext VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration) } + // Parsing a local container header takes a few dozen ms, this is only a guard against a stuck parse + private const val PARSE_TIMEOUT_MS = 3000L + + // Reads container metadata to tell whether there is a video track at all, without decoding a frame. + // libvlc signals the end of parsing with an event, so no polling or frame-decoding budget is needed. + suspend fun hasVideoTrack(uri: URI): Boolean = withContext(previewThread.asCoroutineDispatcher()) { + if (!uri.toFile().exists()) return@withContext false + val media = try { + vlcPreviewFactory.media().newMedia(uri.toFile().absolutePath) + } catch (e: Exception) { + Log.e(TAG, "hasVideoTrack unable to create media: ${e.stackTraceToString()}") + null + } ?: return@withContext false + try { + val parsed = CompletableDeferred<MediaParsedStatus?>() + media.events().addMediaEventListener(object: MediaEventAdapter() { + // vlcj maps an unknown status int to null, and a null here would throw on its event thread + override fun mediaParsedChanged(parsedMedia: Media?, newStatus: MediaParsedStatus?) { + parsed.complete(newStatus) + } + }) + if (!media.parsing().parse(PARSE_TIMEOUT_MS.toInt(), ParseFlag.PARSE_LOCAL)) { + return@withContext false + } + if (withTimeoutOrNull(PARSE_TIMEOUT_MS) { parsed.await() } != MediaParsedStatus.DONE) { + media.parsing().stop() + return@withContext false + } + media.info().videoTracks().isNotEmpty() + } catch (e: Exception) { + Log.e(TAG, "hasVideoTrack error: ${e.stackTraceToString()}") + false + } finally { + media.release() + } + } + val playerThread = Executors.newSingleThreadExecutor() private val previewThread = Executors.newSingleThreadExecutor() private val playersPool: ArrayList<Component> = ArrayList() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt index e9924914ef..3293d4f5bd 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt @@ -9,5 +9,6 @@ fun isVideo(uri: URI): Boolean { path.endsWith(".mp4") || path.endsWith(".mpg") || path.endsWith(".mpeg") || - path.endsWith(".mkv") + path.endsWith(".mkv") || + path.endsWith(".webm") } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index 20fe6a48a3..75782d75d7 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -18,10 +18,12 @@ import org.nanohttpd.protocols.http.response.Status import org.nanohttpd.protocols.websockets.* import java.io.IOException import java.net.BindException -import java.net.URI +import java.security.SecureRandom +import java.util.Base64 private const val SERVER_HOST = "localhost" private const val SERVER_PORT = 50395 +private const val CALL_SERVER_TOKEN_BYTES = 32 val connections = ArrayList<WebSocket>() // Spec: spec/services/calls.md#ActiveCallView @@ -153,14 +155,15 @@ private fun SendStateUpdates() { @Composable fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { val uriHandler = LocalUriHandler.current + val token = remember { newCallServerToken() } val endCall = { val call = chatModel.activeCall.value if (call != null) withBGApi { chatModel.callManager.endCall(call) } } val server = remember { - startServer(onResponse).apply { + startServer(onResponse, token = token).apply { try { - uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/") + uriHandler.openUri("http://${SERVER_HOST}:${listeningPort}/simplex/call/?token=$token") } catch (e: Exception) { Log.e(TAG, "Unable to open browser: ${e.stackTraceToString()}") AlertManager.shared.showAlertMsg( @@ -208,7 +211,11 @@ fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: ( } } -fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): NanoWSD { +fun startServer( + onResponse: (WVAPIMessage) -> Unit, + port: Int = SERVER_PORT, + token: String = newCallServerToken(), +): NanoWSD { val server = object: NanoWSD(SERVER_HOST, port) { override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session) @@ -227,8 +234,18 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na override fun handle(session: IHTTPSession): Response { return when { - session.headers["upgrade"] == "websocket" -> super.handle(session) - session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html") + session.headers["upgrade"] == "websocket" -> + if (hasValidCallServerToken(session.parameters, token)) { + super.handle(session) + } else { + unauthorizedResponse() + } + session.uri.contains("/simplex/call/") -> + if (hasValidCallServerToken(session.parameters, token)) { + resourcesToResponse("/desktop/call.html") + } else { + unauthorizedResponse() + } else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL")) } } @@ -239,11 +256,23 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit, port: Int = SERVER_PORT): Na if (port == 0) throw e Log.w(TAG, "Call server port $port is busy, using a random port: ${e.message}") server.stop() - return startServer(onResponse, port = 0) + return startServer(onResponse, port = 0, token = token) } return server } +internal fun newCallServerToken(): String { + val bytes = ByteArray(CALL_SERVER_TOKEN_BYTES) + SecureRandom().nextBytes(bytes) + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) +} + +internal fun hasValidCallServerToken(parameters: Map<String, List<String>>, token: String): Boolean = + token.isNotEmpty() && parameters["token"]?.any { it == token } == true + +private fun unauthorizedResponse(): Response = + newFixedLengthResponse(Status.UNAUTHORIZED, "text/plain", "Unauthorized") + class MyWebSocket(val onResponse: (WVAPIMessage) -> Unit, handshakeRequest: IHTTPSession) : WebSocket(handshakeRequest) { override fun onOpen() { connections.add(this) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt index a1f70213d0..c875c0c9da 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.desktop.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chatlist -import SectionDivider import androidx.compose.foundation.* import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.layout.* @@ -62,6 +61,6 @@ actual fun ChatListNavLinkLayout( if (selectedChat.value || nextChatSelected.value) { Divider() } else { - SectionDivider() + Divider(Modifier.padding(horizontal = 8.dp)) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt index eb93e7c510..4535857696 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt @@ -42,10 +42,9 @@ actual fun SavePassphraseSetting( } Text( stringResource(MR.strings.save_passphrase_in_settings), - Modifier.padding(end = 24.dp), + Modifier.weight(1f).padding(end = 24.dp), color = Color.Unspecified ) - Spacer(Modifier.fillMaxWidth().weight(1f)) DefaultSwitch( checked = useKeychain, onCheckedChange = onCheckedChange, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt index c4c34a1db9..e857ef91b1 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -29,6 +29,7 @@ import java.net.Proxy import java.nio.file.Files import java.nio.file.StandardCopyOption import kotlin.math.min +import kotlin.system.exitProcess data class SemVer( val major: Int, @@ -315,17 +316,23 @@ private suspend fun downloadAsset(asset: GitHubAsset) { call.execute().use { response -> response.body?.use { body -> body.byteStream().use { stream -> - createTmpFileAndDelete { file -> + val newFile = File(tmpDir, asset.name) + // On Windows the install path exits the app before it can delete the downloaded file, so a + // previous update's installer can be left in the temp dir; remove it here at the next + // download instead of letting it accumulate. Other platforms delete the file after install. + if (desktopPlatform.isWindows()) newFile.delete() + val partFile = File(tmpDir, "${asset.name}.part") + partFile.parentFile.mkdirs() + partFile.deleteOnExit() + try { // It's important to close output stream (with use{}), otherwise, Windows cannot rename the file - file.outputStream().use { output -> + partFile.outputStream().use { output -> stream.copyTo(output) } - val newFile = File(file.parentFile, asset.name) // Moving instead of renameTo: a bare rename can silently fail (returns false, ignored), - // and the enclosing createTmpFileAndDelete then deletes the only copy in its finally block, // leaving the user with an empty download dir. Files.move performs the same in-place rename // when possible, falls back to copy when it can't, and throws (handled below) on real failure. - Files.move(file.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + Files.move(partFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) AlertManager.shared.showAlertDialogButtonsColumn( generalGetString(MR.strings.app_check_for_updates_download_completed_title), @@ -365,6 +372,8 @@ private suspend fun downloadAsset(asset: GitHubAsset) { } } ) + } finally { + partFile.delete() } } } @@ -383,8 +392,12 @@ private fun chooseGitHubReleaseAssets(release: GitHubRelease): List<GitHubAsset> // No need to show download options for Flatpak users emptyList() } else if (desktopPlatform.isLinux() && !isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) { - // Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives) - release.assets.filter { it.name.lowercase().endsWith(".deb") } + // Show desktop .deb packages for the current architecture and user will choose the one that works on his system (for Debian derivatives) + val arch = if (desktopPlatform == DesktopPlatform.LINUX_AARCH64) "aarch64" else "x86_64" + release.assets.filter { asset -> + val name = asset.name.lowercase() + name.startsWith("simplex-desktop-") && name.endsWith("$arch.deb") + } } else { release.assets.filter { it.name == desktopPlatform.githubAssetName } } @@ -433,19 +446,12 @@ private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) { } } desktopPlatform.isWindows() -> { - val process = Runtime.getRuntime().exec("msiexec /i ${file.absolutePath}"/* /qb */).onExit().join() - val startedInstallation = process.exitValue() == 0 - if (!startedInstallation) { - Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") - // Failed to start installation. show directory with the file for manual installation - desktopOpenDir(file.parentFile) - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), - text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) - ) - file.delete() - } + // Launch the installer, then exit so our files are no longer locked. While the app runs it + // holds SimpleX.exe/JRE/DLLs open, which forces the MSI to defer replacement to a reboot and + // corrupts the upgrade (the app then fails to launch). Array form passes the path as a single + // argument so a space in the temp path does not break the command. + Runtime.getRuntime().exec(arrayOf("msiexec", "/i", file.absolutePath)) + exitProcess(0) } desktopPlatform.isMac() -> { // Default mount point if no other DMGs were mounted before diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt index 7341c6af23..79aaf5c727 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt @@ -14,10 +14,12 @@ import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.awt.FileDialog +import java.awt.event.ActionListener import java.io.File import javax.swing.JFileChooser import javax.swing.filechooser.FileFilter import javax.swing.filechooser.FileNameExtensionFilter +import javax.swing.plaf.basic.BasicFileChooserUI @Composable actual fun DefaultDialog( @@ -77,6 +79,13 @@ fun FrameWindowScope.FileDialogChooserMultiple( fileChooser.dialogTitle = title fileChooser.isMultiSelectionEnabled = allowMultiple && isLoad fileChooser.isAcceptAllFileFilterUsed = fileFilter == null + // Only install the glob bypass for the real file-save case (filename != null). When filename + // is null the dialog runs in DIRECTORIES_ONLY mode (e.g. "Save QR code as image"), where the + // Save button must approve the selected directory — the literal-filename handler would instead + // traverse into it (or no-op on an empty field), making directory selection impossible. + if (!isLoad && filename != null && desktopPlatform.isLinux()) { + installUnixSaveGlobBypass(fileChooser) + } if (fileFilter != null && fileFilterDescription != null) { fileChooser.addChoosableFileFilter(object: FileFilter() { override fun accept(file: File?): Boolean = fileFilter(file) @@ -120,6 +129,28 @@ fun FrameWindowScope.FileDialogChooserMultiple( } } +// Replace the Save button's action with a literal-filename handler. This bypasses JFileChooser's +// glob-on-save behaviour, which mis-handles '[' as a glob char on Unix (breaking filenames like +// '[1].pdf') and is not a feature of any native OS save dialog — macOS NSSavePanel and native +// Windows / Linux GTK / KDE save dialogs all treat the typed filename as a literal name. +private fun installUnixSaveGlobBypass(fc: JFileChooser) { + val ui = fc.ui as? BasicFileChooserUI ?: return + val original: ActionListener = ui.approveSelectionAction + val btn = ui.getDefaultButton(fc) ?: return + btn.removeActionListener(original) + btn.addActionListener { + val name = ui.fileName?.takeIf { it.isNotEmpty() } ?: return@addActionListener + val typed = File(name) + val target = if (typed.isAbsolute) typed else File(fc.currentDirectory, name) + if (target.isDirectory && fc.isTraversable(target)) { + fc.currentDirectory = target + } else { + fc.selectedFile = target + fc.approveSelection() + } + } +} + /* * Has graphic glitches on many Linux distributions, so use only on non-Linux systems. Also file filter doesn't work on Windows * */ diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index 8d69607c62..d4c42790d2 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -1,8 +1,6 @@ package chat.simplex.common.views.helpers -import androidx.compose.runtime.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight @@ -12,9 +10,10 @@ import chat.simplex.common.model.CIFile import chat.simplex.common.model.readCryptoFile import chat.simplex.common.platform.* import chat.simplex.common.simplexWindowState -import kotlinx.coroutines.delay +import java.awt.image.BufferedImage import java.io.ByteArrayInputStream import java.io.File +import java.io.IOException import java.net.URI import java.util.* import javax.imageio.ImageIO @@ -24,6 +23,15 @@ import kotlin.io.encoding.ExperimentalEncodingApi private val bStyle = SpanStyle(fontWeight = FontWeight.Bold) private val iStyle = SpanStyle(fontStyle = FontStyle.Italic) private val uStyle = SpanStyle(textDecoration = TextDecoration.Underline) +// Full-screen view target: the smaller side is kept at or above this (the larger side may stay bigger), matching Android +private const val MAX_IMAGE_DIMENSION = 4320 +// Chat render (getLoadedImage) target, matching Android's getLoadedImage target +private const val MAX_THUMBNAIL_DIMENSION = 1000 +// Source images larger than this on either side are rejected (bounds the decoder's per-scanline buffer) +private const val MAX_SOURCE_IMAGE_DIMENSION = 16384 +// Hard ceiling on the decoded raster in pixels, independent of aspect ratio, so an extreme-aspect image within the +// source cap can't blow up memory. Sized to the full-screen target's area (~18.7 MP -> ~75 MB at 4 bytes/px). +private const val MAX_DECODED_PIXELS = MAX_IMAGE_DIMENSION * MAX_IMAGE_DIMENSION private fun fontStyle(color: String) = SpanStyle(color = Color(color.replace("#", "ff").toLongOrNull(16) ?: Color.White.toArgb().toLong())) @@ -108,18 +116,6 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat AnnotatedString(text) } -@Composable -actual fun SetupClipboardListener() { - val clipboard = LocalClipboardManager.current - chatModel.clipboardHasText.value = clipboard.hasText() - LaunchedEffect(Unit) { - while (true) { - delay(1000) - chatModel.clipboardHasText.value = clipboard.hasText() - } - } -} - actual fun getAppFileUri(fileName: String): URI { val rh = chatModel.currentRemoteHost.value return if (rh == null) { @@ -146,10 +142,10 @@ actual suspend fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? return if (filePath != null) { loadedImageCache[filePath] ?: try { val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes() - val bitmap = getBitmapFromByteArray(data, false) - if (bitmap != null) (bitmap to data).also { loadedImageCache[filePath] = it } else null + val bitmap = decodeBoundedImage(data, MAX_THUMBNAIL_DIMENSION) + (bitmap to data).also { loadedImageCache[filePath] = it } } catch (e: Exception) { - Log.e(TAG, "Unable to read crypto file: " + e.stackTraceToString()) + Log.e(TAG, "Unable to load image: " + e.stackTraceToString()) null } } else { @@ -165,6 +161,8 @@ actual fun getFileSize(uri: URI): Long? = uri.toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = try { + // No dimension cap here: this path decodes user-picked local files (image picker, compose, save), + // not untrusted received attachments. The cap is applied in getBitmapFromByteArray (auto-rendered content). uri.inputStream().use { ImageIO.read(it).toComposeImageBitmap() } @@ -177,7 +175,7 @@ actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitma actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? = try { - ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap() + decodeBoundedImage(data, MAX_IMAGE_DIMENSION) } catch (e: Exception) { Log.e(TAG, "Error while encoding bitmap from byte array: ${e.stackTraceToString()}") if (withAlertOnException) showImageDecodingException() @@ -185,6 +183,56 @@ actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean null } +private fun decodeBoundedImage(data: ByteArray, maxDimension: Int): ImageBitmap = + decodeBoundedBufferedImage(data, maxDimension).toComposeImageBitmap() + +// Decodes downloaded/auto-rendered image bytes with bounded memory: rejects absurd source dimensions, +// then downsamples (like Android's inSampleSize) so even large legitimate images decode to a bounded raster. +internal fun decodeBoundedBufferedImage(data: ByteArray, maxDimension: Int): BufferedImage { + val stream = ImageIO.createImageInputStream(ByteArrayInputStream(data)) + ?: throw IOException("Unsupported image format") + stream.use { + val readers = ImageIO.getImageReaders(it) + if (!readers.hasNext()) throw IOException("Unsupported image format") + + val reader = readers.next() + try { + reader.input = it + val width = reader.getWidth(0) + val height = reader.getHeight(0) + if (!sourceDimensionsWithinLimits(width, height)) throw IOException("Image dimensions exceed limit") + val sampleSize = imageSampleSize(width, height, maxDimension) + val param = reader.defaultReadParam.apply { + if (sampleSize > 1) setSourceSubsampling(sampleSize, sampleSize, 0, 0) + } + return reader.read(0, param) ?: throw IOException("Unable to decode image") + } finally { + reader.dispose() + } + } +} + +internal fun sourceDimensionsWithinLimits(width: Int, height: Int): Boolean = + width in 1..MAX_SOURCE_IMAGE_DIMENSION && height in 1..MAX_SOURCE_IMAGE_DIMENSION + +// Power-of-two subsampling factor for bounded decoding. First keeps the smaller side at or above maxDimension +// (mirroring Android's calculateInSampleSize) so portrait images stay sharp instead of being over-subsampled by a +// larger-side cap; then bounds the total decoded pixels so an extreme aspect ratio can't exceed the memory ceiling. +internal fun imageSampleSize(width: Int, height: Int, maxDimension: Int): Int { + var sampleSize = 1 + if (height > maxDimension || width > maxDimension) { + val halfHeight = height / 2 + val halfWidth = width / 2 + while (halfHeight / sampleSize >= maxDimension && halfWidth / sampleSize >= maxDimension) { + sampleSize *= 2 + } + } + while ((width / sampleSize) * (height / sampleSize) > MAX_DECODED_PIXELS) { + sampleSize *= 2 + } + return sampleSize +} + // LALAL implement to support animated drawable actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null @@ -207,6 +255,8 @@ actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolea return VideoPlayer.getBitmapFromVideo(null, uri, withAlertOnException) } +actual suspend fun hasVideoTrack(uri: URI): Boolean = VideoPlayer.hasVideoTrack(uri) + @OptIn(ExperimentalEncodingApi::class) actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encode(this) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 66be736fca..6b36a3b1b2 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -1,10 +1,11 @@ package chat.simplex.common.views.usersettings +import CARD_PADDING import SectionBottomSpacer import SectionDividerSpaced -import SectionSpacer import SectionTextFooter import SectionView +import itemHPadding import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -23,7 +24,7 @@ import chat.simplex.common.model.CloseBehavior import chat.simplex.common.model.SharedPreference import chat.simplex.common.trayIsAvailable import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource @@ -82,10 +83,10 @@ fun AppearanceScope.AppearanceLayout( SectionDividerSpaced() ProfileImageSection() - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() FontScaleSection() - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() DensityScaleSection() SectionBottomSpacer() @@ -110,8 +111,8 @@ private fun MinimizeToTraySection() { @Composable fun DensityScaleSection() { val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) } - SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) { - Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + SectionView(stringResource(MR.strings.appearance_zoom), contentPadding = PaddingValues(horizontal = CARD_PADDING)) { + Row(Modifier.padding(vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) { Box(Modifier.size(50.dp) .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) .clip(RoundedCornerShape(percent = 22)) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index 5b4a044df3..174ad63c7a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -1,27 +1,21 @@ package chat.simplex.common.views.usersettings import SectionView -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier +import androidx.compose.runtime.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.AppUpdatesChannel -import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -actual fun SettingsSectionApp( +actual fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - SectionView(stringResource(MR.strings.settings_section_title_app)) { + SectionView { SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) val selectedChannel = remember { appPrefs.appUpdateChannel.state } val values = AppUpdatesChannel.entries.map { it to it.text } @@ -29,6 +23,8 @@ actual fun SettingsSectionApp( appPrefs.appUpdateChannel.set(it) setupUpdateChecker() } - AppVersionItem(showVersion) } } + +@Composable +actual fun AppShutdownItem() {} diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt new file mode 100644 index 0000000000..800c69f617 --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerAuthTest.kt @@ -0,0 +1,70 @@ +package chat.simplex.app + +import chat.simplex.common.views.call.startServer +import java.net.Socket +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +// Integration test for the desktop call server's token gate (the handle() enforcement), +// which the unit-level CallServerTokenTest does not exercise. +class CallServerAuthTest { + private val token = "integration-test-token" + // port = 0 binds a random free port, avoiding a clash with a real call server on SERVER_PORT + private val server = startServer(onResponse = {}, port = 0, token = token) + private val port get() = server.listeningPort + + @AfterTest + fun tearDown() = server.stop() + + @Test + fun testWebSocketUpgradeRejectedWithoutToken() { + assertEquals(401, requestStatus(webSocketUpgrade(path = "/"))) + } + + @Test + fun testWebSocketUpgradeRejectedWithWrongToken() { + assertEquals(401, requestStatus(webSocketUpgrade(path = "/?token=wrong"))) + } + + @Test + fun testWebSocketUpgradeAcceptedWithToken() { + assertEquals(101, requestStatus(webSocketUpgrade(path = "/?token=$token"))) + } + + @Test + fun testCallPageRejectedWithoutToken() { + assertEquals(401, requestStatus(get(path = "/simplex/call/"))) + } + + @Test + fun testCallPagePassesAuthGateWithToken() { + // Resource serving may differ in the test classpath, so assert only that the auth gate was passed (not 401) + assertNotEquals(401, requestStatus(get(path = "/simplex/call/?token=$token"))) + } + + private fun get(path: String): List<String> = listOf("GET $path HTTP/1.1", "Host: localhost:$port") + + private fun webSocketUpgrade(path: String): List<String> = + listOf( + "GET $path HTTP/1.1", + "Host: localhost:$port", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version: 13", + ) + + // Sends a raw HTTP request and returns the response status code from the status line. + private fun requestStatus(requestLines: List<String>): Int = + Socket("localhost", port).use { socket -> + socket.soTimeout = 5000 + socket.getOutputStream().apply { + write((requestLines.joinToString("\r\n") + "\r\n\r\n").toByteArray()) + flush() + } + val statusLine = socket.getInputStream().bufferedReader().readLine() ?: error("no response from call server") + statusLine.split(" ")[1].toInt() + } +} diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt new file mode 100644 index 0000000000..dc729b1ec2 --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/CallServerTokenTest.kt @@ -0,0 +1,29 @@ +package chat.simplex.app + +import chat.simplex.common.views.call.hasValidCallServerToken +import chat.simplex.common.views.call.newCallServerToken +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CallServerTokenTest { + @Test + fun testCallServerTokenRequiresExactTokenParameter() { + val token = "secret" + + assertTrue(hasValidCallServerToken(mapOf("token" to listOf(token)), token)) + assertFalse(hasValidCallServerToken(mapOf("token" to listOf("wrong")), token)) + assertFalse(hasValidCallServerToken(mapOf("x-token" to listOf(token)), token)) + assertFalse(hasValidCallServerToken(mapOf("token" to listOf(token)), "")) + } + + @Test + fun testCallServerTokenIsUrlSafe() { + val token = newCallServerToken() + + assertTrue(token.length >= 40) + assertFalse(token.contains("+")) + assertFalse(token.contains("/")) + assertFalse(token.contains("=")) + } +} diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ImageDecodeBoundsTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ImageDecodeBoundsTest.kt new file mode 100644 index 0000000000..3a3c1e4de8 --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ImageDecodeBoundsTest.kt @@ -0,0 +1,79 @@ +package chat.simplex.app + +import chat.simplex.common.views.helpers.decodeBoundedBufferedImage +import chat.simplex.common.views.helpers.imageSampleSize +import chat.simplex.common.views.helpers.sourceDimensionsWithinLimits +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.IOException +import javax.imageio.ImageIO +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ImageDecodeBoundsTest { + @Test + fun testSampleSizeKeepsSmallerSideAtTarget() { + // At or below the target on both sides -> no subsampling + assertEquals(1, imageSampleSize(4320, 4320, 4320)) + // Portrait phone screenshot at the chat-render target: the narrow side is already below the target, so it is kept + // at full resolution instead of being over-subsampled (this was the reported blurry-preview regression) + assertEquals(1, imageSampleSize(1179, 2556, 1000)) + // Only the larger side is over the target while the smaller side is tiny -> kept at full resolution + assertEquals(1, imageSampleSize(4321, 100, 4320)) + // Both sides well above the target -> halved until the smaller side approaches the target + assertEquals(2, imageSampleSize(8640, 8640, 4320)) + assertEquals(4, imageSampleSize(4000, 4000, 1000)) + } + + @Test + fun testSampleSizeCapsDecodedPixelsForExtremeAspectRatios() { + // Smaller-side semantics alone would keep these at full resolution (sampleSize 1, since the narrow side is below + // the target), decoding ~131 MB (thumbnail) / ~566 MB (full-screen) rasters. The decoded-pixel ceiling forces + // extra subsampling so memory stays bounded, even though the narrow side then drops below the target. + assertEquals(2, imageSampleSize(16384, 1999, 1000)) // 16384x1999 (~32.7 MP) -> 8192x999 (~8 MP) + assertEquals(4, imageSampleSize(16384, 8639, 4320)) // 16384x8639 (~141 MP) -> 4096x2159 (~8.8 MP) + // A large near-square image is bounded to the ceiling rather than left at the smaller-side target + assertEquals(4, imageSampleSize(16384, 16384, 4320)) // 8192x8192 (~67 MP) would exceed the ceiling -> 4096x4096 + } + + @Test + fun testSourceDimensionsRejectAbsurdSizes() { + assertTrue(sourceDimensionsWithinLimits(16384, 16384)) // generous upper bound, downsampled later + assertFalse(sourceDimensionsWithinLimits(16385, 1)) + assertFalse(sourceDimensionsWithinLimits(0, 1)) + } + + @Test + fun testDecodeRejectsImageExceedingSourceLimit() { + // Rejected at the dimension check before any full-size allocation + assertFailsWith<IOException> { decodeBoundedBufferedImage(encodePng(16385, 1), 4320) } + } + + @Test + fun testDecodeAcceptsElongatedImageWithinSourceLimit() { + // Long, thin image at the source-dimension boundary: the raster is small, so it is decoded at full size + // (like Android) rather than over-subsampled to fit the larger side under the target + val image = decodeBoundedBufferedImage(encodePng(16384, 8), 4320) + assertEquals(16384, image.width) + } + + @Test + fun testDecodeDownsamplesLargeImageInsteadOfRejecting() { + // Large image with both sides above the target: must decode (not reject) AND be subsampled - proves subsampling + // is honored so the decoded raster stays bounded, while keeping the smaller side at or above the target. + val image = decodeBoundedBufferedImage(encodePng(2400, 2400), 1000) + assertTrue(image.width < 2400, "expected downsampled width, got ${image.width}") + assertTrue(image.width >= 1000, "expected smaller side kept at or above target, got ${image.width}") + } + + private fun encodePng(width: Int, height: Int): ByteArray { + val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) + return ByteArrayOutputStream().use { out -> + ImageIO.write(image, "png", out) + out.toByteArray() + } + } +} diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/UtilsFileCopyTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/UtilsFileCopyTest.kt new file mode 100644 index 0000000000..3757b81565 --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/UtilsFileCopyTest.kt @@ -0,0 +1,70 @@ +package chat.simplex.app + +import chat.simplex.common.views.helpers.FileTooLargeException +import chat.simplex.common.views.helpers.copyInputStreamToFile +import java.io.ByteArrayInputStream +import java.io.InputStream +import kotlin.io.path.createTempFile +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class UtilsFileCopyTest { + @Test + fun testCopyInputStreamAllowsLimitBoundary() { + val dest = createTempFile().toFile() + val data = ByteArray(4) { it.toByte() } + + copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4) + + assertTrue(dest.exists()) + assertContentEquals(data, dest.readBytes()) + dest.delete() + } + + @Test + fun testCopyInputStreamRejectsAndDeletesOversizedOutput() { + val dest = createTempFile().toFile() + val data = ByteArray(5) { it.toByte() } + + assertFailsWith<FileTooLargeException> { + copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4) + } + + assertFalse(dest.exists()) + } + + @Test + fun testCopyInputStreamCopiesAcrossMultipleReads() { + val dest = createTempFile().toFile() + val data = ByteArray(20) { it.toByte() } + + // Delivered 3 bytes per read, so the copy loop runs many iterations and accumulates copied > 0 + copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 20) + + assertTrue(dest.exists()) + assertContentEquals(data, dest.readBytes()) + dest.delete() + } + + @Test + fun testCopyInputStreamRejectsAndDeletesWhenLimitCrossedMidStream() { + val dest = createTempFile().toFile() + val data = ByteArray(10) { it.toByte() } + + // With 3-byte reads and a 7-byte limit, 6 bytes are written before the next read would exceed it + assertFailsWith<FileTooLargeException> { + copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 7) + } + + assertFalse(dest.exists()) + } + + // Returns at most chunkSize bytes per read to force the copy loop to iterate, regardless of buffer size + private fun chunkedStream(data: ByteArray, chunkSize: Int): InputStream = + object : ByteArrayInputStream(data) { + override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, minOf(len, chunkSize)) + } +} diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 8f072539e8..7c7455c7f3 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -48,6 +48,14 @@ compose { // 'jdk.unsupported' is for vlcj modules("jdk.zipfs", "jdk.unsupported") } + val os = System.getProperty("os.name", "generic").toDefaultLowerCase() + // 'jdk.accessibility' provides Java Access Bridge on Windows - without it the app + // fails to start with "Failed to launch JVM" when assistive technologies are enabled + // in the system (see #4146). Packages are always built on the target OS, so only + // the Windows build needs to bundle it. + if (os.contains("win")) { + modules("jdk.accessibility") + } //includeAllModules = true outputBaseDir.set(project.file("../release")) appResourcesRootDir.set(project.file("../build/links")) @@ -97,7 +105,6 @@ compose { } } } - val os = System.getProperty("os.name", "generic").toDefaultLowerCase() if (os.contains("mac") || os.contains("win")) { packageName = "SimpleX" } else { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 338660b746..e407448d1b 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -20,16 +20,29 @@ import kotlinx.coroutines.* import java.io.File fun main() { - if (!acquireSingleInstance()) return - // Disable hardware acceleration - //System.setProperty("skiko.renderApi", "SOFTWARE") - initHaskell() - runMigrations() - setupUpdateChecker() - initApp() - tmpDir.deleteRecursively() - tmpDir.mkdir() - return showApp() + try { + if (!acquireSingleInstance()) return + // Clean shared temp dirs only in the owning instance (not in a Files.desktop val + // initializer, which a transient second instance would also run). Early: before settings writes. + preferencesTmpDir.deleteRecursively() + // Disable hardware acceleration + //System.setProperty("skiko.renderApi", "SOFTWARE") + initHaskell() + runMigrations() + setupUpdateChecker() + initApp() + tmpDir.deleteRecursively() + tmpDir.mkdir() + // Only the owning instance cleans tmpDir on exit (see preferencesTmpDir above). + tmpDir.deleteOnExit() + // showApp is inside the try: its first statements (SystemTray probe, Compose setup) are the + // process's first AWT init, which is itself a known startup failure cause (#4146). Crashes + // after the window appears are handled by the WindowExceptionHandler in showApp instead. + return showApp() + } catch (e: Throwable) { + showStartupError(e) // the jpackage launcher otherwise hides the error behind "Failed to launch JVM" (#4146) + throw e + } } @OptIn(ExperimentalComposeUiApi::class) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index d0cc299697..3f3bbfa31b 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5.6 -android.version_code=358 +android.version_name=7.0 +android.version_code=366 -android.bundle=false - -desktop.version_name=6.5.6 -desktop.version_code=148 +desktop.version_name=7.0 +desktop.version_code=155 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/apps/multiplatform/spec/architecture.md b/apps/multiplatform/spec/architecture.md index cfef4d06c2..9911a2670f 100644 --- a/apps/multiplatform/spec/architecture.md +++ b/apps/multiplatform/spec/architecture.md @@ -370,6 +370,8 @@ var platform: PlatformInterface = object : PlatformInterface {} | `androidCreateActiveCallState()` | empty `Closeable` | Create `ActiveCallState` | | `androidIsXiaomiDevice()` | `false` | Check device brand | | `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` | +| `androidIsPlayStoreBuild` | `false` | `BuildConfig.PLAY_STORE` | +| `androidLoadPlayStoreCountry()` | no-op | Request the Play account country (google flavor only) | | `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` | | `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog | | `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) | diff --git a/apps/multiplatform/spec/client/navigation.md b/apps/multiplatform/spec/client/navigation.md index c9939ea3c0..34a3b502ee 100644 --- a/apps/multiplatform/spec/client/navigation.md +++ b/apps/multiplatform/spec/client/navigation.md @@ -105,8 +105,7 @@ fun MainScreen() When onboarding is complete: 1. Shows "advertise lock" alert if conditions met (not shown before, LA not enabled, >3 chats, no active call). -2. Sets up clipboard listener. -3. Routes to `AndroidScreen` or `DesktopScreen` based on platform. +2. Routes to `AndroidScreen` or `DesktopScreen` based on platform. ### Overlay Layers (bottom of MainScreen) diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md index 09457c4dd3..229c30d18e 100644 --- a/apps/multiplatform/spec/state.md +++ b/apps/multiplatform/spec/state.md @@ -155,7 +155,6 @@ Defined at [`ChatModel.kt line 86`](../common/src/commonMain/kotlin/chat/simplex | [`notificationPreviewMode`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L147) | `MutableState<NotificationPreviewMode>` | 147 | Notification content preview level | | [`showAuthScreen`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L156) | `MutableState<Boolean>` | 156 | Whether to show authentication screen | | [`showChatPreviews`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L158) | `MutableState<Boolean>` | 158 | Whether to show chat preview text in list | -| [`clipboardHasText`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L185) | `MutableState<Boolean>` | 185 | System clipboard has text content | | [`networkInfo`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L186) | `MutableState<UserNetworkInfo>` | 186 | Network type and online status | | [`conditions`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L188) | `MutableState<ServerOperatorConditionsDetail>` | 188 | Server operator terms/conditions | | [`updatingProgress`](../common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt#L190) | `MutableState<Float?>` | 190 | Progress indicator for app updates | diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index ff853f403d..893e687d78 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -94,5 +94,7 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName, allowFiles = False} + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False, clientService = False}, + userDisplayName = Nothing, + userImageFile = Nothing } diff --git a/apps/simplex-directory-service/README.md b/apps/simplex-directory-service/README.md index b64e018adb..5c397b6492 100644 --- a/apps/simplex-directory-service/README.md +++ b/apps/simplex-directory-service/README.md @@ -1,5 +1,309 @@ # SimpleX Directory Service -The service is currently a chat bot that allows to register and search for groups. +Chat bot for registering and searching groups. Superusers and admins are configured via CLI flags. -Superusers are configured via CLI options. +--- + +## Prerequisites + +- GHC 9.6.3 — install via [GHCup](https://www.haskell.org/ghcup/) +- Cabal 3.10.2+ + +--- + +## Building + +```sh +git clone https://github.com/simplex-chat/simplex-chat +cd simplex-chat +# OpenSSL build configuration (required) — copy the file for your OS: +cp scripts/cabal.project.local.linux cabal.project.local # on macOS: scripts/cabal.project.local.mac +cabal build simplex-directory-service +``` + +See [docs/CONTRIBUTING.md](../../docs/CONTRIBUTING.md) for the full build setup (toolchain, OpenSSL headers, branch compatibility). + +Find the compiled binary with: +```sh +cabal list-bin simplex-directory-service +``` + +Or run directly without installing: +```sh +cabal run simplex-directory-service -- <flags> +``` + +--- + +## Running the Bot + +### Getting your contact ID (bootstrap) + +`--super-users` is required and takes one or more `CONTACT_ID:DISPLAY_NAME` pairs. On a fresh database you don't yet know your contact ID, so start the bot with `--run-cli` and a placeholder, connect to it, then look up your real ID: + +```sh +simplex-directory-service --run-cli --super-users 999:nobody --database /path/to/db +# on startup the bot prints (and, unless --no-address, creates) its contact address: +# "Bot's contact address is: simplex:/contact#..." +# connect to it from your SimpleX Chat app, then in the bot terminal: +/contacts # lists connected contacts by name +/i alice # shows full info for contact "alice", including their contact ID +# note the ID, then restart without --run-cli: +simplex-directory-service --super-users 2:alice --database /path/to/db +``` + +In each `--super-users` / `--admin-users` pair both `contactId` and `localDisplayName` must match the contact exactly. (A mismatched name doesn't stop the bot from sending admin notifications to that contact ID, but commands from that contact will be rejected.) + +### Minimal run + +```sh +simplex-directory-service \ + --super-users 2:alice \ + --database /path/to/db +``` + +### Flags + +`simplex-directory-service` also accepts the standard SimpleX Chat core options (custom SMP/XFTP servers, `--socks-proxy`, network settings, etc.) — run `simplex-directory-service --help` for the complete list. The directory-specific options are: + +| Flag | Default | Description | +|---|---|---| +| `--super-users ID:NAME[,...]` | *(required)* | Super-user contacts (comma-separated) | +| `--admin-users ID:NAME[,...]` | none | Admin-only contacts (comma-separated) | +| `--owners-group ID:NAME` | none | Group (by group ID) that owners of listed groups are invited into — automatically on listing, or via `/invite` | +| `-d / --database PATH` | `~/.simplex/simplex_directory_service` | Database file path prefix | +| `--directory-file PATH` | none | Append-only log of directory state (see [Directory state log](#directory-state-log)) | +| `--migrate-directory-file check\|import\|export\|listing` | — | Check, import (log → DB), export (DB → log), or regenerate listing files, then exit | +| `--web-folder PATH` | none | Write static listing JSON + group images here (see [Hosting the directory page](#hosting-the-directory-page)) | +| `--no-address` | off | Skip checking/creating the bot's contact address | +| `--service-name NAME` | `SimpleX Directory` | Bot display name (without `*` characters) | +| `--profile-name-limit N` | unlimited | Max display-name length allowed to connect / join groups (used by the `name` join filter) | +| `--blocked-words-file PATH` | none | Words not allowed in profiles (used by the `name` join filter and profile review) | +| `--blocked-fragments-file PATH` | none | Word fragments not allowed in profiles | +| `--blocked-extenstion-rules PATH` | none | Substitution rules that expand the blocked-words list (the flag is spelled this way in the binary) | +| `--name-spelling-file PATH` | none | Character-substitution rules for matching disguised names | +| `--captcha-generator PATH` | none | Executable that renders a captcha image; without it captchas are sent as plain text | +| `--voice-captcha-generator PATH` | none | Executable that renders a voice captcha | +| `--run-cli` | off | Run an interactive CLI alongside the bot (useful for bootstrap) | +| `-v / --version` | — | Print version and exit | + +--- + +## Directory State Log + +`--directory-file` keeps an append-only log of every change to directory state (registrations, status changes, promotions) alongside the SQLite database. It is optional but recommended for operability: it is human-inspectable and can be checked, exported, or re-imported with `--migrate-directory-file`. Without it, directory state lives only in the database. + +--- + +## Hosting the Directory Page + +The `web/` folder contains a ready-to-use page (`directory.html`) that renders a bot's directory as a searchable, paginated list — no build step. Dark mode follows the system preference. + +A few things to know before deploying it: + +- **Copy the files out of the repo.** `web/directory.js` is a symlink to `website/src/js/directory.js` (kept in sync with the main website), so don't edit it in place — copy `directory.html` and the *contents* of `directory.js` to your web root and edit the copy. The data URL can't be overridden from `directory.html` because `directory.js` declares it as a top-level `const`. +- **Serve it under a `/directory` path.** `directory.js` only initialises when the page path starts with `/directory` (e.g. `https://example.com/directory.html` or `https://example.com/directory/`). +- **Provide the fallback image.** Groups without a profile image (and any image that fails to load) fall back to `/img/group.svg`, resolved from the site root — copy `website/src/img/group.svg` to `<web-root>/img/group.svg`. +- **Point it at your bot's data.** Near the bottom of `directory.js`, change: + ```js + const simplexDirectoryDataURL = 'https://your-domain.example/data/'; + ``` + (the default is the official directory, `https://directory.simplex.chat/data/`). The page fetches `listing.json` from that URL and loads group images relative to it. +- **Optional:** the "Also available as a SimpleX chat bot" link in `directory.html` points at the official directory bot — update it to your bot's address. + +Then run the bot with `--web-folder` pointing at the folder served at that URL. The bot writes `listing.json` (used by the page), `promoted.json` (the promoted subset — written but not rendered by the bundled page), and group images, refreshing every 5 minutes and immediately when a group is approved or its listing/promotion status changes: +```sh +simplex-directory-service \ + --super-users 2:alice \ + --database /path/to/db \ + --web-folder /var/www/your-domain.example/data +``` + +--- + +## Command Reference + +This is a reference for all commands accepted by the SimpleX Directory bot. For a guided walkthrough of group submission see [DIRECTORY.md](../../docs/DIRECTORY.md). + +--- + +### 1. Searching + +The bot sends a welcome message automatically when you connect. + +| Action | Syntax | Effect | +|---|---|---| +| Search | `<text>` | Returns up to 10 groups whose name or welcome message matches; sorted by member count | +| Next page | `/next` or `.` | Next page of the most recent search (after ~5 min of inactivity, or with no recent search, falls back to listing all groups) | +| Recent groups | `/new` | Groups added most recently | +| All groups | `/all` | All listed groups, sorted by member count | +| Help | `/help [registration\|r\|commands\|c]`, or `/h` | Show registration or commands help (default: registration) | + +--- + +### 2. Registering a Group + +Registration is a three-step process — see [DIRECTORY.md](../../docs/DIRECTORY.md) for full details: + +1. Invite the directory bot to your group as `admin`. +2. Add the link the bot sends you to the group's welcome message. +3. Wait for admin approval (usually within a day, except holidays). + +If a group with the same display name is already registered (but not yet listed or suspended), the bot asks you to confirm with `/confirm`. If the name is already listed or suspended in the directory, registration is blocked. + +--- + +### 3. Managing Your Groups (user commands) + +> **Note on `<ID>` vs `<ID>:<name>`:** Commands shown with `<ID>[:<name>]` (`/role`, `/filter`, `/link`) accept just the ID — the name is optional. `/confirm`, `/delete`, and all admin/super-user commands require both, written as `<ID>:<name>`. For user commands the ID is the registration ID shown by `/list`; for admin and super-user commands it's the group ID included in the bot's admin notifications. When the bot expects an `<ID>:<name>` argument it normally quotes the whole command for you (e.g. for `/confirm` and `/approve`), so you can copy it directly; for `/delete` you build it from the ID and name shown by `/list`. + +#### List groups + +``` +/list +/ls +``` + +Shows all groups you have registered, with their current status, member count, and the `/role` and `/filter` commands for each. + +#### Confirm duplicate name + +``` +/confirm <ID>:<name> +``` + +When you invite the bot to a group whose display name matches an already-registered group, the bot pauses registration and asks for explicit confirmation. This prevents accidental registration of another group that shares a name. `/confirm` acknowledges the duplicate and proceeds with registration. `ID` and `name` are provided in the bot's prompt. + +If a group with the same name is already *listed* or *suspended* in the directory, registration is blocked entirely and `/confirm` is not offered. + +#### View or set default join role + +``` +/role <ID>[:<name>] [member|observer] +``` + +Omit the role argument to view the current setting. `member` (default) lets new joiners post immediately; `observer` makes them read-only until promoted. + +#### View or configure anti-spam filter + +``` +/filter <ID>[:<name>] [preset | flags] +``` + +Omit the argument to view the current filter. Filters apply to people joining via the directory-managed link. + +**Presets** (mutually exclusive): + +| Preset | Effect | +|---|---| +| `off` | No filter | +| `basic` | For profiles without an image: reject long or inappropriate names | +| `moderate` (or `mod`) | Reject long/inappropriate names from all profiles; require captcha from profiles without an image | +| `strong` | Reject long/inappropriate names from all profiles; require captcha from all profiles | + +**Flags** (combine freely): + +``` +/filter <ID>[:<name>] [name[=all|=noimage]] [captcha[=all|=noimage]] [observer[=all|=noimage]] +``` + +| Flag | Condition | Effect | +|---|---|---| +| `name` | `=all` (default) or `=noimage` | Reject joins from profiles whose name is too long (`--profile-name-limit`) or contains blocked words/fragments (`--blocked-words-file` / `--blocked-fragments-file`) | +| `captcha` | `=all` (default) or `=noimage` | Require the joiner to solve a captcha | +| `observer` | `=all` (default) or `=noimage` | Make new members observers instead of members | + +`=noimage` means the condition applies only to profiles that have no profile image (`=no_image` and `=no-image` are also accepted). + +> The `name` filter only has an effect when the bot was started with `--profile-name-limit` and/or `--blocked-words-file` / `--blocked-fragments-file`; otherwise it does nothing. The `captcha` filter sends a much stronger challenge when `--captcha-generator` (or `--voice-captcha-generator`) is configured — without it the captcha text is sent as a plain message. + +#### View or upgrade group link + +``` +/link <ID>[:<name>] +``` + +Shows the current directory-managed join link. If the link is outdated the bot upgrades it. + +#### Remove group from directory + +``` +/delete <ID>:<name> +``` + +Permanently removes the group from the directory. The group can be re-registered later. + +--- + +### 4. Admin Commands + +Admins receive a notification whenever a group enters the PendingApproval state. The `<approval-id>` in `/approve` is the integer from that notification (or from `/pending`); it increments each time a group re-enters the PendingApproval state so that stale approvals are rejected. + +| Command | Syntax | Effect | +|---|---|---| +| Approve | `/approve <ID>:<name> <approval-id> [promote=on\|off]` | List group in directory; notifies owner | +| Suspend | `/suspend <ID>:<name>` | Hide group from directory; notifies owner | +| Resume | `/resume <ID>:<name>` | Re-list a suspended group; notifies owner | +| List recent | `/last [N]` | Show last N registered groups (default: 10) | +| List pending | `/pending [N]` | Show N groups awaiting approval (default: 10) | +| Message owner | `/owner <ID>:<name> <message>` | Forward a message to the group owner | +| Reject | `/reject <ID>:<name>` | *(Reserved, currently a no-op)* | +| Invite owner | `/invite <ID>:<name>` | Invite the group owner to the owners' group (requires `--owners-group`) | + +--- + +### 5. Super-User Commands + +| Command | Syntax | Effect | +|---|---|---| +| Feature group | `/promote <ID>:<name> on\|off` | Add or remove group from the promoted listing (`promoted.json`) | +| Execute API command | `/exec <command>` or `/x <command>` | Run a raw SimpleX Chat API command | + +--- + +### 6. Group Lifecycle + +Forward path, from invitation to being listed: + +``` + Invited by owner + │ + ┌────────────┴────────────┐ + unique name duplicate name* + │ │ + │ ▼ + │ PendingConfirmation + │ │ owner runs /confirm + └────────────┬─────────────┘ + ▼ + Proposed + │ bot joins the group and creates the link + ▼ + PendingUpdate + │ owner adds the link to the group welcome + ▼ + PendingApproval + │ admin runs /approve + ▼ + Active (listed; visible in search) +``` + +**Transitions out of Active:** + +- → **PendingUpdate** — the directory bot link is removed from the welcome message. +- → **PendingApproval** — most other profile changes (see ** below); the `approval-id` shown to admins is bumped each time, so stale `/approve` commands are rejected. +- → **Suspended** — an admin runs `/suspend`; `/resume` re-lists the group. +- → **SuspendedBadRoles** — the directory bot loses its `admin` role, or the registering owner loses their `owner` role, in the group; automatically restored to **Active** once the roles are corrected. +- → **Removed** — the owner runs `/delete`, the owner is removed from or leaves the group, the bot is removed from the group, or the group is deleted. The group can be re-registered afterwards. + +\* Only when the duplicate is registered but not yet listed or suspended. If the name is already listed or suspended, registration is blocked entirely. + +\*\* Profile changes only trigger re-approval when fields other than the directory bot link are modified. If the only change is swapping the old bot link for the new one, or changing only whitespace in the description, the group stays Active. + +**State notes:** + +- **PendingConfirmation** — the bot was invited but a group with the same display name is already registered (in a pending state); the owner must run `/confirm` to proceed. +- **Proposed** — the name is unique (or the duplicate was confirmed via `/confirm`); the bot is joining the group. +- **PendingUpdate** — the bot has joined the group and created the join link; the owner must add it to the group's welcome message. +- **PendingApproval** — submitted for admin review. The join link works even before approval. +- **Active** — listed in the directory and visible in search results. diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index bfbc025a49..3bff611a28 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -27,7 +27,7 @@ import qualified Data.Attoparsec.Text as A import Data.Char (isSpace) import Data.Either (fromRight) import Data.Functor (($>)) -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) @@ -38,6 +38,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink, MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences (GroupFeature) import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) import Simplex.Messaging.Encoding.String @@ -52,6 +53,7 @@ data DirectoryEvent | DEGroupLinkCheck GroupInfo | DEPendingMember GroupInfo GroupMember | DEPendingMemberMsg GroupInfo GroupMember ChatItemId Text + | DEGroupItemProhibited GroupInfo GroupMember ChatItemId GroupFeature -- a member posted content prohibited by the group's settings | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole | DEContactRemovedFromGroup ContactId GroupInfo @@ -84,8 +86,10 @@ crDirectoryEvent_ = \case CEvtJoinedGroupMember {groupInfo, member = m} | pending m -> Just $ DEPendingMember groupInfo m | otherwise -> Nothing - CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of + CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g scopeInfo) ci : _} -> case ci of ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t + -- only moderate prohibited content in the main group, not in member-support/onboarding scope + ChatItem {chatDir = CIGroupRcv m, content = CIRcvGroupFeatureRejected gf} | isNothing scopeInfo -> Just $ DEGroupItemProhibited g m (chatItemId' ci) gf _ -> Nothing CEvtMemberRole {groupInfo, member, toRole} | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole diff --git a/apps/simplex-directory-service/src/Directory/Listing.hs b/apps/simplex-directory-service/src/Directory/Listing.hs index ef093020bb..d2df341545 100644 --- a/apps/simplex-directory-service/src/Directory/Listing.hs +++ b/apps/simplex-directory-service/src/Directory/Listing.hs @@ -70,6 +70,7 @@ $(JQ.deriveJSON defaultJSON ''PublicLink) data DirectoryEntry = DirectoryEntry { entryType :: DirectoryEntryType, displayName :: Text, + simplexName :: Maybe Text, groupLink :: PublicLink, shortDescr :: Maybe MarkdownList, welcomeMessage :: Maybe MarkdownList, @@ -97,7 +98,7 @@ recentRoundedTime roundTo now t in Just $ systemToUTCTime $ MkSystemTime secs 0 groupDirectoryEntry :: UTCTime -> GroupInfo -> Maybe GroupLink -> Maybe (DirectoryEntry, Maybe (FilePath, ImageFileData)) -groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ = +groupDirectoryEntry now g@GroupInfo {groupProfile, chatTs, createdAt, groupSummary} gLink_ = let GroupProfile {displayName, shortDescr, description, image, memberAdmission, publicGroup} = groupProfile gt = (\PublicGroupProfile {groupType} -> groupType) <$> publicGroup entryType = DETGroup gt memberAdmission groupSummary @@ -112,6 +113,7 @@ groupDirectoryEntry now GroupInfo {groupProfile, chatTs, createdAt, groupSummary DirectoryEntry { entryType, displayName, + simplexName = shortNameInfoStr . SimplexNameInfo NTPublicGroup <$> verifiedGroupDomain g, groupLink, shortDescr = toFormattedText <$> shortDescr, welcomeMessage = toFormattedText <$> description', diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index f566ed5ded..2c94152a1c 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -39,10 +39,14 @@ data DirectoryOpts = DirectoryOpts directoryLog :: Maybe FilePath, migrateDirectoryLog :: Maybe MigrateLog, serviceName :: T.Text, + clientService :: Bool, runCLI :: Bool, searchResults :: Int, webFolder :: Maybe FilePath, linkCheckInterval :: Int, + prohibitedToObserver :: Bool, + alwaysCaptcha :: Bool, + knocking :: Bool, testing :: Bool } @@ -151,6 +155,11 @@ directoryOpts appDir defaultDbName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX Directory)" <> value "SimpleX Directory" ) + clientService <- + switch + ( long "client-service" + <> help "Use client service certificate" + ) runCLI <- switch ( long "run-cli" @@ -171,6 +180,21 @@ directoryOpts appDir defaultDbName = do <> help "Interval in seconds to check public group link data (default: 1800)" <> value 1800 ) + prohibitedToObserver <- + switch + ( long "prohibited-to-observer" + <> help "Set a member to observer (and delete the message) when they post content prohibited by the group's settings" + ) + alwaysCaptcha <- + switch + ( long "always-captcha" + <> help "Require a captcha from joining members in all groups, regardless of per-group filter settings" + ) + knocking <- + switch + ( long "knocking" + <> help "Require admin review (knocking) before joining members are admitted in all groups, regardless of group preference" + ) pure DirectoryOpts { coreOptions, @@ -188,10 +212,14 @@ directoryOpts appDir defaultDbName = do directoryLog, migrateDirectoryLog, serviceName = T.pack serviceName, + clientService, runCLI, searchResults = 10, webFolder, linkCheckInterval, + prohibitedToObserver, + alwaysCaptcha, + knocking, testing = False } @@ -207,7 +235,7 @@ getDirectoryOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: DirectoryOpts -> ChatOpts -mkChatOpts DirectoryOpts {coreOptions, serviceName} = +mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} = ChatOpts { coreOptions, chatCmd = "", @@ -221,7 +249,9 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False} + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService}, + userDisplayName = Nothing, + userImageFile = Nothing } parseMigrateLog :: ReadM MigrateLog diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 63e1a0ff69..a3af872ac3 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -64,11 +64,12 @@ import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) -import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), sameConnReqContact, sameShortLinkContact) +import Simplex.Chat.View (groupSimplexDomain, serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) +import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ACreatedConnLink (..), AgentErrorType (..), ConnectionLink (..), CreatedConnLink (..), SConnectionMode (..), SimplexDomain, sameConnReqContact, sameShortLinkContact) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol (ErrorType (..)) +import Simplex.Messaging.SimplexName (SimplexNameInfo (..), SimplexNameType (..), shortNameInfoStr) import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Util (eitherToMaybe, raceAny_, safeDecodeUtf8, tshow, unlessM, (<$$>)) @@ -271,7 +272,7 @@ directoryService st opts cfg = do acceptMemberHook :: DirectoryOpts -> ServiceState -> GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)) acceptMemberHook - DirectoryOpts {profileNameLimit} + DirectoryOpts {profileNameLimit, alwaysCaptcha, knocking} ServiceState {blockedWordsCfg} g GroupLinkInfo {memberRole} @@ -280,7 +281,8 @@ acceptMemberHook when (useMemberFilter img $ rejectNames a) checkName pure $ if - | useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) + | knocking -> (GAPendingReview, memberRole) + | alwaysCaptcha || useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember) | useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver) | otherwise -> (GAAccepted, memberRole) where @@ -294,6 +296,11 @@ acceptMemberHook groupMemberAcceptance :: GroupInfo -> DirectoryMemberAcceptance groupMemberAcceptance GroupInfo {customData} = (\DirectoryGroupData {memberAcceptance = ma} -> ma) $ fromCustomData customData +recommendedSettingsNotice :: UserGroupRegId -> Text +recommendedSettingsNotice userGroupId = + "We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them.\n\ + \Captcha verification is enabled. Use /'filter " <> tshow userGroupId <> "' to change it." + useMemberFilter :: Maybe ImageData -> Maybe ProfileCondition -> Bool useMemberFilter img_ = \case Just PCAll -> True @@ -311,7 +318,7 @@ readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, na pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling} directoryServiceEvent :: DirectoryLog -> DirectoryOpts -> ServiceState -> User -> ChatController -> DirectoryEvent -> IO () -directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc = \case +directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults, prohibitedToObserver, alwaysCaptcha} env@ServiceState {searchRequests} user@User {userId} cc = \case DEContactConnected ct -> deContactConnected ct DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner @@ -319,6 +326,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName DEGroupLinkCheck g -> deGroupLinkCheck g DEPendingMember g m -> dePendingMember g m DEPendingMemberMsg g m ciId t -> dePendingMemberMsg g m ciId t + DEGroupItemProhibited g m ciId gf -> when prohibitedToObserver $ deGroupItemProhibited g m ciId gf DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role DEServiceRoleChanged g role -> deServiceRoleChanged g role DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g @@ -354,7 +362,15 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let msg = "Error: " <> err <> ", group: " <> tshow groupId <> " " <> localDisplayName <> ", " <> T.pack e notifyAdminUsers msg logError msg - groupInfoText p@GroupProfile {description = d, publicGroup} = groupNameDescr p <> maybe "" ("\nWelcome message:\n" <>) d <> linkToJoin + verifyGroupDomain_ :: GroupInfo -> IO GroupInfo + verifyGroupDomain_ g@GroupInfo {groupId} + | isJust (groupSimplexDomain g) = + sendChatCmd cc (APIVerifyGroupDomain groupId) >>= \case + Right CRGroupDomainVerified {groupInfo = g'} -> pure g' + Right r -> g <$ logError ("verifyGroupDomain_: unexpected response " <> tshow r) + Left e -> g <$ logInfo ("verifyGroupDomain_: error " <> tshow e) + | otherwise = pure g + groupInfoText simplexName_ p@GroupProfile {description = d, publicGroup} = groupNameDescr p <> maybe "" ("\nSimpleX name: " <>) simplexName_ <> maybe "" ("\nWelcome message:\n" <>) d <> linkToJoin where linkToJoin = case publicGroup of Just pg@PublicGroupProfile {groupLink} -> @@ -404,7 +420,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName processInvitation :: Contact -> GroupInfo -> Maybe GroupReg -> IO () processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = \case - Nothing -> addGroupReg notifyAdminUsers st cc ct g GRSProposed joinGroup + Nothing -> addGroupReg notifyAdminUsers st cc user ct g GRSProposed joinGroup Just _gr -> setGroupStatus notifyAdminUsers st env cc groupId GRSProposed joinGroup where joinGroup _ = do @@ -436,7 +452,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Left e -> sendMessage cc ct $ "Error: getDuplicateGroup. Please notify the developers.\n" <> T.pack e where askConfirmation = - addGroupReg notifyAdminUsers st cc ct g GRSPendingConfirmation $ \GroupReg {userGroupRegId} -> do + addGroupReg notifyAdminUsers st cc user ct g GRSPendingConfirmation $ \GroupReg {userGroupRegId} -> do sendMessage cc ct $ "The group " <> groupNameDescr p <> " is already submitted to the directory.\nTo confirm the registration, please send:" sendMessage cc ct $ "/confirm " <> tshow userGroupRegId <> ":" <> viewName displayName @@ -488,6 +504,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName \Please add it to the group welcome message.\n\ \For example, add:" notifyOwner gr' $ "Link to join the group " <> displayName <> ": " <> groupLinkText gLink + notifyOwner gr' $ recommendedSettingsNotice (userGroupRegId gr') 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." @@ -551,9 +568,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName ("The " <> gt <> " " <> userGroupRef <> " is updated" <> byMember) <> ".\nIt is hidden from the directory until approved." notifyAdminUsers $ "The " <> gt <> " " <> groupRef <> " is updated" <> byMember <> "." - sendToApprove g' gr' n' - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case - Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> + verifyAndSendToApprove g' gr' n' + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups Nothing) >>= \case + Right (CRConnectionPlan _ _ _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> case dbOwnerMemberId gr of Just ownerGMId -> withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case @@ -621,8 +638,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cr sl_}} -> let linkBefore_ = profileGroupLinkText fromGroup linkNow_ = profileGroupLinkText toGroup - profileGroupLinkText GroupInfo {groupProfile = gp} = - maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< description gp + profileGroupLinkText GroupInfo {groupProfile = GroupProfile {description = descr_}} = + maybe Nothing (fmap (\(FormattedText _ t) -> t) . find ftHasLink) $ parseMaybeMarkdownList =<< descr_ ftHasLink = \case FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of CLFull cr' -> sameConnReqContact cr' cr @@ -638,7 +655,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName (badRolesMsg <$$> getGroupRolesStatus toGroup gr) >>= \case Left e -> notifyOwner gr $ "Error: getGroupRolesStatus. Please notify the developers.\n" <> T.pack e Right (Just msg) -> notifyOwner gr msg - Right Nothing -> sendToApprove toGroup gr gaId + Right Nothing -> verifyAndSendToApprove toGroup gr gaId dePendingMember :: GroupInfo -> GroupMember -> IO () dePendingMember g@GroupInfo {groupProfile = GroupProfile {displayName}} m @@ -650,6 +667,19 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName "Captcha is generated by SimpleX Directory service.\n\n*Send captcha text* to join the group " <> displayName <> "." <> if canSendVoiceCaptcha g m then "\nSend /audio to receive a voice captcha." else "" + -- gated by --prohibited-to-observer at the dispatch above + deGroupItemProhibited :: GroupInfo -> GroupMember -> ChatItemId -> GroupFeature -> IO () + deGroupItemProhibited GroupInfo {groupId} m@GroupMember {memberRole} ciId gf = + when (memberRole == GRMember) $ do + let gmId = groupMemberId' m + logInfo $ "Member " <> tshow gmId <> " posted prohibited content (" <> tshow gf <> ") in group " <> tshow groupId <> "; deleting and setting to observer" + sendChatCmd cc (APIDeleteMemberChatItem groupId [ciId]) >>= \case + Right CRChatItemsDeleted {} -> pure () + r -> logError $ "deGroupItemProhibited: unexpected delete response: " <> tshow r + sendChatCmd cc (APIMembersRole groupId [gmId] GRObserver) >>= \case + Right CRMembersRoleUser {} -> pure () -- empty members = already observer (idempotent), still success + r -> logError $ "deGroupItemProhibited: unexpected set observer response: " <> tshow r + sendMemberCaptcha :: GroupInfo -> GroupMember -> Maybe ChatItemId -> Text -> Int -> CaptchaMode -> IO () sendMemberCaptcha GroupInfo {groupId} m quotedId noticeText prevAttempts mode = do s <- getCaptchaStr captchaLength "" @@ -776,32 +806,38 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName memberRequiresCaptcha :: DirectoryMemberAcceptance -> GroupMember -> Bool memberRequiresCaptcha a GroupMember {memberProfile = LocalProfile {image}} = - useMemberFilter image $ passCaptcha a + alwaysCaptcha || useMemberFilter image (passCaptcha a) sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () - sendToApprove GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} GroupReg {dbContactId, promoted} gaId = do + sendToApprove g@GroupInfo {groupId, groupProfile = p@GroupProfile {displayName, image = image', publicGroup = pg_}, groupSummary} GroupReg {dbContactId, promoted} gaId = do ct_ <- getContact' cc user dbContactId let gt = maybe "group" groupTypeStr' pg_ + nameStr_ = (\d -> simplexNameStr d <> (if groupDomainVerified g == Just True then "" else " (NOT verified - will not be shown)")) <$> groupSimplexDomain g membersStr = "_" <> membersCountStr p groupSummary <> "_\n" text = either (\_ -> "The " <> gt <> " ID " <> tshow groupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the " <> gt <> " ID " <> tshow groupId <> ": ") ct_ - <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") + <> ("\n" <> groupInfoText nameStr_ p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withAdminUsers $ \cId -> do let approveCmd = MCText $ "/approve " <> tshow groupId <> ":" <> viewName displayName <> " " <> tshow gaId <> if promoted then " promote=on" else "" sendComposedMessages cc (SRDirect cId) [msg, approveCmd] + verifyAndSendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () + verifyAndSendToApprove g gr gaId = verifyGroupDomain_ g >>= \g' -> sendToApprove g' gr gaId + deGroupLinkCheck :: GroupInfo -> IO () deGroupLinkCheck gInfo@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, groupSummary = summary} = withGroupReg gInfo "link check" $ \gr@GroupReg {groupRegStatus, dbOwnerMemberId} -> forM_ pg_ $ \pg@PublicGroupProfile {groupLink} -> when (groupRegStatus == GRSActive || pendingApproval groupRegStatus) $ do let link = ACL SCMContact $ CLShort groupLink - sendChatCmd cc (APIConnectPlan userId (Just link) True Nothing) >>= \case - Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated = BoolDef updated, linkOwners = ListDef owners}))) -> + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups Nothing) >>= \case + Right (CRConnectionPlan _ _ _ _ (CPGroupLink (GLPKnown {groupInfo = g', groupUpdated, linkOwners = ListDef owners}))) -> checkValidOwner dbOwnerMemberId owners $ do - when updated $ reapprove pg gr groupRegStatus g' - when (updated || summary /= groupSummary g') $ listingsUpdated env + -- re-verify every cycle: a name that stopped resolving to the link must lose verified status + g'' <- verifyGroupDomain_ g' + when groupUpdated $ reapprove pg gr groupRegStatus g'' + when (groupUpdated || summary /= groupSummary g'' || groupDomainVerified g'' /= groupDomainVerified gInfo) $ listingsUpdated env Left (ChatErrorAgent {agentError = SMP _ err}) | linkDeleted err -> setGroupStatus logError st env cc groupId GRSRemoved $ \gr' -> notifyOwner gr' "The channel link is no longer valid.\nThe channel is removed from the directory." @@ -845,7 +881,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName notifyOwner gr' $ uCtRole <> ".\n\nThe group is listed in the directory again." notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suCtRole GRSPendingApproval gaId | rStatus == GRSOk -> do - sendToApprove g gr gaId + verifyAndSendToApprove g gr gaId notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." GRSActive | rStatus /= GRSOk -> setGroupStatus notifyAdminUsers st env cc groupId GRSSuspendedBadRoles $ \gr' -> do @@ -872,7 +908,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName notifyAdminUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole GRSPendingApproval gaId | serviceRole == GRAdmin -> whenContactIsOwner gr $ do - sendToApprove g gr gaId + verifyAndSendToApprove g gr gaId notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval." GRSActive | serviceRole /= GRAdmin -> setGroupStatus notifyAdminUsers st env cc groupId GRSSuspendedBadRoles $ \gr' -> do @@ -933,8 +969,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let link = ACL SCMContact $ CLShort connLink mId = MemberId oIdBytes gt' = groupTypeStr gt - sendChatCmd cc (APIConnectPlan userId (Just link) True (Just ownerSig)) >>= \case - Right (CRConnectionPlan _ (ACCL SCMContact ccLink) plan) -> + sendChatCmd cc (APIConnectPlan userId (Just (aConnectTarget link)) PRMAllGroups (Just ownerSig)) >>= \case + Right (CRConnectionPlan _ (ACCL SCMContact ccLink) _ _ plan) -> handleGroupLinkPlan ct ccLink mId ownerSig gt' plan _ -> sendMessage cc ct "Error: could not connect. Please report it to directory admins." deChatLinkReceived ct (MCLGroup {groupProfile = GroupProfile {publicGroup = Just pg}}) _ = @@ -963,8 +999,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName (_, Just (OVFailed reason)) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." (Nothing, _) -> sendMessage cc ct $ "Error: no " <> gt <> " information available via the link." _ -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership. Please report it to directory admins." - GLPKnown {groupInfo = g, groupUpdated = BoolDef updated, ownerVerification} -> case ownerVerification of - Just OVVerified -> deReregistration ct g updated ownerSig + GLPKnown {groupInfo = g, groupUpdated, ownerVerification} -> case ownerVerification of + Just OVVerified -> deReregistration ct g groupUpdated ownerSig Just (OVFailed reason) -> sendMessage cc ct $ "Link signature verification failed: " <> reason <> ".\nYou must be the " <> gt <> " owner to register it." Nothing -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership." GLPConnectingProhibit _ -> sendMessage cc ct $ "Already connecting to this " <> gt <> "." @@ -979,10 +1015,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName let GroupShortLinkData {groupProfile = GroupProfile {displayName}} = groupSLinkData ownerContact = GroupOwnerContact {contactId = contactId' ct, memberId = mId} sendMessage cc ct $ "Joining the " <> gt <> " " <> displayName <> "…" - sendChatCmd cc (APIPrepareGroup userId ccLink False groupSLinkData) >>= \case + sendChatCmd cc (APIPrepareGroup userId ccLink False Nothing groupSLinkData) >>= \case Right (CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _))) -> do let gId = groupId' gInfo - addGroupReg notifyAdminUsers st cc ct gInfo GRSProposed $ \_ -> pure () + addGroupReg notifyAdminUsers st cc user ct gInfo GRSProposed $ \_ -> pure () sendChatCmd cc (APIConnectPreparedGroup gId False (Just ownerContact) Nothing) >>= \case Right CRStartedConnectionToGroup {groupInfo = gInfo'} -> withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user gInfo' mId) >>= \case @@ -1007,9 +1043,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName | contactId' ct `isOwner` gr -> sameOwnerReregistration gr gt | otherwise -> sendMessage cc ct $ "This " <> gt <> " is registered by another owner." Left _ -> - addGroupReg notifyAdminUsers st cc ct g (GRSPendingApproval 1) $ \gr -> do + addGroupReg notifyAdminUsers st cc user ct g (GRSPendingApproval 1) $ \gr -> do void $ setGroupRegOwner cc groupId ownerMember - sendToApprove g gr 1 + verifyAndSendToApprove g gr 1 | role < GROwner -> sendMessage cc ct $ "You must be the " <> gt <> " owner to register it." | otherwise -> sendMessage cc ct $ "Waiting for the owner member to be connected to the " <> gt <> "." Left _ -> sendMessage cc ct $ "Error: could not verify " <> gt <> " ownership. Please report it to directory admins." @@ -1032,7 +1068,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n) $ \gr' -> do notifyOwner gr' $ "The " <> gt <> " " <> userGroupRef <> " is submitted for approval.\nIt is hidden from the directory until approved." - sendToApprove g gr' n + verifyAndSendToApprove g gr' n deReregistration ct _ _ _ = sendMessage cc ct "Error: could not verify ownership. Please report it to directory admins." @@ -1045,7 +1081,8 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName in if role >= GROwner then setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval 1) $ \gr' -> do notifyOwner gr' $ "Joined the " <> gt <> " " <> displayName <> ". Registration is pending approval — it may take up to 48 hours." - sendToApprove g gr' 1 + notifyOwner gr' $ recommendedSettingsNotice (userGroupRegId gr') + verifyAndSendToApprove g gr' 1 else do setGroupStatus notifyAdminUsers st env cc groupId GRSRemoved $ \_ -> pure () sendMessage' cc (dbContactId gr) "The signing key does not belong to a current owner. Registration cancelled." @@ -1180,9 +1217,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Just PCAll -> "_enabled_" Just PCNoImage -> "_enabled for profiles without image_" DCShowUpgradeGroupLink gId gName_ -> - (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, localDisplayName = gName} _ -> case pg_ of + (if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}, localDisplayName = gName} _ -> case pg_ of Just pg@PublicGroupProfile {groupLink} -> sendReply $ "The link to join the " <> groupTypeStr' pg <> " " <> groupReference' gId gName <> ":\n" <> strEncodeTxt groupLink + <> maybe "" (("\nSimpleX name: " <>) . simplexNameStr) (verifiedGroupDomain g) Nothing -> do let groupRef = groupReference' gId gName withGroupLinkResult groupRef (sendChatCmd cc $ APIGetGroupLink groupId) $ @@ -1267,10 +1305,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName where msgs = replyMsg :| map foundGroup gs <> [moreMsg | moreGroups > 0] replyMsg = (Just ciId, MCText reply) - foundGroup (GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary}, _) = + foundGroup (g@GroupInfo {groupId, groupProfile = p@GroupProfile {image = image_, memberAdmission}, groupSummary}, _) = let membersStr = "_" <> membersCountStr p groupSummary <> "_" showId = if isAdmin then tshow groupId <> ". " else "" - text = T.unlines $ [showId <> groupInfoText p, membersStr] ++ knockingStr memberAdmission + text = T.unlines $ [showId <> groupInfoText (simplexNameStr <$> verifiedGroupDomain g) p, membersStr] ++ knockingStr memberAdmission in (Nothing, maybe (MCText text) (\image -> MCImage {text, image}) image_) moreMsg = (Nothing, MCText $ "Send /next for " <> tshow moreGroups <> " more result(s).") @@ -1468,7 +1506,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName membersStr = "_" <> membersCountStr p groupSummary <> "_" cmds = "/'role " <> tshow useGroupId <> "', /'filter " <> tshow useGroupId <> "'" ownerStr = maybe "" (("Owner: " <>) . either (("getContact error: " <>) . T.pack) localDisplayName') ct_ - text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr] ++ knockingStr memberAdmission ++ [cmds] + text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText (simplexNameStr <$> verifiedGroupDomain g) p] ++ [ownerStr | isAdmin] ++ [membersStr, statusStr] ++ knockingStr memberAdmission ++ [cmds] msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ in (Nothing, msg) @@ -1484,12 +1522,16 @@ setGroupStatusPromo sendReply st env cc GroupReg {dbGroupId = gId} grStatus' grP logGUpdatePromotion st gId grPromoted' continue -addGroupReg :: (Text -> IO ()) -> DirectoryLog -> ChatController -> Contact -> GroupInfo -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () -addGroupReg sendMsg st cc ct g@GroupInfo {groupId} grStatus continue = +addGroupReg :: (Text -> IO ()) -> DirectoryLog -> ChatController -> User -> Contact -> GroupInfo -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () +addGroupReg sendMsg st cc user ct g@GroupInfo {groupId} grStatus continue = addGroupRegStore cc ct g grStatus >>= \case Left e -> sendMsg $ "Error creating group registation for group " <> tshow groupId <> ": " <> T.pack e Right gr -> do logGCreate st gr + let d = toCustomData $ DirectoryGroupData newGroupJoinFilter + withDB' "setGroupCustomData" cc (\db -> setGroupCustomData db user g $ Just d) >>= \case + Right () -> pure () + Left e -> sendMsg $ "Error setting default captcha for group " <> tshow groupId <> ": " <> T.pack e continue gr setGroupStatus :: (Text -> IO ()) -> DirectoryLog -> ServiceState -> ChatController -> GroupId -> GroupRegStatus -> (GroupReg -> IO ()) -> IO () @@ -1537,3 +1579,6 @@ unexpectedError err = "Unexpected error: " <> err <> ", please notify the develo strEncodeTxt :: StrEncoding a => a -> Text strEncodeTxt = safeDecodeUtf8 . strEncode + +simplexNameStr :: SimplexDomain -> Text +simplexNameStr = shortNameInfoStr . SimplexNameInfo NTPublicGroup diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 89c5178f7d..e3465e64b1 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -43,6 +43,7 @@ module Directory.Store getAllListedGroups, getAllListedGroups_, searchListedGroups, + verifiedGroupDomain, groupRegStatusText, pendingApproval, groupRemoved, @@ -52,6 +53,7 @@ module Directory.Store basicJoinFilter, moderateJoinFilter, strongJoinFilter, + newGroupJoinFilter, groupDBError, logGCreate, logGDelete, @@ -85,11 +87,13 @@ import Data.Time.Clock.System (systemEpochDay) import Directory.Search import Directory.Util import Simplex.Chat.Controller +import Simplex.Chat.Names (claimDomain) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Store import Simplex.Chat.Store.Groups import Simplex.Chat.Store.Shared (groupInfoQueryFields, groupInfoQueryFrom) import Simplex.Chat.Types +import Simplex.Messaging.Agent.Protocol (SimplexDomain) import Simplex.Messaging.Agent.Store.DB (BoolInt (..), fromTextField_) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding.String @@ -164,6 +168,16 @@ strongJoinFilter = makeObserver = Nothing } +-- Default applied to newly registered groups: a captcha challenge is required +-- from every joining member unless the owner changes it with /filter. +newGroupJoinFilter :: DirectoryMemberAcceptance +newGroupJoinFilter = + DirectoryMemberAcceptance + { rejectNames = Nothing, + passCaptcha = Just PCAll, + makeObserver = Nothing + } + type UserGroupRegId = Int64 type GroupApprovalId = Int64 @@ -211,6 +225,11 @@ grDirectoryStatus = \case GRSRemoved -> DSRemoved _ -> DSRegistered +verifiedGroupDomain :: GroupInfo -> Maybe SimplexDomain +verifiedGroupDomain GroupInfo {groupProfile = GroupProfile {publicGroup}, groupDomainVerified} + | groupDomainVerified == Just True = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim) + | otherwise = Nothing + $(JQ.deriveJSON (enumJSON $ dropPrefix "PC") ''ProfileCondition) $(JQ.deriveJSON defaultJSON ''DirectoryMemberAcceptance) @@ -374,15 +393,19 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC " STSearch search -> case lastGroup_ of Nothing -> do - gs <- groups currentTs $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize) - n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s) + gs <- groups currentTs $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") ((userId, userContactId, GRSActive, s, s, s, s) :. (sDomain, pageSize)) + n <- count $ DB.query db (countQuery' <> searchCond) (GRSActive, s, s, s, s, sDomain) pure (gs, n) Just gId -> do - gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s) + gs <- groups currentTs $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") ((userId, userContactId, GRSActive, gId, s, s, s, s) :. (sDomain, pageSize)) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s, sDomain) pure (gs, n) where s = T.toLower search + -- a bare "#"/"@" maps to "#", matching no stored domain (domains are stored unprefixed) + sDomain = case T.uncons s of + Just (c, rest) | c == '#' || c == '@' -> if T.null rest then "#" else rest + _ -> s countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " where @@ -396,6 +419,7 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa OR LOWER(gp.full_name) LIKE '%' || ? || '%' OR LOWER(gp.short_descr) LIKE '%' || ? || '%' OR LOWER(gp.description) LIKE '%' || ? || '%' + OR (LOWER(gp.group_domain) LIKE '%' || ? || '%' AND g.group_domain_verified = 1) ) |] diff --git a/apps/simplex-directory-service/web/directory.html b/apps/simplex-directory-service/web/directory.html new file mode 100644 index 0000000000..d68d624ab6 --- /dev/null +++ b/apps/simplex-directory-service/web/directory.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>SimpleX Directory + + + + + +
+

SimpleX Directory

+

Communities you can join via SimpleX Chat.

+

Also available as a SimpleX chat bot.

+
+ + +
+
+ +
+ + + + + diff --git a/apps/simplex-directory-service/web/directory.js b/apps/simplex-directory-service/web/directory.js new file mode 120000 index 0000000000..197f1d6e66 --- /dev/null +++ b/apps/simplex-directory-service/web/directory.js @@ -0,0 +1 @@ +../../../website/src/js/directory.js \ No newline at end of file diff --git a/apps/simplex-support-bot/bot.test.ts b/apps/simplex-support-bot/bot.test.ts index a1afd7a660..37521e49dc 100644 --- a/apps/simplex-support-bot/bot.test.ts +++ b/apps/simplex-support-bot/bot.test.ts @@ -1068,24 +1068,26 @@ describe("Team Member Lifecycle", () => { expect(chat.roleChanges.some(r => r.groupId === CUSTOMER_GROUP_ID && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) && r.role === GroupMemberRole.Owner)).toBe(true) }) - test("/team invites team member → apiSetMembersRole(Owner) called at invite time", async () => { + test("/team invites team member → added directly as Owner, single invitation", async () => { await bot.onNewChatItems(customerMessage("/team")) - expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) - expect(chat.roleChanges.some(r => - r.groupId === CUSTOMER_GROUP_ID - && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) - && r.role === GroupMemberRole.Owner + expect(chat.added.some(a => + a.groupId === CUSTOMER_GROUP_ID + && a.contactId === TEAM_MEMBER_1_ID + && a.role === GroupMemberRole.Owner )).toBe(true) + // No role change — that would send a second invitation. + expect(chat.roleChanges.length).toBe(0) }) - test("/join invites team member → apiSetMembersRole(Owner) called at invite time", async () => { + test("/join invites team member → added directly as Owner, single invitation", async () => { await bot.onNewChatItems(teamGroupMessage(`/join ${CUSTOMER_GROUP_ID}`)) - expectMemberAdded(CUSTOMER_GROUP_ID, TEAM_MEMBER_1_ID) - expect(chat.roleChanges.some(r => - r.groupId === CUSTOMER_GROUP_ID - && r.memberIds.includes(5000 + TEAM_MEMBER_1_ID) - && r.role === GroupMemberRole.Owner + expect(chat.added.some(a => + a.groupId === CUSTOMER_GROUP_ID + && a.contactId === TEAM_MEMBER_1_ID + && a.role === GroupMemberRole.Owner )).toBe(true) + // No role change — that would send a second invitation. + expect(chat.roleChanges.length).toBe(0) }) test("/team when team member already in group (any non-terminal status) → apiSetMembersRole NOT re-called", async () => { diff --git a/apps/simplex-support-bot/src/bot.ts b/apps/simplex-support-bot/src/bot.ts index 9b534381de..28b2080793 100644 --- a/apps/simplex-support-bot/src/bot.ts +++ b/apps/simplex-support-bot/src/bot.ts @@ -853,17 +853,12 @@ export class SupportBot { const members = await this.withMainProfile(() => this.chat.apiListMembers(groupId)) const existing = members.find(m => m.memberContactId === teamContactId && isInGroup(m)) if (existing) return existing - const member = await this.withMainProfile(() => - this.chat.apiAddMember(groupId, teamContactId, T.GroupMemberRole.Member) + // Invite directly as Owner in one call. Adding as Member then promoting + // sent a second invitation, since the API re-sends it when a pending + // (GSMemInvited) member's role changes. onMemberConnected re-asserts Owner. + return this.withMainProfile(() => + this.chat.apiAddMember(groupId, teamContactId, T.GroupMemberRole.Owner) ) - try { - await this.withMainProfile(() => - this.chat.apiSetMembersRole(groupId, [member.groupMemberId], T.GroupMemberRole.Owner) - ) - } catch { - // Not yet connected — will be promoted in onMemberConnected - } - return member } async sendToGroup(groupId: number, text: string): Promise { diff --git a/apps/simplex-support-bot/src/messages.ts b/apps/simplex-support-bot/src/messages.ts index c35789d26b..33f7f9ccef 100644 --- a/apps/simplex-support-bot/src/messages.ts +++ b/apps/simplex-support-bot/src/messages.ts @@ -2,7 +2,10 @@ import {isWeekend} from "./util.js" export const welcomeMessage = `Hello! This is a *SimpleX team* support bot - not an AI. *Join public groups* at https://simplex.chat/directory or [via directory bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) -Please ask any questions about SimpleX Chat.` + +We just launched [equity crowdfunding on Wefunder](https://wefunder.com/simplex.chat)! + +Please ask any questions about SimpleX Chat and about our crowdfunding.` export function queueMessage(timezone: string, grokEnabled: boolean): string { const hours = isWeekend(timezone) ? "48" : "24" diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg new file mode 100644 index 0000000000..715a6daf99 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_1@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg new file mode 100644 index 0000000000..f70ecb91db Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_1@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg new file mode 100644 index 0000000000..48bfe6caa7 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_2@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg new file mode 100644 index 0000000000..2bab828717 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_2@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg new file mode 100644 index 0000000000..704cae8710 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_3@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg new file mode 100644 index 0000000000..d7c1f4e088 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_3@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg new file mode 100644 index 0000000000..e39e0836cb Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_4@2x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg b/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg new file mode 100644 index 0000000000..ce9b4a86ef Binary files /dev/null and b/assets/multiplatform/resources/MR/images/crowdfunding_4@3x.jpg differ diff --git a/assets/multiplatform/resources/MR/images/own_stake@2x.png b/assets/multiplatform/resources/MR/images/own_stake@2x.png new file mode 100644 index 0000000000..fadf1b9599 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/own_stake@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/own_stake@3x.png b/assets/multiplatform/resources/MR/images/own_stake@3x.png new file mode 100644 index 0000000000..106ee804ca Binary files /dev/null and b/assets/multiplatform/resources/MR/images/own_stake@3x.png differ diff --git a/assets/multiplatform/resources/MR/images/own_stake_light@2x.png b/assets/multiplatform/resources/MR/images/own_stake_light@2x.png new file mode 100644 index 0000000000..8f02fa3eaa Binary files /dev/null and b/assets/multiplatform/resources/MR/images/own_stake_light@2x.png differ diff --git a/assets/multiplatform/resources/MR/images/own_stake_light@3x.png b/assets/multiplatform/resources/MR/images/own_stake_light@3x.png new file mode 100644 index 0000000000..0a994849d6 Binary files /dev/null and b/assets/multiplatform/resources/MR/images/own_stake_light@3x.png differ diff --git a/blog/20210914-simplex-chat-v0.4-released.md b/blog/20210914-simplex-chat-v0.4-released.md index 31adf022a9..253745015b 100644 --- a/blog/20210914-simplex-chat-v0.4-released.md +++ b/blog/20210914-simplex-chat-v0.4-released.md @@ -30,13 +30,13 @@ To create a group use the `/g ` command. You can then invite contacts to **Please note:** Groups are not stored on any server; they are maintained as a list of members in the app database. Sending a message to the group sends a message to each member of the group. -![simplex-chat](../images/groups.gif) +![simplex-chat](/images/groups.gif) ### File transfer Sharing files is simple! To send a file to a contact, use the `/f @ ` command. The recipient will have to accept before the file is sent. -![simplex-chat](../images/files.gif) +![simplex-chat](/images/files.gif) ## We're always looking for help! diff --git a/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md b/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md index 4a63cb87ca..51f0db7475 100644 --- a/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md +++ b/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md @@ -1,6 +1,6 @@ --- layout: layouts/article.html -title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech" +title: "SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding — to Preserve Freedom of Speech" date: 2026-04-30 previewBody: blog_previews/20260430.html image: images/20260430-home.png @@ -58,7 +58,7 @@ You can *register your interest* to participate in crowdfunding here: https://si Join the channel for updates [here](https://smp10.simplex.im/c#q09nMBmWFGz1m2TvgfZFaEOG5D2a7Ma9mSkl6pHXEsg) — you must install v6.5 to join it — or you can join a [read-only group](https://smp12.simplex.im/g#gJzy7ETpuvltqARIB73TQUpJ11Lz4Xpl9xeH9qNoGCg) from the previous app versions. -_Disclaimer: SimpleX Chat is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._ +_Disclaimer: SimpleX Chat, Inc. is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._ [^release]: v6.5 release also improved how new users make the first connection, increased security of sending web links, and has many other improvements — see *What's new* in the app or full release notes. diff --git a/blog/20260722-simplex-public-names.md b/blog/20260722-simplex-public-names.md new file mode 100644 index 0000000000..1b9220da4a --- /dev/null +++ b/blog/20260722-simplex-public-names.md @@ -0,0 +1,67 @@ +--- +layout: layouts/article.html +title: "SimpleX Public Names — a Name Nobody Can Take From You" +date: 2026-07-22 +preview: "You can now give your channel or business a test SimpleX name — from v7-beta." +image: images/20260722-register-name.png +imageWide: true +permalink: "/blog/20260722-simplex-public-names.html" +--- + +# SimpleX Public Names — a Name Nobody Can Take From You + +**Published:** Jul 22, 2026 + +You can now give your channel or business a test SimpleX name that people can remember. Test names are available from v7-beta[^testing]. + +## Public names for channels and businesses — without user IDs + + + + +Before names, the only way to bring people to your channel or business on SimpleX Network was a link — but you cannot use a link in a podcast or a poster: nobody would remember it. + +Every place where you could get a name so far belongs to someone else: Telegram can revoke your username, a registrar can suspend your domain. + +So we designed SimpleX names for no one to own the registry — on the Ethereum blockchain. If you register `example.simplex`, people type `#example` to join your channel, or `@example.simplex` to message you. If a server operator deletes your link, you can point the name to a new one. Only you control the name with the key in your wallet[^ens]. + +And we did not add user identifiers to do it. Names are only for those who want to be found — channels, businesses, communities — and servers still cannot see who joined your channel or wrote to you. + +We plan to launch `.simplex` names later this year, and to provide the first names to [crowdfunding investors](#community-crowdfunding) as perks. + +## How to register a name + + + +To register a test name you need an Ethereum wallet, such as MetaMask, and your SimpleX address or channel link from the app. + +Setting up a name takes two steps. On the SimpleX Name Service [test webpage](https://testing-names.simplex.chat), search for the name you want — currently 6 characters or more — paste your address or channel link into the page, and complete the registration. + +In the app, you need to claim the name for your channel or contact address — it prevents connecting to your channel or address via any other name. Open your SimpleX address or channel page, tap **Get SimpleX name**, enter the name, and tap Save. + +See [this guide](../docs/guide/register-simplex-name.md) for more details. + +## How to connect via names + + + +Type the name into the search bar — `#example.testing` to join the channel, `@example.testing` to send direct messages. You can also send names in messages — they work as links. + +Connecting via a name is private. Unlike most applications accessing the chain through a centralized RPC service, SimpleX Chat app resolves names via two independent servers of SimpleX Network, so that no server can see both the name and the user's IP address. + +Read more about names in the [whitepaper](https://github.com/simplex-chat/simplex-chat/blob/master/docs/protocol/names-overview.md): their purpose, architecture, security model and planned future work. + +## Community Crowdfunding + +To ensure the long term success of SimpleX Network we established [SimpleX Network Consortium](https://simplexnetwork.org/consortium.html) — an agreement between a non-profit foundation created for protocol licensing and governance and SimpleX Chat, Inc. + +The commercial model for the network that we are building aims to make both our and other businesses on the network profitable. We recently [presented the technology design](https://www.youtube.com/watch?v=UhW8AuoRgxg) for this commercial model at Web3 Summit. + +The planned crowdfunding will fund this development. You can [register your interest](https://simplexchat.typeform.com/crowdfunding), and join the [SimpleX Crowdfunding News channel](https://smp10.simplex.im/c#q09nMBmWFGz1m2TvgfZFaEOG5D2a7Ma9mSkl6pHXEsg) for updates. + +_Disclaimer: SimpleX Chat, Inc. is testing the waters for a possible Reg CF offering. We’re not asking for or accepting any money right now, and we won’t accept any if sent. We can’t accept any offers to buy securities or take any payments until the official filing is done and it’s live through a regulated platform. Our testing the waters and your possible indications of interest doesn’t create any obligation or commitment of any kind._ + + +[^testing]: Test names are free to register; you only need to pay the blockchain fee. The `.testing` namespace is temporary — test names will stop working in the app one month after `.simplex` name sales launch. + +[^ens]: SimpleX Name Service (SNS) is a fork of Ethereum Name Service (ENS), but without its centralized dependencies. ENS depends on an off-chain indexer and a hosted metadata service. SNS is fully decentralized — names are indexed and hosted on the blockchain. See [Differences from ENS](https://github.com/simplex-chat/simplex-chat/blob/master/docs/protocol/names-overview.md#differences-from-ens). diff --git a/blog/README.md b/blog/README.md index 4544dc0f45..6122eb3c39 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,19 @@ # Blog +Jul 22, 2026 [SimpleX Public Names — a Name Nobody Can Take From You](./20260722-simplex-public-names.md) + +You can now give your channel or business a test SimpleX name that people can remember and nobody can take from you. Test names are free in v7-beta. + +--- + +Apr 30, 2026 [SimpleX Channels, SimpleX Network Consortium and Community Crowdfunding - to Preserve Freedom of Speech](./20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.md) + +Freedom of speech needs infrastructure that protects it by design - protocols, governance and funding. + +v6.5 release brings SimpleX Channels: a new model for online publishing built for participation privacy. + +--- + 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: diff --git a/blog/images/20260722-connect-name.png b/blog/images/20260722-connect-name.png new file mode 100644 index 0000000000..7e835073ff Binary files /dev/null and b/blog/images/20260722-connect-name.png differ diff --git a/blog/images/20260722-phone-name-light.webp b/blog/images/20260722-phone-name-light.webp new file mode 100644 index 0000000000..aa9f81918f Binary files /dev/null and b/blog/images/20260722-phone-name-light.webp differ diff --git a/blog/images/20260722-phone-name.webp b/blog/images/20260722-phone-name.webp new file mode 100644 index 0000000000..df812bead6 Binary files /dev/null and b/blog/images/20260722-phone-name.webp differ diff --git a/blog/images/20260722-register-name.png b/blog/images/20260722-register-name.png new file mode 100644 index 0000000000..d64bb21507 Binary files /dev/null and b/blog/images/20260722-register-name.png differ diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index d14435cabd..463dd4088b 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -283,20 +283,21 @@ Send messages. - sendRef: [ChatRef](./TYPES.md#chatref) - liveMessage: bool - ttl: int? +- signMessages: bool - composedMessages: [[ComposedMessage](./TYPES.md#composedmessage)] **Syntax**: ``` -/_send [ live=on][ ttl=] json +/_send [ live=on][ ttl=][ sign=on] json ``` ```javascript -'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript +'/_send ' + ChatRef.cmdString(sendRef) + (liveMessage ? ' live=on' : '') + (ttl ? ' ttl=' + ttl : '') + (signMessages ? ' sign=on' : '') + ' json ' + JSON.stringify(composedMessages) // JavaScript ``` ```python -'/_send ' + ChatRef_cmd_string(sendRef) + (' live=on' if liveMessage else '') + ((' ttl=' + str(ttl)) if ttl is not None else '') + ' json ' + json.dumps(composedMessages) # Python +'/_send ' + ChatRef_cmd_string(sendRef) + (' live=on' if liveMessage else '') + ((' ttl=' + str(ttl)) if ttl is not None else '') + (' sign=on' if signMessages else '') + ' json ' + json.dumps(composedMessages) # Python ``` **Responses**: @@ -1363,28 +1364,28 @@ ChatCmdError: Command error (only used in WebSockets API). ### APIConnectPlan -Determine SimpleX link type and if the bot is already connected via this link. +Determine SimpleX link type and if the bot is already connected via this link or name. *Network usage*: interactive. **Parameters**: - userId: int64 -- connectionLink: string? -- resolveKnown: bool +- connectTarget: string? +- resolveMode: [PlanResolveMode](./TYPES.md#planresolvemode) - linkOwnerSig: [LinkOwnerSig](./TYPES.md#linkownersig)? **Syntax**: ``` -/_connect plan +/_connect plan ``` ```javascript -'/_connect plan ' + userId + ' ' + connectionLink // JavaScript +'/_connect plan ' + userId + ' ' + connectTarget // JavaScript ``` ```python -'/_connect plan ' + str(userId) + ' ' + connectionLink # Python +'/_connect plan ' + str(userId) + ' ' + connectTarget # Python ``` **Responses**: @@ -1393,6 +1394,8 @@ ConnectionPlan: Connection link information. - type: "connectionPlan" - user: [User](./TYPES.md#user) - connLink: [CreatedConnLink](./TYPES.md#createdconnlink) +- planSimplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)? +- otherSimplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)? - connectionPlan: [ConnectionPlan](./TYPES.md#connectionplan) ChatCmdError: Command error (only used in WebSockets API). @@ -1455,26 +1458,26 @@ ChatCmdError: Command error (only used in WebSockets API). ### Connect -Connect via SimpleX link as string in the active user profile. +Connect via SimpleX link or name as string in the active user profile. *Network usage*: interactive. **Parameters**: - incognito: bool -- connLink_: string? +- connTarget_: string? **Syntax**: ``` -/connect[ ] +/connect[ ] ``` ```javascript -'/connect' + (connLink_ ? ' ' + connLink_ : '') // JavaScript +'/connect' + (connTarget_ ? ' ' + connTarget_ : '') // JavaScript ``` ```python -'/connect' + ((' ' + connLink_) if connLink_ is not None else '') # Python +'/connect' + ((' ' + connTarget_) if connTarget_ is not None else '') # Python ``` **Responses**: diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 60cee67d78..79338930c8 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -87,6 +87,7 @@ This file is generated automatically. - [FileProtocol](#fileprotocol) - [FileStatus](#filestatus) - [FileTransferMeta](#filetransfermeta) +- [FileType](#filetype) - [Format](#format) - [FormattedText](#formattedtext) - [FullGroupPreferences](#fullgrouppreferences) @@ -138,12 +139,15 @@ This file is generated automatically. - [MsgReaction](#msgreaction) - [MsgReceiptStatus](#msgreceiptstatus) - [MsgSigStatus](#msgsigstatus) +- [MsgVerified](#msgverified) +- [NameErrorType](#nameerrortype) - [NetworkError](#networkerror) - [NewUser](#newuser) - [NoteFolder](#notefolder) - [OwnerVerification](#ownerverification) - [PaginationByTime](#paginationbytime) - [PendingContactConnection](#pendingcontactconnection) +- [PlanResolveMode](#planresolvemode) - [PrefEnabled](#prefenabled) - [Preferences](#preferences) - [PreparedContact](#preparedcontact) @@ -171,8 +175,11 @@ This file is generated automatically. - [SMPAgentError](#smpagenterror) - [SecurityCode](#securitycode) - [SimplePreference](#simplepreference) +- [SimplexDomain](#simplexdomain) +- [SimplexDomainClaim](#simplexdomainclaim) +- [SimplexDomainError](#simplexdomainerror) +- [SimplexDomainProof](#simplexdomainproof) - [SimplexLinkType](#simplexlinktype) -- [SimplexNameDomain](#simplexnamedomain) - [SimplexNameInfo](#simplexnameinfo) - [SimplexNameType](#simplexnametype) - [SimplexTLD](#simplextld) @@ -312,6 +319,9 @@ FILE: - type: "FILE" - fileErr: [FileErrorType](#fileerrortype) +NO_NAME_SERVERS: +- type: "NO_NAME_SERVERS" + PROXY: - type: "PROXY" - proxyServer: string @@ -459,6 +469,7 @@ TIMEOUT: - chatType: [BusinessChatType](#businesschattype) - businessId: string - customerId: string +- businessDomain: [SimplexDomainClaim](#simplexdomainclaim)? --- @@ -863,7 +874,7 @@ Group: - editable: bool - forwardedByMember: int64? - showGroupAsSender: bool -- msgSigned: [MsgSigStatus](#msgsigstatus)? +- msgVerified: [MsgVerified](#msgverified)? - createdAt: UTCTime - updatedAt: UTCTime @@ -1046,9 +1057,6 @@ NoRcvFileUser: UserUnknown: - type: "userUnknown" -ActiveUserExists: -- type: "activeUserExists" - UserExists: - type: "userExists" - contactName: string @@ -1106,6 +1114,14 @@ ChatStoreChanged: InvalidConnReq: - type: "invalidConnReq" +SimplexDomainNotReady: +- type: "simplexDomainNotReady" +- simplexDomain: [SimplexDomain](#simplexdomain) +- simplexDomainError: [SimplexDomainError](#simplexdomainerror) + +NotResolvedLocally: +- type: "notResolvedLocally" + UnsupportedConnReq: - type: "unsupportedConnReq" @@ -1978,6 +1994,10 @@ EXPIRED: INTERNAL: - type: "INTERNAL" +NAME: +- type: "NAME" +- nameErr: [NameErrorType](#nameerrortype) + DUPLICATE_: - type: "DUPLICATE_" @@ -2103,6 +2123,15 @@ NO_FILE: - cancelled: bool +--- + +## FileType + +**Enum type**: +- "normal" +- "roster" + + --- ## Format @@ -2191,6 +2220,7 @@ Phone: - support: [SupportGroupPreference](#supportgrouppreference) - sessions: [RoleGroupPreference](#rolegrouppreference) - comments: [CommentsGroupPreference](#commentsgrouppreference) +- signMessages: [GroupPreference](#grouppreference) - commands: [[ChatBotCommand](#chatbotcommand)] @@ -2283,6 +2313,7 @@ MemberSupport: - "support" - "sessions" - "comments" +- "signMessages" --- @@ -2319,9 +2350,11 @@ MemberSupport: - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? - customData: JSONObject? - groupSummary: [GroupSummary](#groupsummary) +- rosterVersion: int64? - membersRequireAttention: int - viaGroupLinkUri: string? - groupKeys: [GroupKeys](#groupkeys)? +- groupDomainVerified: bool? --- @@ -2422,6 +2455,7 @@ UpdateRequired: - supportChat: [GroupSupportChat](#groupsupportchat)? - memberPubKey: string? - relayLink: string? +- memberVerifiedCode: [SecurityCode](#securitycode)? --- @@ -2522,6 +2556,7 @@ UpdateRequired: - support: [SupportGroupPreference](#supportgrouppreference)? - sessions: [RoleGroupPreference](#rolegrouppreference)? - comments: [CommentsGroupPreference](#commentsgrouppreference)? +- signMessages: [GroupPreference](#grouppreference)? - commands: [[ChatBotCommand](#chatbotcommand)]? @@ -2739,12 +2774,15 @@ Unknown: - displayName: string - fullName: string - shortDescr: string? +- description: string? - image: string? - contactLink: string? - preferences: [Preferences](#preferences)? - peerType: [ChatPeerType](#chatpeertype)? - localBadge: [LocalBadge](#localbadge)? - localAlias: string +- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)? +- contactDomainVerified: bool? --- @@ -2920,6 +2958,37 @@ Unknown: - "signedNoKey" +--- + +## MsgVerified + +**Discriminated union type**: + +Signed: +- type: "signed" +- sigStatus: [MsgSigStatus](#msgsigstatus) + +SigMissing: +- type: "sigMissing" + + +--- + +## NameErrorType + +**Discriminated union type**: + +NO_RESOLVER: +- type: "NO_RESOLVER" + +NOT_FOUND: +- type: "NOT_FOUND" + +RESOLVER: +- type: "RESOLVER" +- resolverErr: string + + --- ## NetworkError @@ -2956,6 +3025,7 @@ SubscribeError: - profile: [Profile](#profile)? - pastTimestamp: bool - userChatRelay: bool +- clientService: bool --- @@ -3029,6 +3099,16 @@ count= - updatedAt: UTCTime +--- + +## PlanResolveMode + +**Enum type**: +- "allGroups" +- "unknown" +- "never" + + --- ## PrefEnabled @@ -3084,11 +3164,13 @@ count= - displayName: string - fullName: string - shortDescr: string? +- description: string? - image: string? - contactLink: string? - preferences: [Preferences](#preferences)? - peerType: [ChatPeerType](#chatpeertype)? - badge: [BadgeProof](#badgeproof)? +- contactDomain: [SimplexDomainClaim](#simplexdomainclaim)? --- @@ -3137,7 +3219,7 @@ NO_SESSION: **Record type**: - groupWebPage: string? -- groupDomain: string? +- groupDomainClaim: [SimplexDomainClaim](#simplexdomainclaim)? - domainWebPage: bool - allowEmbedding: bool @@ -3319,6 +3401,7 @@ Cancelled: - xftpRcvFile: [XFTPRcvFile](#xftprcvfile)? - fileInvitation: [FileInvitation](#fileinvitation) - fileStatus: [RcvFileStatus](#rcvfilestatus) +- fileType: [FileType](#filetype) - rcvFileInline: [InlineFileMode](#inlinefilemode)? - senderDisplayName: string - chunkSize: int64 @@ -3443,6 +3526,7 @@ ParseError: - "new" - "invited" - "accepted" +- "acknowledgedRoster" - "active" - "inactive" - "rejected" @@ -3519,6 +3603,48 @@ A_QUEUE: - allow: [FeatureAllowed](#featureallowed) +--- + +## SimplexDomain + +**Record type**: +- nameTLD: [SimplexTLD](#simplextld) +- domain: string +- subDomain: [string] + + +--- + +## SimplexDomainClaim + +**Record type**: +- domain: string +- proof: [SimplexDomainProof](#simplexdomainproof)? + + +--- + +## SimplexDomainError + +**Discriminated union type**: + +NoValidLink: +- type: "noValidLink" + +UnknownDomain: +- type: "unknownDomain" + + +--- + +## SimplexDomainProof + +**Record type**: +- linkOwnerId: string? +- presHeader: string +- signature: string + + --- ## SimplexLinkType @@ -3531,23 +3657,13 @@ A_QUEUE: - "relay" ---- - -## SimplexNameDomain - -**Record type**: -- nameTLD: [SimplexTLD](#simplextld) -- domain: string -- subDomain: [string] - - --- ## SimplexNameInfo **Record type**: - nameType: [SimplexNameType](#simplexnametype) -- nameDomain: [SimplexNameDomain](#simplexnamedomain) +- nameDomain: [SimplexDomain](#simplexdomain) --- @@ -4219,8 +4335,9 @@ Handshake: - sendRcptsSmallGroups: bool - autoAcceptMemberContacts: bool - userMemberProfileUpdatedAt: UTCTime? -- uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? - userChatRelay: bool +- clientService: bool +- uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? --- diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 1cd7c78913..46f8b6032d 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -28,7 +28,7 @@ chatCommandsDocs = map toCategory chatCommandsDocsData CCCategory {categoryName, categoryDescr, commands = map toCmd commandsData} toCmd (consName, hideParams, commandDescr, respNames, errors, network, syntax) = case find ((consName ==) . consName') chatCommandsTypeInfo of Just RecordTypeInfo {fieldInfos} -> - let fields = filter ((`notElem` hideParams) . fieldName') $ map (toAPIField consName) fieldInfos + let fields = map (toAPIField consName) $ filter ((`notElem` hideParams) . fieldName) fieldInfos commandType = ATUnionMember (fstToLower consName) fields findResp name = case find ((name ==) . consName') chatResponsesDocs of Just resp -> resp @@ -77,7 +77,7 @@ chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName] chatCommandsDocsData = [ ( "Address commands", "Bots can use these commands to automatically check and create address when initialized", - [ ("APICreateMyAddress", [], "Create bot address.", ["CRUserContactLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_address " <> Param "userId"), + [ ("APICreateMyAddress", ["server_"], "Create bot address.", ["CRUserContactLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_address " <> Param "userId"), ("APIDeleteMyAddress", [], "Delete bot address.", ["CRUserContactLinkDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete_address " <> Param "userId"), ("APIShowMyAddress", [], "Get bot address and settings.", ["CRUserContactLink", "CRChatCmdError"], [], Nothing, "/_show_address " <> Param "userId"), ("APISetProfileAddress", [], "Add address to bot profile.", ["CRUserProfileUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_profile_address " <> Param "userId" <> " " <> OnOff "enable"), @@ -86,7 +86,7 @@ chatCommandsDocsData = ), ( "Message commands", "Commands to send, update, delete, moderate messages and set message reactions", - [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems", "CRChatCmdError"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> " json " <> Json "composedMessages"), + [ ("APISendMessages", [], "Send messages.", ["CRNewChatItems", "CRChatCmdError"], [], Just UNBackground, "/_send " <> Param "sendRef" <> OnOffParam "live" "liveMessage" (Just False) <> Optional "" (" ttl=" <> Param "$0") "ttl" <> OnOffParam "sign" "signMessages" (Just False) <> " json " <> Json "composedMessages"), ( "APIUpdateChatItem", [], "Update message.", @@ -135,10 +135,10 @@ chatCommandsDocsData = ( "Connection commands", "These commands may be used to create connections. Most bots do not need to use them - bot users will connect via bot address with auto-accept enabled.", [ ("APIAddContact", [], "Create 1-time invitation link.", ["CRInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> OnOffParam "incognito" "incognito" (Just False)), - -- `Maybe` in `connectionLink :: Maybe AConnectionLink` is used to signal link parsing error to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers. - ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectionLink"), + -- `Maybe` in `connectTarget :: Maybe ConnectTarget` is used to signal parse failure to the runtime (the handler returns CEInvalidConnReq on Nothing); it is NOT API-level optionality. The parameter is required from callers. + ("APIConnectPlan", [], "Determine SimpleX link type and if the bot is already connected via this link or name.", ["CRConnectionPlan", "CRChatCmdError"], [], Just UNInteractive, "/_connect plan " <> Param "userId" <> " " <> Param "connectTarget"), ("APIConnect", [], "Connect via prepared SimpleX link. The link can be 1-time invitation link, contact address or group link.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/_connect " <> Param "userId" <> Optional "" (" " <> Param "$0") "preparedLink_"), - ("Connect", [], "Connect via SimpleX link as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connLink_"), + ("Connect", [], "Connect via SimpleX link or name as string in the active user profile.", ["CRSentConfirmation", "CRContactAlreadyExists", "CRSentInvitation", "CRChatCmdError"], [], Just UNInteractive, "/connect" <> Optional "" (" " <> Param "$0") "connTarget_"), ("APIAcceptContact", ["incognito"], "Accept contact request.", ["CRAcceptingContactRequest", "CRChatCmdError"], [], Just UNInteractive, "/_accept " <> Param "contactReqId"), ("APIRejectContact", [], "Reject contact request. The user who sent the request is **not notified**.", ["CRContactRequestRejected", "CRChatCmdError"], [], Nothing, "/_reject " <> Param "contactReqId") ] @@ -272,6 +272,7 @@ cliCommands = "SetAddressSettings", "SetBotCommands", "SetChatTTL", + "SetClientService", "SetContactFeature", "SetContactTimedMessages", "SetGroupFeature", @@ -280,6 +281,7 @@ cliCommands = "SetGroupTimedMessages", "SetLocalDeviceName", "SetProfileAddress", + "SetPublicGroupAccess", "SetSendReceipts", "SetShowMemberMessages", "SetShowMessages", @@ -288,6 +290,7 @@ cliCommands = "SetUserGroupReceipts", "SetUserAutoAcceptMemberContacts", "SetUserTimedMessages", + "ShareMyAddress", "SharePublicGroup", "ShowChatItem", "ShowChatItemInfo", @@ -311,6 +314,7 @@ cliCommands = "UpdateLiveMessage", "UpdateProfile", "UpdateProfileImage", + "UpdateProfileImageFromFile", "UserRead", "VerifyContact", "VerifyGroupMember", @@ -408,12 +412,15 @@ undocumentedCommands = "APISetMemberSettings", "APISetNetworkConfig", "APISetNetworkInfo", + "APISetPublicGroupAccess", "APISetServerOperators", "APISetUserContactReceipts", "APISetUserGroupReceipts", + "APISetUserDomain", "APISetUserServers", "APISetUserUIThemes", "APIShareChatMsgContent", + "APIShareMyAddress", "APIStandaloneFileInfo", "APIStorageEncryption", "APISuspendChat", @@ -430,7 +437,9 @@ undocumentedCommands = "APIUserRead", "APIValidateServers", "APIVerifyContact", + "APIVerifyContactDomain", "APIVerifyGroupMember", + "APIVerifyGroupDomain", "APIVerifyToken", "CheckChatRunning", "ConfirmRemoteCtrl", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index c8446e9e67..f0c9352efd 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -188,6 +188,7 @@ undocumentedEvents = "CEvtCustomChatEvent", "CEvtGroupMemberRatchetSync", "CEvtGroupMemberSwitch", + "CEvtServiceSubStatus", "CEvtNewRemoteHost", "CEvtNoMemberContactCreating", "CEvtNtfMessage", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index ddd127241b..f897fc3908 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -148,6 +148,7 @@ undocumentedResponses = "CRContactAliasUpdated", "CRContactCode", "CRContactInfo", + "CRContactDomainVerified", "CRContactRatchetSyncStarted", "CRContactSwitchAborted", "CRContactSwitchStarted", @@ -167,6 +168,7 @@ undocumentedResponses = "CRGroupMemberRatchetSyncStarted", "CRGroupMemberSwitchAborted", "CRGroupMemberSwitchStarted", + "CRGroupDomainVerified", "CRGroupProfile", "CRGroupUserChanged", "CRItemsReadForChat", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 7b268f4ec5..5e1e2bb082 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -34,7 +34,8 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Operators import Simplex.Messaging.Agent.Store.Entity (DBStored (..)) -import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), BadgeType (..), JSONBadge (..)) +import Simplex.Chat.Badges +import Simplex.Chat.Names import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared @@ -45,7 +46,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Client import Simplex.Messaging.Crypto.File import Simplex.Messaging.Parsers (dropPrefix, fstToLower) -import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..)) +import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NameErrorType (..), NetworkError (..), ProxyError (..)) import Simplex.Messaging.Protocol.Types (ClientNotice (..)) import Simplex.Messaging.Transport import Simplex.RemoteControl.Types @@ -270,6 +271,7 @@ chatTypesDocsData = (sti @FileProtocol, STEnum' (consLower "FP"), "", [], "", ""), (sti @FileStatus, STEnum, "FS", [], "", ""), (sti @FileTransferMeta, STRecord, "", [], "", ""), + (sti @FileType, STEnum' (consLower "FT"), "", [], "", ""), (sti @Format, STUnion, "", ["Unknown"], "", ""), (sti @FormattedText, STRecord, "", [], "", ""), (sti @FullGroupPreferences, STRecord, "", [], "", ""), @@ -320,11 +322,14 @@ chatTypesDocsData = (sti @MsgReaction, STUnion, "MR", [], "", ""), (sti @MsgReceiptStatus, STEnum, "MR", [], "", ""), (sti @MsgSigStatus, STEnum, "MSS", [], "", ""), + (sti @MsgVerified, STUnion, "MV", [], "", ""), + (sti @NameErrorType, STUnion, "", [], "", ""), (sti @NetworkError, STUnion, "NE", [], "", ""), (sti @NewUser, STRecord, "", [], "", ""), (sti @NoteFolder, STRecord, "", [], "", ""), (sti @OwnerVerification, STUnion, "OV", [], "", ""), (sti @PendingContactConnection, STRecord, "", [], "", ""), + (sti @PlanResolveMode, STEnum, "PRM", [], "", ""), (sti @PrefEnabled, STRecord, "", [], "", ""), (sti @Preferences, STRecord, "", [], "", ""), (sti @PreparedContact, STRecord, "", [], "", ""), @@ -352,8 +357,11 @@ chatTypesDocsData = (sti @RoleGroupPreference, STRecord, "", [], "", ""), (sti @SecurityCode, STRecord, "", [], "", ""), (sti @SimplePreference, STRecord, "", [], "", ""), + (sti @SimplexDomain, STRecord, "", [], "", ""), + (sti @SimplexDomainClaim, STRecord, "", [], "", ""), + (sti @SimplexDomainError, STUnion, "SDE", [], "", ""), + (sti @SimplexDomainProof, STRecord, "", [], "", ""), (sti @SimplexLinkType, STEnum, "XL", [], "", ""), - (sti @SimplexNameDomain, STRecord, "", [], "", ""), (sti @SimplexNameInfo, STRecord, "", [], "", ""), (sti @SimplexNameType, STEnum, "NT", [], "", ""), (sti @SimplexTLD, STEnum, "TLD", [], "", ""), @@ -489,6 +497,7 @@ deriving instance Generic FileInvitation deriving instance Generic FileProtocol deriving instance Generic FileStatus deriving instance Generic FileTransferMeta +deriving instance Generic FileType deriving instance Generic Format deriving instance Generic FormattedText deriving instance Generic FullGroupPreferences @@ -546,11 +555,14 @@ deriving instance Generic MsgFilter deriving instance Generic MsgReaction deriving instance Generic MsgReceiptStatus deriving instance Generic MsgSigStatus +deriving instance Generic MsgVerified +deriving instance Generic NameErrorType deriving instance Generic NetworkError deriving instance Generic NewUser deriving instance Generic NoteFolder deriving instance Generic OwnerVerification deriving instance Generic PendingContactConnection +deriving instance Generic PlanResolveMode deriving instance Generic PrefEnabled deriving instance Generic Preferences deriving instance Generic PreparedContact @@ -576,8 +588,11 @@ deriving instance Generic RelayProfile deriving instance Generic RelayStatus deriving instance Generic ReportReason deriving instance Generic SecurityCode +deriving instance Generic SimplexDomain +deriving instance Generic SimplexDomainClaim +deriving instance Generic SimplexDomainError +deriving instance Generic SimplexDomainProof deriving instance Generic SimplexLinkType -deriving instance Generic SimplexNameDomain deriving instance Generic SimplexNameInfo deriving instance Generic SimplexNameType deriving instance Generic SimplexTLD diff --git a/bots/src/API/TypeInfo.hs b/bots/src/API/TypeInfo.hs index 8dfba2bbb0..e225659c4d 100644 --- a/bots/src/API/TypeInfo.hs +++ b/bots/src/API/TypeInfo.hs @@ -170,6 +170,7 @@ toTypeInfo tr = "DBEntityId'" -> ST TInt64 [] "Integer" -> ST TInt64 [] "Version" -> ST TInt [] + "VersionRoster" -> ST TInt64 [] "BoolDef" -> ST TBool [] "PQEncryption" -> ST TBool [] "PQSupport" -> ST TBool [] @@ -193,6 +194,7 @@ toTypeInfo tr = primitiveToLower st@(ST t ps) = let t' = fstToLower t in if t' `elem` primitiveTypes then ST t' ps else st stringTypes = [ "AConnectionLink", + "AConnectTarget", "AProtocolType", "AgentConnId", "AgentInvId", @@ -214,11 +216,13 @@ toTypeInfo tr = "Text", "MREmojiChar", "PrivateKey", + "ProofPresHeader", "PublicKey", "ProtocolServer", "SbKey", "SharedMsgId", "Signature", + "StrJSON", "TransportHost", "UIColor", "UserPwd", @@ -236,7 +240,8 @@ toTypeInfo tr = [ "FullDeleteGroupPreference", "ReactionsGroupPreference", "ReportsGroupPreference", - "HistoryGroupPreference" + "HistoryGroupPreference", + "SignMessagesGroupPreference" ] roleGroupPrefTypes = [ "DirectMessagesGroupPreference", diff --git a/cabal.project b/cabal.project index 06622a65a2..3c0526fa6e 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 92598c2ddb06cfc2c19797a2c900cffcb8af4d5c + tag: efaad8e73436d60f5052f07dda6b71151ad5039b source-repository-package type: git diff --git a/docs/CHAT-RELAY.md b/docs/CHAT-RELAY.md new file mode 100644 index 0000000000..a06c06026f --- /dev/null +++ b/docs/CHAT-RELAY.md @@ -0,0 +1,259 @@ +--- +title: Hosting your own Chat Relay +revision: 16.07.2026 +--- + +# Hosting your own Chat Relay + +Chat relays are used to deliver channel messages in SimpleX Network. Read more about channels in this [whitepaper](https://github.com/simplex-chat/simplex-chat/blob/master/docs/protocol/channels-overview.md) and this [blog post](https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html). + +A chat relay is the SimpleX Chat CLI (`simplex-chat`) running in relay mode (`--relay`). It has its own profile (a display name and a picture), its own address, and in addition to delivering messages, it can generate data for web previews of the channels it delivers. + +This guide explains how to set up a chat relay on a Linux server, how to run it, and (optionally) how to configure [Caddy](https://caddyserver.com) to serve data for channel web previews. + +> **Please note**: This guide applies only to SimpleX Chat v7.0.0-beta.4 and later. + +## Table of Contents + +- [Install the CLI](#install-the-cli) +- [Run the relay](#run-the-relay) + - [Relay options](#relay-options) + - [Get the relay address](#get-the-relay-address) + - [Run relay commands](#run-relay-commands) +- [Channel web previews](#channel-web-previews) + - [Relay web options](#relay-web-options) + - [Serve the previews with Caddy](#serve-the-previews-with-caddy) + - [Reload CORS automatically](#reload-cors-automatically) + - [Verify](#verify) + +## Install the CLI + +The relay is the standard `simplex-chat` CLI binary. Install or update it with the install script: + +```sh +curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/stable/install.sh | bash +``` + +Other options (manual binary download, building from source) are in the [CLI guide](./CLI.md#download-chat-client). + +Copy the installed `simplex-chat` binary to `/usr/local/bin/simplex-chat-relay`. The guide uses that name so the relay is separate from any interactive `simplex-chat` you also run on the server. + +Create a dedicated user for the relay (called `relay` below), so it does not run as root and keeps its database in one place: + +```sh +sudo useradd -m relay +``` + +The `useradd -m` flag creates its home directory `/home/relay`, where the guide keeps the database and picture. Run the relay commands (the `-e` commands below) as this user, for example with `sudo -u relay ...`; run the `systemd` and Caddy steps as root. + +## Run the relay + +Run the relay as a `systemd` service. With `--headless` it starts without any interactive prompts. It creates its profile and address on the first start, and writes its output to the journal. + +Create a run script `/usr/local/bin/relay-run`: + +```sh +#!/bin/sh +exec /usr/local/bin/simplex-chat-relay \ + --relay \ + --headless \ + --user-display-name "My Relay" \ + --user-image-file /home/relay/avatar.png \ + -d /home/relay/relay +``` + +```sh +chmod +x /usr/local/bin/relay-run +``` + +Create `/etc/systemd/system/simplex-relay.service`: + +```ini +[Unit] +Description=SimpleX Chat relay +After=network.target + +[Service] +User=relay +ExecStart=/usr/local/bin/relay-run +Restart=always +StandardInput=null + +[Install] +WantedBy=multi-user.target +``` + +Enable and start it: + +```sh +systemctl daemon-reload +systemctl enable --now simplex-relay +``` + +The first start creates the relay profile (with the given name and picture) and its address; later starts reuse them. Both are written to the journal: + +``` +Current user: My Relay +Chat relay address is created: +https://smp4.simplex.im/r#73iEnnvCqPTVGArCAWUcRaj5hxRb7TbPCSZ2JY2VjCQ +``` + +### Relay options + +| Option | Purpose | +| --- | --- | +| `--relay` | Run as a chat relay. Required. | +| `--headless` | Don't ask interactive questions; create the profile and address automatically. On first start it also needs `--user-display-name`. | +| `--user-display-name NAME` | The relay's display name. Creates the profile on first start; on later starts it must match the existing profile. | +| `--user-image-file FILE` | The relay's picture, from a `.png`, `.jpg` or `.jpeg` file. Applied **only when the profile is created**; ignored afterwards. | +| `--relay-address-server SERVER` | Create the relay address on a specific SMP server, e.g. `smp://@smp.example.com`. By default a preset server is used. Requires `--relay`. | + +### Get the relay address + +The address is created and logged on the first start. Read it from the journal at any time: + +```sh +journalctl -u simplex-relay | grep -A1 "address is created" +``` + +### Run relay commands + +The service runs headless, so there is no attached terminal to type into. To run a one-off command, stop the service, run the command against the relay's database with `-e`, then start it again. For example, to change the picture (`--user-image-file` only sets it when the profile is first created): + +```sh +systemctl stop simplex-relay +simplex-chat-relay -d /home/relay/relay -e "/set profile image file /home/relay/new-avatar.png" +systemctl start simplex-relay +``` + +## Channel web previews + +Chat relays can render recent messages of its public channels as JSON files, which can be served over HTTPS using a web server to create channel web previews. This is optional. + +### Relay web options + +Add these to the run script (`--relay-web-domain` and `--relay-web-dir` must be given together): + +```sh + --relay-web-domain relay.example.com \ + --relay-web-dir /var/www/relay-web-channels/channel \ + --relay-web-cors-file /var/www/relay-web-channels/cors.conf \ + --relay-web-interval 30 \ +``` + +| Option | Purpose | +| --- | --- | +| `--relay-web-domain DOMAIN` | Domain the previews are served from. | +| `--relay-web-dir DIR` | Directory the relay writes channel JSON files to. | +| `--relay-web-cors-file FILE` | File the relay writes the generated Caddy CORS config to. | +| `--relay-web-interval SECONDS` | How often previews are regenerated (default `300`). | +| `--relay-web-item-count COUNT` | Recent messages per channel preview (default `50`). | + +Create the web directory, owned by the relay user: + +```sh +mkdir -p /var/www/relay-web-channels/channel +chmod 0755 /var/www/relay-web-channels +chown -R relay:relay /var/www/relay-web-channels +``` + +Restart the relay so the new flags take effect: + +```sh +systemctl restart simplex-relay +``` + +### Serve the previews with Caddy + +This section uses [Caddy](https://caddyserver.com) as the web server. Install it (Debian/Ubuntu): + +```sh +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl &&\ +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg &&\ +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list &&\ +sudo apt update && sudo apt install caddy +``` + +The relay writes files to `/var/www/relay-web-channels/channel/.json`. Serve them, and import the relay's generated CORS rules, in your `Caddyfile`: + +``` +relay.example.com { + encode zstd gzip + + handle /channel/* { + root * /var/www/relay-web-channels # files resolve to .../channel/.json + file_server + import /etc/caddy/simplex-cors.conf + } +} +``` + +Keep `root` at the parent directory with a non-stripping `handle`. That is what makes `/channel/.json` resolve to `.../channel/.json`. Do not point `root` at the `channel` subdirectory: the relay's generated CORS matchers are `/channel/*.json`, which only match when the prefix is kept. + +```sh +touch /etc/caddy/simplex-cors.conf # so the import doesn't fail before the first write +usermod -aG relay caddy # let caddy read the relay user's files +systemctl restart caddy # restart (not reload) to pick up the group +``` + +### Reload CORS automatically + +The relay updates its CORS file as channels change. Copy it into Caddy's config and reload Caddy whenever it changes. + +Create `/usr/local/bin/simplex-cors-sync.sh`: + +```sh +#!/bin/sh +set -eu +SRC=/var/www/relay-web-channels/cors.conf +DST=/etc/caddy/simplex-cors.conf +[ -f "$SRC" ] || exit 0 +cmp -s "$SRC" "$DST" 2>/dev/null && exit 0 +install -m 0644 "$SRC" "$DST" +systemctl reload caddy +logger -t simplex-cors "reloaded caddy" +``` + +Create `/etc/systemd/system/simplex-cors-sync.service`: + +```ini +[Unit] +Description=Sync SimpleX relay CORS config to Caddy +StartLimitIntervalSec=30 +StartLimitBurst=10 +[Service] +Type=oneshot +ExecStartPre=/bin/sleep 2 +ExecStart=/usr/local/bin/simplex-cors-sync.sh +``` + +Create `/etc/systemd/system/simplex-cors-sync.path` to run the service whenever the relay's CORS file changes: + +```ini +[Unit] +Description=Watch SimpleX relay CORS config +After=caddy.service +[Path] +PathChanged=/var/www/relay-web-channels/cors.conf +Unit=simplex-cors-sync.service +[Install] +WantedBy=multi-user.target +``` + +Enable it: + +```sh +chmod +x /usr/local/bin/simplex-cors-sync.sh +systemctl daemon-reload +systemctl enable --now simplex-cors-sync.path +``` + +### Verify + +```sh +systemctl status simplex-cors-sync.path # active (waiting) +ls /var/www/relay-web-channels/channel # a JSON file appears once a public channel renders +curl -sI https://relay.example.com/channel/.json | grep -i access-control +``` + +The `curl` should return `access-control-*` headers, and the channel link should open a web preview in a browser. diff --git a/docs/CLI.md b/docs/CLI.md index 628fe2a4af..dc7f85cd38 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -220,7 +220,7 @@ Use `/help` in chat to see the list of available commands. To create a group use `/g `, then add contacts to it with `/a `. You can then send messages to the group by entering `# `. Use `/help groups` for other commands. -![simplex-chat](../images/groups.gif) +![simplex-chat](/images/groups.gif) > **Please note**: the groups are not stored on any server, they are maintained as a list of members in the app database to whom the messages will be sent. @@ -228,7 +228,7 @@ To create a group use `/g `, then add contacts to it with `/a ` - the recipient will have to accept it before it is sent. Use `/help files` for other commands. -![simplex-chat](../images/files.gif) +![simplex-chat](/images/files.gif) You can send files to a group with `/f # `. @@ -242,4 +242,4 @@ User address is "long-term" in a sense that it is a multiple-use connection link Use `/help address` for other commands. -![simplex-chat](../images/user-addresses.gif) +![simplex-chat](/images/user-addresses.gif) diff --git a/docs/FAQ.md b/docs/FAQ.md index 8c14168811..21cb08417f 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -21,6 +21,7 @@ revision: 13.08.2025 - [Why cannot I delete messages I sent from my contact's device?](#why-cannot-i-delete-messages-i-sent-from-my-contacts-device) - [What do group roles mean?](#what-do-group-roles-mean) - [I don't want to share a web link or a QR code. How can I connect?](#i-dont-want-to-share-a-web-link-or-a-qr-code-how-can-i-connect) +- [How to change servers to my own](#how-to-change-servers-to-my-own) [Troubleshooting](#troubleshooting) - [I do not receive messages or message notifications](#i-do-not-receive-messages-or-message-notifications) @@ -175,6 +176,16 @@ For example, this link: `https://smp18.simplex.im/i#E74vSxMwDnEx6DAvRCZmzBeZwwAs becomes: `simplex:/i#E74vSxMwDnEx6DAvRCZmzBeZwwAseJUD/yVTHjaaH_EzL19DG7fvd46Mjry3IBqYT0UMo5G7l4jQ?h=smp18.simplex.im` +### How to change servers to my own? + +Add your server in Settings -> Network & servers -> Your servers. If you have multiple profiles, you need to add your server in every profile. +After that, disable all preset servers. If you have multiple profiles, you must first add your server to all profiles for this option to be available. + +To resolve SimpleX public names, if your server does not support it, you may leave preset servers enabled only for resolving the public names. + +Please note: configured servers only determine which servers will be used for the new connections (members/contacts). Previously created connections don't switch automatically. You can manually switch all existing contacts and small/important groups (each member needs to be switched), and then leave and re-join all large groups. +You can switch contacts/members manually to another server by going to their profile and clicking on "Change receiving address", note that the current server must be online for the address change to work. + ## Troubleshooting ### I do not receive messages or message notifications diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index c8fdd56bdd..5ea32c73e9 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -163,7 +163,7 @@ In the context of SimpleX network, these are the identifiers generated by SMP re Peer-to-peer (P2P) is the network architecture when participants have equal rights and communicate directly via a general purpose transport or overlay network. Unlike client-server architecture, all peers in a P2P network both provide and consume the resources. In the context of messaging, P2P architecture usually means that the messages are sent between peers, without user accounts or messages being stored on any servers. Examples are Tox, Briar, Cwtch and many others. -The advantage is that the participants do not depend on any servers. There are [multiple downsides](./SIMPLEX.md#comparison-with-p2p9-messaging-protocols) to that architecture, such as no asynchronous message delivery, the need for network-wide peer addresses, possibility of network-wide attacks, that are usually mitigated only by using a centralized authority. These disadvantages are avoided with [proxied P2P](#proxied-peer-to-peer) architecture. +The advantage is that the participants do not depend on any servers. There are [multiple downsides](./SIMPLEX.md#comparison-with-p2p-messaging-protocols) to that architecture, such as no asynchronous message delivery, the need for network-wide peer addresses, possibility of network-wide attacks, that are usually mitigated only by using a centralized authority. These disadvantages are avoided with [proxied P2P](#proxied-peer-to-peer) architecture. [Wikipedia](https://en.wikipedia.org/wiki/Peer-to-peer). diff --git a/docs/SECURITY.md b/docs/SECURITY.md index b9218fbebc..73c050c106 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,7 +1,7 @@ --- title: Security Policy permalink: /security/index.html -revision: 23.04.2024 +revision: 25.05.2026 --- # Security Policy @@ -12,7 +12,7 @@ The implementation security assessment of SimpleX cryptography and networking wa The cryptographic review of SimpleX protocols design was done by Trail of Bits in [July 2024](../blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md). -We are planning implementation security assessment in early 2025. +We have scheduled implementation security assessment for June 2026. ## Reporting security issues diff --git a/docs/SERVER.md b/docs/SERVER.md index a35ede5cd0..547cf6a352 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -28,6 +28,7 @@ revision: 12.10.2024 - [Installation for onion address](#installation-for-onion-address) - [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy) - [Server information page](#server-information-page) +- [Name resolution](#name-resolution) - [Documentation](#documentation) - [SMP server address](#smp-server-address) - [Systemd commands](#systemd-commands) @@ -1217,6 +1218,80 @@ _Please note:_ this configuration is supported since `v6.1.0-beta.2`. 10. Access the webpage you've deployed from your browser (`https://smp.example.org`). You should see the smp-server information that you've provided in your ini file. +## Name resolution + +SimpleX public names (like `alice.simplex`) resolve to contact and channel addresses. These records are stored in the SimpleX Namespace registry contracts on Ethereum, so resolving a name means reading from an Ethereum node. Read more about [SimpleX Public Names](./protocol/names-overview.md). + +With the `[NAMES]` section enabled, your smp-server resolves names for the clients connected to it: it forwards their lookups to a local REST resolver and returns the addresses. Resolution is off by default and requires running a resolver stack (an Ethereum node plus the resolver service), which needs: + +- 300 GB or more of NVMe SSD (TLC, not QLC, which stalls during sync) +- 32 GB RAM and a fast multi-core CPU +- about one day for the initial Ethereum sync + +### 1. Deploy the resolver stack + +The resolver stack (a reth + nimbus Ethereum node plus the resolver service) ships in [`scripts/resolver`](https://github.com/simplex-chat/simplexmq/tree/master/scripts/resolver) and starts with one command. See its [README](https://github.com/simplex-chat/simplexmq/blob/master/scripts/resolver/README.md) for details. + +```sh +git clone https://github.com/simplex-chat/simplexmq +cd simplexmq/scripts/resolver +docker compose up -d +``` + +The shipped `.env` targets Ethereum mainnet and needs no changes. Open the peer-to-peer ports so the node can sync: + +```sh +ufw allow 30303 &&\ +ufw allow 9000 +``` + +The resolver returns errors until the node finishes syncing, which takes about a day. + +### 2. Check the resolver + +Once synced, confirm the resolver is healthy and resolves a name: + +```sh +curl -s http://127.0.0.1:8000/health +curl -s http://127.0.0.1:8000/resolve/foobar.testing +``` + +The first should report `"ok": true`. The second should return the records for the test name `foobar.testing`. + +### 3. Point smp-server at the resolver + +In `/etc/opt/simplex/smp-server.ini`, set the `[NAMES]` section: + +```ini +[NAMES] +enable: on +resolver_endpoint: http://127.0.0.1:8000 +``` + +No authentication is needed while smp-server and the resolver share a host over loopback. Behind a TLS reverse proxy on another host, use its HTTPS address and add credentials (`resolver_auth` over plain `http` is only allowed for loopback): + +```ini +[NAMES] +enable: on +resolver_endpoint: https://names.example.com:443 +resolver_auth: basic : +``` + +### 4. Restart smp-server + +```sh +systemctl restart smp-server +``` + +The server logs the resolver status on start: + +``` +[NAMES] resolver enabled, endpoint=http://127.0.0.1:8000 +[NAMES] endpoint probe ok +``` + +If the node is still syncing the server starts anyway, and name lookups fail until the resolver is ready. + ## Documentation All necessary files for `smp-server` are located in `/etc/opt/simplex/` folder. diff --git a/docs/contributing/PROJECT.md b/docs/contributing/PROJECT.md index 3f7e6e0e54..40417a6539 100644 --- a/docs/contributing/PROJECT.md +++ b/docs/contributing/PROJECT.md @@ -68,14 +68,15 @@ The project uses several custom forks managed via `cabal.project`: ```bash cd apps/multiplatform -# Build Android debug APK -./gradlew assembleDebug +# Build Android debug APK; `foss` ships to F-Droid/GitHub, `google` adds Play Billing. +# The aggregate tasks fail by design, see apps/multiplatform/README.md +./gradlew assembleFossDebug # Build desktop ./gradlew :desktop:packageDistributionForCurrentOS # Run Android tests -./gradlew connectedAndroidTest +./gradlew connectedFossDebugAndroidTest ``` ### iOS diff --git a/docs/guide/README.md b/docs/guide/README.md index 04e7538968..fe1546af0a 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -10,7 +10,9 @@ The first messaging platform that has no user identifiers of any kind — 100% p - [Quick start](#quick-start) - scroll down this page - [Sending messages](./send-messages.md) - [Secret groups](./secret-groups.md) +- [Channel webpage](./channel-webpage.md) - [Chat profiles](./chat-profiles.md) +- [Registering a SimpleX name](./register-simplex-name.md) - [Managing data](./managing-data.md) - [Audio & video calls](./audio-video-calls.md) - [Privacy & security](./privacy-security.md) diff --git a/docs/guide/channel-webpage.md b/docs/guide/channel-webpage.md new file mode 100644 index 0000000000..8cb0f4fbe5 --- /dev/null +++ b/docs/guide/channel-webpage.md @@ -0,0 +1,127 @@ +--- +title: Channel webpage +--- +# Channel webpage + +A channel webpage shows a preview of your channel on the web: its name, description, recent messages and subscriber count. Visitors can see what the channel is about before they subscribe, and the page gives them a "Join" button along with links to download the app. + +You don't have to build the preview yourself. The chat relays that host your channel publish its content as a small file, and a ready-made script renders it on your page. All it takes is to copy the code the app generates and paste it into a web page you host. + +## What you need + +A channel webpage can be set up by the **owner** of the channel, as long as the channel is hosted on chat relays that support webpages. If they don't, the app shows "Used chat relays do not support webpages." and no code is generated. + +You'll also need somewhere to publish an HTML page. Your own site works, but any static hosting will do. + +## Step 1. Open the channel webpage settings + +Open the channel and tap its name at the top to open the channel information. Scroll down to **Advanced options** and tap **Channel webpage**. This button is only shown to channel owners. + +## Step 2. Allow embedding while you build the page + +Turn on **Allow anyone to embed** and tap **Save**. + +With this on, the relay serves your channel preview to any page, so you can build and test from wherever the page lives without it being tied to one domain yet. Leave the webpage URL empty for now; nothing is shown to your subscribers until you set it. + +## Step 3. Copy the code + +Under **Webpage code** you'll see a snippet like the one below. Tap **Copy code**. + +```html +
+ +``` + +Everything specific to your channel is already in the code: its link, its ID, and the relay domains that serve the preview. There's no need to edit those values. + +## Step 4. Add the code to your page and test it + +Paste the snippet into the page you're going to publish. Here's a complete minimal page: + +```html + + + + + + My Channel + + + +
+ + + +``` + +Publish the page and open it in a browser. The channel preview shows up in place of the `
`. Because embedding is still open, it loads no matter which address you test from, so you can adjust the page until it looks right. + +## Step 5. Set the webpage URL and lock it down + +Once the page works, go back to **Channel webpage** in the app: + +- Under **Enter webpage URL**, type the address where the page is published, for example `https://example.com/my-channel`. +- Turn **Allow anyone to embed** off if you don't want other sites to be able to show your channel preview. Leave it on if you're happy for anyone to embed it. +- Tap **Save**. + +The URL now appears as a link in your channel info that every subscriber can see. If you turned embedding off, the relay also restricts the preview to your own domain. + +## Customizing the preview + +You can add optional `data-*` attributes to the `
` to change how it looks. Only `data-channel-id` and `data-relay-domains` are required, and both are already in the generated code. + +| Attribute | Values | Default | What it does | +| --- | --- | --- | --- | +| `data-channel-id` | channel ID (from the app) | none | Required. Identifies the channel to load. | +| `data-relay-domains` | comma-separated domains | none | Required. Relay domains that serve the preview, tried in order. | +| `data-channel-link` | channel link | none | Enables the "Join" button and QR code. Recommended. | +| `data-app-download-buttons` | `on`, `off` | `on` | Shows or hides the app download buttons. | +| `data-color-scheme` | `light`, `dark`, `site` | `light` | Color theme. `site` follows your page's theme (it uses dark styling when a parent element has the `dark` CSS class). | +| `data-light-background` | CSS color | `#ffffff` | Background color in light mode. | +| `data-dark-background` | CSS color | `#000832` | Background color in dark mode. | +| `data-relay-scheme` | `https`, `http` | `https` | Protocol used to load the preview from the relays. Leave it as `https`. | + +For example, here's a dark theme with a custom background and the download buttons hidden: + +```html +
+ +``` + +## Good to know + +The preview updates on its own. The relays republish channel content periodically, so the page picks up new messages without any change on your side. It's a read-only snapshot, so visitors can't post to it. + +Only what the channel already shows publicly is included: recent messages, member display names and avatars, reactions, and the subscriber count. Deleted and disappearing messages are never published. + +If your channel is served by more than one relay, all of them are listed in `data-relay-domains`. The script tries them in order, so the preview still loads when one relay is unavailable. + +## If something doesn't work + +If the preview area stays empty, check that the page is hosted on the same domain as the URL you set in Step 5, or turn **Allow anyone to embed** back on while you sort it out. The relay only lets that domain load the preview. + +If the app says "Used chat relays do not support webpages.", the relays hosting your channel don't support this feature yet, so no code can be generated. + +If there's no **Channel webpage** button, remember that it only appears for channel owners on channels hosted on relays. diff --git a/docs/guide/diagrams/simplex-name-steps.mmd b/docs/guide/diagrams/simplex-name-steps.mmd new file mode 100644 index 0000000000..ca19d3c6f3 --- /dev/null +++ b/docs/guide/diagrams/simplex-name-steps.mmd @@ -0,0 +1,19 @@ +flowchart TD + S([Start]) --> A["Step 1: Create a wallet
save your recovery phrase"] + A --> B["Step 2: Register the name
connect wallet, search, choose years"] + B --> C["On Create your profile, paste your link into
SimpleX contact (@name) or SimpleX channel (#name)"] + C --> H["Confirm: two transactions,
60 seconds apart"] + H --> I["Step 3: search or type your name
shows 'Unconfirmed name'"] + I --> D{"A contact or a channel?"} + D -->|"@name (contact)"| J1["Step 4: set Your SimpleX name
in your SimpleX address"] + D -->|"#name (channel)"| J2["Step 4: set SimpleX name
in the channel"] + J1 --> K["Step 5: from another device,
search or type the name"] + J2 --> K + K --> Z([Connected. Name verified.]) + + classDef start fill:#f4ecf7,stroke:#8e44ad,color:#4a235a; + classDef step fill:#eaf2fb,stroke:#2e86de,color:#1b3a5b; + classDef ok fill:#d5f5e3,stroke:#27ae60,color:#145a32; + class S start; + class Z ok; + class A,B,C,H,I,J1,J2,K step; diff --git a/docs/guide/diagrams/simplex-name-steps.svg b/docs/guide/diagrams/simplex-name-steps.svg new file mode 100644 index 0000000000..4d062ec1c2 --- /dev/null +++ b/docs/guide/diagrams/simplex-name-steps.svg @@ -0,0 +1 @@ +

@name (contact)

#name (channel)

Start

Step 1: Create a wallet
save your recovery phrase

Step 2: Register the name
connect wallet, search, choose years

On Create your profile, paste your link into
SimpleX contact (@name) or SimpleX channel (#name)

Confirm: two transactions,
60 seconds apart

Step 3: search or type your name
shows 'Unconfirmed name'

A contact or a channel?

Step 4: set Your SimpleX name
in your SimpleX address

Step 4: set SimpleX name
in the channel

Step 5: from another device,
search or type the name

Connected. Name verified.

\ No newline at end of file diff --git a/docs/guide/images/before-we-start.png b/docs/guide/images/before-we-start.png new file mode 100644 index 0000000000..eb853fcd96 Binary files /dev/null and b/docs/guide/images/before-we-start.png differ diff --git a/docs/guide/images/create-your-profile.png b/docs/guide/images/create-your-profile.png new file mode 100644 index 0000000000..2d5e96d9ee Binary files /dev/null and b/docs/guide/images/create-your-profile.png differ diff --git a/docs/guide/images/register-name.png b/docs/guide/images/register-name.png new file mode 100644 index 0000000000..94584f54df Binary files /dev/null and b/docs/guide/images/register-name.png differ diff --git a/docs/guide/register-simplex-name.md b/docs/guide/register-simplex-name.md new file mode 100644 index 0000000000..366661b7e5 --- /dev/null +++ b/docs/guide/register-simplex-name.md @@ -0,0 +1,123 @@ +--- +title: Registering a SimpleX name +--- + +# Registering a SimpleX name + +A SimpleX name lets people reach you by typing a short `@name` instead of pasting +a long link. You register the name on a website and add your SimpleX link to it, +then claim the name in the app, to connect it with your SimpleX address or +channel name. + +> Names currently run on a testing network. The registration page is +> [testing-names.simplex.chat](https://testing-names.simplex.chat) and names end +> in `.testing`. The steps stay the same when the main names launch. + +## What you need + +- A crypto wallet such as MetaMask. +- The SimpleX app. +- A link for the name to open: your **contact address** (for one-to-one chats), + your **channel link** (to join a channel), or both. + +## How it works + +Setting up a name takes two parts. On the website you register the name and add +your link, so people can find your link by name. In the app you set the same name +on your profile - to prevent any other names from opening your profile or channel. + +![Steps to register a SimpleX name](./diagrams/simplex-name-steps.svg) + +## Step 1. Create a wallet + +Install MetaMask (browser extension or mobile app) and create a wallet. Write +down your recovery phrase somewhere safe and offline, as anyone with that phrase +controls your names. + +On the testing network you need a small amount of test ETH to pay network fees. + +## Step 2. Register the name and add your link + +Open the registration page and tap **Connect** to link your wallet. Type the +name you want in **Search for a name** and open it. Choose how many years to +register for, then tap **Next**. Names are currently 6 characters or more, and +some names are reserved. + +![Register your name and choose the registration period](./images/register-name.png) + +On **Create your profile**, paste your SimpleX link into the matching field: + +![Create your profile](./images/create-your-profile.png) + +- **SimpleX contact** for one-to-one chats. Copy your address from the app + (**Create SimpleX address**) and paste it here. People reach it with `@name`. +- **SimpleX channel** for a channel. Copy your **Channel link** and paste it here. + People join it with `#name`. +- **Advanced usage:** fill both to use one name for your chat (`@name`) and your + channel (`#name`). + +Then tap **Next**. + +Finally, tap **Begin** and complete the three steps the site shows: + +> Before we start. Registering your name takes three steps: +> 1. Complete a transaction to begin the timer. +> 2. Wait 60 seconds for the timer to complete. +> 3. Complete a second transaction to secure your name. + +These two transactions protect the name you are registering. The first records +only a secret code for the name, not the name itself, so nobody watching the +blockchain can see which name you want. The wait lets that first transaction +settle, and the second transaction then reveals and claims the name. Without this +two-step process, someone could see your registration in progress and grab the +name before you. + +![The three steps to secure your name](./images/before-we-start.png) + +## Step 3. Check what the app sees + +Copy your `#name.testing` (or `@name.testing`). Paste the name into the search bar, or simply type it. + +The app reports that the name is registered but not yet added to your profile: + +> Unconfirmed name. The SimpleX name is registered, but not added to profile. +> Please add it to your address or channel profile, if you are the owner. + +This is expected, and confirms the name points at your link. The next step +finishes the setup. + +## Step 4. Claim the name in the app + +Set the name on the same profile the record points at. + +- For your **contact address**, open your SimpleX address, tap **Get SimpleX + name**, enter the name, and save. +- For your **channel**, open the channel information, tap **Get SimpleX name** (under + **Channel link**), enter the name, and save. + +This proves to people who connect to you that `#name.testing` (or `@name.testing`) is your name. + +## Step 5. Connect by name + +On another device, copy `#name.testing` (or `@name.testing`). Paste the name into the search bar, or simply type it. This time the app connects, and the name +shows a check mark next to it. + +You can paste the name in your own device too - the app will warn you that it is your own name. + +Anyone can now reach you by name, and their app confirms it is really you. + +## If something doesn't work + +| Message | Meaning | Fix | +|---|---|---| +| Unconfirmed name | The name points at a link, but the link's profile does not claim the name yet. | Do step 4: set the name on the profile it points at. | +| Name not found | No name is registered. | Check the spelling, or register it (step 2). | +| No valid link | The name has no contact or channel link. | Add a link to the name on the website, on its **Records** tab. | +| Error saving name | You tried to claim a name that has no matching link on the website. | Add your contact address or channel link to the name, then set the name again. | +| None of your servers are set to resolve SimpleX names | No server in the app is set to resolve names. | In server settings, turn on **To resolve names** for a server. | + +## See also + +- [Making connections](./making-connections.md) +- [Chat profiles](./chat-profiles.md) +- [Channel webpage](./channel-webpage.md) diff --git a/docs/lang/fr/TRANSLATIONS.md b/docs/lang/fr/TRANSLATIONS.md index 1e216900e7..cb7de707ff 100644 --- a/docs/lang/fr/TRANSLATIONS.md +++ b/docs/lang/fr/TRANSLATIONS.md @@ -26,7 +26,7 @@ Ce document est créé pour accélérer ce processus, et partager quelques astuc 2. Certaines des chaînes n'ont pas besoin d'être traduites, mais elles doivent quand même être copiées - il y a un bouton dans l'interface weblate pour cela : -weblate: copy source to translation +weblate: copy source to translation 3. Weblate propose également des suggestions automatiques qui peuvent accélérer le processus. Parfois, elles peuvent être utilisées telles quelles, parfois elles nécessitent quelques retouches - cliquez pour les utiliser dans les traductions. @@ -34,7 +34,7 @@ Ce document est créé pour accélérer ce processus, et partager quelques astuc 5. Quand vous traduisez [l'app iOS](https://hosted.weblate.org/projects/simplex-chat/ios/), la plupart des chaînes de caractères sont identiques, elles peuvent être copiées en un clic dans la section Glossaire. L'indice visuel que cela est possible est que la chaîne source entière est surlignée en jaune. De nombreuses autres chaînes sont très similaires, elles ne diffèrent que par la syntaxe d'interpolation ou la façon dont la police en gras est utilisée - elles ne nécessitent qu'une édition minimale. Certaines chaînes sont propres à la plate-forme iOS. Elles doivent être traduites séparément. -weblate: automatic suggestions +weblate: automatic suggestions ## Une fois la traduction terminée diff --git a/docs/lang/pl/SERVER.md b/docs/lang/pl/SERVER.md index f4bffc1bcc..e3ffe40c16 100644 --- a/docs/lang/pl/SERVER.md +++ b/docs/lang/pl/SERVER.md @@ -482,4 +482,4 @@ Możliwe jest również udostępnienie adresu swojego serwera znajomym, pozwalaj _Uwaga_: Do obsługi haseł wymagany jest serwer SMP w wersji 4.0. Jeśli już posiadasz serwer, możesz dodać hasło do niego poprzez wpisanie hasła do pliku INI serwera. -       +       diff --git a/docs/lang/pl/TRANSLATIONS.md b/docs/lang/pl/TRANSLATIONS.md index 36daa5a148..531177dba5 100644 --- a/docs/lang/pl/TRANSLATIONS.md +++ b/docs/lang/pl/TRANSLATIONS.md @@ -39,7 +39,7 @@ Kroki są następujące: 2. Niektóre ciągi nie wymagają tłumaczenia, ale nadal trzeba je skopiować - w interfejsie użytkownika weblate znajduje się odpowiedni przycisk: -weblate: copy source to translation +weblate: copy source to translation 3. Weblate posiada również automatyczne sugestie, które mogą przyspieszyć ten proces. Czasami mogą być używane w niezmienionej formie, a czasami wymagają edycji - kliknij, aby użyć ich w tłumaczeniach. @@ -68,7 +68,7 @@ My wtedy: Serdecznie dziękujemy! To ogromny wysiłek i wielka pomoc dla rozwoju sieci SimpleX. -weblate: automatic suggestions +weblate: automatic suggestions ## Częste błędy w tłumaczeniu diff --git a/docs/lang/pl/WEBRTC.md b/docs/lang/pl/WEBRTC.md index d279491fb8..e291d887b5 100644 --- a/docs/lang/pl/WEBRTC.md +++ b/docs/lang/pl/WEBRTC.md @@ -139,7 +139,7 @@ To tyle - teraz możesz wykonywać połączenia audio i wideo za pośrednictwem 2. W sekcji **Build up ICE Server List** dodaj: - + - `STUN: stun::` kliknij `Add STUN` - `TURN: turn::`, `Username: `, `Credential: ` kliknij `Add TURN` @@ -148,11 +148,11 @@ To tyle - teraz możesz wykonywać połączenia audio i wideo za pośrednictwem 3. Powinieneś zobaczyć swoje serwery w sekcji **ICE server list**. Jeśli wszystko jest skonfigurowane poprawnie, naciśnij `Start test`: - + 4. W sekcji **Results** powinieneś zobaczyć coś takiego: - + Jeśli wyniki pokazują `srflx` i `relay`, wszystko jest skonfigurowane poprawnie! diff --git a/docs/protocol/channels-overview.md b/docs/protocol/channels-overview.md index d4cd2d2965..7a55f8b6e5 100644 --- a/docs/protocol/channels-overview.md +++ b/docs/protocol/channels-overview.md @@ -182,7 +182,7 @@ The low-level protocol supports multiple owners from the initial release. The ap - **Subscribers** connect to relays and receive content. They cannot send messages by default, but can be given posting rights. -Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol. +Additional roles (moderator, admin, member, author) exist in the hierarchy and are inherited from the group protocol. The owner-signed roster tracks the promoted set - members, moderators, and admins; subscribers are observers until an owner promotes them. For protocol-level detail - wire formats, message types, signing and verification mechanics, delivery pipeline - see [SimpleX Channels Protocol](./channels-protocol.md). @@ -242,6 +242,7 @@ This threat model assumes the [SimpleX network threat model](https://github.com/ - Undetectably substitute content - subscribers on honest relays receive the original. - Alter the channel's authoritative state on the owner's device. - Substitute the channel profile or impersonate an owner - these require valid signatures. +- Replay an old roster or role change to re-elevate a removed or demoted member for existing subscribers - they reject anything older than the roster version they applied (a new joiner with no prior roster can still be served an old one, until it syncs from another relay). - Redirect subscribers to a different channel - the entity ID is validated across link and profile. - Determine subscriber identity or network address - inherited from SMP transport. - Correlate subscriber participation across channels - each connection uses independent SMP queues. The subscriber chooses their SMP router independently, so collusion between a relay and the relay's SMP router does not compromise connections through a different router. diff --git a/docs/protocol/names-overview.md b/docs/protocol/names-overview.md new file mode 100644 index 0000000000..e9fe33a791 --- /dev/null +++ b/docs/protocol/names-overview.md @@ -0,0 +1,271 @@ +Revision 1, 2026-07-14 + +# SimpleX Public Names for Channels and Businesses + +## Table of contents + +- [Introduction](#introduction) + - [Names](#names) + - [Use cases](#use-cases) + - [On-chain registry](#on-chain-registry) + - [Privacy considerations](#privacy-considerations) +- [Architecture](#architecture) + - [Names and records](#names-and-records) + - [Resolution](#resolution) + - [Private decentralized RPC layer](#private-decentralized-rpc-layer) + - [Claiming and verification](#claiming-and-verification) +- [Differences from ENS](#differences-from-ens) +- [Security](#security) + - [Design objectives](#design-objectives) + - [Threat model](#threat-model) + - [Current gaps](#current-gaps) +- [Future work](#future-work) +- [Conclusion](#conclusion) + + +## Introduction + +The SimpleX network provides communication without user identifiers - people connect by exchanging links out of band. This protects private communication, but public entities - channels and businesses - need to be discoverable. Today this requires distributing a long link, which is impossible to remember and can be deleted by the router that hosts it. SimpleX public names provide memorability and ease of connection without adding user identifiers. + +### Names + +A SimpleX name is a human-readable name, registered on a public blockchain, that resolves to SimpleX links: `#example.simplex` opens a channel (for the `.simplex` namespace, the short variant `#example` can also be used for channel names) and `@example.simplex` connects to a contact or business address. One name can include both links - a business can publish `@example.simplex` for customer conversations and `#example.simplex` for its announcement channel from a single registration. Names support subnames (`support.example.simplex`). A separate test namespace (`.testing`) is deployed for early adopters before the main namespace launches. + +A name is not an account and not an identity. It is a record that maps a human-readable string to the links a channel or business already has, controlled by a cryptographic key that only its owner holds. The network does not use or require names; they are a discovery layer on top of it, described in this document. + +This document covers the naming layer built for [SimpleX Channels](./channels-overview.md) and the [SimpleX network](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md). For the user-facing registration steps see [Registering a SimpleX name](../guide/register-simplex-name.md); for protocol-level details see the resolver commands in the [SMP protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). + +### Use cases + +Names are intended for entities that are already public: + +- **Channels.** A channel reachable as `#example` can be easily shared in many ways - conversation, print, social media, and messages. If the channel's link is removed or blocked, the owner can point the name to a new link, and the channel remains reachable via the same name. + +- **Businesses and organizations.** A business address reachable as `@example.simplex` gives customers a way to start a conversation without scanning a QR code or trusting a link from a search result. + +- **Creators and public figures.** Anyone publishing to an audience can be reachable by name while their audience remains private. + +Names are opt-in and are only useful for public entities. Private users do not need them. Even when an entity uses a name, the network cannot track who connects to it - the communication graph remains private. + +### On-chain registry + +Names must be globally unique, so they require a shared registry. Every conventional registry design places names under an operator's control: + +- **A platform registry** (usernames in messaging apps) is owned by the operator: names can be revoked, impersonated, or reassigned, and the platform sees who looks up whom. + +- **DNS** is subject to domain seizure at the registrar and registry level, and requires registrant records. + +A record on a public blockchain has no operator: only the holder of the registration key can change what a name points to, and no authority can delete it. This mitigates censorship at these levels: + +- **SMP routers.** The router that hosts a link can delete it, and the link stops working for everyone who saved it. A name is not affected: the owner points it to a new link on a different router, and clients connect via the same name as before. + +- **Centralized registries.** An on-chain record cannot be revoked, because there is no intermediary. + +### Privacy considerations + +Names do not: + +- **Identify users.** There is no user directory, no requirement to register a name, and nothing links a name to the people who connect to it. + +- **Put communication on the chain.** The blockchain stores only the mapping from a name to links and optional public profile fields. Messages, membership, and channel content are never stored on the blockchain. + +- **Certify identity.** A verified name proves that the name's owner controls the address it points to - not who the owner is. It is the same trust model as a domain name. + +- **Replace links.** Links remain the primary and the more private connection mechanism, since resolving a name reveals interest in it to one resolver operator. Names are for public entities that choose to be contactable more easily. + + +## Architecture + +### Names and records + +Names are lowercase labels of ASCII letters, digits, and single hyphens, forming domains under the `.simplex` top-level name (`example.simplex`, `my-channel.simplex`), with `.simplex` implied when omitted for channels: `#example` is interpreted as `#example.simplex`. The restricted alphabet prevents homograph attacks: visually identical names from mixed scripts cannot exist as distinct records. + +A name's on-chain record stores: + +- **Channel links and contact links** - each an ordered list, primary first. Multiple links give redundancy across SMP routers: clients try them in order, so the name remains usable if one router becomes unavailable. Client support for this redundancy is planned for the `.simplex` namespace launch. +- **Optional profile fields** - a display name, website, location. +- **Optional donation addresses** (Monero, Bitcoin, Ethereum, etc.) that apps can show to channel subscribers. + +Registration is a two-transaction commit-reveal process from the owner's wallet: the first transaction records only a hash, so an observer of the pending registration cannot see the name and front-run it; the second transaction reveals the name and completes the registration. The name is held as an ordinary token in the owner's wallet and is renewable and transferable. Subnames are created by the name's owner, are transferred together with the name, and cannot be separately sold - the same ownership model as DNS subdomains. + +### Resolution + +Connecting by name is two independent resolutions: + +1. **Name to link.** The client queries the on-chain record through the SimpleX network (next section) and obtains the channel or contact link. + +2. **Link to connection.** The link resolves through the existing SimpleX short-link protocol: the client retrieves the link's immutable data - cryptographically bound to the link's owner keys - and the entity's profile, and connects. + +The second step is unchanged from connecting by link. A name adds discoverability on top of the link protocol; it does not weaken the link's own verification, and everything a client checks when joining by link is still checked when joining by name. + +### Private decentralized RPC layer + +Reading blockchain state normally requires querying an RPC service, and in practice almost all applications - including wallets and name services - use a handful of centralized API providers. Those providers see every query, the account it concerns, and the querier's IP address, and can censor or falsify responses. Resolving names through such a provider would reveal every lookup and the client's IP address to a third party. + +SimpleX resolves names through the network itself. Operators can enable a *names role* on their SMP routers: a names router runs its own resolver process and its own Ethereum node, holding a full copy of the relevant chain state - operators must not share this backend with other operators. The lookup is a protocol command sent as ordinary SimpleX traffic: + +1. The client selects a names router among its configured servers and sends the lookup *through an SMP proxy operated by a different operator*, encrypted so the proxy cannot read it. +2. The proxy forwards the lookup without access to its content; the names router responds from its local chain state. + +```mermaid +sequenceDiagram + participant C as Client + participant P as SMP proxy
(operator A) + participant N as Names router
(operator B) + participant E as Ethereum node
(operator B, local) + + C->>P: lookup, encrypted for the names router + note over P: sees the client,
not the name + P->>N: forwarded lookup + note over N: sees the name,
not the client + N->>E: read name record (eth_call) + E-->>N: name record + N-->>P: response, encrypted for the client + P-->>C: name record: channel and contact links +``` + +The result is a knowledge split with no single observer: the proxy sees which client communicates with a names router, but not the name; the names router sees the name, but not which client sent the query - no client address, session, or identity. A passive network observer sees fixed-size encrypted blocks indistinguishable from other traffic. Clients also keep the set of parties that see a lookup minimal: a query is sent to one names router, and after an authoritative response the client does not repeat the name to other servers. + +This layer is not specific to names. Private access to blockchain state via independent operators is a network capability that can be used in other contexts in the future. + +### Claiming and verification + +A name is trustworthy when: + +1. **The on-chain record points to the link.** Only the name owner's wallet key can set this. +2. **The link's profile claims the name.** Only the link owner's keys can publish this, and the claim is signed by the link's owner key, binding it to that specific link. + +For name resolution to succeed, SimpleX clients require both to be true. This mutual binding prevents abuse, such as: + +- Registering a name that points to someone else's address. The profile does not claim the name, so clients refuse to connect to it. +- Claiming a name in a profile without owning it. The on-chain record does not point to that profile's address, so the claim fails verification. + +Apps display the outcome next to the name - verified (the record and the claim match), failed, or not yet verified - and can re-verify on demand or automatically. Verification is a fresh resolution through the private RPC layer, so it reflects the current on-chain state. + +Names are used only to establish connections, not to deliver messages. Once connected, a contact or channel subscription is an ordinary SimpleX connection, independent of the name. A name expiring or being transferred affects future connections only; existing conversations and subscriptions are not affected. + + +## Differences from ENS + +The on-chain layer is the SimpleX Name Service (SNS), a fork of the [Ethereum Name Service](https://ens.domains) (ENS). ENS is the most widely used decentralized naming system, and SNS retains its core design: the registry and resolver contracts, commit-reveal registration, and expiry-based ownership. + +Each top-level name (`.testing`, `.simplex`) is an independent deployment of the same contracts: + +```mermaid +flowchart TD + CT["Controller
commit-reveal registration"] + RG["Registrar
name tokens, on-chain name index"] + PR["Resolver
simplex.contact / simplex.channel records"] + R["Registry
owner and resolver of every name"] + MR["Metadata renderer
token metadata as on-chain JSON + SVG"] + SR["Subname registrar
creates and indexes subnames"] + + CT -->|registers names| RG + CT -->|writes records| PR + RG -->|sets name ownership| R + RG -->|renders tokens| MR + SR -->|owns subname nodes| R +``` + +ENS, however, is not fully decentralized in use. Its applications depend on two off-chain services: an indexer (the subgraph) for queries such as listing the names held by an address (without it the ENS app is unusable), and a hosted metadata service to render name tokens in wallets. SNS removes both dependencies, and simplifies the ownership model: + +1. **No indexer.** The SNS contracts index names on-chain: reverse lookup from an address to the names it holds, from a token to its plaintext label, and from a name to its subnames are all direct contract reads. Every view in the SNS web app (currently, the app for test names is https://testing-names.simplex.chat) - the list of owned names, their subnames, and each name's records - is served by plain `eth_call`s against an Ethereum RPC. + +2. **Fully on-chain tokens.** A name's token metadata and image are generated by the contract as inline JSON and SVG. There is no metadata server; wallets render the name from chain data alone. + +3. **No name wrapper, no separate subname ownership.** ENS's wrapper system lets subnames be split off and owned independently, at the cost of a second token standard and a complex permission system. SNS subnames are owned together with the name and are transferred with it (the DNS model), removing the need for the wrapper. + +Resolution also differs. ENS names are resolved by applications through their RPC providers, so lookups are as centralized and observable as the provider. SNS records are read through the private RPC layer described above: many independent operators hold the chain state, and no single party sees who resolves which name. + +The result is a naming system with no off-chain services: registration, ownership, enumeration, rendering, and resolution are all either on-chain or served by the SimpleX network itself. + +The current implementation uses a web app for registration, which accesses the chain via a centralized RPC provider. We plan to add name purchases to the SimpleX apps, using the same decentralized blockchain access that is already used for name resolution. + + +## Security + +### Design objectives + +1. **Non-custodial.** Only the holder of the name's key can change or transfer the record; there is no registrar, operator, or intermediary with override authority. +2. **No infrastructure censorship.** A name does not depend on any single router: the links it points to can be replaced without changing the name, and resolution uses chain state held by many independent operators. +3. **Impersonation resistance.** A name is shown as verified only when the on-chain record and the signed claim in the link's profile match. +4. **Lookup privacy.** No single party observes both the user and the name being resolved. +5. **Availability.** A record can list multiple links across routers, and any names-capable router of the user's choosing can respond to a lookup. + +### Threat model + +This threat model assumes the [SimpleX network threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/security.md) and the [channels threat model](./channels-overview.md#threat-model), and addresses the naming layer. + +**A compromised names router (or its chain backend)** + +*can:* + +- Deny that a name exists, or serve a stale record, to the clients that query it. Detectable by resolving through a different operator's router. +- Serve a false record - effective only against a client that queries this router *and* only if the false record's target cooperates by claiming the name, since the claim check runs against the attacker-supplied record. Resolver agreement across operators ([future work](#future-work)) makes this require collusion of the queried operators. +- Observe which names are looked up, and how often. + +*cannot:* + +- Learn who is looking up a name - it never sees the client's address, session, or identity. +- Alter the on-chain record, or affect clients resolving through other routers. +- Affect existing connections - they do not depend on names. + +**An SMP proxy forwarding lookups** + +*can:* + +- See that a client communicates with a names router (which is an ordinary SMP router). + +*cannot:* + +- See that a request forwarded to a destination router is a name resolution request. +- See the name being looked up, or the response - both are encrypted between client and names router. + +**An impersonator** + +*can:* + +- Register a confusingly similar name. The restricted alphabet keeps such names visibly different - homographs are not possible. + +*cannot:* + +- Register a visually identical variant of an existing name using other scripts or invisible characters. +- Achieve verified status for a name pointing to someone else's address, or for an address whose name points elsewhere. + +**Compromise of the owner's wallet key** + +An attacker holding the registration key can repoint or transfer the name. New lookups then resolve to the attacker's links; the owner's profile claim no longer matches the record, so clients verifying the owner's profile see a failure. Existing connections are unaffected. Protecting the registration key is the owner's responsibility, the same as protecting the channel's own keys. An expired name that is not renewed can also be registered by someone else, so the key must be backed up and the name renewed on time. + +**A passive network observer** + +*can:* + +- See SimpleX traffic between clients and routers. + +*cannot:* + +- Distinguish name lookups from any other traffic, or learn which names anyone resolves. Inherited from SMP transport. + +### Current gaps + +1. **Single-resolver lookups.** A client currently accepts the response of one names router per lookup. Cross-checking two operators' resolvers is designed but not implemented - see below. +2. **No state proofs.** The names router is trusted to report chain state correctly; responses do not yet include proofs verifiable against a chain header. +3. **Self-hosted routers.** All pre-configured routers support name resolution, but most self-hosted routers do not support it yet. Name resolution requires at least one configured server with the names role. + + +## Future work + +- **Two-resolver agreement.** Resolve each name through two independent operators via two different proxies and compare the responses: a match is trusted; a mismatch is shown as a warning and the record is not used. This removes the single-resolver trust noted above and makes record substitution require cross-operator collusion. + +- **State proofs.** Responses including Merkle proofs of Ethereum state, verified by the client - removing the need to trust the resolver, with or without agreement. + +- **Main namespace launch.** The `.testing` namespace is deployed for early adopters; the `.simplex` namespace launches after the testing period. + +- **Registration via an app.** Registration currently requires an on-chain transaction from a wallet. + +- **DNS-based names.** The client already parses arbitrary domains (`example.com`) as web links. In the future, it will be possible to register owned domains in the SNS contract to use them as channel and contact names (`#example.com` and `@example.com`). + + +## Conclusion + +SimpleX names give channels and businesses memorable addresses that do not depend on any single server and cannot be revoked by a platform or registrar. Resolution runs through the SimpleX network, so no party observes who looks up which name. The naming system is fully decentralized on-chain, with no indexer or metadata service, and access to it is private, with no RPC provider observing requests. Names are optional and used only by public entities; private communication in SimpleX remains identifier-free. diff --git a/docs/rfcs/2026-08-05-markdown-hyperlink-connect.md b/docs/rfcs/2026-08-05-markdown-hyperlink-connect.md new file mode 100644 index 0000000000..2fe40755e8 --- /dev/null +++ b/docs/rfcs/2026-08-05-markdown-hyperlink-connect.md @@ -0,0 +1,33 @@ +# Connecting via a SimpleX link written as a markdown hyperlink + +## Problem + +Pasting a short SimpleX link written as a markdown hyperlink — `[label](https://smp6.simplex.im/a#...)` — into the chat list search, the new chat sheet search, or "Tap to paste link" fails with "Invalid connection link" instead of connecting. + +## Cause + +`markdownP` parses such a link into a single fragment whose `format` is `SimplexLink` but whose `text` is the whole markdown source: + +``` +[{"format":{"type":"simplexLink","showText":"label","linkType":"contact", + "simplexUri":"simplex:/a#...?h=smp6.simplex.im","smpHosts":["smp6.simplex.im"]}, + "text":"[label](https://smp6.simplex.im/a#...)"}] +``` + +`strConnectTarget` returns that `text` as the string to connect with. For a bare link `text` is the link, so it works; for a hyperlink it is `[label](link)`, which the core rejects as `InvalidConnReq`. + +## Design + +Use `simplexUri` — the link the parser already resolved — when the fragment came from the hyperlink parser, and keep using `text` otherwise: + +``` +text = if showText != null then simplexUri else text +``` + +`showText` is an exact discriminator, not a heuristic: `simplexUriFormat` is called with `Just t` only from `sowLinkP` (the hyperlink parser) and with `Nothing` from `wordMD` (bare link). Gating on it leaves every bare-link path unchanged. + +This also matches how the chat item renderer already resolves the same format — `TextItemView.kt` takes `simplexUri`, never the fragment `text`, when `showText` is set. `strConnectTarget` was the outlier. + +## Scope + +Short links only. `sowLinkP` rejects a full link inside a hyperlink (`fail "full SimpleX link in hyperlink"`), so `[label](full-link)` yields no formatting at all and never reaches this code — it stays treated as search text, as before. Bare full links are unaffected. diff --git a/images/github-banner.jpg b/images/github-banner.jpg new file mode 100644 index 0000000000..ef3cb5e6f5 Binary files /dev/null and b/images/github-banner.jpg differ diff --git a/packages/simplex-chat-client/types/typescript/package.json b/packages/simplex-chat-client/types/typescript/package.json index 756e181307..01d88ab770 100644 --- a/packages/simplex-chat-client/types/typescript/package.json +++ b/packages/simplex-chat-client/types/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@simplex-chat/types", - "version": "0.9.0", + "version": "0.10.3", "description": "TypeScript types for SimpleX Chat bot libraries", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index d1b89ffe27..3f4e3ad7ee 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -89,6 +89,7 @@ export interface APISendMessages { sendRef: T.ChatRef liveMessage: boolean ttl?: number // int + signMessages: boolean composedMessages: T.ComposedMessage[] // non-empty } @@ -96,7 +97,7 @@ export namespace APISendMessages { export type Response = CR.NewChatItems | CR.ChatCmdError export function cmdString(self: APISendMessages): string { - return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + ' json ' + JSON.stringify(self.composedMessages) + return '/_send ' + T.ChatRef.cmdString(self.sendRef) + (self.liveMessage ? ' live=on' : '') + (self.ttl ? ' ttl=' + self.ttl : '') + (self.signMessages ? ' sign=on' : '') + ' json ' + JSON.stringify(self.composedMessages) } } @@ -495,12 +496,12 @@ export namespace APIAddContact { } } -// Determine SimpleX link type and if the bot is already connected via this link. +// Determine SimpleX link type and if the bot is already connected via this link or name. // Network usage: interactive. export interface APIConnectPlan { userId: number // int64 - connectionLink?: string - resolveKnown: boolean + connectTarget?: string + resolveMode: T.PlanResolveMode linkOwnerSig?: T.LinkOwnerSig } @@ -508,7 +509,7 @@ export namespace APIConnectPlan { export type Response = CR.ConnectionPlan | CR.ChatCmdError export function cmdString(self: APIConnectPlan): string { - return '/_connect plan ' + self.userId + ' ' + self.connectionLink + return '/_connect plan ' + self.userId + ' ' + self.connectTarget } } @@ -528,18 +529,18 @@ export namespace APIConnect { } } -// Connect via SimpleX link as string in the active user profile. +// Connect via SimpleX link or name as string in the active user profile. // Network usage: interactive. export interface Connect { incognito: boolean - connLink_?: string + connTarget_?: string } export namespace Connect { export type Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError export function cmdString(self: Connect): string { - return '/connect' + (self.connLink_ ? ' ' + self.connLink_ : '') + return '/connect' + (self.connTarget_ ? ' ' + self.connTarget_ : '') } } diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 0fcf0e6eca..f54acfbeb1 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -186,6 +186,8 @@ export namespace CR { type: "connectionPlan" user: T.User connLink: T.CreatedConnLink + planSimplexName?: T.SimplexNameInfo + otherSimplexName?: T.SimplexNameInfo connectionPlan: T.ConnectionPlan } diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 883728f943..e2e30d43bc 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -66,6 +66,7 @@ export type AgentErrorType = | AgentErrorType.NTF | AgentErrorType.XFTP | AgentErrorType.FILE + | AgentErrorType.NO_NAME_SERVERS | AgentErrorType.PROXY | AgentErrorType.RCP | AgentErrorType.BROKER @@ -84,6 +85,7 @@ export namespace AgentErrorType { | "NTF" | "XFTP" | "FILE" + | "NO_NAME_SERVERS" | "PROXY" | "RCP" | "BROKER" @@ -136,6 +138,10 @@ export namespace AgentErrorType { fileErr: FileErrorType } + export interface NO_NAME_SERVERS extends Interface { + type: "NO_NAME_SERVERS" + } + export interface PROXY extends Interface { type: "PROXY" proxyServer: string @@ -283,6 +289,7 @@ export interface BusinessChatInfo { chatType: BusinessChatType businessId: string customerId: string + businessDomain?: SimplexDomainClaim } export enum BusinessChatType { @@ -835,7 +842,7 @@ export interface CIMeta { editable: boolean forwardedByMember?: number // int64 showGroupAsSender: boolean - msgSigned?: MsgSigStatus + msgVerified?: MsgVerified createdAt: string // ISO-8601 timestamp updatedAt: string // ISO-8601 timestamp } @@ -1021,7 +1028,6 @@ export type ChatErrorType = | ChatErrorType.NoSndFileUser | ChatErrorType.NoRcvFileUser | ChatErrorType.UserUnknown - | ChatErrorType.ActiveUserExists | ChatErrorType.UserExists | ChatErrorType.ChatRelayExists | ChatErrorType.DifferentActiveUser @@ -1037,6 +1043,8 @@ export type ChatErrorType = | ChatErrorType.ChatNotStopped | ChatErrorType.ChatStoreChanged | ChatErrorType.InvalidConnReq + | ChatErrorType.SimplexDomainNotReady + | ChatErrorType.NotResolvedLocally | ChatErrorType.UnsupportedConnReq | ChatErrorType.ConnReqMessageProhibited | ChatErrorType.ContactNotReady @@ -1099,7 +1107,6 @@ export namespace ChatErrorType { | "noSndFileUser" | "noRcvFileUser" | "userUnknown" - | "activeUserExists" | "userExists" | "chatRelayExists" | "differentActiveUser" @@ -1115,6 +1122,8 @@ export namespace ChatErrorType { | "chatNotStopped" | "chatStoreChanged" | "invalidConnReq" + | "simplexDomainNotReady" + | "notResolvedLocally" | "unsupportedConnReq" | "connReqMessageProhibited" | "contactNotReady" @@ -1197,10 +1206,6 @@ export namespace ChatErrorType { type: "userUnknown" } - export interface ActiveUserExists extends Interface { - type: "activeUserExists" - } - export interface UserExists extends Interface { type: "userExists" contactName: string @@ -1273,6 +1278,16 @@ export namespace ChatErrorType { type: "invalidConnReq" } + export interface SimplexDomainNotReady extends Interface { + type: "simplexDomainNotReady" + simplexDomain: SimplexDomain + simplexDomainError: SimplexDomainError + } + + export interface NotResolvedLocally extends Interface { + type: "notResolvedLocally" + } + export interface UnsupportedConnReq extends Interface { type: "unsupportedConnReq" } @@ -2163,6 +2178,7 @@ export type ErrorType = | ErrorType.LARGE_MSG | ErrorType.EXPIRED | ErrorType.INTERNAL + | ErrorType.NAME | ErrorType.DUPLICATE_ export namespace ErrorType { @@ -2181,6 +2197,7 @@ export namespace ErrorType { | "LARGE_MSG" | "EXPIRED" | "INTERNAL" + | "NAME" | "DUPLICATE_" interface Interface { @@ -2247,6 +2264,11 @@ export namespace ErrorType { type: "INTERNAL" } + export interface NAME extends Interface { + type: "NAME" + nameErr: NameErrorType + } + export interface DUPLICATE_ extends Interface { type: "DUPLICATE_" } @@ -2375,6 +2397,11 @@ export interface FileTransferMeta { cancelled: boolean } +export enum FileType { + Normal = "normal", + Roster = "roster", +} + export type Format = | Format.Bold | Format.Italic @@ -2503,6 +2530,7 @@ export interface FullGroupPreferences { support: SupportGroupPreference sessions: RoleGroupPreference comments: CommentsGroupPreference + signMessages: GroupPreference commands: ChatBotCommand[] } @@ -2577,6 +2605,7 @@ export enum GroupFeature { Support = "support", Sessions = "sessions", Comments = "comments", + SignMessages = "signMessages", } export enum GroupFeatureEnabled { @@ -2605,9 +2634,11 @@ export interface GroupInfo { uiThemes?: UIThemeEntityOverrides customData?: object groupSummary: GroupSummary + rosterVersion?: number // int64 membersRequireAttention: number // int viaGroupLinkUri?: string groupKeys?: GroupKeys + groupDomainVerified?: boolean } export interface GroupKeys { @@ -2716,6 +2747,7 @@ export interface GroupMember { supportChat?: GroupSupportChat memberPubKey?: string relayLink?: string + memberVerifiedCode?: SecurityCode } export interface GroupMemberAdmission { @@ -2784,6 +2816,7 @@ export interface GroupPreferences { support?: SupportGroupPreference sessions?: RoleGroupPreference comments?: CommentsGroupPreference + signMessages?: GroupPreference commands?: ChatBotCommand[] } @@ -2978,12 +3011,15 @@ export interface LocalProfile { displayName: string fullName: string shortDescr?: string + description?: string image?: string contactLink?: string preferences?: Preferences peerType?: ChatPeerType localBadge?: LocalBadge localAlias: string + contactDomain?: SimplexDomainClaim + contactDomainVerified?: boolean } export enum MemberCriteria { @@ -3177,6 +3213,48 @@ export enum MsgSigStatus { SignedNoKey = "signedNoKey", } +export type MsgVerified = MsgVerified.Signed | MsgVerified.SigMissing + +export namespace MsgVerified { + export type Tag = "signed" | "sigMissing" + + interface Interface { + type: Tag + } + + export interface Signed extends Interface { + type: "signed" + sigStatus: MsgSigStatus + } + + export interface SigMissing extends Interface { + type: "sigMissing" + } +} + +export type NameErrorType = NameErrorType.NO_RESOLVER | NameErrorType.NOT_FOUND | NameErrorType.RESOLVER + +export namespace NameErrorType { + export type Tag = "NO_RESOLVER" | "NOT_FOUND" | "RESOLVER" + + interface Interface { + type: Tag + } + + export interface NO_RESOLVER extends Interface { + type: "NO_RESOLVER" + } + + export interface NOT_FOUND extends Interface { + type: "NOT_FOUND" + } + + export interface RESOLVER extends Interface { + type: "RESOLVER" + resolverErr: string + } +} + export type NetworkError = | NetworkError.ConnectError | NetworkError.TLSError @@ -3230,6 +3308,7 @@ export interface NewUser { profile?: Profile pastTimestamp: boolean userChatRelay: boolean + clientService: boolean } export interface NoteFolder { @@ -3294,6 +3373,12 @@ export interface PendingContactConnection { updatedAt: string // ISO-8601 timestamp } +export enum PlanResolveMode { + AllGroups = "allGroups", + Unknown = "unknown", + Never = "never", +} + export interface PrefEnabled { forUser: boolean forContact: boolean @@ -3329,11 +3414,13 @@ export interface Profile { displayName: string fullName: string shortDescr?: string + description?: string image?: string contactLink?: string preferences?: Preferences peerType?: ChatPeerType badge?: BadgeProof + contactDomain?: SimplexDomainClaim } export type ProxyClientError = @@ -3394,7 +3481,7 @@ export namespace ProxyError { export interface PublicGroupAccess { groupWebPage?: string - groupDomain?: string + groupDomainClaim?: SimplexDomainClaim domainWebPage: boolean allowEmbedding: boolean } @@ -3640,6 +3727,7 @@ export interface RcvFileTransfer { xftpRcvFile?: XFTPRcvFile fileInvitation: FileInvitation fileStatus: RcvFileStatus + fileType: FileType rcvFileInline?: InlineFileMode senderDisplayName: string chunkSize: number // int64 @@ -3811,6 +3899,7 @@ export enum RelayStatus { New = "new", Invited = "invited", Accepted = "accepted", + AcknowledgedRoster = "acknowledgedRoster", Active = "active", Inactive = "inactive", Rejected = "rejected", @@ -3895,6 +3984,41 @@ export interface SimplePreference { allow: FeatureAllowed } +export interface SimplexDomain { + nameTLD: SimplexTLD + domain: string + subDomain: string[] +} + +export interface SimplexDomainClaim { + domain: string + proof?: SimplexDomainProof +} + +export type SimplexDomainError = SimplexDomainError.NoValidLink | SimplexDomainError.UnknownDomain + +export namespace SimplexDomainError { + export type Tag = "noValidLink" | "unknownDomain" + + interface Interface { + type: Tag + } + + export interface NoValidLink extends Interface { + type: "noValidLink" + } + + export interface UnknownDomain extends Interface { + type: "unknownDomain" + } +} + +export interface SimplexDomainProof { + linkOwnerId?: string + presHeader: string + signature: string +} + export enum SimplexLinkType { Contact = "contact", Invitation = "invitation", @@ -3903,15 +4027,9 @@ export enum SimplexLinkType { Relay = "relay", } -export interface SimplexNameDomain { - nameTLD: SimplexTLD - domain: string - subDomain: string[] -} - export interface SimplexNameInfo { nameType: SimplexNameType - nameDomain: SimplexNameDomain + nameDomain: SimplexDomain } export enum SimplexNameType { @@ -4879,8 +4997,9 @@ export interface User { sendRcptsSmallGroups: boolean autoAcceptMemberContacts: boolean userMemberProfileUpdatedAt?: string // ISO-8601 timestamp - uiThemes?: UIThemeEntityOverrides userChatRelay: boolean + clientService: boolean + uiThemes?: UIThemeEntityOverrides } export interface UserChatRelay { diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index 12fa150fc4..10c4ee0a81 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -121,7 +121,7 @@ export class ChatClient { async apiSendMessages(chatType: T.ChatType, chatId: number, messages: T.ComposedMessage[]): Promise { const r = await this.sendChatCmd( - CC.APISendMessages.cmdString({sendRef: {chatType, chatId}, composedMessages: messages, liveMessage: false}) + CC.APISendMessages.cmdString({sendRef: {chatType, chatId}, composedMessages: messages, liveMessage: false, signMessages: false}) ) if (r.type === "newChatItems") return r.chatItems throw new ChatCommandError("unexpected response", r) diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index e5360df2b4..76833f70c8 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -1,6 +1,6 @@ { "name": "simplex-chat", - "version": "6.5.6", + "version": "7.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -24,7 +24,7 @@ "docs": "typedoc" }, "dependencies": { - "@simplex-chat/types": "^0.9.0", + "@simplex-chat/types": "^0.10.3", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.3", "node-addon-api": "^8.5.0" diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 0d3339df9a..958304a8ea 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -423,7 +423,8 @@ export class ChatApi { CC.APISendMessages.cmdString({ sendRef, composedMessages: messages, - liveMessage + liveMessage, + signMessages: false }) ) if (r.type === "newChatItems") return r.chatItems @@ -688,7 +689,7 @@ export class ChatApi { * Network usage: interactive. */ async apiConnectPlan(userId: number, connectionLink: string): Promise<[T.ConnectionPlan, T.CreatedConnLink]> { - const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectionLink, resolveKnown: false})) + const r = await this.sendChatCmd(CC.APIConnectPlan.cmdString({userId, connectTarget: connectionLink, resolveMode: T.PlanResolveMode.Unknown})) if (r.type === "connectionPlan") return [r.connectionPlan, r.connLink] throw new ChatCommandError("error getting connect plan", r) } @@ -707,7 +708,7 @@ export class ChatApi { * Network usage: interactive. */ async apiConnectActiveUser(connLink: string): Promise { - const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connLink_: connLink})) + const r = await this.sendChatCmd(CC.Connect.cmdString({incognito: false, connTarget_: connLink})) return this.handleConnectResult(r) } @@ -866,7 +867,7 @@ export class ChatApi { * Network usage: no. */ async apiCreateActiveUser(profile?: T.Profile): Promise { - const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false}})) + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false, clientService: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 8fbed9efe0..e0685e0123 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,7 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.6'; +const RELEASE_TAG = 'v7.0.0'; const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { diff --git a/packages/simplex-chat-python/examples/squaring_bot.py b/packages/simplex-chat-python/examples/squaring_bot.py index 296b51347e..4d062ad718 100644 --- a/packages/simplex-chat-python/examples/squaring_bot.py +++ b/packages/simplex-chat-python/examples/squaring_bot.py @@ -26,7 +26,15 @@ bot = Bot( profile=BotProfile(display_name="Squaring bot"), db=SqliteDb(file_prefix="./squaring_bot"), welcome="Send me a number, I'll square it.", - commands=[BotCommand(keyword="help", label="Show help")], + commands=[ + # `params=None` (default): the client SENDS `/help` immediately + # when the user taps it in the commands menu. + BotCommand(keyword="help", label="Show help"), + # `params=""`: the client PASTES `/square ` + # into the input box and positions the cursor at the end. The + # user replaces `` with the actual number and sends. + BotCommand(keyword="square", label="Square a number", params=""), + ], ) NUMBER_RE = re.compile(r"^-?\d+(\.\d+)?$") @@ -48,5 +56,19 @@ async def help_cmd(msg: Message, _cmd: ParsedCommand) -> None: await msg.reply("Send a number, I'll square it.") +@bot.on_command("square") +async def square_cmd(msg: Message, cmd: ParsedCommand) -> None: + """Demonstrates the `params` flow: `cmd.args` is the trimmed text + AFTER `/square`. When the user tapped the menu entry above, the + client pasted `/square ` and the user replaced + `` with the actual value before sending.""" + try: + n = float(cmd.args) + except ValueError: + await msg.reply(f"Usage: /square (got {cmd.args!r})") + return + await msg.reply(f"{n} * {n} = {n * n}") + + if __name__ == "__main__": bot.run() diff --git a/packages/simplex-chat-python/src/simplex_chat/_version.py b/packages/simplex-chat-python/src/simplex_chat/_version.py index 0b25146728..e1cbccc2da 100644 --- a/packages/simplex-chat-python/src/simplex_chat/_version.py +++ b/packages/simplex-chat-python/src/simplex_chat/_version.py @@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440 post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged. """ -__version__ = "6.5.6" # PEP 440 — read by hatchling for wheel metadata -LIBS_VERSION = "6.5.6" # simplex-chat-libs release tag (no 'v' prefix) +__version__ = "7.0.0" # PEP 440 — read by hatchling for wheel metadata +LIBS_VERSION = "7.0.0" # simplex-chat-libs release tag (no 'v' prefix) diff --git a/packages/simplex-chat-python/src/simplex_chat/api.py b/packages/simplex-chat-python/src/simplex_chat/api.py index ef37e28384..51de063329 100644 --- a/packages/simplex-chat-python/src/simplex_chat/api.py +++ b/packages/simplex-chat-python/src/simplex_chat/api.py @@ -193,6 +193,7 @@ class ChatApi: "sendRef": send_ref, "composedMessages": messages, "liveMessage": live_message, + "signMessages": False, } ) ) @@ -466,7 +467,7 @@ class ChatApi: ) -> tuple[T.ConnectionPlan, T.CreatedConnLink]: r = await self.send_chat_cmd( CC.APIConnectPlan_cmd_string( - {"userId": user_id, "connectionLink": connection_link, "resolveKnown": False} + {"userId": user_id, "connectTarget": connection_link, "resolveMode": "unknown"} ) ) if r["type"] == "connectionPlan": @@ -487,7 +488,7 @@ class ChatApi: async def api_connect_active_user(self, conn_link: str) -> ConnReqType: r = await self.send_chat_cmd( - CC.Connect_cmd_string({"incognito": False, "connLink_": conn_link}) + CC.Connect_cmd_string({"incognito": False, "connTarget_": conn_link}) ) return self._handle_connect_result(r) @@ -641,7 +642,7 @@ class ChatApi: raise async def api_create_active_user(self, profile: T.Profile | None = None) -> T.User: - new_user: T.NewUser = {"pastTimestamp": False, "userChatRelay": False} + new_user: T.NewUser = {"pastTimestamp": False, "userChatRelay": False, "clientService": False} if profile is not None: new_user["profile"] = profile r = await self.send_chat_cmd(CC.CreateActiveUser_cmd_string({"newUser": new_user})) diff --git a/packages/simplex-chat-python/src/simplex_chat/bot.py b/packages/simplex-chat-python/src/simplex_chat/bot.py index fb511e2818..4e385493b2 100644 --- a/packages/simplex-chat-python/src/simplex_chat/bot.py +++ b/packages/simplex-chat-python/src/simplex_chat/bot.py @@ -33,8 +33,38 @@ from .types import T @dataclass(slots=True) class BotCommand: + """One entry in the bot's advertised slash-command list (wire-side + `groupPreferences.commands` or profile `preferences.commands`). + + `keyword` and `label` are required: `keyword` is what the user + types after `/`; `label` is the human-readable description shown + next to the keyword in the SimpleX client's commands menu. + + `params` is an optional placeholder string that controls how the + client behaves when the user taps the command in the menu: + + * `params=None` (default) — the client SENDS `/` + immediately on tap; no input-box detour. Use this for + zero-argument commands (`/help`, `/ping`) where the action is + unambiguous. + + * `params=""` — the client PASTES `/ ` + into the input box and positions the cursor at the end. The + user edits the placeholder and sends. Use this for commands + that take a required argument (`/review `, + `/order `) so the user sees the expected shape + without having to remember it. + + Mirrors `CBCCommand` in the Haskell core + (`Simplex.Chat.Types.Preferences`) and the wire TypedDict + `ChatBotCommand_command`. Both SimpleX clients (Android/Kotlin + and iOS/Swift) implement the paste-vs-send branch on the + `params` field; see `CommandsMenuView.{kt,swift}` for the + reference UI behaviour. + """ keyword: str label: str + params: str | None = None class Bot(Client): @@ -145,10 +175,24 @@ class Bot(Client): "files": {"allow": "yes" if self._allow_files else "no"}, } if self._commands: - prefs["commands"] = [ - {"type": "command", "keyword": c.keyword, "label": c.label} - for c in self._commands - ] + cmds: list[T.ChatBotCommand] = [] + for c in self._commands: + entry: T.ChatBotCommand_command = { + "type": "command", + "keyword": c.keyword, + "label": c.label, + } + # `params` is `NotRequired[str]` on the wire; omit the + # key entirely when None so the Haskell parser sees + # `Nothing` rather than `Just ""`. The two have + # different client semantics: `Nothing` (`params=None`) + # triggers an immediate send on tap; `Just ""` would + # paste `/ ` (with a trailing space) into the + # input box, which is rarely what the operator wants. + if c.params is not None: + entry["params"] = c.params + cmds.append(entry) + prefs["commands"] = cmds p["preferences"] = prefs p["peerType"] = "bot" return p diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index 3847f44811..a7f4a56465 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -80,11 +80,12 @@ class APISendMessages(TypedDict): sendRef: "T.ChatRef" liveMessage: bool ttl: NotRequired[int] # int + signMessages: bool composedMessages: list["T.ComposedMessage"] # non-empty def APISendMessages_cmd_string(self: APISendMessages) -> str: - return '/_send ' + T.ChatRef_cmd_string(self['sendRef']) + (' live=on' if self['liveMessage'] else '') + ((' ttl=' + str(self.get('ttl'))) if self.get('ttl') is not None else '') + ' json ' + json.dumps(self['composedMessages']) + return '/_send ' + T.ChatRef_cmd_string(self['sendRef']) + (' live=on' if self['liveMessage'] else '') + ((' ttl=' + str(self.get('ttl'))) if self.get('ttl') is not None else '') + (' sign=on' if self['signMessages'] else '') + ' json ' + json.dumps(self['composedMessages']) APISendMessages_Response = CR.NewChatItems | CR.ChatCmdError @@ -434,17 +435,17 @@ def APIAddContact_cmd_string(self: APIAddContact) -> str: APIAddContact_Response = CR.Invitation | CR.ChatCmdError -# Determine SimpleX link type and if the bot is already connected via this link. +# Determine SimpleX link type and if the bot is already connected via this link or name. # Network usage: interactive. class APIConnectPlan(TypedDict): userId: int # int64 - connectionLink: NotRequired[str] - resolveKnown: bool + connectTarget: NotRequired[str] + resolveMode: "T.PlanResolveMode" linkOwnerSig: NotRequired["T.LinkOwnerSig"] def APIConnectPlan_cmd_string(self: APIConnectPlan) -> str: - return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectionLink') + return '/_connect plan ' + str(self['userId']) + ' ' + self.get('connectTarget') APIConnectPlan_Response = CR.ConnectionPlan | CR.ChatCmdError @@ -463,15 +464,15 @@ def APIConnect_cmd_string(self: APIConnect) -> str: APIConnect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError -# Connect via SimpleX link as string in the active user profile. +# Connect via SimpleX link or name as string in the active user profile. # Network usage: interactive. class Connect(TypedDict): incognito: bool - connLink_: NotRequired[str] + connTarget_: NotRequired[str] def Connect_cmd_string(self: Connect) -> str: - return '/connect' + ((' ' + self.get('connLink_')) if self.get('connLink_') is not None else '') + return '/connect' + ((' ' + self.get('connTarget_')) if self.get('connTarget_') is not None else '') Connect_Response = CR.SentConfirmation | CR.ContactAlreadyExists | CR.SentInvitation | CR.ChatCmdError diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py index e85de02c78..bd61e6dae1 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py @@ -57,6 +57,8 @@ class ConnectionPlan(TypedDict): type: Literal["connectionPlan"] user: "T.User" connLink: "T.CreatedConnLink" + planSimplexName: NotRequired["T.SimplexNameInfo"] + otherSimplexName: NotRequired["T.SimplexNameInfo"] connectionPlan: "T.ConnectionPlan" class ContactAlreadyExists(TypedDict): diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 855a967215..96d3f4dea4 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -78,6 +78,9 @@ class AgentErrorType_FILE(TypedDict): type: Literal["FILE"] fileErr: "FileErrorType" +class AgentErrorType_NO_NAME_SERVERS(TypedDict): + type: Literal["NO_NAME_SERVERS"] + class AgentErrorType_PROXY(TypedDict): type: Literal["PROXY"] proxyServer: str @@ -123,6 +126,7 @@ AgentErrorType = ( | AgentErrorType_NTF | AgentErrorType_XFTP | AgentErrorType_FILE + | AgentErrorType_NO_NAME_SERVERS | AgentErrorType_PROXY | AgentErrorType_RCP | AgentErrorType_BROKER @@ -133,7 +137,7 @@ AgentErrorType = ( | AgentErrorType_INACTIVE ) -AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"] +AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "NO_NAME_SERVERS", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"] class AutoAccept(TypedDict): acceptIncognito: bool @@ -200,6 +204,7 @@ class BusinessChatInfo(TypedDict): chatType: "BusinessChatType" businessId: str customerId: str + businessDomain: NotRequired["SimplexDomainClaim"] BusinessChatType = Literal["business", "customer"] @@ -583,7 +588,7 @@ class CIMeta(TypedDict): editable: bool forwardedByMember: NotRequired[int] # int64 showGroupAsSender: bool - msgSigned: NotRequired["MsgSigStatus"] + msgVerified: NotRequired["MsgVerified"] createdAt: str # ISO-8601 timestamp updatedAt: str # ISO-8601 timestamp @@ -727,9 +732,6 @@ class ChatErrorType_noRcvFileUser(TypedDict): class ChatErrorType_userUnknown(TypedDict): type: Literal["userUnknown"] -class ChatErrorType_activeUserExists(TypedDict): - type: Literal["activeUserExists"] - class ChatErrorType_userExists(TypedDict): type: Literal["userExists"] contactName: str @@ -787,6 +789,14 @@ class ChatErrorType_chatStoreChanged(TypedDict): class ChatErrorType_invalidConnReq(TypedDict): type: Literal["invalidConnReq"] +class ChatErrorType_simplexDomainNotReady(TypedDict): + type: Literal["simplexDomainNotReady"] + simplexDomain: "SimplexDomain" + simplexDomainError: "SimplexDomainError" + +class ChatErrorType_notResolvedLocally(TypedDict): + type: Literal["notResolvedLocally"] + class ChatErrorType_unsupportedConnReq(TypedDict): type: Literal["unsupportedConnReq"] @@ -1002,7 +1012,6 @@ ChatErrorType = ( | ChatErrorType_noSndFileUser | ChatErrorType_noRcvFileUser | ChatErrorType_userUnknown - | ChatErrorType_activeUserExists | ChatErrorType_userExists | ChatErrorType_chatRelayExists | ChatErrorType_differentActiveUser @@ -1018,6 +1027,8 @@ ChatErrorType = ( | ChatErrorType_chatNotStopped | ChatErrorType_chatStoreChanged | ChatErrorType_invalidConnReq + | ChatErrorType_simplexDomainNotReady + | ChatErrorType_notResolvedLocally | ChatErrorType_unsupportedConnReq | ChatErrorType_connReqMessageProhibited | ChatErrorType_contactNotReady @@ -1074,7 +1085,7 @@ ChatErrorType = ( | ChatErrorType_exception ) -ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "activeUserExists", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] +ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexDomainNotReady", "notResolvedLocally", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"] ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"] @@ -1557,6 +1568,10 @@ class ErrorType_EXPIRED(TypedDict): class ErrorType_INTERNAL(TypedDict): type: Literal["INTERNAL"] +class ErrorType_NAME(TypedDict): + type: Literal["NAME"] + nameErr: "NameErrorType" + class ErrorType_DUPLICATE_(TypedDict): type: Literal["DUPLICATE_"] @@ -1575,10 +1590,11 @@ ErrorType = ( | ErrorType_LARGE_MSG | ErrorType_EXPIRED | ErrorType_INTERNAL + | ErrorType_NAME | ErrorType_DUPLICATE_ ) -ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "DUPLICATE_"] +ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "NAME", "DUPLICATE_"] FeatureAllowed = Literal["always", "yes", "no"] @@ -1666,6 +1682,8 @@ class FileTransferMeta(TypedDict): chunkSize: int # int64 cancelled: bool +FileType = Literal["normal", "roster"] + class Format_bold(TypedDict): type: Literal["bold"] @@ -1758,6 +1776,7 @@ class FullGroupPreferences(TypedDict): support: "SupportGroupPreference" sessions: "RoleGroupPreference" comments: "CommentsGroupPreference" + signMessages: "GroupPreference" commands: list["ChatBotCommand"] class FullPreferences(TypedDict): @@ -1801,7 +1820,7 @@ class GroupDirectInvitation(TypedDict): fromGroupMemberConnId_: NotRequired[int] # int64 groupDirectInvStartedConnection: bool -GroupFeature = Literal["timedMessages", "directMessages", "fullDelete", "reactions", "voice", "files", "simplexLinks", "reports", "history", "support", "sessions", "comments"] +GroupFeature = Literal["timedMessages", "directMessages", "fullDelete", "reactions", "voice", "files", "simplexLinks", "reports", "history", "support", "sessions", "comments", "signMessages"] GroupFeatureEnabled = Literal["on", "off"] @@ -1826,9 +1845,11 @@ class GroupInfo(TypedDict): uiThemes: NotRequired["UIThemeEntityOverrides"] customData: NotRequired[dict[str, object]] groupSummary: "GroupSummary" + rosterVersion: NotRequired[int] # int64 membersRequireAttention: int # int viaGroupLinkUri: NotRequired[str] groupKeys: NotRequired["GroupKeys"] + groupDomainVerified: NotRequired[bool] class GroupKeys(TypedDict): publicGroupId: str @@ -1914,6 +1935,7 @@ class GroupMember(TypedDict): supportChat: NotRequired["GroupSupportChat"] memberPubKey: NotRequired[str] relayLink: NotRequired[str] + memberVerifiedCode: NotRequired["SecurityCode"] class GroupMemberAdmission(TypedDict): review: NotRequired["MemberCriteria"] @@ -1947,6 +1969,7 @@ class GroupPreferences(TypedDict): support: NotRequired["SupportGroupPreference"] sessions: NotRequired["RoleGroupPreference"] comments: NotRequired["CommentsGroupPreference"] + signMessages: NotRequired["GroupPreference"] commands: NotRequired[list["ChatBotCommand"]] class GroupProfile(TypedDict): @@ -2084,12 +2107,15 @@ class LocalProfile(TypedDict): displayName: str fullName: str shortDescr: NotRequired[str] + description: NotRequired[str] image: NotRequired[str] contactLink: NotRequired[str] preferences: NotRequired["Preferences"] peerType: NotRequired["ChatPeerType"] localBadge: NotRequired["LocalBadge"] localAlias: str + contactDomain: NotRequired["SimplexDomainClaim"] + contactDomainVerified: NotRequired[bool] MemberCriteria = Literal["all"] @@ -2222,6 +2248,31 @@ MsgReceiptStatus = Literal["ok", "badMsgHash"] MsgSigStatus = Literal["verified", "signedNoKey"] +class MsgVerified_signed(TypedDict): + type: Literal["signed"] + sigStatus: "MsgSigStatus" + +class MsgVerified_sigMissing(TypedDict): + type: Literal["sigMissing"] + +MsgVerified = MsgVerified_signed | MsgVerified_sigMissing + +MsgVerified_Tag = Literal["signed", "sigMissing"] + +class NameErrorType_NO_RESOLVER(TypedDict): + type: Literal["NO_RESOLVER"] + +class NameErrorType_NOT_FOUND(TypedDict): + type: Literal["NOT_FOUND"] + +class NameErrorType_RESOLVER(TypedDict): + type: Literal["RESOLVER"] + resolverErr: str + +NameErrorType = NameErrorType_NO_RESOLVER | NameErrorType_NOT_FOUND | NameErrorType_RESOLVER + +NameErrorType_Tag = Literal["NO_RESOLVER", "NOT_FOUND", "RESOLVER"] + class NetworkError_connectError(TypedDict): type: Literal["connectError"] connectError: str @@ -2258,6 +2309,7 @@ class NewUser(TypedDict): profile: NotRequired["Profile"] pastTimestamp: bool userChatRelay: bool + clientService: bool class NoteFolder(TypedDict): noteFolderId: int # int64 @@ -2304,6 +2356,8 @@ class PendingContactConnection(TypedDict): createdAt: str # ISO-8601 timestamp updatedAt: str # ISO-8601 timestamp +PlanResolveMode = Literal["allGroups", "unknown", "never"] + class PrefEnabled(TypedDict): forUser: bool forContact: bool @@ -2335,11 +2389,13 @@ class Profile(TypedDict): displayName: str fullName: str shortDescr: NotRequired[str] + description: NotRequired[str] image: NotRequired[str] contactLink: NotRequired[str] preferences: NotRequired["Preferences"] peerType: NotRequired["ChatPeerType"] badge: NotRequired["BadgeProof"] + contactDomain: NotRequired["SimplexDomainClaim"] class ProxyClientError_protocolError(TypedDict): type: Literal["protocolError"] @@ -2381,7 +2437,7 @@ ProxyError_Tag = Literal["PROTOCOL", "BROKER", "BASIC_AUTH", "NO_SESSION"] class PublicGroupAccess(TypedDict): groupWebPage: NotRequired[str] - groupDomain: NotRequired[str] + groupDomainClaim: NotRequired["SimplexDomainClaim"] domainWebPage: bool allowEmbedding: bool @@ -2553,6 +2609,7 @@ class RcvFileTransfer(TypedDict): xftpRcvFile: NotRequired["XFTPRcvFile"] fileInvitation: "FileInvitation" fileStatus: "RcvFileStatus" + fileType: "FileType" rcvFileInline: NotRequired["InlineFileMode"] senderDisplayName: str chunkSize: int # int64 @@ -2670,7 +2727,7 @@ class RelayProfile(TypedDict): shortDescr: NotRequired[str] image: NotRequired[str] -RelayStatus = Literal["new", "invited", "accepted", "active", "inactive", "rejected"] +RelayStatus = Literal["new", "invited", "accepted", "acknowledgedRoster", "active", "inactive", "rejected"] ReportReason = Literal["spam", "content", "community", "profile", "other"] @@ -2723,16 +2780,35 @@ class SecurityCode(TypedDict): class SimplePreference(TypedDict): allow: "FeatureAllowed" -SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"] - -class SimplexNameDomain(TypedDict): +class SimplexDomain(TypedDict): nameTLD: "SimplexTLD" domain: str subDomain: list[str] +class SimplexDomainClaim(TypedDict): + domain: str + proof: NotRequired["SimplexDomainProof"] + +class SimplexDomainError_noValidLink(TypedDict): + type: Literal["noValidLink"] + +class SimplexDomainError_unknownDomain(TypedDict): + type: Literal["unknownDomain"] + +SimplexDomainError = SimplexDomainError_noValidLink | SimplexDomainError_unknownDomain + +SimplexDomainError_Tag = Literal["noValidLink", "unknownDomain"] + +class SimplexDomainProof(TypedDict): + linkOwnerId: NotRequired[str] + presHeader: str + signature: str + +SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"] + class SimplexNameInfo(TypedDict): nameType: "SimplexNameType" - nameDomain: "SimplexNameDomain" + nameDomain: "SimplexDomain" SimplexNameType = Literal["publicGroup", "contact"] @@ -3419,8 +3495,9 @@ class User(TypedDict): sendRcptsSmallGroups: bool autoAcceptMemberContacts: bool userMemberProfileUpdatedAt: NotRequired[str] # ISO-8601 timestamp - uiThemes: NotRequired["UIThemeEntityOverrides"] userChatRelay: bool + clientService: bool + uiThemes: NotRequired["UIThemeEntityOverrides"] class UserChatRelay(TypedDict): chatRelayId: int # int64 diff --git a/packages/simplex-chat-python/tests/test_bot_registration.py b/packages/simplex-chat-python/tests/test_bot_registration.py index f6f245c344..06837bdc07 100644 --- a/packages/simplex-chat-python/tests/test_bot_registration.py +++ b/packages/simplex-chat-python/tests/test_bot_registration.py @@ -86,8 +86,63 @@ def test_bot_profile_to_wire_with_commands(): ) cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or [] assert len(cmds) == 2 + # `params` defaults to None and must be ABSENT from the wire dict + # (not present as `null`/`""`) so the Haskell parser sees + # `Nothing` and the SimpleX client sends the bare `/keyword` on + # tap rather than pasting a trailing-space placeholder. assert cmds[0] == {"type": "command", "keyword": "ping", "label": "Ping bot"} assert cmds[1] == {"type": "command", "keyword": "help", "label": "Show help"} + assert "params" not in cmds[0] + assert "params" not in cmds[1] + + +def test_bot_command_params_emits_on_wire(): + """When `BotCommand.params` is set, the wire dict carries it as + `params: `. The SimpleX client (verified against + `CommandsMenuView.kt:153-161` and `CommandsMenuView.swift:117-128` + in simplex-chat 6.5) then pastes `/ ` into the + input box on tap, positions the cursor at the end, and lets the + user edit before sending. Use this for commands that take a + required argument (`/review `).""" + bot = Bot( + profile=BotProfile(display_name="x"), + db=SqliteDb(file_prefix="/tmp/test"), + commands=[ + BotCommand(keyword="review", label="Review PR", params=""), + BotCommand(keyword="order", label="Place order", params=""), + ], + ) + cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or [] + assert cmds[0] == { + "type": "command", + "keyword": "review", + "label": "Review PR", + "params": "", + } + assert cmds[1] == { + "type": "command", + "keyword": "order", + "label": "Place order", + "params": "", + } + + +def test_bot_command_distinguishes_none_from_empty_params(): + """`params=None` (immediate send) and `params=""` (paste with + trailing space) are semantically different on the client side. + Verify the wire form preserves the distinction: None → key + absent; empty string → key present with empty value.""" + bot = Bot( + profile=BotProfile(display_name="x"), + db=SqliteDb(file_prefix="/tmp/test"), + commands=[ + BotCommand(keyword="send", label="Send", params=None), + BotCommand(keyword="paste", label="Paste", params=""), + ], + ) + cmds = bot._profile_to_wire().get("preferences", {}).get("commands") or [] + assert "params" not in cmds[0] + assert cmds[1].get("params") == "" def test_client_profile_to_wire_has_no_bot_extras(): diff --git a/packages/simplex-chat-python/tests/test_native_cache.py b/packages/simplex-chat-python/tests/test_native_cache.py index 30a1f43e2a..55084eeae8 100644 --- a/packages/simplex-chat-python/tests/test_native_cache.py +++ b/packages/simplex-chat-python/tests/test_native_cache.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest from simplex_chat._native import _cache_root, _resolve_libs_dir, _download +from simplex_chat._version import LIBS_VERSION def test_cache_root_linux(tmp_path, monkeypatch): @@ -41,7 +42,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch): monkeypatch.setattr("simplex_chat._native._download", fake_download) libs_dir = _resolve_libs_dir("sqlite") - assert libs_dir == tmp_path / "simplex-chat" / "v6.5.2" / "sqlite" + assert libs_dir == tmp_path / "simplex-chat" / f"v{LIBS_VERSION}" / "sqlite" assert called["backend"] == "sqlite" assert (libs_dir / "libsimplex.so").exists() @@ -49,7 +50,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch): def test_resolve_uses_cache_on_second_call(tmp_path, monkeypatch): monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) monkeypatch.setattr("sys.platform", "linux") - cached = tmp_path / "simplex-chat" / "v6.5.2" / "sqlite" + cached = tmp_path / "simplex-chat" / f"v{LIBS_VERSION}" / "sqlite" cached.mkdir(parents=True) (cached / "libsimplex.so").touch() # Should NOT call _download — use the cached file. diff --git a/packages/simplex-chat-python/tests/test_native_url.py b/packages/simplex-chat-python/tests/test_native_url.py index b27c3e09cf..df96fff8ae 100644 --- a/packages/simplex-chat-python/tests/test_native_url.py +++ b/packages/simplex-chat-python/tests/test_native_url.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest from simplex_chat._native import _platform_tag, _libs_url, _libname +from simplex_chat._version import LIBS_VERSION @patch("sys.platform", "linux") @@ -42,7 +43,7 @@ def test_url_sqlite(_): assert ( _libs_url("sqlite") == "https://github.com/simplex-chat/simplex-chat-libs/releases/download/" - "v6.5.2/simplex-chat-libs-linux-x86_64.zip" + f"v{LIBS_VERSION}/simplex-chat-libs-linux-x86_64.zip" ) @@ -51,5 +52,5 @@ def test_url_postgres(_): assert ( _libs_url("postgres") == "https://github.com/simplex-chat/simplex-chat-libs/releases/download/" - "v6.5.2/simplex-chat-libs-linux-x86_64-postgres.zip" + f"v{LIBS_VERSION}/simplex-chat-libs-linux-x86_64-postgres.zip" ) diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index 5f3d2bf332..8441560013 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -583,6 +583,8 @@ const processCommand = (function () { case "capabilities": console.log("starting outgoing call - capabilities") if (activeCall) endCall() + // Stop a preview stream from an earlier pre-connect outgoing call being replaced (activeCall may be null here) + stopNotConnectedCall() let localStream: MediaStream | null = null try { @@ -623,7 +625,8 @@ const processCommand = (function () { if (activeCall) endCall() // It can be already defined on Android when switching calls (if the previous call was outgoing) - notConnectedCall = undefined + // Stop its preview tracks before clearing, otherwise camera/mic stay live + stopNotConnectedCall() inactiveCallMediaSources.mic = true inactiveCallMediaSources.camera = command.media == CallMediaType.Video inactiveCallMediaSourcesChanged(inactiveCallMediaSources) @@ -1444,6 +1447,14 @@ const processCommand = (function () { } } + // Call on any path that abandons notConnectedCall, otherwise its preview camera/mic tracks stay live. + function stopNotConnectedCall() { + if (notConnectedCall) { + notConnectedCall.localStream.getTracks().forEach((track) => track.stop()) + notConnectedCall = undefined + } + } + function resetVideoElements() { const videos = getVideoElements() if (!videos) return diff --git a/packages/simplex-chat-webrtc/src/desktop/ui.ts b/packages/simplex-chat-webrtc/src/desktop/ui.ts index eac659a17a..862c727bd5 100644 --- a/packages/simplex-chat-webrtc/src/desktop/ui.ts +++ b/packages/simplex-chat-webrtc/src/desktop/ui.ts @@ -2,8 +2,8 @@ useWorker = typeof window.Worker !== "undefined" isDesktop = true -// Create WebSocket connection. -const socket = new WebSocket(`ws://${location.host}`) +// Create WebSocket connection. location.search carries the per-call ?token=... capability required by the server. +const socket = new WebSocket(`ws://${location.host}${location.search}`) socket.addEventListener("open", (_event) => { console.log("Opened socket") diff --git a/plans/2026-04-29-member-profile-sending-channels.md b/plans/2026-04-29-member-profile-sending-channels.md index 2ee36b676e..be7d26ecab 100644 --- a/plans/2026-04-29-member-profile-sending-channels.md +++ b/plans/2026-04-29-member-profile-sending-channels.md @@ -1,5 +1,16 @@ # Plan: Member Profile Sending in Channels +## Implementation note (2026-05-18) + +The shipped implementation is **monotonic and reuses `member_relations_vector`**, not a new `sent_profile_vector` column: + +- The introduction bit lives in `group_members.member_relations_vector` with status `MRIntroduced`. The M20251117 backfill already populates this column for channel rows (relay role is not admin/owner), and `createNewGroupMember` writes `Binary B.empty` for new members. +- Bits flip 0 → 1 when the relay first announces the member to a recipient via prepended `XGrpMemNew` (or via `XGrpMemIntro` in `introduceInChannel`'s join-time direct path). They are **never cleared**. +- Profile updates propagate via the sender's own signed `XInfo`, forwarded unchanged by the relay. The relay updates its DB on receipt; subscribers verify with the key obtained from the earlier `XGrpMemNew`. Section 5 below ("Clear vector on profile update") is superseded by this — no clearing happens. +- The mutually-exclusive two-column delivery-jobs storage (`single_sender_group_member_id` + `sender_group_member_ids`) collapses into a single nullable `sender_group_member_ids BYTEA` column: `[s]` for single-sender jobs, `[s1, s2, ...]` for multi-sender batches, NULL for sender-less jobs (`DJRelayRemoved`). + +The plan body below is preserved for historical context. + ## Context In channels (relayed groups), subscribers don't know profiles of other subscribers. When subscriber A sends a reaction/message that gets forwarded to subscriber B, B creates an "unknown member" record with a synthesized name. This degrades UX — subscribers see "unknown member" instead of real profiles. diff --git a/plans/2026-05-08-public-groups-via-relays-overview.md b/plans/2026-05-08-public-groups-via-relays-overview.md new file mode 100644 index 0000000000..c0114e237d --- /dev/null +++ b/plans/2026-05-08-public-groups-via-relays-overview.md @@ -0,0 +1,45 @@ +# Public groups via relays — plan summary + +A third kind of group: relay-mediated like channels, but every member can post like a +secret group. Resolves the scale ceiling of full-mesh groups without the broadcast-only +governance of channels. Two orthogonal axes, already in the model: + +| `useRelays` | `groupType` | Name | +|-------------|--------------|--------------| +| false | (none) | Secret group | +| true | `GTChannel` | Channel | +| true | `GTGroup` | Public group | ← new +| true | `GTUnknown` | refuse | ← older client sees this for `"group"` + +`useRelays` is transport; `groupType` is the governance model (broadcast vs +participatory). The joiner role is a per-group value the owner sets at creation, +carried on the (owner-signed) channel profile, so every relay derives the same role for +the same group — not from a relay-side global config and not from `groupType`. The +blocker is narrow: no path produces `GTGroup` today, and the channel profile carries +no joiner-role field yet. + +## Shape of the work + +Backend: wire/version bump, type helpers, create command, owner-configured joiner-role +field on the channel profile, relay role derivation from that field. Clients (iOS + +Kotlin mirror): model, audit splitting transport-vs-governance call sites, unified +create flow with a Channel/Public-group toggle that picks the joiner-role default, +views, connect-plan messaging. + +## Threat model deltas vs. channels + +**Relay can fabricate content as any member** (broader than channels, where it could +only forge as owners). Same deniability property as channels by design; future fix is +opt-in content signing. + +Everything else in the channel threat model carries over unchanged. Out of scope for +now: member-to-member DMs in relay-mediated groups — deferred, not killed. + +## Sequencing & boundary + +Hard prerequisite: the member-profile dissemination plan +(`2026-04-29-member-profile-sending-channels.md`) lands first. Then backend → iOS → +Kotlin; platforms ship independently; older clients refuse to join. Owner→relay +role/rejection-rule communication and owner-signature verification on the channel +profile by relays are not planned here — both apply to channels equally; neither blocks +Public groups. diff --git a/plans/2026-05-08-public-groups-via-relays.md b/plans/2026-05-08-public-groups-via-relays.md new file mode 100644 index 0000000000..702f06fe7f --- /dev/null +++ b/plans/2026-05-08-public-groups-via-relays.md @@ -0,0 +1,378 @@ +# Plan: Public groups via relays + +Date: 2026-05-08 + +## 1. Overview + +Channels (shipped) are relay-mediated groups in which the relay forwards +content from any sender, but subscribers are pinned to `GRObserver` and +cannot post. Public groups are the second value of the same two-axis design: +same wire, same transport, members can post. The blocker is narrow — no +path produces `groupType = GTGroup`, and the relay's joiner-role default +comes from a global config instead of the owner-signed channel profile. +Add a `memberRole` field to the profile, plumb it (with `groupType`) +through the create command, derive the relay's joiner role from it, audit +clients for sites that conflate transport with governance, ride on the +approved member-profile dissemination plan. Member-to-member DMs in +relay-mediated groups are deferred (§10). + +## 2. Concept summary: the `useRelays × groupType` matrix + +| `useRelays` | `groupType` | Name | Wire shape | UX | +|-------------|-------------------------|-------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| `false` | (no `publicGroup`) | **Secret group** | P2P `x.grp.inv` invitations; full mesh between members; JSON array batch. | Today's group: all members can post; profiles known eagerly; admins moderate. | +| `true` | `GTChannel` | **Channel** | Relay-mediated; subscribers join via channel link; binary signed-batch format; profile carries `memberRole` (default `GRObserver` at creation). | Today's channel: only owners post; subscribers anonymous to each other. | +| `true` | `GTGroup` | **Public group** | Same wire as channel; profile carries `memberRole` (default `GRMember` at creation); profile dissemination on demand. | New: every member can post; member-to-member DMs prohibited (deferred); member roster grown lazily via on-demand profile send. | +| `true` | `GTUnknown _` (decode) | (refuse to join) | Channel link from a newer client; older client sees unknown discriminator. | New clients reject with a clear "needs newer version" message; pre-existing channels unaffected. | + +Three axes: **transport** = `useRelays` (topology, batch, signatures, +delivery); **governance model** = `groupType` (profile dissemination, +member affordances; member DMs prohibited in any relay-mediated group); +**joiner role** = `memberRole` on the owner-signed profile, set at +creation, type-keyed default. Today's client sites branch on `useRelays` +as a proxy for `isChannel` — that's the audit work (§4.2, §5.2). + +## 3. Backend changes + +### 3.1 Wire format / protocol + +New optional `memberRole :: Maybe GroupMemberRole` on `PublicGroupProfile` +(owner-signed; relays read from cache). New chat-protocol version +`publicGroupsVersion` signals understanding of `groupType = "group"` and +`memberRole`. Older peers decode unknown `groupType` as `GTUnknown` +(lossless tag preservation already exists) and ignore unknown JSON fields +— §7 covers behavior. Channel-protocol docs gain a paragraph naming +`groupType` the discriminator and `memberRole` the owner-set joiner role. + +### 3.2 Type changes + +`PublicGroupProfile` gains `memberRole :: Maybe GroupMemberRole`. New +single-line helpers in the same module as `useRelays'`: `groupType'` / +`memberRole'` (accessors); `isPublicGroup'` (`useRelays' && groupType' +== Just GTGroup`); `defaultMemberRoleFor` (`GTChannel → GRObserver`, +`GTGroup → GRMember`, `GTUnknown _ → GRObserver` defensive); +`joinerRoleFor` (canonical resolver — `memberRole'` if present, else +`defaultMemberRoleFor groupType'`). `requiresSignature` unchanged for +MVP; opt-in content signing is future-work mitigation per §6. + +### 3.3 API / command changes + +`APINewPublicGroup` / `/public group` gain `groupType` (default channel) +and optional `memberRole` (default `defaultMemberRoleFor groupType`); both +are written onto the constructed profile. The subscriber-side prepare-group +flow reads `memberRole` from the resolved link with the same fallback. The +`channelSubscriberRole` config is removed (no callers after §3.4); tests +that flipped it migrate to Public groups or explicit `memberRole`. + +#### 3.3.1 Default group preferences + +Public-group defaults equal secret-group defaults — the channel override +(`support = OFF`) does not apply, since member-to-moderator escalation is +expected. Parameterize the existing channel-prefs parser by `GroupType` +(Channel keeps its path; Public group and `GTUnknown` use secret-group). +`directMessages` stays ON by inheritance but is **dormant** in any relay- +mediated group (relay doesn't forward `XGrpDirectInv`; clients hide the +toggle); keeping the wire ON lets a future plan re-enable DMs without a +profile-shape change. + +### 3.4 Message processing + +- **Relay joiner-role derivation** (today reads `channelSubscriberRole`): + switch to `joinerRoleFor gInfo`. Eliminates cross-relay disparity. +- **Member-DM defensive refusal** (`xGrpDirectInv`): when `useRelays'`, + emit `messageError` and create no contact. Belt-and-suspenders with the + §4/§5 client suppression; unreachable today (no forwarding, no P2P). +- **Legacy `x.grp.inv`**: existing channel rejection covers Public groups. +- **`unverifiedAllowed`**: unchanged. Tightening becomes possible once + the dissemination plan distributes member keys; existing TODO is + updated to name that precondition. +- **Inherited unchanged** (add a test each): `checkSendAsGroup` + (role-based), receipts cutoff (count-based), introduce-in-channel + + history (`useRelays`-keyed). +- **`memberAdmission` on relay-mediated join**: hardcoded `GAAccepted` + bypasses review/captcha. Generic relay-mediated-groups gap; §8. + +### 3.5 Database migrations + +No schema migration: `groupType` and `memberRole` ride the existing +JSON-serialized profile; absent fields resolve via `defaultMemberRoleFor`. +The dissemination plan's `sent_profile_vector BLOB` migration is a hard +prerequisite owned by that plan. + +### 3.6 Test scenarios + +Add Public-group helpers paralleling the channel helpers, plus: + +1. Member sends content; all members receive it (no "unknown member" lines). +2. Multi-author session: no "unknown member" lines anywhere. +3. Member edit / delete / react forwarded by relay to all members. +4. Member-DM refused on receive: inject `XGrpDirectInv`, expect `messageError`, no contact created; repeat for Channel. +5. Role changes propagate through signed forwarding. +6. Blocked member's subsequent messages not forwarded. +7. Multi-relay delivery with cross-relay deduplication. +8. History on join. +9. `asGroup=true` from a non-owner member rejected with existing error. +10. Receipts disabled above the 20-member limit. +11. Older-client refusal on `groupType = "group"` shows needs-newer-version. +12. Incognito member posting attributes the incognito profile to others. +13. `memberRole` propagates: explicit `GRAuthor` at creation → joiners get `GRAuthor`; resolved link data carries the value. +14. `memberRole` defaults: Channel → `GRObserver`; Public group → `GRMember`. +15. Old-profile fallback: `memberRole = Nothing` → `defaultMemberRoleFor groupType` (`GRObserver` for Channel). + +## 4. iOS changes + +### 4.1 Model + +Add `case group` to `GroupType` (with serializer arms); +`memberRole: GroupMemberRole?` on `PublicGroupProfile`; `isPublicGroup`, +`groupType`, `memberRole` accessors on `GroupProfile`/`GroupInfo`. Client +uses `memberRole` for display only; authoritative resolution stays on +Haskell. + +### 4.2 Audit `useRelays` vs `isChannel` (≈73 sites) + +Per-site rule: **transport** (link/relay management, owner-can't-leave-own- +relay-group, relay-status indicator, incognito flag display, typing-state +gating, member-DM-affordance suppression) → keep `useRelays`. **Governance** +(titles, "subscribers" vs "members" framing, "Channel preferences" labels, +channel-style vs group-style member display) → switch to `isChannel`. +Roughly 70% flip to `isChannel`. Visually compare Public / Channel / Secret +after. + +### 4.3 Create flow + +Unified view with a "Channel / Public group" segmented control above the +display-name field, defaulting to Channel. The toggle drives the screen +title, link-step label, success screen, and two API parameters: `groupType` +and `memberRole` (`.observer` for Channel, `.member` for Public group — +no separate role picker in MVP). Default `groupPreferences` builder is +`groupType`-keyed per §3.3.1. The `directMessages` toggle is hidden in +the create-flow prefs section when `useRelays`. When `groupType = .group`, +render below the title: + +> "In a Public group, every member can post. Messages are delivered through +> relays you choose, which means a malicious relay could change or +> fabricate messages from any member. Pick relays you trust." + +### 4.4 Strings, views, icons, connect-plan + +- **Strings:** ~5–10 keys mirroring channel forms with `_public_group` + suffixes (create/add/leave/delete/link/temporarily-unavailable/no- + relays), plus `create_public_group_threat_model_note`. Reuse + `group_members_*` for "members" framing; channels keep `_subscriber*`. +- **Compose:** existing role-based gates allow members to post; **suppress + the member-tap "send direct message" affordance in any relay-mediated + group** (client side of the DM prohibition; receive gate at §3.4). +- **Views:** `GroupChatInfoView` and the link view branch three ways at + §4.2 sites; the link view takes `groupInfo` and derives variant inside. + `GroupPreferencesView` hides `directMessages` when `useRelays`. +- **Icon:** `chatIconName` gains a Public-group arm with a distinct icon + (different from channel-antenna and secret-group-people — §8). +- **Members view:** show the relay-known roster; header "subscribers" for + channels, "members" for Public groups. No filtered view in MVP. +- **Connect-plan:** wording keyed on resolved `groupType` — "ok to + subscribe via relays" (channel) vs "ok to join via relays" (Public + group). CLI string changes alongside; tests follow. + +## 5. Kotlin changes + +Mirror of §4 across the Compose surface. Subsections parallel §4 and +note divergences only. + +### 5.1 Model + +`GroupType` gains `Group`; `memberRole` / `isPublicGroup` accessors on +`GroupInfo` / `GroupProfile`. + +### 5.2 Audit `useRelays` vs `isChannel` (≈74 sites) + +Same transport-vs-governance rule as §4.2; ~70% flip to `isChannel`. + +### 5.3 Create flow + +Single-view create with Channel / Public-group toggle driving +`groupType`+`memberRole`; threat-model note below the title; +`directMessages` toggle hidden under `useRelays`. + +### 5.4 Strings, views, icons, ConnectPlan + +Strings, views, icons, and ConnectPlan mirror §4.4. **Kotlin-only:** +chat-list filter chips place Public groups in the "groups" bucket +(mental model: "things I can post in"), not "channels". + +## 6. Threat model: changes from channels + +This section assumes the channel threat model +(`docs/protocol/channels-overview.md` §"Threat model"). Public groups +inherit every property listed there. One threat is *broader* (channels +have a narrower form of the same threat). The relay's "can / cannot" +framing matches the existing doc style; the items below are written so +they can be folded directly into a future revision of +`channels-overview.md` once Public groups ship. + +### 6.A.1 A relay can fabricate content as any member + +Content messages (`XMsgNew`, `XMsgUpdate`, `XMsgDel`, `XMsgReact`, +`XFileCancel`) are unsigned in both channels and Public groups +(`Protocol.hs:1221`, `requiresSignature` lists only roster / +administrative events). In channels this gives a compromised relay +the ability to fabricate content attributed to owners — already +documented in `channels-overview.md` §"Threat model" ("Substitute +unsigned content or selectively drop messages for its subscribers"). +In Public groups, the same property has a **broader blast radius**: +the relay can fabricate content attributed to *any* member, not just +to owners. + +This matches the channel deniability property by design (see +`channels-overview.md` §"Signing scope: roster only, content +optional"): unsigned content is precisely what enables cryptographic +deniability — no third party can prove a member authored anything. +The trade-off is that the operator on the delivery path cannot be +prevented from forging in the same channel. + +**A single compromised relay** + +*can:* + +- Fabricate content messages attributed to any member, not just to + owners. Detectable by other members through cross-relay + consistency (same TODO as the channel case: difference detection + not yet implemented). +- Modify the text or content of messages in transit and re-attribute + the modified message to its original author. +- Drop content messages selectively — same property as channels. + +*cannot:* + +- Forge signed administrative events: `XGrpInfo`, `XGrpPrefs`, + `XGrpMemRole`, `XGrpMemRestrict`, `XGrpMemDel`, `XGrpDel`, + `XGrpLeave`, `XInfo` (`Protocol.hs:1221`). Roster manipulation, + profile changes, and member-attributed leave / profile-update + events all require valid signatures. +- Substitute the channel profile or impersonate an owner — the + channel's entity ID and owner authorization chain are validated + by every recipient against the channel link. The new `memberRole` + field is part of the (owner-signed) channel profile, so a + compromised relay also cannot fabricate a different joiner role + than the owner configured. +- Alter authoritative state on owner devices. + +**Mitigation.** No code change for the MVP. The future-work fix is +opt-in content signing per the channel roadmap +(`channels-overview.md` §"Future work" / "Transcript integrity" / +"Opt-in content signing"). When that ships, owners of Public groups +will be able to require all content (member or owner) to carry a +signature; member keys are already disseminated to other members +via the prior plan (`2026-04-29-member-profile-sending-channels.md`), +so verification on the recipient side is not a separate effort. + +In the meantime, the create-flow help text for "Public group" on +both platforms (§4.3, §5.3) carries this trade-off framing: "In a +Public group, the relay forwards messages on behalf of every member. +A compromised relay could change message text or attribute fabricated +messages to any member. Use a secret group if you need non- +repudiable peer-to-peer messaging." This is the same trade-off +channels make for owner posts; making it explicit at create time +lets users choose Public-group-via-relay vs secret-group based on +whether they value scale or content integrity. + +### 6.A.2 What is unchanged from channels + +Every other property of the channel threat model carries over +without change. In particular: + +- A relay cannot impersonate an owner or substitute the channel + profile (signed events, validated entity ID). The configured + `memberRole` is part of the signed profile, so the relay cannot + unilaterally elevate or demote joiners relative to what the owner + specified. +- A relay cannot determine subscriber / member real identity or + network address (inherited from SMP transport). +- All-relays-compromised-and-colluding cannot forge signed events + or alter owner-authoritative state. +- A passive network observer cannot determine which Public group a + member is in, or correlate Public-group activity with other + SimpleX activity. + +Public-group members get the same participant-privacy guarantees as +channel subscribers, and Public-group owners get the same key-loss +risk profile as channel owners (see `channels-overview.md` +§"Compromise of owner keys" and §"Loss of all owner devices"). + +**Out of scope for now: member-to-member DMs in relay-mediated +groups.** In channels, members do not DM each other today. In Public +groups, this plan prohibits the affordance (client-side and +defensively on the receive path) and the relay does not forward +`XGrpDirectInv`. The relay therefore does not see a "member DM +graph" — that threat (which a forwarded-DM design would have +introduced) does not exist under this plan. A future plan can +re-introduce member-to-member DMs and revisit the metadata trade-off +explicitly; the design space is sketched in §10. + +### 6.A.3 Release-notes line + +For the Public-groups release notes, include a one-line summary of +the new property: + +> "In a Public group, the relay you choose could in principle alter +> or fabricate group messages attributed to any member. Pick relays +> you trust, or use a secret group if you need peer-to-peer message +> integrity." + +## 7. Migration / compatibility + +- **Existing channels unaffected.** Pre-upgrade profiles have no + `memberRole`; readers fall back to `defaultMemberRoleFor GTChannel = + GRObserver`. No data migration. +- **Older clients** decode `groupType = "group"` as `GTUnknown` and must + refuse to join with a "needs newer version" alert; they ignore unknown + fields on channel profiles otherwise. +- **Older relays** forward Public-group traffic but resolve joiner role + from their global config — joiners via un-upgraded relays get the legacy + role and cannot post. Mitigation: warn the owner at create time if any + selected relay's chat version is below `publicGroupsVersion`. Soft + warning, not a hard block. + +## 8. Open questions + +1. **Future member-DM design.** (i) Relay-forwarded `XGrpDirectInv` + (simple; leaks DM-graph metadata); (ii) relay-blind rendezvous via + per-member queues on the profile (privacy-preserving; new protocol). + Either re-derives §6. +2. **`memberAdmission` on relay-mediated join.** Hardcoded `GAAccepted` + bypasses review/captcha; generic relay-mediated-groups gap; defer. +3. **Distinct icon for Public groups.** Visually different from channel- + antenna and secret-group-people metaphors. Pending design review. +4. **`channelSubscriberRole` removal.** Verify no out-of-tree consumer + reads it before deleting. +5. **`memberRole` on profile edit.** MVP exposes no UI; Haskell accepts + the edit but role-rebase of existing members is undefined. Deferred. +6. **Roster filter in members view.** Paginate/filter for 100K+ members? + Generic relay-roster question; defer. +7. **Connect-plan wording.** "subscribe / join / connect via relays" — + pick per `groupType`; update tests when CLI string changes. + +## 9. Sequencing + +1. **Prerequisite:** member-profile dissemination plan lands first. +2. **Backend:** types, `memberRole` field, command parameters, profile- + based role derivation, removal of `channelSubscriberRole`, defensive + `XGrpDirectInv` refusal, tests 1–15. +3. **iOS** then **Kotlin** (independent of each other; API defaults are + backward compatible): model, audit, create flow, strings; then views, + icons, ConnectPlan, DM-affordance suppression. +4. **Older-client refusal, version-bump release notes, channel-docs + updates** ship with the backend release. + +## 10. Adjacent work (not planned here) + +- **Owner→relay communication of rejection rules.** Joiner-role side is + fixed here (travels on the signed profile); rejection-rule side + (admission/captcha) is still relay-side config. Future plan: carry it + on the profile too. +- **Owner-signature verification on the channel profile by relays.** + Affects channels equally; does not gate this plan. +- **Member-to-member DMs in relay-mediated groups.** Deferred (§1, §8 Q1). + A future plan must re-derive §6 — relay-forwarded DMs would re-introduce + (sender, target, time) metadata exposure this plan + avoids. diff --git a/plans/2026-05-20-fix-hold-on-long-msg-android.md b/plans/2026-05-20-fix-hold-on-long-msg-android.md new file mode 100644 index 0000000000..f89aa26582 --- /dev/null +++ b/plans/2026-05-20-fix-hold-on-long-msg-android.md @@ -0,0 +1,68 @@ +# Fix chat item long-press menu and ripple shape + +Branch: `nd/fix-hold-on-long-msg-android` · PR [#6997](https://github.com/simplex-chat/simplex-chat/pull/6997) · issue [#6991](https://github.com/simplex-chat/simplex-chat/issues/6991). + +## 1. Problem statement + +Two issues with the chat-item bubble on the multiplatform UI: + +- **Android (#6991):** long-pressing the lower part of a very tall text message did not open the select/copy/reply context menu. Long-press on the top/middle worked. Reproduced with a long multi-line message (~150+ lines — e.g. 5000 random bytes as hex); never reproduced on short messages. Occurs **only with the message tail enabled** (bubble shape); with the tail preference disabled, messages use a plain rounded-rectangle shape and the bug does not reproduce. iOS unaffected. +- **Desktop:** the chat-item press ripple, in some cases, rendered as a rectangle instead of following the rounded bubble shape. + +## 2. Solution summary + +One function — `Modifier.clipChatItem` in `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt`. It clipped the chat item with `Modifier.clip(shape)` for every shape style. It now clips the **bubble** (`GenericShape`) in the draw pass with `drawWithCache` + `clipPath`, and keeps `Modifier.clip` for the **`RoundRect`** shape, which is unaffected by the bug (§3). + +```kotlin +return when (style) { + is ShapeStyle.Bubble -> { + val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) + this.drawWithCache { + val path = Path().apply { addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache)) } + onDrawWithContent { clipPath(path) { this@onDrawWithContent.drawContent() } } + } + } + is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness)) +} +``` + +Net diff: 1 file (`ChatItemView.kt`), +20 / −5 — the `clipChatItem` function restructured plus two imports. + +## 3. Root cause + +`Modifier.clip(shape)` is defined in Compose as `graphicsLayer(shape = shape, clip = true)`. A clipping graphics layer restricts **both** drawing **and** pointer hit-test to the shape. + +`clipChatItem` is the first (outermost) modifier on the chat-bubble `Column`, and that same `Column` carries the `combinedClickable` long-press handler. So the layer's hit-test region gates every press on the bubble. + +- **Android:** for a very tall chat item the layer's hit-test region does not cover the bubble's lower portion — a press there is never delivered to `combinedClickable`, so the long-press menu does not open. This is specific to the bubble's `GenericShape` clip: with the tail disabled the item is clipped with a `RoundedCornerShape`, which hit-tests correctly. The exact reason the `GenericShape` clip's hit-test falls short on tall content was not isolated; the fix does not depend on it (see §4). +- **Desktop:** the layer's clip did not always extend to the `combinedClickable` press ripple, so the ripple drew to its own rectangular bounds instead of the bubble shape. + +## 4. The fix + +For the bubble shape, `clipChatItem` clips with a draw modifier instead of a graphics layer. `drawWithCache` builds the shape's `Path` once per size change; `onDrawWithContent { clipPath(path) { drawContent() } }` wraps the whole content draw — bubble background, text, and the press ripple — in a canvas clip. + +A draw modifier affects **only drawing**. It is not a layout or pointer-input node and has no effect on hit-test. Therefore: + +- the bubble and ripple are still clipped to the shape — visually identical to `Modifier.clip`; +- pointer hit-test is no longer clipped — `combinedClickable` receives presses anywhere in the `Column`'s bounds, fixing the Android long-press; +- the canvas `clipPath` clips the ripple reliably, fixing the rectangular desktop ripple. + +The `RoundRect` shape keeps `Modifier.clip`: it hit-tests correctly (no bug) and keeps its antialiased outline clip. Scoping by shape — rather than draw-clipping every shape — leaves every non-bubble chat item (service/event messages, tails-off messages, old Android) byte-for-byte unchanged. + +## 5. Alternatives rejected + +- **Remove `clipChatItem` from the bubble `Column`.** Fixes the Android long-press, but the press ripple loses its shape and renders as a rectangle. Intermediate state during development; replaced. +- **Draw-pass clip for every shape, unconditionally.** Also correct and a hair simpler (no `when`), but it needlessly moves the `RoundRect` shape off `Modifier.clip`'s antialiased outline clip onto a canvas `clipPath` — a behaviour change with no benefit, since `RoundRect` has no bug. Scoping to the bubble shape keeps `RoundRect` unchanged. +- **Keep `Modifier.clip`, move `combinedClickable` off the clipped `Column`.** A larger structural change to the chat-item layout tree; the draw-pass clip fixes both issues without moving anything. + +## 6. Verification + +- **Android** (debug APK): long-press on the lower half of a 150+-line message opens the context menu; top/middle still work; the tap ripple stays bubble-shaped; swipe-to-reply and link tap/long-press are unaffected. +- **Desktop** (Linux AppImage): the chat-item press ripple follows the bubble shape (rounded corners and tail), not a rectangle — confirmed against a build without the fix. +- The bubble draw-pass clip above was verified on those Android and desktop builds; this revision additionally keeps `Modifier.clip` for the `RoundRect` shape, which is the unchanged pre-fix behaviour. + +## 7. Risk and rollback + +- Blast radius: the `Bubble` branch of `clipChatItem`. The `RoundRect` branch is unchanged (`Modifier.clip` as before), so service/event items, tails-off messages and old-Android items are untouched. For the bubble, drawing is clipped identically; the single behavioural change is that pointer hit-test on the bubble is no longer shape-clipped — benign (bubble corners are transparent; a rectangular hit area is a marginally larger touch target). +- iOS is a separate codebase and is untouched. +- Rollback: revert the fix commit on the branch, or drop it before merge. diff --git a/plans/2026-05-20-member-deletion-fulldelete.md b/plans/2026-05-20-member-deletion-fulldelete.md new file mode 100644 index 0000000000..9d6e516b00 --- /dev/null +++ b/plans/2026-05-20-member-deletion-fulldelete.md @@ -0,0 +1,73 @@ +# Full delete on member removal under fullDelete preference + +Plan for the next attempt at the change previously tried in PR #6831 (closed as too messy: the member row was deleted twice on one path, and the user's own membership row was deleted when the user was the one removed). The change is small: two SQL-function edits, one new chat-layer helper, an explicit fullDelete branch plus order swap in two backend handlers, and one in-memory removal branch in `removeMemberItems` on each UI platform. + +## Problem + +When a member is removed via `XGrpMemDel` with `withMessages = True` and the group's `fullDelete` preference is on for the deleter's role, the member's chat items are currently rewritten to `CIModerated` placeholders by `updateMemberCIsModerated`, and the member row is preserved by `deleteOrUpdateMemberRecord` when any item references it. The intent of `fullDelete` is physical deletion. The current behavior leaves placeholder rows and, because `deleteOrUpdateMemberRecord` runs before the items pass, the relay subpath of the latter deletes the member row first and the subsequent file collection returns nothing — files on disk leak. The same ordering bug exists on the moderator side (`APIRemoveMembers`). + +## What changes + +In `xGrpMemDel` (`src/Simplex/Chat/Library/Subscriber.hs:3157`), only on the branch where `withMessages = True` AND `groupFeatureMemberAllowed SGFFullDelete m gInfo`: + +**Case A — the deleted member is the user themselves (`memId == membership.memberId`).** The user's own sent items (those with `group_member_id IS NULL AND item_sent = 1`) and their files are physically deleted. The `membership` row stays with status `GSMemRemoved`, so the group can still be loaded in the chat list and opened. + +**Case B — the deleted member is somebody else.** The member's chat items and their files are physically deleted, then the `group_members` row is deleted. Historical system event items that referenced this member as `item_deleted_by_group_member_id` survive with NULL via the existing `ON DELETE SET NULL`. + +The non-fullDelete branch, the `withMessages = False` branch, and the entire message-moderation path (`XMsgDel`, `APIDeleteMemberChatItem`, `deleteGroupCIs`, `markGroupCIsDeleted`, `createCIModeration`, `chat_item_moderations`) are not changed. + +## Implementation + +The whole change is two SQL-function edits in `Store/Messages.hs`, a new member-record helper in `Library/Internal.hs`, and explicit branching plus an order swap in both `xGrpMemDel` (recipient side) and `APIRemoveMembers` (moderator side). + +**Edit 1 — rewrite `updateMemberCIsModerated` to physically delete.** Recommended rename: `deleteMemberCIs`. Keep the existing `memId == groupMemberId' membership` branch unchanged (the membership branch selects `WHERE group_member_id IS NULL AND item_sent = 1`; the other branch selects `WHERE group_member_id = ?`). Change the body from "UPDATE chat_items SET moderated content" to "physically delete chat_items + side-table rows analogous to `deleteGroupChatItem` in bulk": delete from `chat_item_messages`, `chat_item_versions`, `chat_item_reactions`, then `DELETE FROM chat_items`. The function loses the `byGroupMember`, `msgDir`, and `deletedTs` parameters since they were only used to construct the moderated content. The chat-layer wrappers `deleteGroupMemberCIs` and `deleteGroupMembersCIs` follow the same signature simplification. + +**Edit 2 — extend `getGroupMemberFileInfo` to handle the membership case.** Today it queries `WHERE group_member_id = ?` only, so for Case A it returns nothing and files for the user's own sent items leak (this is a pre-existing bug on both the off and on paths — `markGroupMemberCIsDeleted_` also relies on this function to cancel in-progress transfers). Add the same `memId == groupMemberId' membership` branch as in `deleteMemberCIs`: for the membership case, query `WHERE group_member_id IS NULL AND item_sent = 1`. The only two callers (`deleteGroupMemberCIs_` and `markGroupMemberCIsDeleted_`) both benefit from the fix. + +**Edit 3 — add `fullyDeleteMemberRecord` helper in `Library/Internal.hs` next to `deleteOrUpdateMemberRecord`.** Wraps `deleteSupportChatIfExists` + `deleteGroupMember`, returns updated `GroupInfo`. No `isRelay` branch and no `checkGroupMemberHasItems` query — the caller has already physically deleted the member's items, so the existence check would be a wasted query and the function communicates intent explicitly: unconditional row deletion. The `CM` wrapper plus an `IO` variant (`fullyDeleteMemberRecordIO`) mirror the shape of the existing `deleteOrUpdateMemberRecord` / `deleteOrUpdateMemberRecordIO`. + +**Edit 4 — swap order and add explicit branching in `xGrpMemDel` Case B (the `else` branch).** Move `when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv` to run *before* the member-record decision, on the same `gInfo` (the new `deleteMessages` reads only `groupId` and `membership` from the passed `gInfo`). Replace the current member-record dispatch with an explicit branch: + +``` +gInfo' <- case deliveryScope of + Just (DJSMemberSupport _) | shouldForward -> updateMemberRecordDeleted user gInfo deletedMember GSMemRemoved + _ -> if withMessages && groupFeatureMemberAllowed SGFFullDelete m gInfo + then fullyDeleteMemberRecord user gInfo deletedMember + else deleteOrUpdateMemberRecord user gInfo deletedMember +``` + +`deleteMemberItem` (the RGE event creation) keeps its current position after `updatePublicGroupData`. Case A (the `then` branch) needs no order change — the membership row is never deleted there, and `deleteMessages` already runs in the right relative position. + +The `DJSMemberSupport _ | shouldForward` subcase keeps its existing `updateMemberRecordDeleted` call regardless of fullDelete — the row is preserved for support-scope forwarding. Under fullDelete the items are still gone (the `deleteMessages` step ran first), the row stays. + +**Edit 5 — mirror the swap and explicit branching in `APIRemoveMembers` (`src/Simplex/Chat/Library/Commands.hs:2834`).** Inside `deleteMemsSend`, compute `fullDelete = withMessages && groupFeatureUserAllowed SGFFullDelete gInfo` once. Move the items pass to before `delMember`: run `deleteMessages user gInfo memsToDelete` inside `deleteMemsSend` before the `withStoreBatch'` that calls `delMember`. Change `delMember` to branch explicitly: + +``` +delMember db m = do + if fullDelete + then void $ fullyDeleteMemberRecordIO db user gInfo m + else void $ deleteOrUpdateMemberRecordIO db user gInfo m + pure m {memberStatus = GSMemRemoved} +``` + +`deletePendingMember` flows through `deleteMemsSend` and inherits the new behavior. The outer line 2864 call (`when withMessages $ deleteMessages user gInfo' deleted`) collapses — items are already handled inside `deleteMemsSend` for current and pending members, and invited members (handled by `deleteInvitedMems`) have no chat items. Remove it. + +**Edit 6 — extend `removeMemberItems` on both UI platforms to physically remove items from the in-memory list when `fullDelete.on`.** Today (iOS `apps/ios/Shared/Model/ChatModel.swift:814-846`, Kotlin `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt:699-734`) the function walks the in-memory items, identifies matches by direction and member id, and sets `itemDeleted = .moderated(...)`; under `fullDelete.on` it additionally rewrites content to `Snd/RcvModerated`. Items are never removed from `im.reversedChatItems` / `chatItems.value`. After the backend change, the chat_item rows are physically gone in DB while the UI keeps stale moderated placeholders until the next refetch — flicker. Extend the existing `fullDelete.on` branch so it also removes matching items from the in-memory list (iOS: drop them from `im.reversedChatItems`, decrement unread counters, stop voice playback on dropped items; Kotlin: `removeAllAndNotify { isMemberItem(it) }` equivalent, decrement counters, stop audio). The fullDelete-off branch is unchanged (still marks moderated in place). + +The three callers — iOS `removeMember` in `GroupChatInfoView.swift:977`, Kotlin `removeMembers` in `GroupChatInfoView.kt:1316`, Kotlin `removeMember` in `GroupMemberInfoView.kt:339`, and the event handlers for `.deletedMember`/`.deletedMemberUser` in `SimpleXAPI.swift:2578-2596` and `SimpleXAPI.kt:2945-2973` — all converge on the same `removeMemberItems` function on each platform and inherit the new behavior automatically. The chat-list preview path inside `removeMemberItems` (the `else` branch that updates `chat.chatItems[0]`) also needs to drop the preview item under fullDelete so the chat list doesn't show a stale moderated last-message. + +The `fullDelete.on` gate matches the backend's `groupFeatureMemberAllowed SGFFullDelete` / `groupFeatureUserAllowed SGFFullDelete` because FullDelete is a `GroupFeatureNoRoleI` feature — the role check collapses to the `.on` check. + +## Anti-patterns from PR #6831 to avoid + +No path may call `deleteGroupMember` twice. No path under Case A may delete the `membership` row — that row must survive. File info must be collected before any chat-item deletion, since `getGroupMemberFileInfo` reads `chat_items`. Do not rely on `ON DELETE SET NULL` to clean up the deleted member's authored items — they are deleted explicitly first. `fullyDeleteMemberRecord` is the only function that should call `deleteGroupMember` directly on the new path; do not duplicate that call in the handler. + +## Tests + +Add cases in `tests/ChatTests/Groups.hs` for: Case A (user removed by admin, fullDelete on — user's sent items and their files gone, `membership` row exists with `GSMemRemoved`, group still loadable); Case B (member removed by admin, fullDelete on — member's items and files gone, `group_members` row gone, system event items previously referencing the removed member now have NULL `item_deleted_by_group_member_id` and still display correctly); regression for fullDelete=off (items become `CIModerated` placeholders via `markMemberCIsDeleted`); regression for `withMessages = False` (items untouched, row handled by existing path); regression that message moderation under fullDelete=on still produces `CIModerated` placeholders, confirming the moderation path is unchanged. Verify the same Case A and Case B behaviors over both XGrpMemDel (recipient side, Subscriber.hs) and APIRemoveMembers (moderator side, Commands.hs). + +UI checks for the manual smoke test: in a group with fullDelete on, remove a member with messages — that member's bubbles disappear immediately from the open chat view on both moderator's and recipients' devices, the chat list preview updates to the previous non-deleted message, and the unread/report counters decrement; with fullDelete off, the same removal produces moderated placeholders as today. Verify on iOS, Android, and Desktop. + +## Open items for review + +Naming of the rewritten `updateMemberCIsModerated`: `deleteMemberCIs` is the natural rename (the function physically deletes chat items associated with a member, handling the membership case internally). Naming of the new chat-layer helper: `fullyDeleteMemberRecord` (parallels `deleteOrUpdateMemberRecord`). Confirm or amend before implementation. diff --git a/plans/2026-05-25-fix-e2e-encryption-section-divider.md b/plans/2026-05-25-fix-e2e-encryption-section-divider.md new file mode 100644 index 0000000000..69d4bd630a --- /dev/null +++ b/plans/2026-05-25-fix-e2e-encryption-section-divider.md @@ -0,0 +1,50 @@ +# Fix E2E encryption section divider rendered inside the section + +Branch: `nd/fix-e2e-encryption-section-divider` · base: `master`. + +## Problem + +On the contact info screen (Android and desktop, current `master`), the "E2E encryption — Quantum resistant / Standard" card has a horizontal divider line cutting across it under its single row, followed by extra padded space — visually reading as the card being sliced in two with a second, empty card underneath. Repros for any 1:1 contact with an active connection. Behaviour of the row itself is correct; bug is purely visual. + +## Fix + +One line in `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` — move `SectionDividerSpaced()` out of the `SectionView { ... }` block: + +```diff + if (conn != null) { + SectionView { + InfoRow("E2E encryption", if (conn.connPQEnabled) "Quantum resistant" else "Standard") +- SectionDividerSpaced() + } ++ SectionDividerSpaced() + } +``` + +Total diff: 1 file, +1 / −1. + +## Cause + +Two unrelated changes combined to produce the visible bug: + +1. PR #4060 (`9e3f528d4`, "android: remove experimental PQ toggle") removed the conditional `AllowContactPQButton` / `SectionTextFooter` that used to sit between the `InfoRow` and the divider — but left `SectionDividerSpaced()` inside the `SectionView { ... }` block. At that point `SectionView` was a plain column, so the leftover divider only looked like extra inter-section spacing. + +2. PR #6777 (`df5ea3d46`, "android, desktop: new settings section design") wrapped `SectionView`'s content in `CardColumn` with `SectionCardShape`, giving each section a rounded card background. After this, *anything* drawn by the section content — including the leftover `Divider` — is drawn inside the card. + +Rendered structure on current master: + +``` +SectionView (card background, rounded shape) + └ CardColumn + ├ InfoRow("E2E encryption", ...) + ├ Divider ← line cutting across the card + └ 18 dp bottom padding ← reads as a second, empty card +``` + +After the fix the divider re-parents to the enclosing `ChatInfoLayout` column and sits between the E2E card and the next section's card, matching the pattern used by every other section on the screen. + +## Risk + +- One composable call site, structural move of a single node; no logic, state, or styling change. +- iOS is a separate codebase and is unaffected. +- Grep confirms no other `SectionDividerSpaced` call sits inside a `SectionView { ... }` in `apps/multiplatform`. +- Rollback: `git revert` the fix commit. diff --git a/plans/2026-05-26-fix-invalid-mention-on-edit.md b/plans/2026-05-26-fix-invalid-mention-on-edit.md new file mode 100644 index 0000000000..2fe9bad5a5 --- /dev/null +++ b/plans/2026-05-26-fix-invalid-mention-on-edit.md @@ -0,0 +1,107 @@ +# Fix "error store invalidMention" and preserve mention bindings across in-place edits + +## Symptom + +Rare "Error sending message: error store invalidMention" when sending a group +message that contains an @mention. + +## Root cause + +When the user picks a member from the picker, the client inserts `@Name` into +the message text and records `mentions[Name] = memberId` in the compose +state. The original `removeUnusedMentions` only pruned that map when the +parsed `@name` count was **strictly less** than the map size: + +```kotlin +if (usedMentions.size < composeState.value.mentions.size) { ... } +``` + +If the user **edits the inserted token in place** (e.g. fixes a typo, deletes +one character) without re-picking from the picker, the parser sees one +mention with the new name while the map still contains the old key. Both +have size 1, so the guard does not fire and the stale entry is sent. The +core's `getCIMentions` (`Internal.hs:267-271`) requires every key in the +client-supplied mentions map to also appear as a parsed `@name` token in the +message text, and throws `SEInvalidMention` otherwise. + +A naive fix (prune whenever any key is missing from the parsed names) stops +the crash but also drops the binding the instant a letter is removed — so +typing the letter back leaves an unresolved `@name` rather than a real +mention. + +## Approach + +Stop mutating the compose-state mentions map while editing. Treat the map +as a sticky cache of `name → memberId` bindings recorded by the picker, and +filter against the currently-parsed text only at the points where a "current +set of mentions" is actually needed: sending, picker UX, name +disambiguation. This both eliminates the `SEInvalidMention` failure and +lets a round-trip edit (`@Usernama` → `@Usernam` → `@Usernama`) re-resolve +the original member. + +## Changes + +### 1. Drop `removeUnusedMentions` + +- `apps/multiplatform/.../GroupMentions.kt`: delete the function and its call + in `messageChanged`. +- `apps/ios/.../GroupMentions.swift`: same. + +### 2. Filter at send time via `memberMentions` + +`ComposeState.memberMentions` intersects the cached map with the names +currently parsed in `parsedMessage`. All sends already route through this +getter, so this is the single chokepoint that determines what reaches the +core. + +### 3. Picker "max reached" uses parsed-mention count + +`GroupMentions.kt:213,231` and the Swift equivalent at `GroupMentions.swift:50` +switch from `composeState.mentions.size` to a count of `Format.Mention` +entries in `parsedMessage`. This keeps the limit aligned with what the +server will see, regardless of how many stale entries the cache holds. + +### 4. `mentionMemberName` disambiguation uses parsed names + +`ComposeView.kt`'s `mentionMemberName` walks the set of parsed mention names +instead of `mentions.containsKey`. Same for Swift. A stale cache entry no +longer pushes a fresh pick to an awkward `_1` suffix, and a picker tap on a +broken `@alic` cleanly replaces it with `@alice`. + +### 5. Editing-an-existing-item path + +`ComposeState(editingItem, ...)` seeds `mentions = editingItem.mentions`. +That stays as-is — it is already a sticky cache and the new `memberMentions` +getter handles filtering. + +## Manual reproduction (original bug) + +1. In a group, type `@` and pick a member. Text becomes `@Name ` and the + mentions map gets `{Name → memberId}`. +2. Place the cursor inside the inserted name and add or delete one character + (`@Nam`, `@Names`, etc.) — do not use the picker. +3. Tap send. Before the fix: send fails with `error store invalidMention`. + After the fix: message is sent. + +## Edge cases to test + +- Pick `Usernama`, delete last `a`, retype it → send: mention resolves to + the original member (restoration behaviour). +- Pick `Usernama`, delete the whole token → send: no mention. Cache still + holds a stale `Usernama` entry but `memberMentions` is empty. +- Pick `alice` (member A), delete the `@alice` text, then pick a different + `alice` (member B) → inserted as `@alice` (the cleaner, visible name); + cache key rebinds A→B as an explicit user action. +- Two members with same display name, pick A, edit `@alice` → `@alic` → + `@alice` by typing only → A's binding is restored (typing never mutates + the cache; only picker taps do). +- Pick 3 members, delete one of the tokens — picker should not show + "max reached"; only 2 mentions are now in the text. +- Edit an existing sent message: original mentions render, surviving edits + re-bind, removed ones disappear from `memberMentions` on send. + +## Out of scope + +- No core changes. `getCIMentions` (`Internal.hs:267-271`) stays as-is; + this is purely client-side cache lifecycle. +- No change to the @-picker trigger heuristic in `messageChanged`. diff --git a/plans/2026-05-26-fix-video-drag-and-drop.md b/plans/2026-05-26-fix-video-drag-and-drop.md new file mode 100644 index 0000000000..16258dea7f --- /dev/null +++ b/plans/2026-05-26-fix-video-drag-and-drop.md @@ -0,0 +1,44 @@ +# Fix desktop drag-and-drop of videos attached as files + +Branch: `nd/fix-video-drag-and-drop` · base: `master`. + +## Problem + +On desktop, dragging a video file into a chat attaches it as a generic file (paperclip + filename) instead of as a video (thumbnail + duration). Dragging an image works. Picking the same video via "Gallery → Video" attaches it correctly — so only the drag-and-drop routing is wrong. + +## Fix + +One file: `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`. Recognise videos as media in `onFilesAttached`'s classifier. + +```diff + fun MutableState.onFilesAttached(uris: List) { +- val groups = uris.groupBy { isImage(it) } +- val images = groups[true] ?: emptyList() ++ val groups = uris.groupBy { isImage(it) || isVideoUri(it) } ++ val media = groups[true] ?: emptyList() + val files = groups[false] ?: emptyList() +- if (images.isNotEmpty()) { +- CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) } ++ if (media.isNotEmpty()) { ++ CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) } + } else if (files.isNotEmpty()) { + processPickedFile(uris.first(), null) + } + } ++ ++private fun isVideoUri(uri: URI): Boolean { ++ val name = getFileName(uri)?.lowercase() ?: return false ++ return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") || ++ name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") ++} +``` + +Total diff: 1 file, +11 / −5. + +## Cause + +`onFilesAttached` classified URIs by `isImage` only — non-images (including videos) fell through to `processPickedFile`, producing a `FilePreview`. The downstream `processPickedMedia` already handles video correctly (its `else` branch builds `UploadContent.Video`); the classifier above it just never reached that branch. The existing `isVideo` in `Videos.desktop.kt` is `desktopMain`-only and not visible from `ComposeView.kt` in `commonMain` — the structural gap that left the classifier video-blind. The inline `isVideoUri` uses the cross-platform `getFileName`, so the same fix also corrects the paste path (`onFilesPasted` at `ComposeView.kt:1378`). + +## Risk + +One file, no interface change. Image and non-media drops are bit-identical. Video extension list is now duplicated with `Videos.desktop.kt`; adding a new format means updating both — accepted as the cost of a single-file fix. iOS unaffected. Rollback: revert the commit. diff --git a/plans/2026-05-26-public-groups-via-relays-unified.md b/plans/2026-05-26-public-groups-via-relays-unified.md new file mode 100644 index 0000000000..91f7c3a6ce --- /dev/null +++ b/plans/2026-05-26-public-groups-via-relays-unified.md @@ -0,0 +1,227 @@ +# Plan: Public groups via relays (unified) + +Date: 2026-05-26 + +This plan is self-contained. It supersedes `2026-05-08-public-groups-via-relays.md` and folds in the privileged-roster mechanism understood since. Implementers should work from this document alone. File:line anchors are current as of this date — confirm before editing. + +## Overview + +Channels (shipped) are relay-mediated groups where the relay forwards content from owners only; subscribers are pinned to `GRObserver` and cannot post. Public groups are the second value of the same two-axis design: same wire, same transport, but every member can post, and there are moderators/admins who can act. + +| `useRelays` | `groupType` | Name | Posting | Notes | +|---|---|---|---|---| +| `false` | (none) | Secret group | all members | today's P2P full-mesh group | +| `true` | `GTChannel` | Channel | owners only | shipped; subscribers anonymous to each other | +| `true` | `GTGroup` | **Public group** | every member | **new**; member-to-member DMs deferred | +| `true` | `GTUnknown _` | (refuse) | — | newer-client link seen by older client → refuse to join | + +Three concepts, kept distinct: **transport** = `useRelays` (topology, batch, signatures, delivery); **governance** = `groupType` (who may post, member affordances); **joiner role** = the default role new joiners get, set by the owner on the signed profile. + +Two things make public groups work and neither exists today: + +1. **The joiner role must come from the owner-signed profile**, not a relay-side global config — otherwise relays disagree on the default role. (Section 2.) +2. **Members must learn who the moderators/admins are — their identity, signing key, and role — in a way a relay cannot forge.** Today a relay can fabricate a moderator (Section 1, Problem). This is the load-bearing piece and ships first. + +`GTGroup`, `PublicGroupProfile`, `useRelays'`, and the relay/signing/forwarding machinery already exist (anchors below). The work is additive. + +--- + +# Section 1 — Privileged roster (`XGrpRoster`) + +This is a **general relay-group mechanism**: public groups use it now; channels inherit it for their multi-owner/moderator future. It is the first, self-contained task. + +## 1.1 Problem and trust model + +Owners are trusted because their keys come from the **link**, never the relay: on join, `createLinkOwnerMember` (`Store/Groups.hs:3072`) writes each owner's `member_pub_key` from the link's `OwnerAuth` chain, validated against `publicGroupId == sha256(rootKey)`. `xGrpMemIntro` even nulls the key when `mRole == GROwner` (`Subscriber.hs:3029`): *"owner key must only come from link data, not from relay intro."* + +Non-owner privileged members have no such anchor. Today `xGrpMemIntro` **keeps** the relay-asserted key for `GRModerator`/`GRAdmin`, and `introduceInChannel` (`Internal.hs:1165`) introduces all of `getGroupModerators` (which returns mod+admin+owner, `Store/Groups.hs:1190`). So a malicious relay can assert "X is a moderator, here is X's key," and the subscriber will then trust the relay-chosen key to verify X's signed administrative actions (`XGrpMemDel`, `XGrpMemRestrict`, `XGrpMemRole`). The `when (memRole > GRMember)` gate in `xGrpMemNew` (`Subscriber.hs:2957`) blocks the *dissemination* path but not the *join-time intro* path — the protection is half-applied. Dormant for channels (single-owner broadcast), activated by public groups. + +**Conclusion:** a non-owner privileged member's `(memberId, name, key, role)` must be **owner-signed**, exactly like owners are link-signed. That is the roster. + +## 1.2 Wire event and signing + +New event `XGrpRoster` (add to `ChatMsgEvent`, `Protocol.hs:422`), JSON-encoded, carrying: + +- `version :: Word32` — monotonic, from 0. +- `roster :: [{ memberId, name, key, role }]` — the complete current privileged set, `role ∈ {GRModerator, GRAdmin}`. `name` is a display name only (to avoid ugly "unknown member" records, as `XGrpMsgForward` already carries one). Owners are **not** in the roster. + +Add `XGrpRoster_` to `requiresSignature` (`Protocol.hs:1231`) ⇒ `True`. This makes the owner sign it via the existing `groupMsgSigning` (`Internal.hs:1962`, binding `CBGroup <> (publicGroupId, ownerMemberId)`, key `groups.member_priv_key`) and makes recipients require a valid owner signature via `withVerifiedMsg` (`Subscriber.hs:3461`). No new crypto. + +**The handler MUST assert the resolved author is an owner** (`memberRole' author == GROwner`). `withVerifiedMsg` verifies the signature against the *author's* key, and the relay chooses `fwdSender` — so without this assertion a relay could route a roster as a member whose key it controls and the signature would verify. Owners exist on recipients only via the link `OwnerAuth` chain, so a relay can neither fabricate an owner nor sign as one. This assertion is the crux of the roster's integrity. + +## 1.3 Authoritative model — versioned snapshot, latest-wins, TOFU keys + +Each `XGrpRoster` is the complete current privileged set. Recipients treat the highest-version valid roster as authoritative for *who is privileged and their keys*; absence from the newest accepted roster means *not privileged* (reverts to the joiner default unless an accompanying `XGrpMemRole` sets a specific role — see 1.6). This is self-healing: a member who missed one change gets the full current state on the next roster. + +**Key handling is trust-on-first-use, pinned per `memberId`** (per entry): + +- `memberId` unknown, or known without a key → store the key (first sight, from the owner). Set name/role. +- `memberId` already has a key: + - same key → fine; update name/role. + - **different key → error.** Never overwrite; keep the old key; surface a suspicious-roster event. + +There is **no in-place key rotation**: a genuine re-key is modeled as a *new member* — the owner removes the old `memberId` (`XGrpMemDel` + roster drop) and adds a new `memberId` with the new key. Consistent with SimpleX's no-mutable-identity stance, and with the `xGrpMemNew` rule in 1.5. + +**Anti-replay / rollback.** The relay cannot forge a signed roster but can replay an older one. + +- The current roster `version` is anchored in the owner-controlled link mutable data (which already holds `OwnerAuth`, profile, subscriber count). The relay cannot forge it. A roster change that bumps the version also updates link data. **Status:** the write side is implemented; the join-time **read/detect** is deferred — comparing the anchor against the relay-served roster at join is racy (the forwarded roster may not have arrived yet → false positives), so correct staleness detection must be triggered by roster arrival, not at join. The residual new-joiner rollback gap below stands; the hard anti-replay (member + relay) is in place. +- **Existing members** reject any roster with `version` below the highest already accepted — full anti-replay for them. +- **New joiners** process the latest version the relay actually serves, even if it lags the link anchor, so honest relay propagation lag never blocks a join. The anchor is used for staleness *detection*, not a hard gate, in v1. +- **Documented residual gap:** a stale/malicious relay can serve an old-but-valid roster to a brand-new joiner. Documented in `channels-overview.md` with future mitigations: escalate verification to an owner, or have the client compare the relay's version to the link anchor and refuse/retry above a staleness threshold. + +## 1.4 Cap on the privileged set + +Bound the privileged set so the signed roster always fits one message — never paginate. A hard **cap on moderators + admins** (owners are on the link, not counted), enforced **at promotion time** on the owner: refuse to elevate beyond the cap with a clear error, so the roster is always constructible as one signed message. + +Derive the number from the single encoded-message budget (verify the exact constant — the encoded-message-length limit minus signature + JSON overhead) divided by worst-case entry size. With `{memberId, name, key, role}` entries this is comfortably in the tens-to-~100 range; pick the final value from the measured worst-case entry. + +## 1.5 Remove the dissemination gates; gate on the roster instead + +Because the relay forwards the roster on join **before anything else**, a privileged member's key/role is owner-established before any relay-asserted introduction arrives. So the relay may now disseminate privileged members' full profiles like any other member, and the gates come out: + +- **Remove** the `when (memRole > GRMember)` throw in `xGrpMemNew` (`Subscriber.hs:2957`). +- **Remove** the forward-side `memberRole' s <= GRMember` filter in `sendBodyToMembers` (confirm exact site in `Subscriber.hs`). +- **Replace** with a roster check in `xGrpMemNew`: for an announcement of a privileged role, require that a member record with that `memberId` already exists **with that privileged role** (roster-established). If found → accept the **profile** update only; **never overwrite `key`, `memberId`, or `role`** (roster-authoritative). If not found → reject (a relay conjuring a privileged member not in the roster). + +`introduceInChannel` forwards the cached roster to the new member first, then proceeds; it may still announce the newcomer to moderators and maintain the relations vector. The per-mod `XGrpMemIntro` carrying keys is no longer the trust path for privileged members. + +## 1.6 Delivery — what is sent, when, to whom + +Two orthogonal axes, and the roster owns only one: + +- **Axis A — privileged set + keys** (who is mod/admin, their key/name/role): owned by the roster. +- **Axis B — group-membership lifecycle** (removed / restricted / left): owned by `XGrpMemDel` / `XGrpMemRestrict` / `XGrpLeave`, unchanged, applies to everyone. + +Dispatch by whether an operation touches the {moderator, admin} set (the *roster roles*). Owner is not a roster role — promotion to/from owner uses `XGrpMemRole` (+ link `OwnerAuth`), never the roster. + +**`APIMembersRole` (role change, possibly batched):** + +- Emit `XGrpMemRole(M, target)` for each affected member exactly as today — this conveys the exact target role for any role, including owner and specific ≤member roles. +- **Additionally** build and **broadcast** the full signed roster (version++) **iff the {mod, admin} set changed** (any member entered, left, or moved within mod/admin). +- A mixed batch fires both. Example: target=member over `[moderator M, observer O]` → `XGrpMemRole` for both (exact roles) **and** a roster (M left the set). The promotion case `XGrpMemRole(M, mod)` + roster is mildly redundant on the role field and harmless; the key comes only from the roster. + +The broadcast reuses the existing owner-admin-event forwarding (`shouldForward = isUserGrpFwdRelay gInfo && not forwarded`, `Subscriber.hs:3191`). Privileged-set changes are rare administrative events, so this is on the order of an `XGrpInfo`/`XGrpPrefs` broadcast — not per-message. The broadcast roster is the **self-healing** mechanism: a member who missed a prior `XGrpMemRole` is corrected by the snapshot. + +**`XGrpMemDel` (removal):** broadcast `XGrpMemDel` as today (it neutralizes the member for existing members). If the removed member was privileged, the owner sends a refreshed roster (version++), which the relay broadcasts like any other version bump (see below). + +**Relay broadcast rule — always broadcast on a strict version bump.** A newer-version roster is applied, cached, and broadcast to current members, uniformly — promotion, key/role change, demotion, or privileged removal. We do **not** try to make removal cache-only: a demotion (member stays in the group) is indistinguishable from a deletion at the roster-diff level, so suppressing the broadcast would silently drop the self-healing the spec requires for role changes. The only cost is one redundant roster broadcast alongside `XGrpMemDel` on the rare deletion of a privileged member — and even there the broadcast is not waste, since it self-heals the privileged-set side if the `XGrpMemDel` was lost. (This supersedes an earlier "cache-only on deletion" idea, which could not be implemented without either a wire flag or a fragile demotion-vs-deletion diff.) + +**On relay add:** the owner sends the current roster to the new relay so it can serve joiners. + +**Joiners:** the relay forwards the cached roster at join (1.5). + +**Short offline gaps** are covered by ordinary queued delivery: the role-change roster broadcast sits in the member's SMP queue (FIFO) ahead of any later moderator events, so it is processed first on reconnect. + +**Quota-blocked catch-up.** A member offline long enough to fill its queue causes the relay to be quota-blocked — the broadcast may never have been enqueued, so naive queueing would leave the member without the current roster, rejecting moderator events indefinitely. Fix: when the queue drains, the relay **sends the current cached roster ahead of the resumed backlog**, so the member holds the current privileged set before processing the events it couldn't verify. + +The hook is confirmed: QCONT is delivered to the **sender** when the recipient drains (`simplexmq Agent.hs:3402`), and the relay receives it per subscriber in the group-member connection handler (`Subscriber.hs:1215` — `continueSending` + `sendPendingGroupMessages user gInfo m conn`, with `gInfo`/`m`/`conn` in scope; a relay→subscriber connection is a group-member connection). Implementation must ensure **roster-first ordering** relative to both the agent-level `continueSending` flush and the re-driven delivery tasks, and gate the extra send on a per-member "delivered roster version" so it fires only when the member is behind. + +The roster is **never** delivered through the profile-dissemination prepend. That path (`member_relations_vector` → `XGrpMemNew`) carries **profiles** only; with the gate removed (1.5), a privileged member's profile disseminates through it like any other member's, but only after the roster has established their key/role. Profile via prepend, key/role via roster — orthogonal, no double-prepend. + +## 1.7 Relay-side cache (the one new storage pattern) + +Relays already forward signed bytes verbatim (`encodeFwdElement` `Batch.hs:106`, `verifiedMsgParts` `Protocol.hs:1445`; `messages.msg_chat_binding` + `msg_signatures`; reconstructed in `toTask` `Delivery.hs:154`). What does **not** exist is "store the latest roster and re-emit to joiners" — `sendHistory` (`Internal.hs:1207`) reconstructs content and does *not* preserve signatures, so it is not a template. + +Add a small per-group cache holding the latest signed roster message bytes, plus the roster `version` as a **separate column** alongside them (so the relay compares versions without re-parsing the blob). On receiving `XGrpRoster` from an owner the relay: verifies the owner signature; **checks `version` strictly greater than the cached version** (lower → reject as rollback; equal → idempotent no-op); then updates its own member-role records, overwrites the cache + stored version, and (for a role-change-origin roster) creates a delivery task to all current members. On join, it forwards the cached bytes verbatim. + +The relay-side version check protects an **honest** relay's cache from being rolled back by a replayed signed roster — which in turn protects every joiner that relay serves. It does not constrain a **malicious** relay (it controls its own cache); that remains the documented new-joiner residual gap (1.3), bounded by the member-side check (1.3) and the link version anchor. + +## 1.8 Races and tests + +- **Promotion vs. action ordering.** A newly-promoted mod could act before its roster reaches a recipient ⇒ `RGEMsgBadSignature`. Covered for MVP by causal ordering (the roster is broadcast at promotion, before the mod learns of and acts on it), QCONT catch-up (item 7), and recipient tolerance (a rejected first action is re-sent; the next roster repairs trust). The fuller fix — the **roster-specific prepend** (prepend the cached signed roster ahead of a privileged member's forwarded action for recipients below the current version, reusing the item-7 delivered-version tracker, distinct from the `XGrpMemNew` profile prepend) — touches the hot per-recipient delivery loop that carries every forwarded message, so it is **deferred to a focused, separately-tested pass** rather than shipped untested. +- **Multi-owner roster signed by an unknown owner** (owner added after the recipient fetched the link): recipient cannot verify ⇒ buffer/refetch link. Cannot occur for single-owner MVP; flag for v7. +- **Roster vs. profile-update concurrency:** benign (different fields); verify the roster's relations-vector handling does not clobber the profile `MRIntroduced` semantics they share. + +Tests: relay-fabricated moderator key is rejected (forgery); promotion delivers a verifiable key; demotion via roster + `XGrpMemRole` reconciles; removed privileged member does not reappear for a new joiner; replayed older roster rejected by existing members; TOFU key-change rejected; batch `APIMembersRole` emits roster + `XGrpMemRole` correctly; self-healing after a dropped role event. + +## 1.9 Key anchors for Section 1 + +`ChatMsgEvent` `Protocol.hs:422`; `requiresSignature` `Protocol.hs:1231`; `groupMsgSigning` `Internal.hs:1962`; `withVerifiedMsg` `Subscriber.hs:3461`; `xGrpMemNew` `Subscriber.hs:2957`; `xGrpMemIntro` `Subscriber.hs:3015`; `introduceInChannel` `Internal.hs:1165`; `getGroupModerators` `Store/Groups.hs:1190`; `memberInfo` `Internal.hs:1187`; `createLinkOwnerMember` `Store/Groups.hs:3072`; `GroupKeys` `Types.hs:462`; `member_relations_vector` machinery in `Types/MemberRelations.hs` (`MemberRelation`, `MRIntroduced`, `IDSubjectIntroduced`, `setNewRelations`); forwarding `Batch.hs:106` / `Protocol.hs:1445` / `Delivery.hs:154`. New columns go in a **new tail migration** (`M20260222_chat_relays` is the *pattern* for relay group columns but is not the tail — never edit an applied migration; `M20260525_member_removed_at` is the current tail). + +--- + +# Section 2 — Joiner role on the signed profile + +Today the relay derives a joiner's role from `channelSubscriberRole` (`Controller.hs:161`, default `GRObserver` `Chat.hs:119`), a global config — so relays can disagree and the owner cannot set it per group. Move it onto the owner-signed profile. + +## 2.1 Types and helpers + +- Add `joinerRole :: Maybe GroupMemberRole` to `PublicGroupProfile` (`Types.hs:798`). No migration: JSON derives via `deriveJSON defaultJSON` with `omitNothingFields = True`, so `Nothing` is omitted on encode and a missing field decodes to `Nothing`. +- Add a `groupType` accessor and `isChannel` on `GroupInfo`/`GroupProfile`, and a resolver `joinerRoleFor :: GroupInfo -> GroupMemberRole` = `joinerRole` if set, else type-keyed default (`GTChannel → GRObserver`, `GTGroup → GRMember`, `GTUnknown _ → GRObserver`). Reuse the existing `publicGroupEditor`/`memberRole'` (`Types.hs:499`/`506`); do **not** introduce a profile-side `memberRole'` (name collision). + +## 2.2 Replace the global config + +Switch every `channelSubscriberRole` reader to `joinerRoleFor gInfo` and delete the config: `Controller.hs:161`, `Chat.hs:119`, `Commands.hs:2053`, `Commands.hs:2546`, `Subscriber.hs:3248`, `Subscriber.hs:4019`. Verify no out-of-tree consumer reads it. + +## 2.3 Command, preferences, defensive refusal + +- `APINewPublicGroup` (`Controller.hs:526`, handler `Commands.hs:2495`) gains `groupType` (default `GTChannel`) and optional `joinerRole` (default `joinerRoleFor` of the type); both written onto the constructed profile (today hardcodes `groupType = GTChannel` at `Commands.hs:2538`). +- Parameterize the channel-prefs parser by `GroupType`: Channel keeps its override (`support = OFF`); Public group and `GTUnknown` use secret-group defaults (member-to-moderator escalation is expected). Do not duplicate the parser — parameterize it. +- `directMessages` stays ON by inheritance but is dormant in any relay-mediated group; hide its toggle when `useRelays` and refuse `xGrpDirectInv` defensively when `useRelays'` (`Subscriber.hs:3321`, currently ungated): emit `messageError`, create no contact. + +## 2.4 Compatibility + +Existing channels: no `joinerRole` ⇒ falls back to `GRObserver` for `GTChannel`. No data migration. Older relays without this change resolve the joiner role from their global config — warn the owner at create time if a selected relay's chat version is below the public-groups version (soft warning, not a block). + +--- + +# Section 3 — Backend tests + +Public-group helpers paralleling the channel helpers, plus: + +1. Member posts; all members receive it (no "unknown member" lines). 2. Multi-author session: no "unknown member" anywhere. 3. Member edit/delete/react forwarded to all. 4. `xGrpDirectInv` refused under `useRelays` (no contact created); repeat for Channel. 5. Blocked member's messages not forwarded. 6. Multi-relay delivery with cross-relay dedup. 7. History on join. 8. `asGroup=true` from a non-owner rejected. 9. Receipts disabled above the member limit. 10. Older client refuses `groupType = "group"` (needs-newer-version). 11. Incognito member posting attributes the incognito profile. 12. `joinerRole` propagates and defaults correctly (Channel→observer, Public group→member; absent→type default). Plus the roster tests in 1.8. + +--- + +# Section 4 — Clients (iOS, then Kotlin) + +## 4.1 Audit `useRelays` vs `isChannel` (structural commit, on its own) + +~70–75 sites per platform branch on `useRelays` as a proxy for "is a channel." Split per a mechanical rule and land as a pure structural commit (no behavior change in the same diff): + +- **Transport** (keep `useRelays`): link/relay management, owner-can't-leave-own-relay-group, relay-status indicator, incognito flag, typing-state gating, member-DM-affordance suppression. +- **Governance** (switch to `isChannel`): titles, "subscribers" vs "members" framing, "Channel preferences" labels, channel-style vs group-style member display. + +## 4.2 Model and behavior + +- Model: add `group` arm to `GroupType` (with serializers); `joinerRole`, `groupType`, `isChannel` accessors. Authoritative role resolution stays in Haskell; clients use it for display. +- **Narrow the existing refusal:** PR #7009 (merged to `stable`) added `GLPUpdateRequired` for `groupType /= GTChannel` (`Controller.hs:1051`, `Commands.hs` `unsupportedGroupType`). Change it to refuse only `GTUnknown _`; `GTGroup` proceeds to a public-group join. +- Create flow: one view with a Channel / Public-group segmented control (default Channel) driving the title, link-step label, success screen, and two API params (`groupType`, `joinerRole` = observer for Channel, member for Public group — no role picker in MVP). Hide `directMessages` in create prefs when `useRelays`. Render the threat-model note below the title for Public groups (text in Section 5). +- Suppress the member-tap "send direct message" affordance in any relay-mediated group. +- Members view shows the relay-known roster; header "subscribers" (channel) vs "members" (public group). No filtered view in MVP. +- Strings/icons: ~5–10 `_public_group` string keys mirroring channel forms; reuse `group_members_*` for "members" framing; a distinct Public-group icon (pending design). Kotlin-only: chat-list filter chips place Public groups in the "groups" bucket. + +Platforms ship independently (API defaults are backward compatible). + +--- + +# Section 5 — Threat model, docs, release + +Fold into `channels-overview.md` (public groups inherit the entire channel threat model; deltas only): + +- **A relay can fabricate content as any member** (channels: only as owners). Content (`XMsgNew`/`Update`/`Del`/`React`) is unsigned by design for deniability (`requiresSignature` lists roster/admin events only); broader blast radius in public groups. Detectable via cross-relay consistency. Mitigation is the future opt-in content signing on the channel roadmap; the create-flow note states the trade-off ("a malicious relay could change or fabricate messages from any member — pick relays you trust, or use a secret group for peer-to-peer integrity"). +- **Roster rollback for new joiners** (1.3): documented bounded delta + future mitigations. +- Unchanged: relay cannot impersonate an owner or substitute the profile (signed events, validated entity ID); `joinerRole` and the privileged roster are owner-signed, so the relay cannot unilaterally change a joiner's default role or fabricate a moderator. + +Document `XGrpRoster` (event, signing, versioning, TOFU, delivery) in `channels-protocol.md`. Bump the chat protocol version (the public-groups version that gates `GTGroup` and `joinerRole`). Release notes include the relay-fabrication line. + +--- + +# Sequencing + +1. **Section 1 — privileged roster** (backend). The core; gates the rest of the value. Land the gate-removal/roster-check and the event together so no half-applied trust window exists. +2. **Section 2 — joiner role on profile** (backend). Independent of Section 1. +3. **Section 3 — backend tests** (alongside 1–2). +4. **Section 4 — clients**: audit (structural) first, then iOS, then Kotlin. +5. **Section 5 — docs/version/release** with the backend release. + +Backend (1–3) gates the clients. iOS and Kotlin are independent of each other. + +--- + +# Out of scope (deferred) + +- **Member-to-member DMs in relay-mediated groups.** Prohibited here (client affordance suppressed, receive-path refusal, relay does not forward `XGrpDirectInv` — so no relay-visible DM graph). A future plan must re-derive the threat model: relay-forwarded DMs would expose (sender, target, time) metadata; relay-blind rendezvous via per-member queues is the privacy-preserving alternative. +- **`memberAdmission` on relay-mediated join** (hardcoded `GAAccepted` bypasses review/captcha — generic relay-groups gap). +- **Roster filter/pagination in the members view** for very large groups. +- **Multi-owner** roster signing/verification and owner promotion via link `OwnerAuth` (v7); **opt-in content signing** (v7 roadmap); **full anti-rollback for new joiners** (link-version hard gate). diff --git a/plans/2026-06-01-roster-members-multipart.md b/plans/2026-06-01-roster-members-multipart.md new file mode 100644 index 0000000000..aca98c4698 --- /dev/null +++ b/plans/2026-06-01-roster-members-multipart.md @@ -0,0 +1,220 @@ +# Roster: regular members + larger rosters via inline file + +Date: 2026-06-01 (revised). Extends Section 1 of `2026-05-26-public-groups-via-relays-unified.md`. + +> Anchors below were re-verified against `f/public-groups-members-in-roster` **after** PR #7036 (`core: signed XMember in public group`, commit `0773ccd05`) merged in. Most line numbers shifted; the header-fits check uses `maxEncodedMsgLength = 15602` (now `Protocol.hs:905`). Confirm before editing. + +## Reconciliation with PR #7036 (merged into this branch) + +PR #7036 landed things this plan predates. Read this section first — it changes the relay flow the plan builds on. + +**Renames (the plan's old names no longer exist — grep will miss them):** + +| Was | Now | Location | +|---|---|---| +| `forwardCachedRoster` | `forwardGroupRoster` | `Internal.hs:1172` | +| `setCachedGroupRoster` | `setGroupRoster` | `Store/Groups.hs:1415` | +| `getCachedGroupRoster` | `getGroupRoster` | `Store/Groups.hs:1428` | +| `setRelayLinkAccepted` | `setRelayKey` (no longer sets relay status) | `Store/Groups.hs:1543` | + +"Cached roster" is now "saved roster" throughout; the `roster_msg_*` columns and the `roster_blob` this plan adds are unchanged in intent. + +**Roster version baseline is now `Just 0`, not NULL, for relay groups.** `createNewGroup` initializes `roster_version = Just (VersionRoster 0)` for `useRelays` groups (`Store/Groups.hs:365, 427`), and an old channel materializes `0` the first time a relay connects (`Subscriber.hs:905-910`). Consequences: the first promotion bumps `0 -> 1` (not NULL -> 0); the owner and already-onboarded members/relays compare against a real `0`, **but a relay's own `roster_version` is still NULL the first time it receives v0** — applying v0 from NULL is exactly what lets it ack and become publishable (verified: today's `maybe False (newVer <=) (rosterVersion gInfo)` at `Subscriber.hs:3207` applies v0 only because the relay is at `Nothing`; `Just 0` would reject it and the relay would hang `RSInvited`). So the multipart version guards MUST be `Maybe`-comparisons that treat `Nothing` as below `0` (spelled out under *Header handler* and *Completion*); and the **empty roster (v0)** must round-trip through the header+blob path (a 2-byte blob: `Word16` count `0`, one chunk, `chunkSize >= fileSize` -> `RcvChunkFinal` on chunk 1). The empty/small blob is the *common* case for relay onboarding, not an edge case. + +**NEW relay roster-ack handshake (`XGrpRosterAck`) — this plan MUST integrate it.** PR #7036 added `XGrpRosterAck :: VersionRoster -> Maybe Text` (`Protocol.hs:499`; tag `x.grp.roster.ack`; NOT in `requiresSignature` — it rides the relay's authenticated connection). Flow: + +- On relay connect (`GCInviteeMember` + `isRelay`) the owner **always** sends the current roster via `sendGroupRosterToRelay`, and the relay stays `RSInvited` (**unpublishable**) until it acks (`Subscriber.hs:900-910`). +- The relay applies the roster in `relayApplyRoster` and, **only while its own status is `RSAccepted`**, sends `XGrpRosterAck author newVer Nothing` (or an error string) — `Subscriber.hs:3210-3221`, `sendRosterAck` at `3276`. +- The owner's `xGrpRosterAck` handler (`Subscriber.hs:3279-3297`) transitions the relay `RSInvited -> RSAccepted` (and publishes via `setGroupLinkDataAsync`) on a version-matching success ack, or `RSInvited -> RSRejected` on error. + +Impact on the multipart design (a REQUIRED change, not just a rename): under this plan the header only *starts* a transfer, so on a relay the **apply, `setGroupRoster`, the ack, AND the broadcast all move to blob completion**, not header receipt. The relay becoming publishable now **gates on the blob transfer completing**: header -> chunks -> verify digest -> `processRoster` + `setGroupRoster` -> (`relayOwnStatus == RSAccepted`) `sendRosterAck` -> broadcast. This is the desired fail-safe (an unpublishable relay can't serve a half-applied roster), but it puts owner->relay blob delivery on the relay-onboarding critical path, not just self-healing. The error branch MUST still ack-with-error (digest mismatch / parse failure -> `sendRosterAck author newVer (Just "...")`) so the owner marks the relay `RSRejected` instead of leaving it hung at `RSInvited`. The current `relayApplyRoster` to fork is `tryAllErrors (setRoster sm)`, where `setRoster` = `processRoster` + `setGroupRoster` (`Subscriber.hs:3205-3228`); the multipart version splits this across header (write `roster_pending_*`) and completion (run `setRoster` + ack + broadcast). + +## Goal + +Let owners promote channel subscribers to **regular members** who can post, and carry more named members than fit one message. + +The JSON roster already exists (event, signing, relay cache, TOFU apply, broadcast, join forward, QCONT). This plan **widens the roster set to include plain members and changes the delivery** to a binary blob over the inline file transfer; the apply logic (`processRoster`) is reused. + +The member list moves out of the `XGrpRoster` message into a binary blob sent over the existing inline file transfer. `XGrpRoster` becomes a small signed header (version + the blob's size and digest). + +## Roster set: the promoted set {member, mod, admin} + +Owners stay on the link, never in the roster. Two edits, then every gate follows. + +**1. Widen `isRosterRole`** (`Internal.hs:1237`) to `{GRMember, GRModerator, GRAdmin}`. Every call site wants the promoted set, so this single edit covers: + +- `validateGroupRoster` filter (`Internal.hs:1243`) — fixes the bug where member entries are dropped. +- `buildGroupRoster` filter (`Internal.hs:1255`). +- promotion gates / cap / trigger / counts (`Commands.hs:2737, 2739, 2746, 2762, 2763, 2768`); update the cap error text at `2740`. +- owner-remove roster refresh (`Commands.hs:2888`, guarded by `anyPrivilegedRemoved` computed from `isRosterRole` at `2899`) — so removing a plain member, not just a mod/admin, refreshes the roster. +- receive gates: `xGrpMemNew` (`Subscriber.hs:3011/3026/3045`) and `xGrpMemRole` owner-only (`3185`). +- **join key-proof gate (NEW in PR #7036, `Subscriber.hs:1620`, `memberJoinRequestViaRelay`)**: a join claiming a `memberId` already roster-established as `isRosterRole` must prove possession of the pinned key (signature + `memberPubKey` match + `viaRelay == this relay's memberId`). Widening to members **extends this proof to promoted members** — a promoted member re-connecting through a relay must sign its `XMember` with the roster-pinned key. This is correct and desirable (it is the receive-side counterpart of the promote-time key invariant in *Known limitations*), and it composes with PR #7036's `acceptGroupJoinRequestAsync existingMem_` path that attaches the connection to the existing roster record. Confirm the promoted member signs its join `XMember` (it does — `encodeXMemberConnInfo`, `Internal.hs`). + +**2. Split the role query.** `getGroupRosterMembers` (`Store/Groups.hs:1215`) currently serves two now-diverging needs: + +- **Build / revert** wants the promoted set. Redefine `getGroupRosterMembers` to `member_role IN (GRMember, GRModerator, GRAdmin)` (current members). Callers: `bumpAndBroadcastRoster` (`Internal.hs:2175`), `sendGroupRosterToRelay` (`2188`), and the `processRoster` revert set `currentPriv` (`Subscriber.hs:3245`). Build and revert MUST be the same query, or a dropped member is never reverted. +- **`introduceInChannel`** (`Internal.hs:1188`) wants only the moderation set (mod+admin). Widening it would announce every joiner to every member and introduce every member to every joiner (traffic + anonymity blowup). Reuse the existing `getGroupModerators` (`Store/Groups.hs:1204-1209`, returns mod+admin+owner) rather than adding a function: keep `getGroupOwners` for the owner-first intro, and take mod+admin as `getGroupModerators` minus owners. **Re-apply `filter memberCurrent`** — `getGroupModerators` does NOT filter current members (unlike the old `getGroupRosterMembers`), so without it a removed or left moderator would be introduced to joiners. Members are learned from the roster blob, not introductions. + +**Owner-only (confirmed decision).** Only the owner changes any roster role. The alternatives were considered and rejected for v1 — letting a mod/admin set member roles would need either the owner co-signing rosters from a mod/admin (owner round-trip + load) or a separate roster-signing key trusted from mod/admin (broader trust surface) — so owner-only keeps the single owner-key trust anchor. + +**Leave and owner-remove differ.** This plan **removes** the `xGrpLeave` roster-bump block (`Subscriber.hs:3439`): since `isRosterRole` is widened, it would otherwise fire `bumpAndBroadcastRoster` on every plain-member leave. So a member **leave** does NOT bump the roster — the leave is the membership axis (`XGrpLeave` neutralizes the member on the relay). An owner **remove** (`APIRemoveMembers`) DOES still bump via `bumpAndBroadcastRoster` (`Commands.hs:2888`, widened to cover plain members). `bumpAndBroadcastRoster` thus stays only for promotion (`APIMembersRole`) and owner-remove. + +## Wire: signed header + unsigned blob + +**Authoritative metadata is in the signed header.** `version`, blob `fileSize`, and `fileDigest` all live in the owner-signed `XGrpRoster`; the unsigned `BFileChunk`s carry no authoritative metadata. (This is why "total parts in the unsigned part", an earlier review question, is a non-issue here.) + +- **Header**: `XGrpRoster { version :: VersionRoster, fileInv :: InlineFileInvitation }`, JSON, signed, forwarded. `InlineFileInvitation { fileSize, fileDigest :: FD.FileDigest }` is a lean `FileInvitation` (no name/connReq/inline/descr; always inline). Tiny; fits `maxEncodedMsgLength` (15602, `Protocol.hs:905`). +- **Blob**: the binary member list. `RosterMember { memberId, key, role, privileges :: Word16 }` — drop `name`, add `privileges` (reserved: always `0`, parsed and ignored in v1). ~60 B/entry. Members get a placeholder name from `nameFromMemberId`; real profiles arrive on first post. +- **Serializer/parser**: a binary codec for the blob (a `Word16`-count-prefixed `[RosterMember]`); `RosterMember` becomes binary-only. Full code in *Blob format* below. Owner serializes → digest → chunks; receiver concatenates chunk bytes → verifies the digest → parses. +- **Cap** `maxGroupRosterSize` → **256** (tunable). Enforce at promotion over the promoted set (`Commands.hs:2739`, via the widened predicate); the receive-side entry-count bound is the parser alone (`rosterBlobP`'s `n > maxGroupRosterSize`); reject a signed `fileSize > cap × max-entry-size` before creating a file. Roster files are exempt from the inline `offer/receiveChunks` ceiling (at 256 ≈ 15 KB the blob is about one `fileChunkSize` chunk; the multipart path handles two if role words push it over). + +**Type changes.** + +- `GroupRoster` (`Protocol.hs:372-376`): `{version, roster :: [RosterMember]}` → `{version, fileInv :: InlineFileInvitation}`. It stays JSON (the signed header), so `InlineFileInvitation` needs a JSON instance; update its stale doc comment ("Owner-signed snapshot of the privileged (moderator/admin) set"). +- `RosterMember` (`Protocol.hs:378`): drop `name`, add `privileges :: Word16`; remove `deriveJSON` (`Protocol.hs:812`) — binary-only now — and add the `Encoding` below. `buildGroupRoster`'s constructor (`Internal.hs:1255`, currently `name = memberShortenedName m`) drops the `name` field; the consumer side already maps to `nameFromMemberId`. +- `validateGroupRoster` (`Internal.hs:1241-1242`): was `GroupRoster -> GroupRoster` over `.roster`; now `[RosterMember] -> [RosterMember]`, run on the parsed blob. + +### Blob format (serializer / parser) + +`RosterMember` is **binary-only** (carried in the blob, never in a JSON message) and gets the `Encoding` below. `MemberKey` (`Types.hs:972`, only `StrEncoding`) and `GroupMemberRole` (`Types/Shared.hs:33`, only `TextEncoding`) lack a binary `Encoding`: `MemberKey` delegates to the underlying `PublicKey` (`Crypto.hs:568`), and the role delegates to its canonical `TextEncoding` (the same `"member"/"moderator"/"admin"` form JSON and the DB use — single source of truth; `GRUnknown` round-trips). + +```haskell +-- MemberKey gains a binary Encoding (it only had StrEncoding); delegate to the Ed25519 key. +instance Encoding MemberKey where + smpEncode (MemberKey k) = smpEncode k + smpP = MemberKey <$> smpP + +-- General instance (belongs beside GroupMemberRole's TextEncoding in Types/Shared.hs, not here). +instance Encoding GroupMemberRole where + smpEncode = smpEncode . textEncode + smpP = maybe (fail "bad GroupMemberRole") pure . textDecode =<< smpP + +-- Tuple encoding (Encoding (a,b,c,d), Encoding.hs:192), as GrpMsgForward / FwdSender do. +instance Encoding RosterMember where + smpEncode RosterMember {memberId, key, role, privileges} = smpEncode (memberId, key, role, privileges) + smpP = RosterMember <$> smpP <*> smpP <*> smpP <*> smpP + +-- Blob = Word16 count (NOT smpEncodeList: its 1-byte count overflows at the 256 cap) followed +-- by that many entries. This is the byte sequence the digest is computed over and verified +-- against before parsing. +encodeRosterBlob :: [RosterMember] -> ByteString +encodeRosterBlob ms = smpEncode (fromIntegral (length ms) :: Word16) <> B.concat (map smpEncode ms) + +rosterBlobP :: Parser [RosterMember] +rosterBlobP = do + n <- fromIntegral <$> smpP @Word16 + when (n > maxGroupRosterSize) $ fail "roster: too many entries" + A.count n smpP +``` + +- **Owner**: `encodeRosterBlob` over the promoted set → `FileDigest` (SHA-512, as the file machinery computes it, `LC.sha512Hash`) → chunk; the digest goes in the signed `XGrpRoster` header. +- **Receiver**: concatenate chunk bytes → verify the digest (S1, over plaintext) → `parseAll rosterBlobP` (consume all input; reject trailing bytes). Parsing runs only after the digest matches, so the bytes are owner-attested; the `n > maxGroupRosterSize` guard and `parseAll` are defensive against a buggy/garbled blob. +- **Per-entry layout**: `memberId` (1-byte len + id) + `key` (1-byte len + Ed25519 pubkey) + role (1-byte len + role word, e.g. `member` = 7 B) + `privileges` (2 bytes) ≈ ~60 B/entry. The file-transferred blob has no tight size budget, so canonical text is fine. +- `privileges` is reserved: serialized as `0`, parsed and ignored in v1. + +## Delivery: send → header → chunks → completion + +### Owner send + +`bumpAndBroadcastRoster` and `sendGroupRosterToRelay` build the blob (`buildGroupRoster` over the widened query), compute its `FileDigest`, send the `XGrpRoster` header, then send the blob as `BFileChunk`s against that message's `shared_msg_id`. + +`sendFileInline_` reads from a file, so add a send-from-bytes variant (shared with the relay re-serve). The owner's own version bump stays as today (in `bumpAndBroadcastRoster`, `Internal.hs:2176`) — the owner is the source of truth; "bump only at completion" is a receive-side rule. + +### Header handler (`xGrpRoster`, member and relay) + +The header no longer applies anything — it starts a transfer. It only writes `roster_pending_*`; it never writes `roster_version` or the live `roster_msg_*`. + +- **Short-circuit** unless `version > max(roster_version, roster_pending_version)` — strictly greater than both applied and pending — before creating a file. These are **`Maybe` comparisons: `Nothing` (un-materialized version) counts as below `0`** (mirror today's `maybe False (newVer <=) …`, `Subscriber.hs:3207`), so a relay's first receipt at NULL **applies** v0 while a re-receive at `Just 0` short-circuits — the v0 onboarding depends on this. + - Why both: the QCONT re-serve is unconditional, so the relay may re-forward a still-cached v5 while a member is mid-receiving v6 (applied 4). Compared only to applied, v5 > 4 would supersede v6, then the arriving v6 chunks fail the v5 digest → stuck. + - Why never bump here: a header-time bump makes the genuine blob complete as an equal-version no-op, leaving the receiver at `vN` with `v(N-1)` data. +- **Create the rcv-file** with `cryptoArgs = Nothing` (see Security), `file_type = roster`, `chat_item_id` NULL, `shared_msg_id` = the header's id. Accept it via `startRcvInlineFT` (chat-item-free), not `acceptRcvInlineFT`, so chunk 1 isn't rejected on `RFSNew`. +- **One in-flight per group is automatic**: the single `groups` row makes `roster_pending_*` single-valued, and there is one `(group_id, file_type = roster)` file. A duplicate header is idempotent. A version greater than both applied and pending supersedes: `UPDATE roster_pending_*` and delete the existing roster file (cleanup below), then create the new. + +### Chunks + +The header is enqueued before chunk 1 (per-connection FIFO). + +**Reset-on-chunk-1** (decision 4): if chunk 1 arrives with partial chunks, discard and restart so relay restart / re-subscribe / QCONT can re-drive from the start. Discarding MUST (GAP 3): + +- delete the `rcv_file_chunks` rows, +- truncate/remove the on-disk file, and +- evict its handle from the `rcvFiles` map (`closeFileHandle`). + +`appendFileChunk` opens in AppendMode and caches the handle (`Internal.hs:1781`, handle at `1794`), so clearing only the rows would append after the stale bytes and corrupt the blob (digest fails — the stuck state decision 4 avoids). + +**Orphaned chunk**: a roster `BFileChunk` matching no in-flight `(group_id, shared_msg_id, file_type = roster)` file is **ACKed and ignored**, never errored (the version is already applied or superseded). This is how an up-to-date member tolerates the unconditional re-serve: the re-served header short-circuits (no file), then its chunks arrive with no transfer in flight. Distinct from reset-on-chunk-1, which fires only when partial chunks exist. + +### Completion (on `RcvChunkFinal`) + +1. Verify the assembled file's digest against `roster_pending_digest`. On mismatch, discard (delete the file, clear `roster_pending_*`); do not apply or bump. +2. **Version guard**: apply only if `roster_pending_version > roster_version` (same `Maybe` semantics — a `Nothing` applied version counts as below `0`, so a first v0 completion from NULL applies). A stale/out-of-order completion is rejected, not applied as a downgrade. +3. Parse → `validateGroupRoster` → `processRoster` (TOFU keys, role updates, revert absent promoted members, role-change items; pass `nameFromMemberId` where it used the entry name). + +In **one transaction**: `processRoster` → set `roster_version = roster_pending_version` → set `roster_blob` → clear `roster_pending_*` → delete the file. A **relay** also promotes the pending signed-header columns into the live `roster_msg_*` (this is what `setGroupRoster` writes today at header time — it moves here) and applies to its own records, then **sends the roster ack and broadcasts** (below). So a joiner never sees a live header at `vN` paired with a blob at `vN-1`. + +**Relay ack at completion (PR #7036 integration).** The relay's `XGrpRosterAck` (previously sent in `relayApplyRoster` at header receipt) moves to completion, gated exactly as today on `relayOwnStatus gInfo == Just RSAccepted`: on a successful completion send `sendRosterAck author roster_pending_version Nothing`; on digest-mismatch or parse failure send `sendRosterAck author roster_pending_version (Just "...")` so the owner marks the relay `RSRejected` rather than leaving it `RSInvited` forever. A relay therefore becomes publishable only after the full blob arrives and applies — the desired fail-safe, but it makes owner→relay blob delivery part of the relay-onboarding path, so it MUST be reliably driven (see *Owner send* and *Relay re-serve*; for a freshly-connecting relay the owner drives it via `sendGroupRosterToRelay`, including the empty v0 blob). The `relayOwnStatus == Just RSAccepted` gate is ported unchanged but now evaluated at completion rather than header receipt — confirm `relayOwnStatus` cannot change across the header→completion window (it shouldn't: a relay can't reach `RSActive` before acking, since the ack is what publishes it). + +The version guard plus the per-version `shared_msg_id` keying are what make the design correct; the short-circuit and one-in-flight-per-group are optimizations. + +### Relay re-serve (broadcast / join / QCONT) + +Per recipient, forward the signed header (as `forwardGroupRoster` does today, `Internal.hs:1172`) AND re-send the blob as `BFileChunk`s from `groups.roster_blob` (the send-from-bytes variant). An incoming `BFileChunk` returns no delivery task (`Subscriber.hs:1089`), so the blob send is driven here. + +**No per-member version gate in v1 (GAP 2).** QCONT/SENT re-forwards the saved roster unconditionally today (`Subscriber.hs:1143`, `1237`), and no per-member delivered-version tracker exists in the tree — this plan adds none. So a re-serve re-sends the whole blob on every drain; at cap 256 that is ~15 KB — one (occasionally two) `BFileChunk`s per drain — acceptable. + +It is safe because: an up-to-date member short-circuits the header and ACK-ignores the orphaned chunks; a stale (≤ pending) re-forward mid-transfer is a no-op via the short-circuit; and the completion version guard rejects any stale completion. + +If the cap is later raised so the blob spans many chunks, add a per-member `delivered_roster_version` column (read on QCONT/join/broadcast, written on confirmed delivery) and re-serve only when behind — future work. + +### Supersede / cancel cleanup + +Cleanup spans ALL of these — miss none: + +- `files`, `rcv_files`, `rcv_file_chunks`, +- the on-disk file and its `rcvFiles` handle (`closeFileHandle`), +- the `roster_pending_*` columns on `groups` (set NULL). + +## File-machinery changes (only these) + +- **Lookup**: add `files.shared_msg_id`; resolve roster chunks by `(group_id, shared_msg_id, file_type = roster)`. Leave `getGroupFileIdBySharedMsgId` (`Store/Files.hs:310`, chat-item JOIN) for normal files; branch on `file_type` / `chat_item_id IS NULL`. +- **Fork the three receive sites that call `getChatItemByFileId`** (they throw with no chat item): + - `startReceivingFile` (`Internal.hs:827`, reached on chunk 1) — skip the chat item + `CEvtRcvFileStart`. + - `receiveFileChunk` `RcvChunkFinal` (`Subscriber.hs:1329`) — replace with the completion path above. + - `FileChunkCancel` (`Subscriber.hs:1313`) — delete file + drop in-flight state, no chat item. +- **Cleanup keyed on `group_id`** (not chat items): `getGroupFileInfo` INNER-JOINs `chat_items`, so group delete (`Commands.hs:1270`) and clear (`1305`) skip roster files; the DB row cascades on group delete but the on-disk file leaks. Add a roster-file cleanup for delete/clear, cancel, and supersede. + +## Storage / migration + +In-flight state lives on `groups` (mirroring the live saved roster) and `files` (located by `shared_msg_id`) — no join table. These columns go into the in-progress **`M20260602_group_roster`** migration (already part of this work, not yet merged — so it's editable, not an applied migration), SQLite + Postgres; tests regenerate the schema files. + +| `groups` column(s) | Holds | Lifecycle | +|---|---|---| +| `roster_version` *(kept)* | applied version | bumped at completion | +| `roster_msg_*` *(kept)* | live signed header (was full JSON) | relay forwards verbatim; promoted from pending at completion | +| `roster_blob` *(new)* | durable completed blob | written at completion; relay re-serves it | +| `roster_pending_version`, `roster_pending_digest` *(new)* | in-flight version + digest | set on header receipt; cleared at completion | +| `roster_pending_msg_*` *(new, relay-only)* | in-flight signed header | set on header receipt; promoted to live at completion (NULL on members) | + +The kept `roster_msg_*` columns stay the relay's verbatim-forward source and trust anchor: `forwardGroupRoster` re-forwards them so the joiner verifies the owner signature, and the digest inside authenticates the unsigned blob. + +`files` adds `shared_msg_id` and `file_type`. The in-flight transfer is the `files` / `rcv_files` / `rcv_file_chunks` rows with `(group_id, file_type = roster)`. + +## Security + +- **Owner-signed header**: assert `memberRole' author == GROwner` (`Subscriber.hs:3198`); keep `XGrpRoster_` in `requiresSignature`. +- **Integrity is entirely the digest** (S1): verify the assembled **plaintext** blob against the owner-signed `fileDigest` at completion. Hence `cryptoArgs = Nothing` — a set cryptoArgs makes `appendFileChunk` re-encrypt the file in place (`Internal.hs:1801`), so the on-disk bytes would be ciphertext and the check would fail. A corrupted chunk fails the digest and the roster is rejected. +- **TOFU** key pinning per `memberId` unchanged (different key for a known id → keep the trusted key). +- **Rollback (S2)**: the signature binds `publicGroupId + version` and the digest binds the blob to that header, so cross-group/version substitution stays blocked. But the blob now carries plain members, so a same-group replay of an old `(header, blob)` to a **new joiner** can re-introduce a removed poster or mask a demotion (existing members are protected by the version check). Update `channels-overview.md`. + +## Known limitations / out of scope + +- A malicious relay can withhold/corrupt chunks → the member stays on its last-applied roster (it can drop any message anyway); new-joiner rollback now covers plain members. +- A just-promoted member's first posts may show "unknown member" until the file arrives — self-healing. +- A member who **leaves** lingers in the roster blob until the next bump (this plan drops the leave-triggered refresh). Harmless: they have no relay connection and cannot post, so a new joiner sees only a ghost row; the owner's explicit remove (`APIRemoveMembers`) drops them. +- Out of scope: granting/enforcing `privileges`; member content signing; joiner-role-on-profile; clients. Do not couple the roster set to the joiner-role mechanism (decision 2) — it is the absolute `{member, mod, admin}`. + +## Tests (`tests/ChatTests/Groups.hs`) + +The roster tests now live under the `describe "promoted members roster"` block (PR #7036 moved them and added `testChannelAddRelayWithRoster`, which onboards a 2nd relay through the roster-ack handshake). Update those to header+file delivery — `testChannelAddRelayWithRoster` in particular now exercises the v0/empty-roster blob transfer feeding the relay ack, so it must drive the header+chunk(s) to completion before the relay acks. + +Then add: digest-mismatch blob rejected (no apply, no version bump) **and the relay acks-with-error → owner marks it `RSRejected`** (PR #7036 path); a relay does **not** ack / become publishable until the blob completes (ack moved to completion); member promotion enters the broadcast roster and can post; reset-on-chunk-1 recovery; superseding version cleans up the in-flight older file; version not bumped on header receipt or on a failed blob; `introduceInChannel` still mod+admin only (no member introductions); on-disk roster file cleaned on group delete/clear mid-transfer; non-owner promotion refused; a promoted member re-connecting through a relay is accepted only with a valid signed `XMember` over the roster-pinned key (the widened `memberJoinRequestViaRelay` gate). Existing mod/admin tests must still pass. diff --git a/plans/2026-06-04-channel-message-signing.md b/plans/2026-06-04-channel-message-signing.md new file mode 100644 index 0000000000..be7cc53a92 --- /dev/null +++ b/plans/2026-06-04-channel-message-signing.md @@ -0,0 +1,316 @@ +# Plan: optional signing of channel content messages (`XMsgNew` / `XMsgUpdate`) + +Anchored on branch `f/msg-signing` (master merged in); all symbols re-verified 2026-06-25 — see **§Verification status** at the end for the current line map and the few mislabeled anchors. Re-confirm by symbol before editing; line numbers are advisory. The public-groups roster already provides everything this builds on: `GroupKeys {publicGroupId, memberPrivKey}` (`Types.hs:465`), `verifyGroupSig` (`Subscriber.hs:116`), per-member public-key distribution via the signed roster, and optional signing of group-state events (`requiresSignature`, `Protocol.hs:1334`). Content messages are *not* signed today. + +**PR 1** (this plan): a member can optionally sign their own channel posts and edits so recipients holding the signed roster can verify authorship + integrity; and — once an item is held signed — its edits and deletes are **enforced** to be signed (an unsigned mutation of a signed item is rejected at receive), closing the edit/delete-downgrade spoof at the source. The remaining hard part, history signature preservation (so catch-up members also hold posts signed), is **PR 2** (at the end). + +## Goal / user problem + +In relay channels, content (`XMsgNew`) is forwarded by relays and is not signed — only group-state events are. A relay can therefore forge or alter content attributed to a member. This feature lets a member optionally attach their member signature; recipients with the roster verify it. + +## Decisions + +- UI: **per-send long-press override only** — no device-stored default preference. Signing is opt-in for each send. +- Default off, with an in-UI explanation of the tradeoff (a signature is transferable, non-repudiable proof of authorship). +- Recipient indicator in scope (iOS + Kotlin), **chat view only** — not the conversation list. Glyph: `checkmark.seal`. +- Scope: `XMsgNew` + `XMsgUpdate` + `XMsgDel`, **including as-channel posts** (see §5 — signing an as-channel post is verifiable and de-anonymizing, a deliberate team-accepted tradeoff). Edits sign iff the original was signed; deletes sign per-item (self-delete iff the target was signed; moderation/admin delete always-signs — §7). Mutations of a held-signed item are enforced to be signed (§7). Reactions stay unsigned. + +## Threat model + +Actors: the sending member, recipients, and untrusted **chat relays** that forward content + roster. + +- **Forgery of member content** — closed for signed messages: the relay lacks the Ed25519 key, and the signature binds `(publicGroupId, memberId, body)`, so no forgery, cross-bind, or alteration. +- **Downgrade / stripping** (residual, narrowed) — optional signing lets a relay strip the signature from an *original* post and deliver it unsigned (the recipient never holds it signed, so nothing is enforced). Absence of a badge is *not* proof of forgery; only the presence of a verified badge is a guarantee. Once a recipient holds an item signed, its edits/deletes are enforced (next bullet), so the residual is now only the original-delivery case. A future "required signing" group setting would close even that (out of scope). +- **Signed-mutation enforcement** (fail-closed) — an `XMsgUpdate`/`XMsgDel` targeting an item the recipient holds signed (`msgSigned = Just _`) MUST itself carry a verifying signature; an unsigned mutation is rejected (drop + `RGEMsgBadSignature`). Closes the edit/delete-downgrade spoof (a relay forging an unsigned edit/delete to overwrite or censor a signed post) at the source. The legitimate sender produces the required signature: edits sign iff the original was signed; self-deletes iff the target was signed; moderation deletes always sign (§7). Coverage is for items the recipient already holds signed — catch-up members holding the post unsigned are outside it until PR 2 history preservation. +- **As-channel posts: anonymity for unsigned, accepted de-anonymization for signed.** An owner can "publish as the channel"; Design Objective 6 (`docs/protocol/channels-overview.md:214`) hides *which* owner authored a post from subscribers, and owners are "cryptographically indistinguishable to subscribers" (`:159`). This anonymity holds for **unsigned** as-channel posts: they forward via `FwdChannel` (no `memberId`), and a relay revealing the owner is only a deniable, detectable leak (`:237`). **Signing** an as-channel post is opt-in and deliberately gives it up: to be verifiable it forwards via `FwdMember` (§5), so every subscriber's device receives, verifies, and holds non-repudiable proof of the authoring owner. The owner is trading anonymity + deniability (`:198`, `:221`, `:103`) for verifiability on that post; the UI must say so. Verifiable-*and*-anonymous (ring signature / channel-level key) is out of scope. +- **Non-repudiation** (tradeoff, by design) — a verified signature is transferable proof of authorship; hence opt-in/off. +- **What "verified" proves** — the signed input is `encodeChatBinding CBGroup (publicGroupId, memberId) <> msgBody`, and `msgBody` embeds `sharedMsgId`, `MsgScope`, `asGroup`, content. It proves authorship + integrity + group/member/scope/message binding, and nothing else — not `fwdBrokerTs` (relay-controlled), ordering, or completeness. Surface in help. +- **Bad signature is fail-closed** — a signature that fails to verify drops the message and creates an `RGEMsgBadSignature` item (`Subscriber.hs:3828`). Member keys do **not** rotate (communicated once on join, no rotation planned), so a have-the-key-but-mismatch can only mean forgery, tampering, or corruption — a genuine signal, and dropping is correct. New consequence for content (higher-volume, user-visible): such a message is **dropped**, not shown unsigned, exactly as state events already behave. The lagging-roster case is *not* a drop — if the recipient's roster lacks the author's key, that is the `MSSSignedNoKey` path (accepted, shown without badge), not the mismatch path. So there is no honest false-positive for the drop. Needs an edge-case test and a help note. +- **As-channel spoofing** — because signed as-channel posts arrive as `FwdMember`, the recipient MUST verify the (verified) author is an owner before rendering as-channel (§5); otherwise a non-owner's signed `asGroup=True` post would display as "from the channel". +- **Replay** — the binding covers `sharedMsgId` + `MsgScope`; cross-scope/group replay is blocked, same-message replay is a dedup duplicate. + +## What already exists (reused unchanged) + +- **Send / sign**: `groupMsgSigning` (`Internal.hs:2110`) → `createSndMessages` threads `Maybe MsgSigning` → `createNewSndMessage` Ed25519-signs `encodeChatBinding CBGroup (publicGroupId, memberId) <> msgBody`, storing `signedMsg_` in `SndMessage` (`Messages.hs:1156`). +- **Wire**: `batchMessages` prepends the signature via `encodeBatchElement` (`Batch.hs:45,69`); relay groups always batch. +- **Forward**: live delivery preserves the original signed bytes by reconstructing `VMSigned` from the stored `msg_chat_binding`/`msg_signatures` (`Store/Delivery.hs:155-165`); `fwdSender` is derived from the stored `showGroupAsSender` (`:158`). +- **Receive / verify**: `withVerifiedMsg` (`Subscriber.hs:3819`) wraps member-authored messages (non-forwarded path `:1037`, forwarded `FwdMember` path `:3780`). `XMsgNew_`/`XMsgUpdate_`/`XMsgDel_` are not in `requiresSignature` ⇒ `signatureOptional` (`:3848`): signed ⇒ `MSSVerified` (key present) / `MSSSignedNoKey` (no roster key), unsigned ⇒ accepted (§7 adds the held-signed enforcement on top). `FwdMember` verifies against the author's key (`verifyGroupSig`, `:3833`); `FwdChannel` is delivered as `VMUnsigned` (`:3783`). No protocol-version bump. +- **Persistence**: own item — `createNewSndChatItem` sets `MSSVerified <$ signedMsg_` (`Store/Messages.hs:548`); received item — `RcvMessage.msgSigned` (`Messages.hs:1174`) is stored by `createNewRcvChatItem` (`Store/Messages.hs:563`); `CIMeta.msgSigned` (`Messages.hs:520`). +- **CLI**: `sigStatusStr` (`View.hs:389`) renders "(signed)" / "(signed, no key to verify)". + +Missing: (1) the decision to sign content; (2) per-send plumbing from the API; (3) as-channel forward/display/guard (§5); (4) edit reuse; (5) the badge fix (§7); (6) the apps. + +## Core changes (Haskell) + +### 1. `signableContent` predicate + +Next to `requiresSignature` (`Protocol.hs:1334`): + +```haskell +-- | Content events a member may sign (XMsgNew opt-in; XMsgUpdate/XMsgDel when the target was signed). +signableContent :: CMEventTag e -> Bool +signableContent = \case + XMsgNew_ -> True + XMsgUpdate_ -> True + XMsgDel_ -> True + _ -> False +``` + +### 2. Signing decision takes the opt-in flag + +`groupMsgSigning` (`Internal.hs:2110`) gains a leading `Bool` — it stays blind to `showGroupAsSender` (as-channel posts sign with the owner's `CBGroup` binding like any member post): + +```haskell +groupMsgSigning :: Bool -> GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning +groupMsgSigning sign gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {publicGroupId, memberPrivKey}} evt + | useRelays' gInfo && shouldSign = + Just $ MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey + where + tag = toCMEventTag evt + shouldSign = requiresSignature tag || (sign && signableContent tag) +groupMsgSigning _ _ _ = Nothing +``` + +Three call sites — all but the content/edit chain pass `False`: `sendGroupMemberMessages` (`Internal.hs:2119`, hardcode `False`), `sendGroupMessages_` (`:2364`, passes its threaded flag), and the direct `XGrpLeave` send (`Commands.hs:3019`, `False`). + +### 3. Thread `sign :: Bool` through the send functions + +Add the flag to `sendGroupMessages` (`Internal.hs:2329`), `sendGroupMessages_` (`:2362`), `sendGroupMessage` (`:2255`), and the content wrappers `sendGroupContentMessages` / `sendGroupContentMessages_` (`Commands.hs:4430` / `:4439`). Keep `sendGroupMessage'` (`Internal.hs:2261`) and `sendGroupMemberMessages` (`:2116`) unchanged by hardcoding `False`. + +Behavior-preserving (every existing caller passes `False`) ⇒ its own commit. The only two variable-flag sites are content send (`Commands.hs:4469`) and edit (`:751`). Other callers pass `False`: `sendGroupMessages` — `Commands.hs:812,819,2821,2958`; `sendGroupMessages_` — `Commands.hs:2869,3912`; `sendGroupMessage` — `Commands.hs:908,2721,3327,3875,3878,3882`. + +`sendGroupContentMessages` has three callers, all reached via §4 except the first: the content-send handler (`Commands.hs:667`, real flag), `APIReportMessage` (`:698`, `False`), and `APIForwardChatItems` (`:994`, `False`). + +`Bool`-choice note: `sign` joins `showGroupAsSender :: ShowGroupAsSender (= Bool)` and `live :: Bool` in `sendGroupContentMessages`/`_`. Place `sign` away from the other two in each signature (e.g. after `itemTTL`) to reduce silent transposition. + +### 4. API: per-send `sign` flag + +Add a field to `APISendMessages` (`Controller.hs:382`): + +```haskell +| APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, signMessages :: Bool, composedMessages :: NonEmpty ComposedMessage} +``` + +Parser (`Commands.hs:5094`), defaulting off so old command strings still parse: + +```haskell +"/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> signMessagesP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)) +-- signMessagesP = " sign=" *> onOffP <|> pure False (after sendMessageTTLP) +``` + +The nine internal positional constructors of `APISendMessages` must gain the field (`False`): `Commands.hs:2394,2403,2423,2443,2451,2496,3238,3279,3288`. Compiler-caught, part of the behavior-preserving commit. + +The handler (`Commands.hs:654-667`) flows `signMessages` to `sendGroupContentMessages` for group sends and ignores it for direct sends. `APIReportMessage` (`:693-698`) passes `False`. + +### 5. As-channel signing: forward with member id, display from `asGroup`, enforce owner + +An owner may sign an as-channel post (no gate on `showGroupAsSender`). Three pieces make it verifiable while displaying as the channel: + +- **Forward (`Store/Delivery.hs:158`)**: route signed as-channel posts via `FwdMember` so subscribers can verify; keep unsigned as-channel anonymous via `FwdChannel`: + ```haskell + fwdSender = if showGroupAsSender && isNothing chatBinding_ then FwdChannel else FwdMember senderMemberId senderMemberName + ``` + (`chatBinding_`/`sigs_` are already in scope here for `verifiedMsg`; `isNothing chatBinding_` ⇔ unsigned. Non-as-channel posts already use `FwdMember`, unchanged.) +- **Display**: the recipient already derives "as channel" from the signed `asGroup` flag, independent of `fwdSender` — `newGroupContentMessage` `sentAsGroup = asGroup_ == Just True` (`Subscriber.hs:2177`), `groupMessageUpdate` `showGroupAsSender = fromMaybe (isNothing m_) asGroup_` (`:2235`). So a `FwdMember` + `asGroup=True` post verifies against the owner and renders as the channel with the verified badge. +- **Owner guard (security, MUST)**: the forwarded `XMsgNew` path (`xGrpMsgForward` `Subscriber.hs:3771` → `FwdMember` branch `:3775` → `withVerifiedMsg` `:3784` → `newGroupContentMessage` `:3795`) MUST reject as-channel display unless the verified author is an owner — parity with `groupMessageUpdate`'s owner guards (`:2236` send-time, `:2282` store-time) and the direct send path's `checkSendAsGroup` (`Subscriber.hs:1057`, `asGroup == Just True && memberRole' m'' < GROwner ⇒ messageError`); reuse the `validSender … CIChannelRcv ⇒ GROwner` pattern (`:2135`). Without it a non-owner's signed `asGroup=True` post renders as "from the channel". The guard is reliable for *legitimate* owner posts: `GROwner` identity is established from root-key-verifiable link data at connect time (`createLinkOwnerMember`, `Store/Groups.hs:3381`), and `isRosterRole` (`Internal.hs:1257`) excludes `GROwner` — so owner identity is never carried by the roster or `XGrpMemRole` and is therefore independent of the known role-propagation issue. Single owner today. +- **Invariant**: `FwdChannel` never carries a signature (signed posts always route via `FwdMember`). Assert this in `encodeFwdElement` (`Batch.hs:108`) as a regression guard. + +`sendGroupContentMessages_` passes the API `sign` straight through (no `&& not showGroupAsSender` gate). The owner's own as-channel item is marked signed/verified like any signed send. + +### 6. Edit reuse (`XMsgUpdate`) + +Group edit, `Commands.hs:739-757`. The own sent item is loaded with `CIMeta` (`:739`); add `msgSigned` to the pattern and reuse it: + +```haskell +let reuseSign = isJust msgSigned +SndMessage {msgId, signedMsg_} <- sendGroupMessage user gInfo scope recipients reuseSign event +``` + +For own sent items, `msgSigned` is `Just MSSVerified` iff signed (`createNewSndChatItem`, `Store/Messages.hs:548`), so `isJust` is the right test. An edit is signed exactly when the original was — now **mandatory**, not optional: recipients enforce it (§7). The author's own item is authoritative, so the author always emits a signature exactly when recipients require one (no divergence). Direct/local edits need no change. + +### 7. Enforcement: a held-signed item's mutations must be signed (security) + +**Replaces the earlier "badge refresh" approach.** Principle: if a recipient holds an item as signed (`msgSigned = Just _`), any subsequent `XMsgUpdate` or `XMsgDel` targeting it MUST itself carry a verifying signature; an unsigned mutation of a signed item is **rejected** (drop, reusing `RGEMsgBadSignature`), not applied. This closes the edit/delete-downgrade spoof at the source (fail-closed) instead of applying attacker content and relabelling the badge. + +`updateGroupChatItem` / `updateGroupChatItem_` (`Store/Messages.hs:2749`/`:2758`) and their five callers are left **unchanged** — no `Maybe MsgSigStatus` param, no `msg_signed` in the `UPDATE`, no override of `ci'`. A legitimate enforced-signed edit keeps the original `MSSVerified`, so the badge stays correct without a refresh. (The one thing a refresh would have added — reflecting a rare `MSSSignedNoKey → MSSVerified` transition after a lagging roster catches up — is deliberately dropped.) + +**Receive check (both paths; the data is already in scope):** + +- **Update** — `groupMessageUpdate` (`Subscriber.hs:2225`): it already loads the target item (`:2277`) and has the incoming `RcvMessage` (`:2226`). Before applying the update, if the target's `meta.msgSigned` is `Just _` and the incoming `rcvMsg.msgSigned` is `Nothing`, reject (`messageError` → drop → `RGEMsgBadSignature`). +- **Delete** — `groupMessageDelete` (`Subscriber.hs:2307`): the target `CChatItem` from `findItem` (`:2357`) carries `meta.msgSigned`; add `msgSigned` to the `RcvMessage` pattern (`:2356`) to get the incoming verdict. Same check. **One site covers self-delete, moderation, and forwarded delivery** (all route through `groupMessageDelete`). It composes with — does not touch — the existing `checkRole` WHO-gate (`:2382`): a moderation delete verifies against the *moderator* (its sender) via `withVerifiedMsg`, and `checkRole` independently enforces authority. + +"Signed" for the requirement means `MSSVerified` **or** `MSSSignedNoKey` (the sender signed); only `Nothing` (unsigned) is rejected. Since keys do not rotate, an item held `MSSVerified` verifies its mutations too, so this never spuriously rejects a legitimate live-held mutation. + +**Send side — produce the required signature so legitimate mutations are accepted:** + +- **Edits** (`XMsgUpdate`): §6 already signs the edit iff the original was signed (`reuseSign = isJust msgSigned`); under enforcement this is mandatory. The author's own item is authoritative, so the author emits a signature exactly when recipients require one — no divergence. +- **Deletes** (`XMsgDel`) — per-item, because deletes batch a heterogeneous set of targets (`Commands.hs:811/818/3909`) and signing is currently keyed off event *type*, not instance. Compute the signer per delete at the send sites (the target `items` and their `msgSigned` are already in scope) and thread it in. **Do not** add `XMsgDel_` to `requiresSignature` (over-signs, ignores the per-item condition). Generalize the batch send to carry a per-event `Maybe MsgSigning` rather than recomputing one `groupMsgSigning` per batch — parameterize the existing body, do **not** duplicate it; keep a uniform-sign wrapper so non-delete callers are unchanged (this generalization lands in commit 1, behavior-preserving). Two cases: + - **Self-delete** (`memberId = Nothing`, `Commands.hs:811/818`): sign iff the deleter's own copy of the target was signed. The self-deleter's view is authoritative, so signed-holders and catch-up-unsigned-holders both accept — no divergence; and it preserves the deniability choice (an unsigned, deniable post's self-delete stays unsigned/deniable). + - **Moderation/admin delete** (`memberId = Just`, `Commands.hs:3909`): **always sign** in relay channels. A moderator who holds the target unsigned (joined late, caught up via unsigned history) would otherwise emit an unsigned delete that members holding the post signed would reject — silently failing moderation. Moderation deletes already carry the target `memberId` (attributable), so always-signing costs no deniability and removes the divergence. + +`signableContent` (§1) includes `XMsgDel_` so `groupMsgSigning sign …` produces a signer when the per-item/per-action decision is to sign. + +### 8. Paths deliberately left unsigned + +- Reactions (`XMsgReact`, `Commands.hs:908`): pass `False` — never signed (a reaction does not mutate the post's content/integrity and is not enforced). +- Auto-reply welcome content via `sendGroupMessage'` ⇒ `False`. +- Deletes are signed conditionally and enforced (§7) — no longer left unsigned. + +## App changes (iOS + Kotlin) + +Locate by symbol — app line numbers drift independently. + +- **A. Decode the status.** JSON tags come from `enumJSON (dropPrefix "MSS")`: `MSSVerified → "verified"`, `MSSSignedNoKey → "signedNoKey"` — not the DB strings "verified"/"no_key". Add an optional `msgSigned: MsgSigStatus?` to `CIMeta` on both platforms (iOS `ChatTypes.swift`; Kotlin `ChatModel.kt`), decoding `verified`/`signedNoKey`. Optional ⇒ old core JSON decodes safely. +- **B. Composer long-press option + thread `sign` to the API.** No device preference — the long-press is the only entry. Add `sign: Bool` to the send closure (default off) and a long-press item next to "Disappearing message" ("Sign message" / "Send without signing", iOS `SendMessageView.swift`; Kotlin `SendMsgView.kt`). Show it for any relay channel, gated on the app's existing relay-channel indicator (`useRelays'` — the same signal that drives the as-channel composer). No key-derived flag is needed: every sendable member holds a signing key (the only keyless state, prepared/`GSMemUnknown`, is non-current and cannot send, so the composer is not available there). Confirm the app's `GroupInfo` exposes a member-agnostic relay-channel boolean (the as-channel toggle is owner-scoped; signing is offered to all members) — if the only existing signal is owner-scoped, add a plain non-secret relay-channel boolean, not a `memberPrivKey`-derived one. It is shown for as-channel sends too, with an explicit note that signing an as-channel post reveals you as the author (§5 tradeoff). Append `sign=on|off` in `apiSendMessages` on both platforms. +- **C. Recipient indicator.** In the message meta row (`CIMetaView`, chat view only), show `checkmark.seal` when `meta.msgSigned == verified`, in the trust cluster next to `lock` and before the timestamp. iOS: append `statusIconText("checkmark.seal", color)` in `ciMetaText`. Kotlin: add an `Icon` branch in `CIMetaText` **and** the matching `iconSpace` branch in `reserveSpaceForMeta` (the in-file contract requires the two to match); add the matching seal vector to `MR.images`. Omit `signedNoKey` (only `.verified` is badged). Own signed items use the same glyph. Conversation list (`ChatPreviewView`) unchanged. Surface the "verified ≠ timestamp/ordering/completeness" caveat in help. + +## Compatibility + +- Wire format unchanged (the batch-element signature prefix already exists); no `chatVRange` bump; pre-feature relay-capable peers verify/accept. +- API command `sign=` is additive with a default; app + core ship together. +- No DB migration — `chat_items.msg_signed` already exists (written by `createNewChatItem_`, `Store/Messages.hs:614`; read by `mkCIMeta`). +- The new optional app `msgSigned` decodes as absent on older cores. + +## Edge cases, races, correctness + +- **Bad signature → drop** (threat model): a signed content message whose signature doesn't verify against the recipient's roster key (forgery/tamper — keys don't rotate) is dropped + `RGEMsgBadSignature`, not shown unsigned. Test it; note it in help. Distinct from §7 enforcement, which rejects an *unsigned* mutation of a *signed* item. +- **Member without keys** (`groupKeys = Nothing`): only the prepared/`GSMemUnknown` relay-channel state is keyless (`createPreparedGroup`, `Store/Groups.hs:654`, with the `TODO [member keys]` marker), and that state is non-current/non-active — it cannot send content, so the composer is unavailable. Any sendable membership has `groupKeys = Just` (key written before the membership becomes current). `groupMsgSigning` returning `Nothing` for `groupKeys = Nothing` is thus a harmless backstop, never reached on a real content send. +- **Non-relay groups**: the `useRelays'` guard ⇒ never signed; UI must not offer it. +- **Live messages**: each `XMsgUpdate` reuses the item's `msgSigned`, so every increment is signed iff the original was — exactly what §7 enforcement requires. Acceptable cost. +- **Mutation enforcement (§7)**: a forged *unsigned* `XMsgUpdate`/`XMsgDel` of a held-signed item is rejected (`RGEMsgBadSignature`), content/visibility unchanged — not applied-then-unbadged. A legitimate signed edit/delete of a signed item is accepted. Test both, on both paths. +- **Moderation-delete divergence**: a moderator holding the target *unsigned* (caught up via unsigned history) must still emit a *signed* delete so members holding the post signed accept it — hence moderation always-sign (§7). Without it, moderation of a signed post silently fails for those members. Test a moderation delete from a catch-up moderator. +- **Non-batched path**: `sndMessageMBR` (`Internal.hs:2428`) uses raw `msgBody`, never reached in relay groups (`memberSendAction → MSASendBatched`). Add a test-asserted invariant; optionally route it through `encodeBatchElement signedMsg_`. +- **History downgrade (posts, by design this PR)**: relay history catch-up rebuilds content unsigned and as-channel via `FwdChannel` (`sendHistory` / `processContentItem`, `Internal.hs:1278` / `:1349`; the relay re-encodes from `MsgContent` and has no author key). So a signed post (channel or member) is delivered unsigned on catch-up — no badge, and an as-channel post re-anonymizes. Graceful (absence ≠ forgery); document and test. Consequently PR 1's mutation enforcement (§7) does not fire for catch-up members (they hold the post unsigned), so a forged unsigned edit/delete still lands for them — the documented honest limit. PR 2 preserves signatures through history. +- **Concurrency**: signing/verification are pure given keys; no new shared state. Send holds `withGroupLock`; receive runs under existing serialization. No new races. + +## Tests + +- Protocol (`tests/ProtocolTests.hs`): round-trip signed `XMsgNew`/`XMsgUpdate`/`XMsgDel`; assert binding `CBGroup <> (publicGroupId, memberId)`; verify accepts the right key, rejects wrong key / altered body / altered binding. +- Integration (`tests/ChatTests/Groups.hs`, relay/channel setup): sign+verify ⇒ "(signed)"; off/default ⇒ none; missing roster key ⇒ "(signed, no key to verify)"; edit reuse keeps/omits the badge; **edit enforcement** — unsigned forged `XMsgUpdate` over a signed item ⇒ rejected (`RGEMsgBadSignature`), content unchanged (§7), and a legitimate signed edit of a signed item ⇒ accepted; **delete enforcement** — unsigned forged `XMsgDel` of a signed item ⇒ rejected, item not deleted (§7); signed self-delete of a signed item ⇒ deletes; unsigned self-delete of an unsigned item ⇒ deletes (no requirement); **moderation delete** — moderator's signed delete of a signed post ⇒ accepted + role-checked, and moderation always-sign holds even when the moderator holds the target unsigned; **as-channel signed** — owner `as_group=on sign=on` ⇒ recipient verifies and shows "(signed)" while displaying as the channel; **as-channel unsigned** — forwards via `FwdChannel`, no member id on the wire; **as-channel spoof** — non-owner `asGroup=on sign=on` ⇒ rejected (§5 guard); **history downgrade** — live recipient "(signed)", catch-up recipient not, and enforcement does not fire for the catch-up recipient; **bad-signature drop**; forgery rejection ⇒ `RGEMsgBadSignature`. +- App: minimal decode test that `"verified"` / `"signedNoKey"` parse to the right enum on both platforms. + +## Commit plan (PR 1) + +1. **Structural (behavior-preserving)**: add `signableContent` (`XMsgNew_`, `XMsgUpdate_`, `XMsgDel_`); parameterize `groupMsgSigning` with `sign :: Bool`; thread `sign` through the content/edit send chain; generalize the batch send to carry a per-event `Maybe MsgSigning` (uniform-sign wrapper so existing callers are unchanged); add `signMessages :: Bool` to `APISendMessages` + its nine positional constructors. Every caller passes `False`/`Nothing` ⇒ no behavior change. +2. **Content signing + update enforcement (core)**: `APISendMessages` parser; content send passes the real flag; edit signs iff the original was signed (§6); reject an unsigned `XMsgUpdate` of a signed item in `groupMessageUpdate` (§7); as-channel forward/display/owner-guard (§5). After this commit the update path is complete and spoof-free. +3. **Delete signing + delete enforcement (core)**: per-item delete signing at the three send sites — self-delete conditional, moderation always-sign (§7); reject an unsigned `XMsgDel` of a signed item in `groupMessageDelete` (§7). After this commit the delete path is complete. +4. **App**: decode `msgSigned` + recipient indicator (§C). +5. **App**: composer long-press option + `apiSendMessages` wiring (§B). +6. **Tests** (may accompany 2/3). + +Each commit builds and passes tests independently. §7's earlier `updateGroupChatItem` plumbing is dropped — that helper and its five callers are untouched, so there is no badge-fix commit. + +### Pre-implementation gates + +- **MUST**: the mutation-enforcement check is on **both** `groupMessageUpdate` and `groupMessageDelete` — an unsigned `XMsgUpdate`/`XMsgDel` of a `Just`-signed target is rejected via `RGEMsgBadSignature`. A missed path reopens the spoof. +- **MUST**: per-item delete signing is wired at all three delete send sites (`Commands.hs:811/818` self-delete conditional; `:3909` moderation always-sign), and `XMsgDel_` is **not** added to `requiresSignature`. +- **MUST**: the as-channel owner guard (§5) is on the forwarded `XMsgNew` path, and `FwdChannel` carries no signature. +- **SHOULD**: re-grep `groupMsgSigning` / `sendGroupMessage` / `sendGroupMessages` / `sendGroupMessages_` callers; the batch send is generalized (per-event `Maybe MsgSigning`) without duplicating its body; only content-send, edit, and delete pass a variable signer. +- **SHOULD**: the "verified" caveats (no timestamp/ordering; history downgrade; bad-signature drop) and the as-channel de-anonymization warning are surfaced in UI/help, and those tests exist. + +## Deferred to PR 2: history signature preservation + +Signable deletes and recipient enforcement moved into PR 1 (§7). What remains is the hard part PR 1's enforcement degrades around. + +**History signature preservation for posts.** On catch-up the relay rebuilds content unsigned (`processContentItem` re-encodes from `MsgContent`, has no author key; re-encoding invalidates the original signature — `Store/Delivery.hs:162`). So a catch-up member holds an originally-signed post as `Nothing`, and PR 1's enforcement does not fire for them (target held unsigned ⇒ no signature required). Two residual gaps that only preservation closes: + +- a relay can forge an unsigned delete/edit of a signed post for catch-up members (they hold it unsigned, so nothing is enforced); +- the deeper inconsistency behind the moderation-divergence handling — members disagreeing on a post's signed status — is fully resolved only when all members hold the post signed. + +Design question (unchanged): does the `messages` row survive long enough to forward the original signed bytes on catch-up, or must signed bytes be persisted on the chat item (migration)? + +Honest limit until then: enforcement protects a post a recipient already holds verified; a relay that delivered the original unsigned sidesteps it (visible as a missing badge, not prevented). + +## Out of scope / future + +- Group-level "required signing" owner setting — rejects unsigned messages group-wide, closing the optional-downgrade gap. +- **Verifiable-anonymous as-channel** — a channel-level signature (ring signature over the owner set, or a shared/authorization-chain channel key) that proves "a valid owner" without revealing which, so an as-channel post is verifiable *and* keeps sender anonymity. This PR's per-member signing cannot do both; signing an as-channel post reveals the owner (§5). +- Signing reactions; signing auto-reply content; verifiable reports (signed `MCReport`). + +## Verification status (2026-06-25, branch `f/msg-signing`) + +Every symbol the plan names was located and its logic re-checked against current code. **No logic drift** was found in any anchored function — the plan's described behavior still holds everywhere. Three classes of correction below: mislabeled anchors (fix before relying on them), one structural clarification (threading is slightly larger than the prose implies), and a current line map (most anchors moved a little; re-confirm by symbol regardless). + +### Mislabeled / materially-moved anchors + +- `MsgSigStatus` is defined in **`src/Simplex/Chat/Types/Shared.hs:134`** (`MSSVerified | MSSSignedNoKey`), not in `Types.hs`/`Messages.hs`. DB encoding: `verified` / `no_key`; JSON tags (`enumJSON $ dropPrefix "MSS"`): `verified` / `signedNoKey`. The JSON/DB divergence the app section (§A) relies on is confirmed. Any core edit to the type itself targets `Types/Shared.hs`. +- §5 "the direct path (`:1064`)" is wrong: the non-forwarded as-channel owner enforcement lives in **`checkSendAsGroup` (`Subscriber.hs:1057`)**; `:1064` is just the `XMsgNew → newGroupContentMessage` dispatch (fixed inline in §5). +- §6 wording "add `msgSigned` to the pattern" — confirmed the own-item `CIMeta` pattern at `Commands.hs:739` does **not** bind `msgSigned` today; it must be added to the record pattern (the field exists on `CIMeta`, `Messages.hs:520`). +- §3 "`sendGroupMessage` … 6 callers" — there are **7** `sendGroupMessage` sites; the 7th is the edit path itself (`Commands.hs:751`, the only variable-`sign` one). The six that pass `False`: `Commands.hs:908,2719,3325,3873,3876,3880`. +- `signatureOptional` is at `Subscriber.hs:3848` (plan said ~:3844); `RGEMsgBadSignature` creation at `:3828` (plan said :3824). Behavior unchanged. + +### Structural clarification (affects §2/§3 threading) + +`groupMsgSigning` is invoked at **three** sites, two of them inside functions that do **not** currently receive any as-channel/sign flag: + +- `Internal.hs:2122` — inside `sendGroupMemberMessages` (hardcode `False`). +- `Internal.hs:2367` — inside `sendGroupMessages_`. +- `Commands.hs:3017` — direct `XGrpLeave` via `createSndMessages` (hardcode `False`). + +`sendGroupMessages_` has **no** `ShowGroupAsSender`/`asGroup` parameter, and `sendGroupMessages` (`:2332`) consumes its `ShowGroupAsSender` only for `shouldSendProfileUpdate` — it does **not** pass it down. So threading `sign` to the `groupMsgSigning` call inside `sendGroupMessages_` is genuinely new wiring: `sendGroupContentMessages_`/`sendGroupMessage` → `sendGroupMessages` → **(new param)** → `sendGroupMessages_` → `groupMsgSigning sign …`. The plan's §3 list already includes all these functions; this note just flags that the param add to `sendGroupMessages_` is load-bearing, not a pass-through that already exists. + +### Current line map (re-confirm by symbol) + +| Symbol | File:line | +|---|---| +| `requiresSignature` (insert `signableContent` next to it) | Protocol.hs:1334 | +| `MsgSigning` (4-field record, applied positionally) | Protocol.hs:469 | +| `encodeChatBinding` / `CBGroup` / `signChatMsgBody` | Protocol.hs:476 / 442 / 479 | +| `FwdSender` (`FwdMember MemberId ContactName` / `FwdChannel`) | Protocol.hs:372 | +| `groupMsgSigning` | Internal.hs:2113 (calls: 2122, 2367, Commands.hs:3017) | +| `sendGroupMessages` / `_` | Internal.hs:2332 / 2365 | +| `sendGroupMessage` / `'` | Internal.hs:2258 / 2264 | +| `sendGroupMemberMessages` | Internal.hs:2119 | +| `sndMessageMBR` (non-batched; never hit in relay groups) | Internal.hs:2431 (`memberSendAction` 2455, `MSASendBatched` 2453) | +| `sendHistory` / `processContentItem` (as-channel → `FwdChannel` at 1375) | Internal.hs:1281 / 1352 | +| `createNewSndMessage` (`signedMsg_`) | Store/Messages.hs:235 | +| `APISendMessages` ctor | Controller.hs:382 | +| `/_send` parser / `onOffP` | Commands.hs:5111 / 5490 | +| 9 positional `APISendMessages` ctors (add `False`; `:2449` passes `live=True`) | Commands.hs:2392,2401,2421,2441,2449,2494,3236,3277,3286 | +| `APISendMessages` handler / group send | Commands.hs:654 / 667 | +| `sendGroupContentMessages` / `_` | Commands.hs:4447 / 4456 (callers 667, 698, 994) | +| group edit (own-item pattern / send) | Commands.hs:739 / 751 | +| `XGrpLeave` group send (`sendGroupMessage'`) | Commands.hs:3027 | +| `verifyGroupSig` / `withVerifiedMsg` / `signatureOptional` | Subscriber.hs:116 / 3823 / 3848 | +| `RGEMsgBadSignature` | Subscriber.hs:3828 | +| forwarded `XMsgNew`: `xGrpMsgForward` / `FwdMember` / `withVerifiedMsg` / `newGroupContentMessage` / `FwdChannel→VMUnsigned` | Subscriber.hs:3771 / 3775 / 3784 / 3795 / 3787 | +| `newGroupContentMessage` (`sentAsGroup` 2177) / `groupMessageUpdate` (owner guards 2236, 2282) | Subscriber.hs:2142 / 2225 | +| `validSender` (`CIChannelRcv ⇒ GROwner`) / `checkSendAsGroup` | Subscriber.hs:2135 / 1057 | +| `updateGroupChatItem` / `_` / `UPDATE` / `updatedChatItem` | Store/Messages.hs:2749 / 2758 / 2766 / 2547 | +| §7 `updateGroupChatItem` callers (all 5) | Commands.hs:757; Subscriber.hs:1200,1566,2258,2298 | +| `createNewSndChatItem` (`MSSVerified <$ signedMsg_`) / `createNewRcvChatItem` / `createNewChatItem_` (`msg_signed`) / `mkCIMeta` | Store/Messages.hs:548 / 562 / 614 / Messages.hs:528 | +| `Store/Delivery.hs` `fwdSender` / `VMSigned` reconstruction | Delivery.hs:158 / 162–164 | +| `sigStatusStr` | View.hs:389 | +| iOS `CIMeta` / composer menu / `ciMetaText` / `/_send` build | ChatTypes.swift:3825 / SendMessageView.swift:238 / CIMetaView.swift:93 / AppAPITypes.swift:243 | +| Kotlin `CIMeta` / composer menu / `CIMetaText` + `reserveSpaceForMeta` / `/_send` build | ChatModel.kt:3529 / SendMsgView.kt:198 / CIMetaView.kt:67 + 118 / SimpleXAPI.kt:3870 | +| channels-overview.md anchors (:103,:159,:198,:214,:221,:237) | all confirmed | + +## Open design questions (2026-06-25) + +Status as of 2026-06-29: all resolved (Q1/Q3/Q5 by the team; Q2/Q4 by code verification). **Design change (2026-06-29):** §7's "badge refresh" was replaced by receive-time **enforcement** — a held-signed item's `XMsgUpdate`/`XMsgDel` must be signed or is rejected (`RGEMsgBadSignature`); signed deletes + enforcement moved from PR 2 into PR 1 (update *and* delete); §7's `updateGroupChatItem` plumbing dropped. One sub-rule baked in pending confirmation: moderation/admin deletes **always** sign in relay channels (self-deletes sign conditionally) to avoid the catch-up-moderator divergence — see §7. + +1. **RESOLVED — keep drop + `RGEMsgBadSignature`.** Member keys do not rotate (one key per member, communicated on join), so a have-the-key-but-mismatch is a genuine forgery/tamper signal, and a downgrade-to-unsigned would buy nothing against a malicious relay (which can simply drop the whole message). Behavior stays identical to existing signed events; the lagging-roster case is the `MSSSignedNoKey` accept path, not the drop path, so there is no honest false-positive. Threat-model bullet updated accordingly. Action: edge-case test + help note only. + +2. **RESOLVED — gate on `useRelays'` alone, no key-derived flag.** Verified: the only relay-channel state with `groupKeys = Nothing` is prepared/`GSMemUnknown` (`createPreparedGroup`, `Store/Groups.hs:654`), which is non-current/non-active and cannot send — the key is written before the membership becomes sendable. So every sendable member has `groupKeys = Just`. §B simplified to a `useRelays'` gate; §B asks the app task to confirm a member-agnostic relay-channel boolean exists (the as-channel toggle is owner-scoped), adding a plain non-secret one if not. The "member without keys" edge case is now documented as a harmless backstop. + +3. **RESOLVED — keep raw `Bool`.** No `SignMessages` newtype. Retain §3's placement guidance (put `sign` away from `showGroupAsSender`/`live` in each signature) as the transposition mitigation. + +4. **RESOLVED — guard reads stable, link-data owner identity; no false-negative.** Verified: `GROwner` is established from root-key-verifiable link data at connect time (`createLinkOwnerMember`, `Store/Groups.hs:3381`), and `isRosterRole` (`Internal.hs:1257`) excludes `GROwner`, so owner identity is never carried by the roster or `XGrpMemRole`. The as-channel guard is therefore independent of the known role-propagation bug, and (single owner today) a legitimate owner post never fails it. The §5 propagation note is retracted. The guard stays a MUST — it blocks a non-owner's signed `asGroup=True` post from rendering as the channel. + +5. **WITHDRAWN — non-issue.** A send's `sign` flag applies to its whole composed-message batch; `signableContent` filters to `XMsgNew`/`XMsgUpdate` and content sends do not interleave non-signable events. + +## Implementation divergences from plan (2026-07-06, branch `f/msg-signing`) + +Where the shipped implementation departs from the sections above. Each entry supersedes the referenced section for that detail; everything else was implemented as described. Re-confirm by symbol — line numbers are advisory. + +**D1 — §7 enforcement is verified-only, not signature-presence (supersedes §7's "'Signed' for the requirement means `MSSVerified` **or** `MSSSignedNoKey`").** The receive-time guard requires `MSSVerified` on **both** sides: an item held `MSSVerified` accepts a mutation only if the incoming mutation is itself `MSSVerified` (`itemSigned == Just MSSVerified && msgSigned /= Just MSSVerified` ⇒ reject). Reason: `withVerifiedMsg` verifies **only** `CBGroup` bindings — a signature under any other binding (`CBDirect`/`CBChannel`) resolves to `MSSSignedNoKey` *without verification*, even for a recipient that holds the author's key. So a presence-based check (`isNothing msgSigned`) would accept a relay-forged mutation carrying a garbage non-`CBGroup` signature — including against a badged `MSSVerified` item. Enforcement is therefore real only for `MSSVerified`; `MSSSignedNoKey` items get no enforcement (they are never badged, and authorship is unverifiable without the key, so nothing can be protected). The two helpers are `requireVerifiedEdit` / `requireVerifiedDelete` (`Subscriber.hs`; `requireVerifiedEdit` renamed from `requireVerifiedMutation` per review); both create the `RGEMsgBadSignature` item. + +**D2 — §7 channel history-delete always-signs (supersedes §7/§8 "self/history deletes sign conditionally").** `delEventSigned` (`Commands.hs`) signs on `onlyHistory || isJust msgSigned`: **channel history-delete (`CIDMHistory`) always signs**, matching moderation; only self-delete (`CIDMEBroadcast`) stays conditional (to preserve deniability of unsigned posts). Reason: the same catch-up divergence the plan fixed for moderation — a future non-authoring owner who caught up via unsigned history would otherwise emit an unsigned history-delete that live members holding the post verified would reject (D3/§7). Latent today (single-owner channels). + +**D3 — preemptive-moderation verification added (extends §7).** §7's receive check only guards items the recipient **already holds**. The `Left e` (item-not-found) branch of `groupMessageDelete` — which plants a `CIModeration` that auto-applies when the post later arrives — was unguarded, so a relay could deliver a forged **unsigned** moderation-delete *before* the signed post to pre-censor it. Added: in relay channels, reject an unverified moderation-delete of a not-yet-received item before planting the moderation (`useRelays' gInfo && msgSigned /= Just MSSVerified`, `Subscriber.hs`). Legitimate moderation always-signs (§7) and any recipient that passes `checkRole` holds the moderator's key, so no legitimate case is rejected. + +**D4 — §5 `encodeFwdElement` regression guard NOT added (supersedes §5's "Assert this in `encodeFwdElement` … as a regression guard" and the matching Edge-cases bullet).** `encodeFwdElement` (`Batch.hs`) is left as the original one-liner. The asserted invariant (a `FwdChannel` element never carries a signature) is already structural: `Store/Delivery.hs` derives `FwdChannel` only when `isNothing chatBinding_`, and `signedMsg_` is derived from the same value, so `FwdChannel ⟹ unsigned` by construction. An `error` guard would be unreachable and would only risk crashing the delivery worker; a `FwdMember` fallback is impossible there (`FwdChannel` carries no `memberId`). + +**D5 — CLI recipient indicator extended to content events (extends §C).** §C scopes the indicator to the apps (`CIMetaView`). The CLI (`View.hs`) additionally shows `(signed)` on content-message events — new messages, edits, and deletes — by appending `sigStatusStr msgSigned` in `viewReceivedMessage_` / `viewSentMessage` (new `appendLast` helper). Because `viewItemDelete` renders via `viewReceivedMessage`, delete events show the deleted post's signed status too. Unsigned content is unaffected (`sigStatusStr Nothing = ""`), so only the signing tests' event assertions changed. + +**D6 — bot/client API fixed at the generator (extends §4).** Beyond adding `signMessages` to core `APISendMessages`, the bot/client API was aligned at its source: the flag was added to the generator (`bots/src/API/Docs/Commands.hs`, `OnOffParam "sign" "signMessages" (Just False)`) and the outputs regenerated (`bots/api/COMMANDS.md`, `packages/simplex-chat-client/types/typescript/src/commands.ts`, `packages/simplex-chat-python/src/simplex_chat/types/_commands.py`) so the `cmdString` builders now emit `sign=on`; and `signMessages` was threaded through the three hand-written wrappers (`packages/simplex-chat-python/.../api.py`, TS `client.ts`, nodejs `api.ts`) whose `APISendMessages` construction otherwise omitted the now-required field. diff --git a/plans/2026-06-04-fix-corrupted-video-upload-error.md b/plans/2026-06-04-fix-corrupted-video-upload-error.md new file mode 100644 index 0000000000..e88e781a5d --- /dev/null +++ b/plans/2026-06-04-fix-corrupted-video-upload-error.md @@ -0,0 +1,100 @@ +# Fix: IndexOutOfBoundsException when uploading media with an undecodable preview + +## Symptom + +``` +java.lang.IndexOutOfBoundsException: Index 6 out of bounds for length 6 + at java.util.ArrayList.get(ArrayList.java:434) + at chat.simplex.common.views.chat.ComposeViewKt.ComposeView$sendMessageAsync(ComposeView.kt:827) + ... +``` + +The crash fires when sending a batch of picked media (e.g. 7 items) in which at least +one item produces no preview bitmap — most commonly a corrupted or unusual video whose +first frame cannot be extracted. + +## Root cause + +`ComposePreview.MediaPreview` carries two parallel lists that are assumed to be +**equal-length and index-aligned**: + +```kotlin +class MediaPreview(val images: List, val content: List) +``` + +Both consumers cross-index one list by the other's index, so the invariant is load-bearing: + +- `ComposeImageView` (preview row) iterates `media.images` and reads `media.content[index]`. +- `ComposeView.sendMessageAsync` iterates `preview.content` and reads `preview.images[index]` + (the `MCImage` / `MCVideo` preview string). This is the crash site. + +`processPickedMedia` built the two lists out of step: + +- `imagesPreview` was appended **only when `bitmap != null`**. +- `content` was appended **unconditionally** for videos, and for animated images that + passed the size check — regardless of whether a preview bitmap was produced. + +`getBitmapFromVideo` returns `PreviewAndDuration(null, …)` whenever Android's +`MediaMetadataRetriever` cannot extract a frame, **even when no exception is thrown** +(`Utils.android.kt:351`). So a single undecodable video appends to `content` but not to +`images`, leaving `content.size == images.size + 1`. Iterating `content` then indexes +`images[lastIndex+1]` → `Index N out of bounds for length N`. + +This is a pre-existing bug in the shared media picker; it is unrelated to any in-flight +feature work and reproduces on both Android and Desktop (both use the `commonMain` +`processPickedMedia`). + +## Fix + +Keep `content` and `imagesPreview` strictly paired at the source. The `when` now yields an +`UploadContent?` instead of mutating `content` inside its branches, and both lists are +appended together, gated on a non-null preview bitmap: + +```kotlin +if (bitmap != null && uploadContent != null) { + content.add(uploadContent) + imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) +} +``` + +Each iteration now adds exactly zero or one entry to **both** lists, so the +equal-length / index-aligned invariant holds by construction. + +### Behavior change + +Media that yields no decodable preview frame is now **skipped** rather than enqueued. +Previously such a video crashed the send; now only the bad item is dropped and the rest of +the picked batch sends normally (the loop evaluates each URI independently). + +The skip is **not silent**. A skipped video shows `showVideoDecodingException()`, gated on +`AlertManager.hasAlertsShown()` so the alert neither stacks across several bad items in one +batch nor duplicates the one `getBitmapFromVideo` already shows on its exception path. The +genuinely silent gap this closes is the video path that returns a null frame **without** +throwing (Android `getFrameAtTime` returns null; Desktop snapshot times out) — that path +previously produced no alert and then crashed on send. + +Image decode failures need no new alert here: `getBitmapFromUri` is already called for every +image (animated or not) with `withAlertOnException = !hasAlertsShown()`, so a null image +bitmap is surfaced before this point. Only the video null-frame case lacked any notice. + +## Why this approach + +- **Fixes the invariant at its origin** rather than papering over it at the two read + sites. Guarding `images[index]` in `sendMessageAsync` would stop the crash but leave the + preview row (`ComposeImageView`) silently mismatched and the actual media set ambiguous. +- **Minimal, surgical diff** confined to `processPickedMedia`; no API/type changes, no new + placeholder assets, no touch to the read sites. +- **Cross-platform by construction**: the change lives in `commonMain`, so Android and + Desktop are both covered. iOS has a separate Swift compose implementation and is out of + scope for this fix. + +## Other `MediaPreview` construction sites (verified aligned) + +- `cs.preview` → single-element `listOf(mc.image)` / `listOf(content)` (edit path): aligned. +- `constructFailedMessage` takes `last()` of each list: aligned if the input was aligned. + +## Test notes + +Manual repro: pick a multi-item batch including a corrupted/zero-frame video and send. +- Before: `IndexOutOfBoundsException` on send. +- After: the undecodable item is dropped; remaining media sends normally. diff --git a/plans/2026-06-09-perf-group-members-merge-on2.md b/plans/2026-06-09-perf-group-members-merge-on2.md new file mode 100644 index 0000000000..3000103b05 --- /dev/null +++ b/plans/2026-06-09-perf-group-members-merge-on2.md @@ -0,0 +1,95 @@ +# Perf — index member merge in `setGroupMembers` to O(n) + +**PR:** #7061 (`nd/group-members-merge-on2`) +**Scope:** client-only (multiplatform: android + desktop). One-line change, no behavioral change. +**File:** `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt:254` + +## Root cause (verified) + +`setGroupMembers` is the shared loader for the in-memory member list. After fetching +members from core (`apiListMembers`) it merges the freshly-loaded list with the +connection stats already held in memory, so an in-flight `connectionStats` isn't lost +when the list is reloaded — `ChatListNavLinkView.kt:257-267`: + +```kotlin +val currentMembers = chatModel.groupMembers.value +val newMembers = groupMembers.map { newMember -> + val currentMember = currentMembers.find { it.id == newMember.id } // O(n) scan, inside an O(n) map → O(n²) + ... +} +``` + +Two compounding costs: + +1. **O(n²) merge.** `currentMembers.find { it.id == newMember.id }` is a linear scan + run once per new member — `n` lookups × `n` scan = O(n²). +2. **~n² String allocations + GC pressure.** `GroupMember.id` is a *computed* property, + not a stored field — `ChatModel.kt:2424`: + + ```kotlin + val id: String get() = "#$groupId @$groupMemberId" + ``` + + Every `it.id` and `newMember.id` access allocates a fresh `String`. The nested + `find` evaluates `it.id` for (worst case) every current member on every iteration, + so the merge allocates on the order of n² short-lived strings, each compared by + value. In groups with thousands of members this is a visible main-thread lag spike. + +## Worst case observed + +`setGroupMembers` reloads `chatModel.groupMembers` (and runs the merge) whenever the +member list is (re)loaded while members are already in memory. The most noticeable +case: the **Chats with members** support-chat modal (`MemberSupportView`) reloads the +whole list via `LaunchedEffect(Unit) { setGroupMembers(...) }` every time it (re)enters +composition — e.g. after reading and closing a member's support chat — so it pays the +full O(n²) merge and produces a lag spike on close in large groups +(`MemberSupportView.kt:44`). + +## The fix (minimal — one change) + +Index the current members by id **once**, then look up in O(1): + +```kotlin +val currentMembersById = chatModel.groupMembers.value.associateBy { it.id } +val newMembers = groupMembers.map { newMember -> + val currentMember = currentMembersById[newMember.id] + ... +} +``` + +- `associateBy { it.id }` builds the index in a single O(n) pass; each subsequent + lookup is O(1). Total merge cost drops from O(n²) to O(n). +- String allocations drop from ~n² to ~2n (one `it.id` per current member while + building the map, one `newMember.id` per lookup). + +## Why it's safe (no behavioral change) + +- **Member ids are unique** — `id = "#$groupId @$groupMemberId"` is unique per member + within a group, and `setGroupMembers` always works within a single `groupInfo`. So + `associateBy { it.id }` cannot collide; `map[id]` returns exactly what + `find { it.id == id }` returned. +- Same result set, same merged `GroupMember` objects, same order of `newMembers` + (the `map` over `groupMembers` is unchanged). Only the lookup strategy changes. +- Everything downstream of the merge is untouched — `groupMembersIndexes`, + `groupMembers.value`, `membersLoaded`, `populateGroupMembersIndexes()` + (`ChatListNavLinkView.kt:268-271`). + +## Also sped up (same shared loader) + +`setGroupMembers` is the common in-memory member loader, called from several screens; +all of them get the same speedup in large groups (identical results, just O(n)): + +- Group member list / member management — `GroupChatInfoView` (`:121, :1250, :1267`) +- @-mention autocomplete — `GroupMentions` (`:119, :134`) +- Channel relays — `ChannelRelaysView` (`:38, :124`) +- Add members — `AddGroupView` (`:52`) +- The group chat's member load on open — `ChatView` (multiple sites) +- Chats with members — `MemberSupportView` (`:45, :67`) + +## Verification + +- Reasoned: ids unique within a group → `associateBy`/lookup is semantically identical + to the linear `find`. No caller observes a difference. +- Manual: open **Chats with members** in a large group, read and close a member's + support chat repeatedly — the lag spike on close should be gone. Member list, + @-mentions, relays, and add-members screens should remain identical, just faster. diff --git a/plans/2026-06-09-support-chat-unread-on-group-read.md b/plans/2026-06-09-support-chat-unread-on-group-read.md new file mode 100644 index 0000000000..5d24cc5ff1 --- /dev/null +++ b/plans/2026-06-09-support-chat-unread-on-group-read.md @@ -0,0 +1,200 @@ +# Fix: support-chat unread stats not cleared after no-scope group read + +Branch: `nd/fix-support-chat-unread-on-group-read` (off `origin/master`) + +## Symptom + +In a group with member admission ("chat with admins"), some members keep showing +as unread in the *chats with members* list even after the admin opens their support +chat and reads everything. Reopening never clears them. It is most visible for +members who have already been given a role (e.g. `member`), because pending members +always render the flag icon anyway, so the stuck badge is invisible for them. + +## Root cause + +The *chats with members* badge is driven by the per-member counters +`support_chat_items_unread / _member_attention / _mentions` on `group_members` +(surfaced as `GroupSupportChat`, `Types.hs:1087`). These are denormalized counters, +maintained by increment on receive and decrement on read. + +Reading a group **without a scope** (`APIChatRead` with `scope = Nothing`, +`Commands.hs:1153-1166`) runs `updateGroupChatItemsRead` (`Store/Messages.hs:2078`), +whose `UPDATE` filtered only by `group_id`: + +```sql +UPDATE chat_items SET item_status = RcvRead, ... +WHERE user_id = ? AND group_id = ? AND item_status = RcvNew +``` + +This flips **support-scope** items to `RcvRead` too, but never touches the +`support_chat_items_*` counters. The result is a permanent desync: + +- the items are read (so the chat "looks" fully read), but +- the per-member counters stay > 0 (so the member stays "unread" in the list), and +- reopening the support chat can't fix it: the scoped read decrement + (`updateGroupScopeUnreadStats`, `Store/Messages.hs:2202`) only fires on a real + `RcvNew -> RcvRead` transition, and there are no `RcvNew` items left. + +This is inconsistent with how the group's main unread **count** is computed, which +already excludes support items (`Store/Messages.hs:916`, +`... AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL`). The read +path simply forgot the same predicate. + +Note: `member_attention` self-heals on open via `checkSupportChatAttention` +(`Commands.hs:593-613`), recomputing it from loaded items. `unread` and `mentions` +have **no** such correction, which is why the badge persists. + +## Fix (Option A — prevent new corruption) + +Add the main-scope predicate to the two queries in the no-scope group-read path so +that reading a group without a scope leaves support-scope items (and their +disappearing-message timers) entirely untouched: + +- `updateGroupChatItemsRead` (`Store/Messages.hs`) — the read itself. +- `getGroupUnreadTimedItems`, `Nothing` branch (`Store/Messages.hs`) — its companion + timed-items query. + +```sql +AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL +``` + +Both lines are required and neither is droppable: + +- Without the read filter, support items get marked read (the bug). +- Without the timed-items filter, a disappearing support message would have its + delete timer started by a main-group read while still unread — a *new* + inconsistency introduced by a half-fix. The no-scope `APIChatRead` handler + (`Commands.hs:1160-1166`) calls `getGroupUnreadTimedItems` then + `setGroupChatItemsDeleteAt` then starts deletion threads, so both must agree that + support items are out of scope. + +Support items continue to be read (and their counters decremented) the correct way: +via the scoped read paths (`updateSupportChatItemsRead` / `updateGroupChatItemsReadList` +with a `MemberSupport` scope), exercised when the support chat is opened. + +### Why this is consistent / safe + +- Selects exactly the main-scope rows, identical to the authoritative unread-count + query (`Store/Messages.hs:916`). No main item is ever wrongly excluded — inserts + always set `group_scope_tag` and `group_scope_group_member_id` together + (`Store/Messages.hs:600-603`). +- `groups.members_require_attention` becomes *more* consistent, not less: the + no-scope read no longer zeroes support items behind a still-positive group counter. +- The added predicate is constant, parameterized SQL — no injection surface — and is + the existing codebase idiom (916 / 1214 / 1524 / 1745). + +## Test + +`tests/ChatTests/Groups.hs :: testScopedSupportUnreadStatsGroupReadNoScope` +(registered under the support-chat `describe` block). + +A member sends one support message (`markRead = False`), the admin marks the whole +group read with `/_read chat #1` (no scope), then reads the item in scope with the +**per-item** read `/_read chat items #1(_support:2) `. The per-item read only +decrements on a genuine `RcvNew -> RcvRead` transition, so: + +- on master the no-scope read already consumed the item -> decrement is a no-op -> + `unread: 1, require attention: 1` (test fails); +- with the fix the item is still unread -> decrement applies -> + `unread: 0, require attention: 0` (test passes). + +The full scoped read `/_read chat #1(_support:2)` is deliberately *not* used to +assert this, because `updateSupportChatItemsRead` hard-zeroes the counters +regardless of item state and would mask the bug. + +## Performance + +The added predicate makes both queries **faster**, not slower, and needs no new +index. Verified two ways. + +### Query plan (EXPLAIN QUERY PLAN, via the checked-in plan snapshots) + +Regenerating `chat_query_plans.txt` shows both statements switch index: + +``` +- SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention + (user_id=? AND group_id=? AND item_status=?) -- 3-column seek ++ SEARCH chat_items USING INDEX idx_chat_items_group_scope_stats_all + (user_id=? AND group_id=? AND group_scope_tag=? AND + group_scope_group_member_id=? AND item_status=?) -- 5-column seek +``` + +SQLite compiles `group_scope_tag IS NULL` / `group_scope_group_member_id IS NULL` +into equality constraints (`=?`), so the two new columns *deepen the index seek* +rather than adding a post-seek filter or scan. The read now uses +`idx_chat_items_group_scope_stats_all` +(`user_id, group_id, group_scope_tag, group_scope_group_member_id, item_status, …`, +`M20250721_indexes.hs`), which already exists — this is the same index the +authoritative unread-count query uses. No migration, no new index. + +Note: master was **not** degraded here — it already got a full 3-column seek via +`idx_chat_items_groups_user_mention`. So this is a modest improvement, not the repair +of a regression. The plan file only records two hunks (the two flipped statements); +the `saveQueryPlans` test also churns an unrelated `group_members` count entry and the +whole `agent_query_plans.txt`, both of which were reverted to keep the diff to exactly +those two queries. + +### Micro-benchmark (synthetic 200k-row group) + +On a synthetic group of ~200k `chat_items` where ~25% are support-scope, running the +old vs. new `UPDATE` in alternating trials: + +| Query | mean wall-clock | +|-----------------|-----------------| +| old (no filter) | ~10.5 s | +| new (scoped) | ~7.4 s (≈ −29%) | + +The speedup tracks the row reduction: the new query touches only the ~75% main-scope +rows, so it does ~25% less work and the tighter seek trims a bit more. This also +directly demonstrates the bug — the old query flips ~50k support rows to `RcvRead` +(without decrementing counters); the new one flips **0**. + +Caveat: absolute timings were noisy (concurrent GHC builds on the box); only the +*relative* comparison across alternating trials is reliable, and it was consistent in +direction and magnitude across runs. The gain scales with the support-item share — a +group with no support chats sees no measurable change. + +## Limitations (accepted, by decision) + +1. **No retroactive repair.** This change only prevents *new* corruption. Rows already + stuck (counter > 0 while the items are `RcvRead`) stay stuck until either the + per-member "Mark read" action (`apiSupportChatRead` -> `updateSupportChatItemsRead`, + which hard-zeroes) or, for `member_attention` only, the self-heal on open + (`checkSupportChatAttention`). A one-shot migration recomputing the three counters + from `chat_items` was considered and **intentionally not done**. + +2. **`setUserChatsRead` is not fixed.** The global "mark all chats read" + (`APIUserRead` -> `setUserChatsRead`, `Store/Direct.hs:640-646`) has the same flaw: + its `UPDATE chat_items ... WHERE user_id = ? AND item_status = ?` has no chat-type + and no scope filter, so it reads support items user-wide without decrementing the + counters. It is **left as-is on purpose**: no GUI client invokes `APIUserRead` + (it exists only as the Haskell command type `Controller.hs:357` and the CLI + binding), so it is not reachable from Android / desktop / iOS — only the terminal + CLI. The same one-line predicate would close it if a client ever exposes it. + +3. **`getUpdateGroupItem` relies on a UI contract.** The per-item read + (`updateGroupChatItemsReadList` -> `getUpdateGroupItem`, `Store/Messages.hs:2178`) + has no scope filter on its `UPDATE` and its `Nothing` branch decrements nothing, so + passing a support item ID with `scope = Nothing` would reproduce the desync. This is + defended only by the in-code assumption ("we rely on UI to not pass item IDs from + incorrect scope", `Store/Messages.hs:2162-2163`), not by the query. Not reachable in + normal flow; left unchanged to keep the diff surgical. + +## Files changed + +- `src/Simplex/Chat/Store/Messages.hs` — two one-line scope predicates. +- `tests/ChatTests/Groups.hs` — one regression test + its `it` registration. + +## Build & test run + +Library compiled clean via `bash /home/user/build/linux.sh`. The full test suite was +subsequently built and run against this branch (968 examples). The regression test +`should not read support chat items when reading group without scope` passes with the +fix and fails when the fix is reverted (confirmed by rebuilding master and re-running), +so it genuinely catches the desync. + +The suite had 8 failures on the run; each was classified as flaky/environmental, +by-design, or pre-existing on master — none attributable to this change. In particular +the timed-message failure (`both users have configured timed messages … restart`) was +reproduced identically on master with the fix reverted (3/3), proving it pre-existing; +it is a direct-chat test that never touches the group-scope read path. diff --git a/plans/2026-06-12-fix-migrate-text-overlap.md b/plans/2026-06-12-fix-migrate-text-overlap.md new file mode 100644 index 0000000000..1bb1d3a5c6 --- /dev/null +++ b/plans/2026-06-12-fix-migrate-text-overlap.md @@ -0,0 +1,71 @@ +# Fix overlapping warning texts after finalizing migration + +Branch: `nd/fix-migrate-text` · regression from PR [#6777](https://github.com/simplex-chat/simplex-chat/pull/6777) (`df5ea3d46`, new settings section design). + +## 1. Problem statement + +On the "Migrate device" screen (Android and desktop), after tapping **Finalize migration** the finished state renders broken: the two warning texts — "You **must not** use the same database on two devices." and "**Please note**: using the same database on two devices will break the decryption of messages…" — are painted on top of each other and on top of the "Migration complete" section card, directly under the section header. + +Reproduced on desktop with default settings. The screen immediately before (`LinkShownView`, with the QR code) renders correctly. + +## 2. Solution summary + +Move the two `SectionTextFooter` calls in `FinishedView` out of the `Box` and place them after it, so they render as sequential children of the screen's scroll `Column` — the same placement `LinkShownView` already uses for its footers. + +```diff + } +- SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) +- SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) + if (chatDeletion) { + ProgressView() + } + } ++ SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device)) ++ SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption)) + } +``` + +Total diff: 1 file, 2 lines moved (+2 / −2 at different indentation). + +## 3. Root cause + +PR #6777 added card chrome to `SectionView` and, in a sub-commit ("Migrate views: move all SectionTextFooter / SectionSpacer out of SectionView lambdas"), moved footers out of the card lambdas so they read as captions below the cards. In `FinishedView` (`MigrateFromDevice.kt`) the footers were moved out of the `SectionView` — but left **inside the wrapping `Box`**: + +```kotlin +Box { + SectionView(stringResource(MR.strings.migrate_from_device_migration_complete)) { + // "Start chat" / "Delete database" buttons + } + SectionTextFooter(…you_must_not_start_database_on_two_device…) // Box child 2 + SectionTextFooter(…using_on_two_device_breaks_encryption…) // Box child 3 + if (chatDeletion) { + ProgressView() // Box child 4 (overlay) + } +} +``` + +That `Box` exists for exactly one reason: to overlay `ProgressView` (a fullscreen-centered spinner) over the section while the chat database is being deleted. `Box` stacks its children at `TopStart`, so both footers render at the Box's top-left corner — over the card's top edge and over each other. This is the only migration sub-view where the refactor produced this shape: the other `Box`-wrapped states keep all flow content inside one child (a `SectionView` or an inner `Column`), and `LinkShownView` has no `Box` at all. + +`FinishedView` is composed inside `ColumnWithScrollBar` (via `SectionByState`), so composables emitted at the function's top level land in the scroll `Column` and stack vertically — which is where the footers belong. + +## 4. The fix in detail, and why this shape + +Three candidate fixes were compared: + +- **Move the 2 footer lines after the `Box`** (chosen). Smallest possible diff, zero re-indentation. Footers become siblings of the Box in the scroll `Column`, identical to the working `LinkShownView` pattern in the same file. `ProgressView` keeps its overlay semantics unchanged (same as `DatabaseInitView`, `ArchivingView`, `LinkCreationView`). Only behavioural delta beyond the bug fix: during the transient `chatDeletion` spinner, the overlay centers over the card rather than card + footers — matching every other migration sub-view. +- **Wrap card + footers in a `Column` inside the Box.** Behaviorally near-identical, but ~40 lines of indentation churn and a layout shape no sibling view uses. Rejected: larger diff, no benefit. +- **Also hoist `ProgressView` out of the Box.** Changes overlay semantics (spinner would flow below content instead of over it). Rejected: touches behavior the bug report doesn't concern. + +Regression risk: the change is placement-only — no logic, no state, no measurement changes. The new arrangement is the proven pattern of the adjacent view. + +## 5. Scope verification — no other instances of the bug class + +The class ("flow content as direct children of an overlay `Box`") was searched for across all Kotlin source sets (`commonMain`, `androidMain`, `desktopMain`, `android`, `desktop`) with three complementary structural scans: + +1. Every `Box` block with ≥2 stacking flow children (section views, footers, spacers, settings items): **only** `FinishedView`. +2. All 384 footer/spacer call sites classified by nearest enclosing block: the only ones directly inside a `Box` are the two fixed lines. +3. All ~25 composable functions that emit footers at function top level (placement decided by caller): no caller invokes them inside a `Box`. + +iOS is structurally immune: SwiftUI footers are part of `Section { } footer: { }` inside a `List`; `MigrateFromDevice.swift`'s `finishedView` was verified correct. + +Related but distinct (not fixed here): 10 `SectionTextFooter` calls app-wide still sit *inside* `SectionView` card lambdas (6 in migration views, plus `LinkAMobileView`, `ConnectMobileView`, 2 in `NetworkAndServers`), rendering inside the white card instead of as captions below it. Cosmetic placement inconsistency with #6777's stated pattern, no overlap — left for a separate change if desired. diff --git a/plans/2026-06-15-fix-cli-outdated-help.md b/plans/2026-06-15-fix-cli-outdated-help.md new file mode 100644 index 0000000000..e0105ef5bb --- /dev/null +++ b/plans/2026-06-15-fix-cli-outdated-help.md @@ -0,0 +1,42 @@ +# Remove CLI help entries for long-removed commands + +Branch: `nd/fix-cli-outdated-help` · file `src/Simplex/Chat/Help.hs`. + +## 1. Problem statement + +Typing `/get stats` in the terminal CLI does nothing useful — it is documented in `/help` but no parser accepts it, so it fails to parse. Investigation found this is not isolated: four documented commands no longer exist in the parser. + +## 2. Solution summary + +Remove the four stale entries (five lines, including one continuation note) from `Help.hs`: + +- `/pq @ on/off` + its "(both have to enable…)" note — `contactsHelpInfo` +- `/pq on/off` — `settingsInfo` +- `/get stats` — `settingsInfo` +- `/reset stats` — `settingsInfo` + +The stats pair were the tail of `settingsInfo`, so the now-orphaned trailing comma on the preceding `/(un)mute #` element is also dropped to keep the list literal valid. + +No replacement text is added: PQ has no command (it is automatic), and the stats functionality has no argument-compatible successor (see §4). + +## 3. Root cause + +Both removals were core changes that deleted parser, handler, and command constructor but left `Help.hs` untouched: + +- **`/pq` (both forms)** — commit `756779186` "core: enable PQ encryption for contacts (#4049)", 2024-04-22. It removed the parsers `"/pq @" *> (SetContactPQ …)` and `"/pq " *> (APISetPQEncryption …)`; post-quantum encryption for contacts became automatic, so the manual toggle was obsolete. `SetContactPQ` and `APISetPQEncryption` no longer exist in `src/`. +- **`/get stats` / `/reset stats`** — commit `5907d8bd0` "core: remove legacy agent stats (#4375)", 2024-07-01. It removed the parsers `"/get stats" $> GetAgentStats` and `"/reset stats" $> ResetAgentStats`, their handlers, the `GetAgentStats`/`ResetAgentStats` constructors in `Controller.hs`, and the `View.hs` rendering — but its diff touched `Chat.hs`, `Controller.hs`, `View.hs`, `cabal.project`, `sha256map.nix`, not `Help.hs`. + +In both cases the help text became a promise the binary could no longer keep. + +## 4. Scope verification — no other stale entries, no replacements documented + +All 120 commands documented across every section of `Help.hs` were extracted and matched against the parser string literals in `Library/Commands.hs` (`chatCommandP`). Every entry resolves to a live parser except the four above. ~10 entries that a naive prefix match flagged were manually confirmed valid: incognito-suffix forms parsed by `incognitoP` (`/accept incognito`, `/connect incognito`, `/simplex incognito`), usage examples (`/file bob ./photo.jpg`, `/group team`), and inline sub-alternatives (`/start remote host new`, `/stop remote host new`, `/switch remote host local`, `/chats all`). + +Why no replacement text: + +- **PQ** — there is no command; encryption is negotiated automatically. Documenting nothing is correct. +- **Stats** — the nearest live commands are `/get servers summary ` and `/reset servers stats`, but they require a `userId` argument and return the agent servers summary, not the old argument-less usage statistics. They were never in CLI help; adding them is a separate documentation enhancement, deliberately out of scope for a "remove what no longer exists" fix. + +## 5. Why this shape + +Pure deletion of dead documentation — no behavioral change, smallest diff that makes `/help` truthful. Comma handling is the only subtlety: the `/pq @` and `/pq on/off` removals sit before comma-bearing neighbors (a `""` separator and `/network` respectively) and need no adjustment; the `/get stats` + `/reset stats` removal makes `/(un)mute #` the last `settingsInfo` element, so its trailing comma is removed to avoid a dangling-comma parse error before `]`. diff --git a/plans/2026-06-15-fix-file-upload-long-name.md b/plans/2026-06-15-fix-file-upload-long-name.md new file mode 100644 index 0000000000..f0917783bd --- /dev/null +++ b/plans/2026-06-15-fix-file-upload-long-name.md @@ -0,0 +1,77 @@ +# Fix: long file name hides the close icon in the compose file preview + +Date: 2026-06-15 +Branch: `nd/fix-file-upload-with-long-name` +Platforms affected: Android, Desktop, iOS + +## Problem + +When a file is attached for sending, the compose area shows a preview row with the +file icon, the file name, and a close (X) icon to cancel/remove the file before +sending. If the file name is long, the close icon is not shown, so the user cannot +dismiss the attachment. + +## Cause + +The bug is the same layout defect on both codebases: the file-name text is +unconstrained, so a long name consumes all horizontal space and squeezes the +trailing close button to zero width. + +### Android / Desktop — `ComposeFileView.kt` + +The row was laid out as: + +``` +Icon(fixed) | Text(fileName) | Spacer(weight 1f) | IconButton(close) + ^ unweighted, no maxLines +``` + +In a Compose `Row`, unweighted children are measured first and take the remaining +width before weighted children get anything. The unweighted `Text` therefore grabbed +the whole remaining width on a long name, leaving the weighted `Spacer` — and the +`IconButton` after it — with ~0 width. The flexible element was the `Spacer`, but a +`Spacer` can only distribute the space the rigid `Text` did not already eat. + +### iOS — `ComposeFileView.swift` + +``` +Image(fixed) | Text(fileName) | Spacer() | Button(close) + ^ no lineLimit +``` + +A `Text` with no `lineLimit` reports its full single-line ideal width and refuses to +truncate, so a long name collapses the `Spacer` and pushes the `Button` past the +`.frame(maxWidth: .infinity)` edge, off-screen. + +## Fix + +Make the file name the element that yields space and let it truncate, so the +fixed-size close control's space is always reserved. + +- **Kotlin:** give the `Text` the `weight(1f)` (instead of the `Spacer`) and + `maxLines = 1`, and drop the now-redundant `Spacer`. This matches the existing + idiom — `ComposeImageView` puts `weight(1f)` on its content, and `CIFileView` + caps file-name text with `maxLines = 1`. +- **Swift:** add `.lineLimit(1)` to the `Text`, so it truncates instead of + overflowing, matching how file names are shown elsewhere on iOS. + +## Why this is the right fix (not a workaround) + +`ComposeFileView` was the only compose preview that gave the weight to a `Spacer` +rather than to its content; every sibling preview (`ComposeImageView`, +`ContextItemView`) reserves space for the trailing close control by weighting the +content. The change brings the file preview in line with the established pattern +rather than adding a special case. It is purely structural — no behavior changes +beyond layout. + +## Scope / risk + +- One-spot edit per file; no API or behavior change. +- Android and Desktop share the Kotlin file, so both are fixed together; iOS is the + separate Swift file. +- No string/translation keys touched. + +## Verification + +- Visual: attach a file with a very long name on Android, Desktop, and iOS; confirm + the name truncates and the close (X) icon stays visible and tappable. diff --git a/plans/2026-06-17-fix-group-garbled-error.md b/plans/2026-06-17-fix-group-garbled-error.md new file mode 100644 index 0000000000..c660d13a48 --- /dev/null +++ b/plans/2026-06-17-fix-group-garbled-error.md @@ -0,0 +1,27 @@ +# Fix garbled error when saving group profile (member admission) + +## Problem + +Saving a group profile change — e.g. enabling member admission (Review = "All") from Group preferences → Member admission — can fail with an unreadable alert: + +``` +chat.simplex.common.model.API$Error@3ea295c.err +``` + +The user sees an object reference instead of the actual error, so there is no way to tell what went wrong. + +## Cause + +In `apiUpdateGroup` the `API.Error` branch builds the alert message with `"$r.err"` (`SimpleXAPI.kt:2292`). In a Kotlin string template `"$r.err"` interpolates `r.toString()` — and `API.Error` has no custom `toString`, so it yields `chat.simplex.common.model.API$Error@` — then appends the literal text `.err`. The meaningful message (`r.err.string`) is never read. + +This surfaces whenever the core rejects the update. A concrete trigger is a **desynced member role**: the client shows the Save controls because `groupInfo.isOwner` is true, but the core's `assertUserGroupRole gInfo GROwner` (`Commands.hs:3840`) disagrees and returns `CEGroupUserRole`. The display bug then hides which error it was. + +## Fix + +Render the error message instead of the object reference: + +```kotlin +AlertManager.shared.showAlertMsg(generalGetString(errorTitle), "${r.err.string}") +``` + +One-line change in `SimpleXAPI.kt`. This is the only occurrence of the `"$r.err"` pattern in the codebase. The underlying core rejection is unchanged — but it is now shown clearly to the user. diff --git a/plans/2026-06-17-message-info-file-xftp-servers.md b/plans/2026-06-17-message-info-file-xftp-servers.md new file mode 100644 index 0000000000..f7803984cf --- /dev/null +++ b/plans/2026-06-17-message-info-file-xftp-servers.md @@ -0,0 +1,140 @@ +# Plan: Show XFTP servers used for a file in Message Info + +## Goal +In the message-info screen, when the message contains a file, show the list of XFTP servers that hosted the file's chunks ("servers used to upload the file"). + +## Why this is needed +For transparency: a recipient downloads a file's chunks from XFTP relays without any visible indication of which servers those are. Surfacing them in message info lets a user see exactly what servers they are downloading from (and, for sent files, uploading to) whenever they want to know — useful for trust, debugging, and deciding whether to download. + +## Decisions (confirmed) +- **Direction:** Both sent and received files. +- **Visibility:** Always shown when the item has a file (not gated behind Developer tools). +- **Platform:** Android, desktop, and iOS. + +## Why this needs a core change (not Kotlin-only) +The server list is **already known to the app** — the file description (which lists each chunk's XFTP server replicas) is what the agent uses to upload/download. But that data lives in the **core (Haskell) layer**: it's stored in the `files` table (`private_snd_file_descr` for sent, the rcv file descr row for received) and parsed inside the core. It is **never surfaced across the core→client API boundary**: the client-facing `CIFile` and `ChatItemInfo` types carry no server info. So this is a **plumbing task** (expose existing data through the API response), not an algorithmic one. The core already has the extraction helper. + +## Existing building blocks (no new logic needed) +- `Internal.hs:764-766` — `fileServers` extracts the unique `[XFTPServer]` from a parsed description's chunk replicas. Currently a local `where` helper; will be lifted to a reusable top-level function. +- `Internal.hs:747` — `parseFileDescription :: Text -> CM (ValidFileDescription 'FRecipient)`. +- `Store/Files.hs:42` — `getRcvFileDescrByRcvFileId` returns `RcvFileDescr {fileDescrText, fileDescrComplete}` (received files). +- `Store/Files.hs:197` — `setSndFTPrivateSndDescr` stores `private_snd_file_descr` (sent files). A matching getter must be added (none exists today). +- `XFTPServer` already JSON-encodes to a string on the wire (see `XFTPServerSummary.xftpServer :: XFTPServer` → Kotlin `xftpServer: String`), so the Kotlin field can be `List`. + +--- + +## Implementation + +### 1. Core — extraction helper (`src/Simplex/Chat/Library/Internal.hs`) +- Lift `fileServers` out of `receiveViaCompleteFD`'s `where` block to a top-level function, generalized to work on any party's description chunks: + ```haskell + fileDescrServers :: FD.FileDescription p -> [XFTPServer] + fileDescrServers FD.FileDescription {chunks} = + S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks + ``` + (Keep `receiveViaCompleteFD` working by calling the lifted helper.) +- Add a convenience that resolves a file's servers by direction, returning `[]` when no XFTP description exists (inline/legacy files): + ```haskell + getChatItemFileServers :: User -> ChatItem c d -> CM [XFTPServer] + ``` + - For **received** (`SMDRcv`) with an XFTP file: `getRcvFileDescrByRcvFileId` → `parseFileDescription` → `fileDescrServers`. + - For **sent** (`SMDSnd`) with an XFTP file: read `private_snd_file_descr` (new getter) → parse the sender description → `fileDescrServers`. + - Guard on `fileProtocol == FPXFTP`; wrap parse in tolerant error handling so a missing/partial descr yields `[]` (no crash, no section). + +### 2. Core — store getter (`src/Simplex/Chat/Store/Files.hs`) +- Add `getSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> IO (Maybe Text)` reading `private_snd_file_descr` from `files` (mirror of `setSndFTPrivateSndDescr`; column already selected at `Files.hs:799`). Export it. + +### 3. Core — extend `ChatItemInfo` (`src/Simplex/Chat/Messages.hs`) +- Add field: + ```haskell + data ChatItemInfo = ChatItemInfo + { itemVersions :: [ChatItemVersion], + memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus), + forwardedFromChatItem :: Maybe AChatItem, + fileXftpServers :: [XFTPServer] -- NEW (empty when no file / not XFTP) + } + ``` +- `$(JQ.deriveJSON defaultJSON ''ChatItemInfo)` at `Messages.hs:1531` regenerates the instance automatically. Import `XFTPServer` if not already in scope. +- Note: using `[XFTPServer]` (not `Maybe`) keeps the JSON additive and the Kotlin side simple; empty list = nothing to show. + +### 4. Core — populate in handler (`src/Simplex/Chat/Library/Commands.hs:619`) +- In `APIGetChatItemInfo`, after computing `forwardedFromChatItem`: + ```haskell + fileXftpServers <- getChatItemFileServers user ci + pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem, fileXftpServers} + ``` + +### 5. Kotlin model (`apps/multiplatform/.../model/ChatModel.kt:5207`) +```kotlin +class ChatItemInfo( + val itemVersions: List, + val memberDeliveryStatuses: List?, + val forwardedFromChatItem: AChatItem?, + val fileXftpServers: List = emptyList() // NEW; server hosts as strings +) +``` +(`XFTPServer` serializes to a string, matching the existing `XFTPServerSummary.xftpServer: String`.) + +### 6. Desktop/shared UI (`apps/multiplatform/.../views/chat/ChatItemInfoView.kt`) +- In `Details()` (`:250-283`), after the existing file-status block, add (always-visible, when present): + ```kotlin + if (ci.file != null && ciInfo.fileXftpServers.isNotEmpty()) { + // section header + one InfoRow/host per server + } + ``` + - Render as its own `SectionView` with a title (e.g. "File servers") and one row per server host, styled like the existing info rows. For long host strings, single-line with ellipsis (consistent with the long-name handling already on this branch). +- Add the servers to the shareable text in `itemInfoShareText` (`:534-580`), near the existing file-status share line (`:564-565`), so "Share" / copy includes them. + +### 7. Translations (additive only — never rename/remove keys) +- Add one new key to `apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml`, e.g.: + ```xml + File servers + ``` + Base locale only; Weblate fans out to other locales. Do not touch existing keys. + +### 8. iOS model (`apps/ios/SimpleXChat/ChatTypes.swift:5773`) +Same core change (shared `libsimplex`), so iOS only needs the Swift model + view. Add the optional field (optional so an absent key never breaks decoding): +```swift +public struct ChatItemInfo: Decodable, Hashable { + public var itemVersions: [ChatItemVersion] + public var memberDeliveryStatuses: [MemberDeliveryStatus]? + public var forwardedFromChatItem: AChatItem? + public var fileXftpServers: [String]? // NEW +} +``` + +### 9. iOS UI (`apps/ios/Shared/Views/Chat/ChatItemInfoView.swift`) +- In `details()`, after the `developerTools` block (always-visible when present): + ```swift + if ci.file != nil, let servers = chatItemInfo?.fileXftpServers, !servers.isEmpty { + infoRow("File servers", servers.map(serverHostname).joined(separator: "\n")) + } + ``` + `serverHostname(_:)` is public in `SimpleXChat` (`ErrorAlert.swift:142`). +- Add the same to `itemInfoShareText()` (joined with ", "). Strings are inline `NSLocalizedString` (no separate resource file to edit). + +--- + +## Edge cases / behavior +- **Inline / legacy files** (no XFTP description): `fileXftpServers == []` → section hidden. No crash. +- **Sent file before upload completes** (`private_snd_file_descr` not yet set): `[]` → hidden until available. +- **Received file not yet accepted/downloaded:** the rcv description exists as soon as the offer is received (it's what enables download), so servers are available even before download. ✓ +- **Multiple chunks across different servers:** deduped via `S.fromList`; all distinct servers listed. +- **Large files (any size up to the limits):** servers are accurate regardless of size. Chat-message files **never** use XFTP redirect — the description is split across multiple `XMsgFileDescr` messages (`splitFileDescr`) and reassembled in full by the recipient (`appendRcvFD`), so a received chat file always carries the real data-chunk servers. (`maxFileSize` = 1 GB soft, `maxFileSizeHard` = 5 GB; XFTP chunk sizes 64 KB / 256 KB / 1 MB / 4 MB.) +- **Redirect descriptions:** XFTP redirect (a small `redirect = Just` description whose chunks point to the relay hosting the real description) is used **only** for *standalone file links* (`SFDONE` `Nothing` branch → `xftpSndFileRedirect`), which have no chat item and never appear in message info. The chat-message branch never redirects, so no redirect description can reach `getChatItemFileServers` — no guard is needed. + +## Build / verification +- **Heaviest step:** rebuild the native core lib (`libsimplex`) consumed by the multiplatform app, since core types changed. Without the rebuild, the new JSON field is simply ignored by the old lib (the Kotlin default `emptyList()` keeps it backward-compatible — section just won't appear). +- Tests: a core unit/integration check that `APIGetChatItemInfo` returns a non-empty `fileXftpServers` for an XFTP file (sent and received), empty for a text message and for an inline file. +- Manual: send a file → open message info → confirm the server list; repeat on the receiving device for the received item. + +## Touch list +- `src/Simplex/Chat/Library/Internal.hs` — lift `fileDescrServers`, add `getChatItemFileServers`. +- `src/Simplex/Chat/Store/Files.hs` — add `getSndFTPrivateSndDescr` (+ export). +- `src/Simplex/Chat/Messages.hs` — add `fileXftpServers` to `ChatItemInfo`. +- `src/Simplex/Chat/Library/Commands.hs` — populate it in `APIGetChatItemInfo`. +- `apps/multiplatform/.../model/ChatModel.kt` — add Kotlin field. +- `apps/multiplatform/.../views/chat/ChatItemInfoView.kt` — UI section + share text. +- `apps/multiplatform/.../resources/MR/base/strings.xml` — new label key. +- `apps/ios/SimpleXChat/ChatTypes.swift` — add `fileXftpServers` to the Swift `ChatItemInfo`. +- `apps/ios/Shared/Views/Chat/ChatItemInfoView.swift` — UI row in `details()` + `itemInfoShareText()`. diff --git a/plans/2026-06-19-channel-received-remove-right-gap.md b/plans/2026-06-19-channel-received-remove-right-gap.md new file mode 100644 index 0000000000..5ada84b2b0 --- /dev/null +++ b/plans/2026-06-19-channel-received-remove-right-gap.md @@ -0,0 +1,95 @@ +# Remove the right gap on received messages in channels + +## Problem + +In groups, received messages are laid out as left-aligned chat bubbles whose +maximum width is capped well short of the right edge, leaving a large empty gap +on the right so long content wraps early. In channels this wastes horizontal +space — channel posts are broadcast/feed-style content that reads better using +nearly the full row width. + +## Change + +For channels only, received messages drop the right-side gap so content can use +nearly the full row width (a small edge margin remains). This only changes the +maximum available width: long text uses more of the row, short messages still +size to content, and media stays within its existing cap. Sent messages keep +their existing layout. + +### Android / desktop (`apps/multiplatform`) + +The `end` padding becomes `12.dp` (the same edge margin sent messages use) +instead of `adjustTailPaddingOffset(66.dp, …)`, at the four received-message +layout sites in `ChatItemsList` (`ChatView.kt`): the `GroupRcv` +(member-attributed) and `ChannelRcv` (unattributed) branches, each with and +without an avatar. + +```kotlin +end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp + else adjustTailPaddingOffset(66.dp, start = false) +``` + +### iOS (`apps/ios`) + +iOS computes one per-message `maxWidth` in `ChatView.swift` and applies it to +every bubble; the `* 0.84` factor is the gap. For a received message in a +channel that factor is dropped (full width minus the avatar inset) — the same +geometry the voice-message case already uses: + +```swift +let channelReceived = !ci.chatDir.sent && cInfo.isChannel +let maxWidth = cInfo.chatType == .group +? voiceNoFrame || channelReceived +? (g.size.width - 28) - 42 +: (g.size.width - 28) * 0.84 - 42 +: ... +``` + +The received check (`!ci.chatDir.sent`) is explicit here because, unlike the +Kotlin layout (which has a separate received branch), iOS shares one `maxWidth` +between sent and received. + +## Why gate on `ChatInfo.isChannel` (`useRelays`) + +The change is gated per chat on `ChatInfo.isChannel`, which is +`groupInfo?.useRelays == true` — `chatInfo.isChannel` on both Android/desktop and +iOS (`cInfo.isChannel`). + +This is the robust signal. The whole channel feature on both platforms keys on +`useRelays` (channel preferences, member management, info view, broadcast +compose, etc.); `useRelays` is a non-optional `Bool` that is always present on a +group. + +- **Not on the group-type `isChannel`** (`publicGroup?.groupType == channel`). + This was the first attempt and it left the gap in place on iOS. The likely + mechanism: `publicGroup` is an optional reconstructed from nullable DB columns + (`src/Simplex/Chat/Store/Groups.hs` `toGroupProfile`, plus a creation path that + sets `publicGroup = Nothing`), so when it is not populated for a chat the + optional chain silently evaluates to `false` and the gap is never removed. + `useRelays` cannot fail this way — it is a required `Bool` set at group + creation (`useRelays = not direct`, `Commands.hs:2080`). Independent of the + exact mechanism, `useRelays` is the safer signal. It is also as precise: the + only group type ever constructed is `GTChannel` (`GTGroup` is defined but never + instantiated), and `useRelays == true` is set on exactly that same + public-group/channel path, so `useRelays == true` ⟺ "is a channel" for every + chat today — regular groups, business chats and direct chats all have + `useRelays` false/absent (verified: no non-channel path sets it true). +- **Not on the item direction.** The unattributed `ChannelRcv` direction is + produced for any group message without an attributed member, not only in + channels, and channels also contain member-attributed (`GroupRcv`) posts. + Gating on direction would both over- and under-match, so the gate is the + per-chat `isChannel`. + +## Scope + +Regular groups, business chats, and direct chats are unchanged (`isChannel` is +false for them). Sent messages are untouched. + +## Verification + +- Android/desktop: `:common:compileKotlinDesktop` compiles clean. +- iOS: change is a small, type-safe Swift expression; build/verify on macOS + (Xcode) — not compilable on the Linux build host used here. +- Visual (both platforms): in a channel, long received messages widen toward the + right edge; in a regular group and in direct chats the right gap is unchanged; + sent messages are unchanged everywhere. diff --git a/plans/2026-06-20-channel-received-no-avatar-left-padding.md b/plans/2026-06-20-channel-received-no-avatar-left-padding.md new file mode 100644 index 0000000000..13ee4ebc4f --- /dev/null +++ b/plans/2026-06-20-channel-received-no-avatar-left-padding.md @@ -0,0 +1,103 @@ +# Remove left padding on consecutive (no-avatar) received messages in channels + +## Problem + +In a channel, received messages show the sender avatar on the first message of a +run and hide it on consecutive messages, but those consecutive messages still +reserve the avatar-sized **left padding** so they line up under the first. For a +channel's feed-style layout this indentation wastes horizontal space — +consecutive received messages should sit flush-left where the avatar would be. +This applies to **both** the channel owner's broadcasts and contributors' posts. + +Desired behaviour: in channels, any received message that does **not** show an +avatar (a consecutive post from the same sender) drops the avatar-sized left +padding. The first message of a run still shows the avatar and keeps its layout; +when the run is broken (a different sender, or a time gap), the next message +shows the avatar again — this run logic is unchanged, only the no-avatar left +padding is reduced. + +## The two received directions in a channel + +Received items in a channel arrive as one of two directions +(`Subscriber.hs`, `saveRcvCI`): + +- **`ChannelRcv`** (no member) — the **owner's** broadcast, sent "as the channel". + The backend permits sending-as-group only to the owner, and a channel owner's + main-scope messages are always sent as group (`ChatInfo.sendAsGroup` is true + for `useRelays && memberRole >= Owner` in the main scope), so received owner + posts arrive as `ChannelRcv`. Shows the channel avatar. +- **`GroupRcv(member)`** (attributed) — a **contributor's** post, carrying the + member. Shows the member avatar. + +Both are received messages, and the change now applies to **both** when they hide +the avatar. (An earlier revision scoped this to `ChannelRcv`/owner only; it now +covers contributors too, per request.) + +## Change + +In channels — gated on `ChatInfo.isChannel` (the `useRelays` flag, which is +reliably present, unlike the optional group-type predicate) — the no-avatar +branches for **both** `ChannelRcv` and `GroupRcv` drop the avatar-sized left +padding down to the base inset where the avatar itself starts. In non-channel +groups the `GroupRcv` no-avatar layout is unchanged (`isChannel` is false). The +avatar-shown layouts, sent messages, and all other chats are unchanged. + +The same Row's `end` padding is already gated on `chatInfo.isChannel` (the merged +right-gap change #7106), so gating `start` on `isChannel` keeps each Row +internally consistent and the change precisely "in channels". + +### Android / desktop (`apps/multiplatform`, `ChatView.kt`, `ChatItemsList`) + +Both the `CIDirection.GroupRcv` and `CIDirection.ChannelRcv` `showAvatar == false` +rows: + +```kotlin +// before +.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …) +// after +.padding(start = if (chatInfo.isChannel) 8.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …) +``` + +### iOS (`apps/ios`, `ChatView.swift`, `chatItemListView`) + +Both the `.groupRcv` and `.channelRcv` no-avatar branches: + +```swift +// before +.padding(.leading, 10 + memberImageSize + 12) +// after +.padding(.leading, chat.chatInfo.isChannel ? 12 : 10 + memberImageSize + 12) +``` + +## Run behaviour (unchanged) + +`shouldShowAvatar(current, older)` shows the avatar on the first message of a +same-sender run and hides it on consecutive ones; a different sender or a gap +resets the run. For `GroupRcv` "same sender" is the same `memberId`; for +`ChannelRcv` consecutive channel broadcasts count as the same sender. Only the +no-avatar left padding is changed. + +## Scope + +- Affects: all received consecutive (no-avatar) messages **in channels** — owner + broadcasts (`ChannelRcv`) and contributor posts (`GroupRcv`). This includes a + channel's member-support sub-scope, which renders through the same + `ChatItemsList` with the channel's `isChannel`; treating it the same way is + consistent with the merged right-gap change (#7106), which also gates that + Row's `end` padding on `isChannel` without a scope filter. +- Unchanged: the first message of each run (avatar shown), sent messages, regular + groups, business chats and direct chats (`isChannel` false — the `else` branch + preserves the original avatar-inset value exactly), and any non-channel + `ChannelRcv` welcome item. + +## Verification + +- Android/desktop: `:common:compileKotlinDesktop` compiles clean. +- iOS: small, type-safe constant change; build/verify on macOS (Xcode) — not + compilable on the Linux build host used here. +- Visual (both platforms), in a channel: + - First message of a run (owner or contributor): avatar shown, layout unchanged. + - Following messages from the same sender (no avatar): now flush-left. + - A different sender / time gap resets the run — the next message shows the + avatar again. + - Regular groups, business and direct chats keep their existing indentation. diff --git a/plans/2026-06-22-fix-card-section-stray-dividers.md b/plans/2026-06-22-fix-card-section-stray-dividers.md new file mode 100644 index 0000000000..72beec92bd --- /dev/null +++ b/plans/2026-06-22-fix-card-section-stray-dividers.md @@ -0,0 +1,40 @@ +# Fix stray card dividers in server info, connect-to-desktop, appearance and migration screens + +Branch: `nd/fix-ui-lines` · base: `master`. + +## Problem + +On several card screens (Android and desktop, current `master`), near-black horizontal lines appear inside section cards where they don't belong — most visible in dark/black themes: + +- **Servers info → tap an SMP or XFTP server**: the server-detail card is sliced by stray lines around its nested stats / subscriptions / sessions blocks. +- **Use from desktop → "Connected to desktop"** (also the connecting / found / verify states): two black lines between the desktop name and its version (e.g. "Desktop" and "v7.0.0.1"). +- **Settings → Appearance** with a custom image wallpaper: a double line with a gap between "Remove image" and "Color mode". +- **Migrate from this device**, "error stopping chat" state: a double line between the error text and the stop-chat action. + +Behaviour is correct everywhere; the bugs are purely visual. + +## Cause + +PR #6777 (`df5ea3d46`, "android, desktop: new settings section design") wrapped `SectionView`'s content in `CardColumnLayout`, which draws a 2 dp divider (`canvasColorForCurrentTheme()`, near-black in dark/black themes) at the bottom of every direct child except the last. The same PR turned these screens into card screens (`ModalView(cardScreen = true)`) but left three content patterns that pre-date the card chrome, each of which now produces stray lines: + +1. **Nested `SectionView`s** inside one outer `SectionView` (server detail) → card-in-card, plus a line around every nested block and spacer. +2. **Loose `Text` + `Spacer` + `Text`** as separate children of a card `SectionView` (connect-to-desktop name + version) → a line after the text and after the spacer. +3. **`SectionDividerSpaced()` as a middle child** (Appearance, migration) → on a card screen this is a 30 dp `Spacer`, so it gets a line above *and* below = a double line bracketing a gap. + +This is the same bug class as `2026-05-25-fix-e2e-encryption-section-divider.md` (#7012); #6777 reintroduced further instances that earlier fix did not cover. + +## Fix + +`apps/multiplatform/common/...`, layout-only, matching the reference sibling pattern already used by `DetailedSMPStatsLayout`: + +- **`ServersSummaryView.kt`** — `SMPServerSummaryLayout` / `XFTPServerSummaryLayout`: stop wrapping everything in one outer `SectionView`; make the address its own card and each sub-section a top-level sibling separated by `SectionDividerSpaced()` placed *between* them. `SMPSubscriptionsSection`'s rows are wrapped in their own `SectionView` so they keep card chrome. +- **`ConnectDesktopView.kt`** — wrap each device-name + version block in a single `Column` (4 states), so the card sees one child and draws no internal divider. +- **`Appearance.kt`** / **`MigrateFromDevice.kt`** — delete the in-card `SectionDividerSpaced()`; the auto-divider between the two now-adjacent rows already separates them (the #6777 precedent for in-card spacers). + +## Risk + +- Layout-only: no logic, state, side-effect, or click-handler change. +- iOS is a separate codebase (native grouped lists) and is unaffected. +- Verified: `:common:compileKotlinDesktop` and the full desktop AppImage build succeed, and the running app renders the screens correctly. Independent adversarial review confirmed every conditional branch and separator is preserved across all stats/subs/sessions combinations. +- Deliberately out of scope: `SectionTextFooter`-inside-card instances (NetworkAndServers, Migrate*) — these render as a single normal row-separator above an in-card caption, not the stray-line bug; a separate #6777-style cleanup. +- Rollback: `git revert` the fix commit. diff --git a/plans/2026-06-22-roster-catchup-subscribers.md b/plans/2026-06-22-roster-catchup-subscribers.md new file mode 100644 index 0000000000..fd1979cda0 --- /dev/null +++ b/plans/2026-06-22-roster-catchup-subscribers.md @@ -0,0 +1,83 @@ +# Roster catch-up for channel subscribers + +Continues the public-groups roster work (privileged roster, member keys, `VersionRoster` monotonic gate, task-047's `Maybe VersionRoster` on `XGrpMemRole`/`XGrpMemDel`). + +## Problem + +A channel subscriber learns the full roster only on join and on resume (`serveRoster`, Subscriber.hs:1174/1269), then tracks it via forwarded `XGrpMemRole`/`XGrpMemDel` deltas, each carrying an incrementing `VersionRoster`. The gate (`applyAtRosterVersion`, Subscriber.hs:3248) accepts any delta at `v >= cur` and advances to `v`. + +If the subscriber misses intermediate deltas and then receives one at a version more than one above its known version, it advances to `v` and applies that one change — but the roster changes carried by the skipped versions (other members' roles, removals, keys) are lost until the next resume. The subscriber can then reject a freshly-promoted member's signed action (`RGEMsgBadSignature`) for a key it never learned. + +Catch-up: on detecting a gap, the subscriber asks the relay that forwarded the delta to re-serve the full roster, which carries the complete set and keys. + +## Design + +Four pieces. No schema change, no new store function. + +### 1. Owner sends the roster before the delta (the enabler) + +Today the owner sends `XGrpMemRole`/`XGrpMemDel` first, then `broadcastRoster` (Commands.hs:2754/2885). The relay forwards the delta to subscribers *before* its own roster-blob transfer completes, so for the whole transfer its stored `roster_blob` lags its `roster_version`. A catch-up request in that window serves a stale blob the subscriber rejects (`notBelowRoster`) — which is the common case, not an edge. + +Fix: in `APIMembersRole` / `APIRemoveMembers`, reorder to **apply the change to the owner's members → `broadcastRoster v` → send the delta**. This restructures `changeRoleCurrentMems` / `deleteMemsSend` to separate "apply to owner DB" from "send delta to relays" (the roster is still built after the change, so it reflects the new roles / excludes the removed member). + +Effect, relying on FIFO order of the owner→relay connection (a guarantee the roster design already assumes): the relay applies and stores the blob at `v` (`setGroupLiveRoster`) before it processes and forwards the delta at `v`. So a relay's `roster_version` always reflects its stored blob, and any request triggered by a forwarded delta finds a current blob. The now-redundant delta hits the existing no-op suppression (`fromRole == memRole`, Subscriber.hs:3298, added in task-047) and is still forwarded to subscribers — no new relay logic, subscribers still see the delta. + +Behavioral change to core delivery (all relays/subscribers) → its own commit. Implementation must confirm FIFO holds owner→relay (the test will expose a violation). Residual: a failed roster send (rare; already `catchAllErrors`) leaves that relay's gate briefly above its blob until the next change — heals on resume. + +### 2. New event `XGrpRosterRequest VersionRoster` (Protocol.hs) + +A directed subscriber→relay control message carrying the subscriber's pre-gap version, so the relay can skip serving when it holds nothing newer. + +```haskell +XGrpRosterRequest :: VersionRoster -> ChatMsgEvent 'Json +``` + +Wire plumbing (standard single-`VersionRoster` `'Json` event): GADT constructor; `CMEventTag` `XGrpRosterRequest_`; `strEncode` `"x.grp.roster.request"`; `strP` case; `toCMEventTag`; `appJsonToCM` (`XGrpRosterRequest <$> p "version"`); `chatToAppMessage` (`o ["version" .= v]`). NOT in `isForwardedGroupMsg` (point-to-point). NOT in `requiresSignature` (subscribers/observers have no key). + +### 3. Subscriber detects the gap and requests (Subscriber.hs, `applyAtRosterVersion`) + +`applyAtRosterVersion` already reads `cur`, compares `v >= cur`, advances the gate — the one path shared by `xGrpMemRole` and `xGrpMemDel`. In its accepting branch, when the receiver is a subscriber (`not (isUserGrpFwdRelay gInfo)`) and `cur = Just c` with `v > c + 1`, send `XGrpRosterRequest c` to the relay that forwarded the delta, then run the action unchanged (the delta is still applied; the roster heals `c+1 .. v-1`). + +To target the forwarding relay, thread it as `Maybe GroupMember` through `xGrpMemRole` / `xGrpMemDel` / `applyAtRosterVersion`: `Just m` on the forwarded path (`m` in scope in `processForwardedMsg`, Subscriber.hs:3804/3806), `Nothing` on the direct path (1092/1096; the receiver there is a relay and never requests). Send via `sendGroupMessage' user gInfo [relay] (XGrpRosterRequest c)`. + +One forwarding relay only, not all relays: avoids N² requests under concurrent multi-relay gaps, and the owner-signed roster needs no cross-relay verification. The gate advances to `v` on the first gap in a batch, so later `+1` deltas aren't gaps — normally one request per batch. + +### 4. Relay serves on request (Subscriber.hs, `processEvent` + new `xGrpRosterRequest`) + +The request arrives on the direct member connection, dispatched in `processEvent` (alongside other direct group events, ~Subscriber.hs:1103): + +```haskell +XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer +``` + +```haskell +xGrpRosterRequest :: GroupInfo -> GroupMember -> VersionRoster -> CM () +xGrpRosterRequest gInfo m reqVer = + when (isUserGrpFwdRelay gInfo) $ do + cur <- withStore' $ \db -> getGroupRosterVersion db gInfo + when (maybe True (> reqVer) cur) $ serveRoster user gInfo m +``` + +Reuses `serveRoster` unchanged (signed header + inline blob chunks to the one requester — the join/resume path). The version check serves only when the relay holds something newer than the requester, rate-limiting same-version spam. With piece 1, the served blob is current, so the subscriber accepts it (`notBelowRoster`) and `rosterCompletion` heals the skipped set and keys. `serveRoster` is a no-op without a stored roster. + +## Files touched + +- `src/Simplex/Chat/Library/Commands.hs` — reorder owner sends in `APIMembersRole`/`APIRemoveMembers` (restructure `changeRoleCurrentMems`/`deleteMemsSend`). +- `src/Simplex/Chat/Protocol.hs` — new event, end to end. +- `src/Simplex/Chat/Library/Subscriber.hs` — gap detection + request in `applyAtRosterVersion`; thread forwarding relay through `xGrpMemRole`/`xGrpMemDel`; new `xGrpRosterRequest`; one `processEvent` dispatch line. +- No schema change, no new store fn (reuses `getGroupRosterVersion`, `getGroupRelayMembers`, `getGroupRoster`, `serveRoster`). +- `tests/ChatTests/` — one channel catch-up test. + +## Tests + +A `channels` test asserting a subscriber that observes a version gap recovers the skipped set. The hard part is staging a deterministic gap (FIFO forwarding doesn't drop deltas). Candidate: two relays where an intermediate delta reaches only one path, so the subscriber receives a later delta at a jumped version; assert it emits exactly one `XGrpRosterRequest`, the relay re-serves, and a member promoted in the skipped interval is afterwards known (its signed action accepted, no `RGEMsgBadSignature`). Confirm the gap is reliably reproducible before finalizing; fall back to asserting "a forwarded delta at a jumped version triggers one request + re-serve" if the two-relay setup is flaky. Iterate with `-m "channels"`. + +## Commits + +1. Reorder owner sends (roster before delta) — relay blob is current before it forwards deltas. +2. Protocol: add `XGrpRosterRequest`. +3. Relay: `xGrpRosterRequest` + `processEvent` dispatch. +4. Subscriber: gap detection + request to the forwarding relay. +5. Test + schema/plan regen. + +Build and run `-m "channels"` after each. diff --git a/plans/2026-06-23-fix-windows-msi-update-running-app.md b/plans/2026-06-23-fix-windows-msi-update-running-app.md new file mode 100644 index 0000000000..d276dbfe6f --- /dev/null +++ b/plans/2026-06-23-fix-windows-msi-update-running-app.md @@ -0,0 +1,83 @@ +# Fix: Windows desktop fails to launch after in-app MSI update + +Issue: https://github.com/simplex-chat/simplex-chat/issues/7105 + +## Symptom + +After updating SimpleX Desktop on Windows via the in-app updater (MSI), the MSI reports +success but `SimpleX.exe` never launches afterwards — no window, no process in Task Manager, +and it persists across a reboot. No application crash is logged. Reported on Windows 11 +(6.5.4 → 7.0.0), and the reporter saw it on a prior update too. + +Event Viewer shows the cause: RestartManager 10010 (`SimpleX.exe … cannot be restarted - +Application SID does not match Conductor SID`) followed by MsiInstaller 1038 +(`requires a system restart … Reason for Restart: 1` = files in use). + +## Root cause + +The Windows branch of `installAppUpdate` (`AppUpdater.kt`) ran the MSI **while the app was +still running** and waited for it (`Runtime.getRuntime().exec("msiexec /i …").onExit().join()`). +The desktop MSI is a per-machine major upgrade (`desktop/build.gradle.kts`: +`perUserInstall = false`, fixed `upgradeUuid`), so it must replace `SimpleX.exe`, the bundled +JRE and the app DLLs in `C:\Program Files\SimpleX`. The running JVM holds those files open, so +the installer cannot replace them: Restart Manager can't close the app across the +user/elevated security boundary (the "SID does not match" event), the locked files are +deferred to `PendingFileRenameOperations`, and a reboot is requested. At reboot the deferred +uninstall-then-install operations leave the install directory inconsistent, so the executable +never starts. + +macOS and Linux are unaffected because those kernels allow a running executable's file to be +replaced by inode; Windows locks open files, so the app must release its locks before the MSI +replaces them. + +## Fix + +Launch the installer and **exit the app immediately** instead of waiting, so the files are +unlocked before the MSI reaches its file-replacement phase (which happens only after UAC +elevation and the installer's costing phase — well after the JVM is gone). With no running +instance there are no locked files, no reboot deferral and no Restart Manager conflict. + +Two small changes, both confined to the Windows path: + +1. `installAppUpdate` Windows branch: replace the `msiexec … .onExit().join()` + result + handling with `Runtime.getRuntime().exec(arrayOf("msiexec", "/i", file.absolutePath))` + followed by `exitProcess(0)`. The array form also fixes a latent bug where a space in the + path broke the single-string `exec`. +2. `downloadAsset`: before downloading, `if (desktopPlatform.isWindows()) File(tmpDir, + asset.name).delete()`. Because the app now exits before it can delete the installer, a + leftover MSI is removed at the next download instead of accumulating in the temp dir. + +The user reopens the updated app from the Start Menu (the MSI recreates the shortcut) — the +same hand-off the updater already uses for the `.deb` path and all failure branches. + +## Alternatives considered + +- **Helper script that waits for exit, installs, then relaunches** — adds a generated batch + with PID-polling, relaunch and self-delete; its only benefit over exiting is auto-relaunch, + which is not worth the complexity and platform-specific fragility. +- **Register the app with Restart Manager** so the elevated MSI can close/restart it — + jpackage apps don't register RM, the SID mismatch shows RM can't manage the process across + the elevation boundary anyway, and it is far more code. +- **Keep the app running with `REBOOT=ReallySuppress` / repair flags** — does not address the + lock; in-use files still defer to a reboot. + +## Scope / out of scope + +- In scope: the Windows branch of `installAppUpdate` and a Windows-guarded cleanup in + `downloadAsset`. macOS, Linux AppImage and `.deb` paths are unchanged. +- Trade-off accepted: the app no longer shows its own "installed successfully" dialog (it has + exited); msiexec shows its own progress UI and the relaunched app is the success signal. +- Out of scope (separate follow-ups): the macOS install branch ignores `renameTo` results; + download failures are only logged with no user-facing error; no signature/SHA verification + of the downloaded artifact. + +## Test plan (Windows VM) + +1. Install a per-machine MSI built at a version below the latest release. +2. Settings → Check for updates → download + Install the newer MSI. +3. Before the fix: RestartManager 10010 + MsiInstaller 1038 (Reason 1) appear and the app + fails to launch after reboot (reproduces #7105). +4. After the fix: the app exits, the MSI installs with no reboot request, and the new version + launches normally from the Start Menu; `C:\Program Files\SimpleX` is a complete install + with no queued `PendingFileRenameOperations`. +5. Re-run with a user profile path containing a space to confirm the array-form `exec` fix. diff --git a/plans/2026-06-23-wide-image-crash.md b/plans/2026-06-23-wide-image-crash.md new file mode 100644 index 0000000000..b409befda2 --- /dev/null +++ b/plans/2026-06-23-wide-image-crash.md @@ -0,0 +1,108 @@ +# Fix crash on opening a chat containing an extremely wide image + +## Problem + +Sending/receiving an image with an extreme aspect ratio (reproduced with a +**4000×1** image) makes the chat **unopenable**: every render of the chat throws + +``` +java.lang.IllegalArgumentException: Can't represent a width of 4660000 and height of 1165 in Constraints + at androidx.compose.foundation.layout.AspectRatioNode.measure(AspectRatio.kt:117) + ... + at chat.simplex.common.views.chat.item.FramedItemViewKt$PriorityLayout$1$1.measure(FramedItemView.kt:482) +``` + +Because the exception fires during measurement on every frame, the chat cannot +be opened again without clearing it. Affects **Android and desktop** (the Compose +`apps/multiplatform` UI). iOS is unaffected (see below). + +## Cause + +The framed image preview Box sizes itself with `Modifier.aspectRatio(...)` driven +by the image's real proportions +(`apps/multiplatform/.../chat/item/CIImageView.kt`): + +```kotlin +// before +Modifier.width(w).aspectRatio((previewBitmap.width.toFloat() / previewBitmap.height.toFloat()).coerceAtLeast(1f / 2.33f)) +``` + +The ratio was clamped only on the **low** side (`coerceAtLeast(1f / 2.33f)`, +added in #6959 to stop very *tall* previews overflowing the caption). The **high** +side was left unbounded. For a 4000×1 image the ratio is `4000`. + +During Compose's intrinsic-measurement pass, `AspectRatioNode` derives a fixed +`width = height × ratio`. Compose `Constraints` pack each dimension into at most +18 bits, so the maximum representable value is **262142 px**. With the observed +intrinsic height of 1165 px, `1165 × 4000 = 4,660,000` — ~18× over the limit — +and `Constraints.fixed(...)` throws. Any ratio above roughly `262142 / height` +(≈ 225 at this height) overflows; tall images never hit this because the existing +lower bound already caps their ratio. + +The bitmap decoders (`Images.android.kt`, `Images.desktop.kt`) only made this +reachable: they reject pathologically *tall* images +(`outHeight > outWidth * 256`) and any dimension `> 4320`, but have **no +symmetric wide guard**, so a 4000×1 image (width 4000 ≤ 4320, not tall) decodes +and reaches the unbounded `aspectRatio`. + +## Fix + +An initial fix (already merged) added a symmetric upper bound to the ratio, +`coerceIn(1f / 2.33f, 2.33f)`. That stops the crash but reshapes every image +wider than 2.33:1 — including legitimate panoramas — to 2.33:1. This change +**supersedes** it: stop routing the box height through `aspectRatio` and compute +it **directly**, so the dangerous `width = height × ratio` derivation never +happens. The wide side is then left at its **natural ratio** (no upper clamp); +only the tall side is capped at `2.33`, exactly as before. This mirrors what the +iOS app already does (`height = w × heightRatio`, `heightRatio = min(h / w, 2.33)`): + +```kotlin +// before (merged interim fix) +Modifier.width(w).aspectRatio((previewBitmap.width.toFloat() / previewBitmap.height.toFloat()).coerceIn(1f / 2.33f, 2.33f)) +// after +Modifier.width(w).height(w * (previewBitmap.height.toFloat() / previewBitmap.width.toFloat()).coerceAtMost(2.33f)) +``` + +With this form the box width is pinned (`≤ DEFAULT_MAX_IMAGE_WIDTH = 500.dp`) and +the height is `w × min(h / w, 2.33)`, which is always in `(0, w × 2.33]` — at most +`≈ 1165.dp`. Both dimensions are therefore always far inside the 262142 px +`Constraints` limit **at any aspect ratio and any screen density**, so the crash +is structurally impossible rather than merely bounded. A very wide image renders +at its true proportions (a thin strip) instead of being reshaped to 2.33:1. + +This does not disturb the framed item's text-width adaptation: that uses +`.width(IntrinsicSize.Max)` (`FramedItemView.kt`), which reads the image box's +intrinsic **width** — still pinned to `w` — so only the wide side's height +derivation changes. + +## Why compute height directly (vs clamping the ratio) + +The earlier candidate fix clamped the ratio (`coerceIn(1f / 2.33f, 2.33f)`). That +is safe, but it reshapes every image wider than 2.33:1 — including legitimate +panoramas — to 2.33:1, because `ContentScale.FillWidth` cannot fill the +over-tall box and the image letterboxes. Computing the height directly removes +the overflow-prone code path entirely *and* preserves the natural shape of wide +images, so there is no display trade-off to accept. It also makes the Compose and +iOS image-sizing logic parallel. + +## Scope / non-goals + +- Only the `!smallView` framed Box derived its size from the image ratio; it now + computes height directly. The chat-list `smallView` preview is locked to a fixed + `36.sp` square, and all other image/video/link paths size with `.width(...)` + + `ContentScale` (no `aspectRatio`), so none of them can hit this overflow. +- One follow-up was identified but intentionally left out to keep the diff + minimal: a **symmetric wide guard** in the bitmap decoders + (`outWidth > outHeight * 256`, mirroring the existing tall guard in + `Images.android.kt` / `Images.desktop.kt`) for defense-in-depth, so any future + consumer rendering a decoded bitmap is protected at the source. + +## iOS + +iOS is **not** affected by the crash, and the fix above brings the two platforms +into alignment. iOS already sizes the preview by computing the height directly — +`height = w × heightRatio`, `heightRatio = min(size.height / size.width, 2.33)` +(`apps/ios/SimpleXChat/ImageUtils.swift`) — which is exactly the form now used on +Android/desktop. (Even before, SwiftUI lays out with `CGFloat` frames and has no +`Constraints` packing limit, so a 4000×1 image yielded a valid sub-pixel-height +frame rather than throwing.) No iOS change is required. diff --git a/plans/2026-06-25-name-resolution.md b/plans/2026-06-25-name-resolution.md new file mode 100644 index 0000000000..737de7fe99 --- /dev/null +++ b/plans/2026-06-25-name-resolution.md @@ -0,0 +1,506 @@ +# SimpleX names — simplification plan + +Status: design agreed while reviewing branch `sh/namespace`. This supersedes the +name handling currently on that branch. Line refs are to the working tree at +review time; re-check before editing. + +## 1. Goal + +Reduce the feature to the minimum coherent, secure shape: + +- **One** name per entity, on the **profile** (the entity's claimed identity), + typed `Maybe SimplexNameInfo`. No second "locally-known" copy on the entity row. +- A local **verification status** as a 3-state `Maybe Bool` (not a timestamp): + not-attempted / failed / verified. A failed check never blocks connecting. +- Connect-by-name requires the entity to claim the name, and the result is created as verified. +- The **proof** (address-key signature, context-bound) is **in scope for link + contexts** — connect-by-name, 1-time invitations, contact addresses, channel + join links — so those names are verifiable in this release. Only a name with + **no link context** (a group member, or a name on an `XInfo` profile over an + established connection) is deferred: stored, but not shown until that gap closes. + +Removed from the current branch: the `*.simplex_name` entity columns (the +"locally-known/ct" copy), the `connections.simplex_name` carrier, the four +partial UNIQUE indexes + "newer-claim-wins" clearing, and the `_verified_at` +timestamps. + +## 2. Why (trust model) + +A name resolves to a link via the agent (`resolveSimplexName`); a `NameRecord` +has **lists** of links (`nrSimplexContact`, `nrSimplexChannel`), so +verification matches against **any** of them. "Match any" is safe **only because +the namespace is an on-chain, ENS-style registry**: `nrOwner`/`nrResolver` are +Ethereum addresses (`Names/Record.hs:22-23,36`), so each name has a **single +owner** who sets all its links. An attacker can register *their* name → your +address (the offensive-name case, handled by the claim check below) but **cannot +add a link to your name** — on-chain ownership blocks impersonation. Everything +here depends on that; if names were not single-owner, "match any" would be exploitable. + +The registrant of a name controls what it points to, with no proof that the +target address agreed to it — anyone can publish `@offensive → your address` or +`#offensive → your channel`. Therefore: + +- One-directional verification ("does `resolve(name)` equal a stored link") is + insufficient: it confirms the registrant's assertion, not the address owner's. +- A profile **claiming** a name (and even a link) proves nothing — anyone can + copy a link into a profile. **Control of a link is proven only by an actual + connection/join through it.** +- Sound verification is the intersection: `resolve(name) → link`, **and** the + entity advertises that name in its profile (it claims the name), **and** that link is one + we connected/joined through (control-proven). +- Verifying a name without having connected through its link needs a **signature + by the address key over the name, bound to the presentation context** (§4.8). + This is **in scope** for link contexts: a 1-time invitation includes such a proof + bound to the invite, so resolving the name → address → address key verifies it. + +Consequence: names are verifiable in this release for **channels** (join link), +**contacts connected via their address/name** (the link connected through), and +**1-time-invite contacts** (the in-scope address-key proof). The only case still +deferred is a name with **no link context at all** — a group member, or a name on +an `XInfo` profile over an established connection — which stays stored-but-not-shown. + +## 3. Current branch state (to be changed) + +Migration `M20260603_simplex_name` currently adds, on both SQLite and Postgres: + +- `contacts.simplex_name`, `contacts.simplex_name_verified_at` +- `groups.simplex_name`, `groups.simplex_name_verified_at` +- `connections.simplex_name` +- `contact_profiles.simplex_name`, `group_profiles.simplex_name` +- UNIQUE indexes `idx_contacts_simplex_name`, `idx_groups_simplex_name`, + `idx_contact_profiles_simplex_name`, `idx_group_profiles_simplex_name` +- `server_operators.smp_role_names` (= 1 for `'simplex'`) + +…and threads two names per entity through the types: `Contact.simplexName` / +`GroupInfo.simplexName` (from `*.simplex_name`, "locally known"), and +`LocalProfile.simplexName` (from `*_profiles.simplex_name`, "peer claim"), plus +a `verified_at` timestamp on the entity. `apiVerifySimplexName` verifies the +entity (ct) name against the **profile's self-asserted `contactLink`** for +contacts (wrong link), and against `preparedGroup.connLinkToConnect` for groups +(correct). The `connections.simplex_name` carrier is plumbed through +`createConnection_` but **never written** (all callers pass `Nothing`). + +## 4. Target design + +### 4.1 Name type and JSON encoding + +The name type is **`SimplexNameInfo`** (simplexmq `Simplex/Messaging/SimplexName.hs:37`), +which has: + +- `StrEncoding` (`SimplexName.hs:89`) — canonical `simplex:/name@…` / `#…` string, +- a text `ToField` (`SimplexName.hs:146`) — stores as TEXT, +- a JSON **object** instance: `$(J.deriveJSON defaultJSON ''SimplexNameInfo)` + (`SimplexName.hs:154`) → `{nameType, nameDomain}`. + +**Keep the object JSON** — the UI/API needs the structured form (it reads the name +off `LocalProfile`, `CRSimplexNameVerified`, …). The conflict is only on the +**wire**: `PublicGroupAccess.groupDomain` is a **released** field typed +`Maybe Text` (a JSON string), so the wire form of the name must stay a string. + +Resolve by wrapping **only the wire fields** with simplexmq's generic newtype +`StrJSON` (`Simplex/Messaging/Encoding/String.hs:265`), whose `ToJSON`/`FromJSON` +go through `StrEncoding` → a JSON string (`String.hs:267–272`; idiom +`deriving (ToJSON, FromJSON) via (StrJSON "X" T)`): + +- wire (`Profile`, `GroupProfile`/`PublicGroupAccess`): + `Maybe (StrJSON "SimplexName" SimplexNameInfo)` → JSON **string**, byte-identical + on the wire to the released `Maybe Text`. +- local/UI (`LocalProfile`): `Maybe SimplexNameInfo`, **unwrapped** → JSON **object**. + +Wrap/unwrap at the `Profile` ⇄ `LocalProfile` boundary (`toLocalProfile` / +`fromLocalProfile`). `SimplexNameInfo`'s own JSON instance is untouched, so +`CRSimplexNameVerified` and any other UI-facing use stay object. + +Inherent asymmetry: there is no `LocalGroupProfile`, so the channel name reaches +the UI as a **string** (via `GroupProfile`, which is `StrJSON`), while the contact +name reaches it as an **object** (via `LocalProfile`). If the UI needs the channel +name as an object too, add a decoded field on `GroupInfo` (follow-up). The bot-API +binding generator must render `StrJSON`-wrapped fields as `string`. + +DB stays TEXT: `ToField` (`SimplexName.hs:146`) on write; on read a **hard** +`FromField SimplexNameInfo` (add it — `SimplexName.hs:141-145` says to define it +"when a consumer requires the row-fail behaviour"), so an invalid stored name fails the +row — matching the wire, where a name that won't `strDecode` fails the profile. **No +soft-decode** (`decodeSimplexName` dropped for the name columns). + +### 4.2 Types (field changes) + +- `Profile.contactDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` — NEW; + JSON string (§4.1). The entity's advertised contact name. (Replaces the branch's + `Profile.simplexName`.) +- `Profile.contactDomainProof :: Maybe ClaimProof` — NEW; a flat sibling of + `contactDomain`/`contactLink`, a **wire profile field like `Profile.badge`**. The + **stored** own profile (`contact_profiles`) has the name but **no proof**; the + proof is generated and **added to the outgoing profile at send/save** (§4.8), exactly + as the badge is. `PHSimplexLink` (verifiable) when the profile is saved to a contact + address / 1-time invite; `PHTest` over an established connection (the gap). +- `LocalProfile.contactDomain :: Maybe SimplexNameInfo` (unwrapped → object JSON) + and `LocalProfile.contactDomainVerification :: Maybe Bool` — local status, **not** + on wire `Profile`. `toLocalProfile` unwraps the `StrJSON`; `fromLocalProfile` + re-wraps `contactDomain` and drops the status (mirrors how `localBadge`'s status + is dropped). +- `PublicGroupAccess.groupDomain :: Maybe (StrJSON "SimplexName" SimplexNameInfo)` + — RETYPE from `Maybe Text` (`Types.hs:857`); JSON string, wire-identical to the + released text (§4.1). Owner-set, broadcast in `XGrpInfo`. **Only public, relay-backed + groups** (`groups.use_relays`) can have a name — they have a channel join link to + resolve to and an owner key (`groups.member_priv_key`, `chat_schema.sql:193`) to sign + with; p2p groups have neither. (`contactDomain` on a member is a contact attribute, unaffected.) +- `GroupInfo.groupDomainVerification :: Maybe Bool` — local status, read from the + `groups` table (there is no `LocalGroupProfile`, and `GroupProfile` is the wire + type, so the status cannot be sent with the profile). +- DROP `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`, + and both `*VerifiedAt` timestamps. + +Asymmetry, intentional: contact name + status both live on `contact_profiles` +(exposed via `LocalProfile`); the group name lives on `group_profiles` +(exposed via `GroupProfile`/`PublicGroupAccess`) but its status lives on +`groups` (exposed via `GroupInfo`). This is forced — `group_profiles` is the +shared wire profile with no local columns, while `contact_profiles` already +holds local state (`local_alias`). + +### 4.3 Schema — rewrite `M20260603_simplex_name` (branch unreleased) + +Add: +- `contact_profiles`: `contact_domain TEXT`, `contact_domain_verification` + (nullable `INTEGER` SQLite / `SMALLINT` Postgres). +- `groups`: `group_domain_verification` (nullable `INTEGER`/`SMALLINT`). +- `server_operators.smp_role_names` (= 1 for `'simplex'`) — keep. +- `user_contact_links`: the contact-address **root signing key** (BLOB, mirroring + `groups.root_priv_key`) — captured from the 2-step short-link creation — so the + contact-address / 1-time-invite proof can be signed chat-side. (Not present today.) + +No DB change for `group_profiles.group_domain` — it already exists (from +`M20260515_public_group_access`); only the Haskell type and JSON change. + +Remove (vs the current branch migration): `contacts.simplex_name`, +`contacts.simplex_name_verified_at`, `groups.simplex_name`, +`groups.simplex_name_verified_at`, `connections.simplex_name`, +`contact_profiles.simplex_name`, `group_profiles.simplex_name`, and all four +UNIQUE indexes. No name uniqueness is enforced at the DB level — identity comes +from verification, not a constraint. + +Verification column decode: `Maybe BoolInt → Maybe Bool` (`NULL` = not attempted, +`0` = failed, `1` = verified). + +### 4.4 Storage functions (`Store/Shared.hs`, `Store/Direct.hs`, `Store/Groups.hs`) + +- Read the name columns as `Maybe SimplexNameInfo` via the **hard** `FromField` + (§4.1) — an invalid name fails the row; no soft `decodeSimplexName`. +- `toContact` / `toGroupInfo`: read `contact_domain` → `LocalProfile.contactDomain` + and `contact_domain_verification` → `LocalProfile.contactDomainVerification`; + `group_profiles.group_domain` → `PublicGroupAccess.groupDomain` and + `groups.group_domain_verification` → `GroupInfo.groupDomainVerification`. Delete + the ct/cp split and the entity-`simplex_name` reads. +- `createContact_` / `createGroup_` / `createPreparedContact` / `createPreparedGroup`: + set the name on the **profile** columns only; drop the entity-`simplex_name` + argument and the `connections.simplex_name` carrier param on `createConnection_`. +- `updateContactProfile` / `updateGroupProfile`: write `contact_domain` / + `group_domain` from the received profile. Reset the verification to `NULL` + (not-attempted) **only when the name changes**; an XInfo/XGrpInfo with the + **same** name keeps the existing status (a verified name stays verified), exactly as + a badge does. No conflict clearing (no UNIQUE index). +- `getContactBySimplexName` / `getGroupIdBySimplexName`: look up by the **verified** + profile name (the name column joined with verification = `Just True`); on a miss + or unverified, fall through to resolve-and-connect. Consistent with the existing + by-address lookup `getContactViaShortLinkToConnect` (`Direct.hs:963`), which + matches the link with no verification check — a link is the identity, a name is a + claim that only becomes a usable pointer once verified. + +### 4.5 Redaction (`Library/Internal.hs:1246`) + +```haskell +redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile +redactedMemberProfile g m Profile {…, contactLink, contactDomain} = + let allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect + in Profile { … + , shortDescr = removeSimplexLink =<< shortDescr -- via allowSimplexLinks + , contactLink = if allowSimplexLinks then contactLink else Nothing + , contactDomain = if allowDirect then contactDomain else Nothing + , contactDomainProof = Nothing } -- member profiles are contextless; never include a proof +``` + +- `allowDirect` is the single primitive (the `DirectMessages` permission); it + controls the name and is reused inside `allowSimplexLinks`. No `allowName` flag, + no second lookup. +- **Behavior changes vs current:** `contactLink` flips from unconditionally + dropped to controlled by `allowSimplexLinks` (a member's contact address becomes + visible whenever links+DMs are allowed — the meaning of "links allowed"); the + name follows the looser `allowDirect`. Rationale: a link is one-tap-to-connect + (low friction), a name only resolves if the recipient deliberately looks it up + (higher friction), so a group can forbid links yet allow name discovery, with + "DMs allowed" the floor for both. +- Signature takes `(GroupInfo, GroupMember)` and derives both flags inside, so + the rule lives in one place. Callers pass `(g, m)`; the own-profile path passes + `(g, membership g)` — behavior-preserving because + `groupFeatureUserAllowed f g ≡ groupFeatureMemberAllowed f (membership g) g` + (both reduce to `groupFeatureMemberAllowed' f (memberRole (membership g)) + (fullGroupPreferences g)`, `Types.hs:646–652`). +- Collapses `groupUserAllowSimplexLinks` (`Types.hs:656`) and the pre-computed + `allowSimplexLinks` wiring at the call sites (`Internal.hs:1244`, + `Subscriber.hs:842,2817,3239`, `Commands.hs:3748,4057`). `Commands.hs:3748` + has `Maybe GroupInfo` → "no group ⇒ no redaction" at the call site. + +### 4.6 Resolution + verification (`Library/Commands.hs`) + +Two cases that differ in whether verification can *fail*: + +**Connect-by-name** (`connectPlanName` / `dispatchResolvedRecord`) — verification is +a **precondition** of connecting. After decoding the resolved short link's embedded +profile, **add the claim check**: require that profile's `contactDomain` / +`groupDomain` to equal the resolved name, else fail with `CESimplexNameNotFound` +("name unknown") and **do not connect**. (The current branch decodes the profile but +never compares the name — this is the missing check.) On success the prepared +contact/group is created with the name on the profile, `connLinkToConnect` = the +resolved link, verification = `Just True` (**created as verified**). There is no "failed" +outcome here — failing to resolve or to claim the name just means no connection. + +**Connected NOT by name** (via an address link or a 1-time link) — the peer's profile +may *claim* a name; it starts **unverified** (`Nothing`). Verification is **post-hoc +and non-blocking**: keep `apiVerifySimplexName` (`/_verify simplex name`). + - **Contacts** verify by a **single path** — check `contactDomainProof` (§4.8): + resolve the claimed name → its link, validate the owner chain and **select the key by + the proof's `linkOwnerId`** (that owner's `ownerKey`, or the root key if `Nothing` — + the usual contact-address case), check the `ClaimProof` signature over + `name <> presHeader`, and check the proof's `presHeader` link == + `preparedContact.connLinkToConnect` (**not** `profile.contactLink` — the branch bug). + Contact addresses and 1-time invites both include the proof. + - **Channels** verify by **presence / link-match**: `resolve(#name)` includes + `preparedGroup.connLinkToConnect` (the join link), whose owner-signed data already + has `groupDomain` (no `ClaimProof` — §4.8). + Result is `Just True` (holds) or `Just False` (fails) — and **`Just False` must NOT + prevent the connection**, exactly as a failed *badge* verification doesn't. **This is + why the status must be 3-state** (`Nothing` not attempted / `Just False` failed / + `Just True` verified). Names that stay `Nothing` have **no proof/link context** — + group members, and names on an `XInfo` profile over an established connection. + +Open option: **auto-verify on connect** (run the same non-blocking check +automatically when connecting via address/1-time link), instead of only on demand. +Either way the status stays 3-state — auto-verification can fail without blocking. + +Keep the pure helpers `firstNameLink` (per-type link pick, cross-type rejection) and +`linksMatch` (scheme-normalized compare). + +### 4.7 Display (`View.hs`) + +A name is shown **when there is a proof to verify**, together with its status — +verified, failed, or not-yet-verified. All three states are shown (a failed or +pending check still shows the name, flagged), so the user sees the name and its +trust level rather than a silent omission. Rendering those three states is **out of +scope for this PR** (UI work), but the core stores the data — the 3-state status and +the proof — to drive it. A name with **no proof to verify** — a group member, or a +name on an `XInfo` profile over an established connection — is **not shown**. + +### 4.8 Proof — a flat profile field, signed by the address key, context-bound + +A name resolves to a **contact address** (the persistent identity link). The proof +asserts "the owner of that address asserts this name", so it is **signed by the +address's key** (the resolved address's own key) — **not** by the +per-connection or 1-time-link key. Every proof is **tied to the link it's shown +through**, so a proof made for one link can't be reused on another; these are +distinct proofs. Store the link in a presentation header: +**rename `BadgePresHeader` (`Badges.hs:212`) → `ProofPresHeader`** (now shared by +badge and name proofs) and add a **`PHSimplexLink AConnShortLink`** constructor. +`AConnShortLink` (`Agent.Protocol.hs:1536`, +`forall m. ConnectionModeI m => ACSL (SConnectionMode m) (ConnShortLink m)`) is the +existing existential, so the context is either a 1-time invitation or a contact +address. The signed payload includes this header, and the **verifier compares the +header's link against the link the proof is presented through** (the current +invite/address) — that comparison is what makes a proof non-replayable across +links. The tag enum (`Badges.hs:200`), the `StrEncoding` (`:216`), and +`badgePresHeaderAccepted` (`:226`, → `proofPresHeaderAccepted`) each gain the new +variant. + +**Type & wire** — a JSON object like `BadgeProof` (`Badges.hs:393`): + + data ClaimProof = ClaimProof + { linkOwnerId :: Maybe OwnerId, -- which owner signed; Nothing = root key (see below) + presHeader :: ProofPresHeader, -- context: PHSimplexLink | PHTest + signature :: C.Signature 'C.Ed25519 -- by that owner's key, over: smpEncode name <> smpEncode presHeader + } + +The signature is by the key of the signer's **owner identity** in the link's owner +chain (`OwnerAuth`, `Agent/Protocol.hs:1829`); `ClaimProof` has +**`linkOwnerId :: Maybe OwnerId`** (`OwnerId`, `:1827`) to name it, so the verifier +checks exactly that key (no iterating the owner list): + +- **Channels** (which can have owners other than the address) sign with the user's **owner key**, + `linkOwnerId = Just oid` — **not** the root key — `groups.member_priv_key` + (`chat_schema.sql:193`). +- A **contact address** has a single owner = its creator, so it signs with the **root + key**, `linkOwnerId = Nothing`. + +The root key (`ShortLinkCreds.linkPrivSigKey`/`linkRootSigKey`, `:1482-83`) otherwise +only *authorizes* owners (`validateOwners`/`validateLinkOwners`, `:1846–1859`); +`Nothing` (root) is **allowed at validation**. Both keys live **chat-side** so signing +is in the chat layer — **but only channels store theirs today** (`groups`); a contact +address (`user_contact_links`, `:386`) has **no key column**, so we must add one +(mirroring `groups.root_priv_key`), captured from the 2-step short-link creation, to +sign the contact-address / 1-time-invite proofs. Signed payload = +`smpEncode name <> smpEncode presHeader` (`name` from the profile's `contactDomain`). +`presHeader` serialises as its `StrEncoding` string, `signature` base64url. + +Per presentation context: + +- **Contact address** (the name's own resolved link): presence in the address link's + owner-signed data already proves the name — but we **include the explicit `ClaimProof` + here too** (bound to the address) so contact verification is + one uniform path (always check the proof) rather than presence-for-addresses / + proof-for-invites. The address can also serve as the **badge** proof's context. +- **1-time invitation**: include a proof **signed by the address key, bound to the + 1-time link** as context. Feasible **now** — the 1-time link is a unique, + single-use context; no general mechanism required. This is what makes + a 1-time-invite contact verifiable. +- **`XInfo` over an established connection**: no link context → the step that adds the proof sets + `PHTest` (unbound) → unverifiable → not shown. Left for later, same as the badge's + `PHTest`. (For **group members** the proof is dropped entirely by redaction, §4.5.) + +Home: **inside the profile** — `Profile.contactDomainProof :: Maybe ClaimProof`, a flat +sibling of `contactDomain`/`contactLink`, exactly like `Profile.badge`. It is **not** +stored on the own profile; like the badge, it is **generated fresh and added to the +outgoing profile at send/save** — when the profile is sent to a peer (`XInfo`) or saved +to a link — by signing `name <> presHeader` with the address root key and setting the +**destination as the `presHeader` context**. Because `presentUserBadge` +(`Internal.hs:2037`) already adds the badge proof to the outgoing profile at *both* +peer-sends and link-data writes, the name-proof step belongs in the **same +function**. The context decides whether a proof is useful: saving to a contact address / +1-time invite sets `PHSimplexLink(that link)` (verifiable — the in-scope cases); an +established-connection peer-send sets `PHTest` (unbound — left for later, same as the +badge). The receiver verifies and stores the 3-state status on +`LocalProfile.contactDomainVerification`, as `localBadge` holds the badge status. +Connect-by-name is **created as verified**. + +**Scope:** the **useful** (`PHSimplexLink`) proofs — set when the profile is saved to +a contact address or 1-time invite — are in this change; the verifier resolves the name → +address → address key → checks the signature and the `presHeader` link. Channels use +presence (below). The contextless established-connection case (a `PHTest` name proof) and +group members are left for later. + +Badges stay **in the profile** — person-scoped, presented per connection +(`presentUserBadge`, `Internal.hs:2037`), including over established connections via +`XInfo`. They share the presentation-header context type with name proofs but sign +with the **badge credential key**, not the address key. Two scopes, two homes. + +The shared step sets **both** proof kinds the same way: `PHSimplexLink(link)` +when the destination is a link (contact address, 1-time invite — verifiable), `PHTest` +over an established connection (unbound — the gap). So name proofs and badges both use +`PHTest` only for the contextless established-connection case. + +No group proof field: a channel is always joined via its **join link** (its own +address), whose owner-signed data already has `groupDomain`, and there is no +"advertised link ≠ resolved address" case for channels — so channels verify by +presence/link-match and need no `ClaimProof`. (If full symmetry is wanted later, add +`GroupProfile.groupDomainProof` the same way.) + +### 4.9 Set-name API (in scope — currently missing) + +Names are **pre-registered out of band** — the app does **not** call RNAME. The API +only **verifies** the name and **adds it to the profile**. The user must be able to +add/change/remove their own name from the UI; the branch has no command for the +**contact** name. + +- **Contact name** — add `APISetUserName :: Maybe SimplexNameInfo -> ChatCommand` + (`Nothing` clears). On set: + 1. Require an **address** — fail if none (the UI won't offer the action without one). + 2. Require it to be a **short link** — if only a long link exists, create the short + link (the name resolves to a short link, and the check below is short-link-based). + 3. Ensure that short link is **in the profile** (`contactLink`) — add it if missing. + 4. **Verify**: resolve the name and compare the short link it points to against the + profile's `contactLink`; fail if they don't match (name not preregistered to this + address). + 5. Set `LocalProfile.contactDomain` and **re-publish the contact-address link data**. + The `ClaimProof` is added when the profile is saved to that link (the send/save + proof-adding step, §4.8) — **not** produced by the API itself. (Steps 1–4 are the verify; + this step is set + re-publish.) + Needs a `ChatCommand` constructor + parser + handler. +- **Channel name** (public, relay-backed groups only) — a **separate** + `APISetPublicGroupName`, parallel to the contact one (not folded into + `SetPublicGroupAccess`): same require-address / require-short-link + / link-in-profile / verify / set-`groupDomain` flow against the channel's **join + link**. Rationale: it mirrors the contact API, and the name's fail-able verify + + preconditions don't mix cleanly with the plain `web=`/`embed=`/`domain_page=` writes. + **Drop `domain=` from `SetPublicGroupAccess`** (`Commands.hs:5461`) so there's a single + verified path; factor out the shared `GroupProfile`-update + `XGrpInfo` broadcast so + both commands reuse it. Verify is **TLD-dependent**: resolve+compare for `TLDSimplex`, + a different/no check for `TLDWeb` (web domains don't resolve through the namespace). +- The own name has **no stored verification status** — the verify step checks it at + add time; the 3-state status is only for peers' names. No RNAME wiring (out of scope). + +## 5. Removal checklist (from the current branch) + +- `Contact.simplexName`, `GroupInfo.simplexName`, `Connection.simplexName`. +- `*VerifiedAt` timestamps (→ `Maybe Bool` status fields). +- `connections.simplex_name` column + the `createConnection_` carrier param + the + `XInfo` carrier consumption in `Subscriber.hs`. +- `contacts.simplex_name`, `groups.simplex_name` columns. +- The four partial UNIQUE indexes + `clearConflictingContactProfileSimplexName_` + / `clearConflictingGroupProfileSimplexName_` + their call sites. +- `getContactBySimplexName` / `getGroupIdBySimplexName` against entity columns + (re-point or remove per 6.b). + +## 6. Resolved decisions + +a. **Wire-string encoding (§4.1):** `SimplexNameInfo` keeps object JSON; wire + fields are wrapped in `StrJSON` (string), `LocalProfile` stays unwrapped + (object). Channel name reaches the UI as a **string** (via `GroupProfile`) and + that is fine — no decoded-object field on `GroupInfo` (no reason to re-connect + to a channel you're in; channel names are only shown verified). The object form + matters for **contacts** (connect-from-groups, sharing), which `LocalProfile` + provides. Residual chore: teach the bot-API binding generator to emit `string` + for `StrJSON` fields and pick the `StrJSON` `name` Symbol. +b. **Lookup (§4.4):** re-point `getContactBySimplexName` (its one caller is + connect-by-name, `Commands.hs:4241`) to the **verified** `contact_profiles. + contact_domain`; miss/unverified ⇒ resolve-and-connect. Keeps the no-network + shortcut for already-known contacts. (`getGroupIdBySimplexName` has no external + caller — drop it.) +c. **Proof (§4.8):** a flat **`Profile.contactDomainProof :: Maybe ClaimProof`**, a wire + profile field like `Profile.badge`. Not stored on the own profile; **generated fresh and + added to the outgoing profile at send/save** (peer `XInfo` or save-to-link) by the + **same function** as the badge (`presentUserBadge`, which already runs at + peer-sends *and* link-data writes). Signed by the signer's **owner-identity key** — a + channel's **owner key** (`groups.member_priv_key`, `linkOwnerId = Just oid`) + or a contact address's **root key** (sole owner, `linkOwnerId = Nothing`) — over + `name <> presHeader`; `linkOwnerId` selects the verification key (`Nothing` = root, + allowed at validation). `PHSimplexLink` for address/invite saves, `PHTest` over + established connections (the gap). Verify checks the signature **and `presHeader`'s link + == the link actually used** (`connLinkToConnect`). Channels use presence (owner-signed + link data). **Gap:** the contact-address key isn't stored chat-side today + (`user_contact_links` has no key column) — add one to sign chat-side. Receiver status + on `LocalProfile.contactDomainVerification`. +d. **`redactedMemberProfile` contactLink exposure (§4.5):** intended — a member's + contact address becomes group-visible when links + DMs are allowed. +e. **Verify command + status (§4.6):** keep `apiVerifySimplexName` — required to + verify a claimed name for entities connected **not** by name (address / 1-time + link). Status is **3-state** because a failed name (or badge) verification must + **not** block the connection; connect-by-name is the only created-as-verified path + (and there a failure to resolve or to claim the name means no connection, not a failed state). + **Auto-verify on connect** (vs. on-demand only) is left open; it doesn't change + the 3-state requirement. + +## 7. Rollout & scope + +The **claim check** (§4.6) — a name resolves only if the **resolved link's own +data claims it** — is the anti-stray-names protection and **must ship regardless**: +a name someone registers against a real address must not "work" without that +address owner's agreement. It needs no signature (the address-case presence suffices), +so it lands with the core change. + +The signed proof (§4.8) can ship in the **same** release (add + verify together) or +be **staged** — implemented at the core level with the user-facing name-addition +hidden in the UI until ready. Either way it stays a *core* change. + +The `ProofPresHeader` rename + `PHSimplexLink` + letting badges opt into the link +context ripples through the badge code — accepted, and bounded: + +- `Badges.hs`: `BadgePresHeader` → `ProofPresHeader`, `BadgePresHeaderTag` → + `ProofPresHeaderTag`, `badgePresHeaderAccepted` → `proofPresHeaderAccepted` + (`:200, :212, :216, :226`); add the `PHSimplexLink AConnShortLink` + tag/constructor/`StrEncoding`/accepted-case; `badgeProof` (`:314`) takes the + renamed type. +- `presentUserBadge` (`Internal.hs:2037`) + its ~15 call sites — the **shared point that adds + proofs to the outgoing profile** (already runs at peer-sends *and* link-data writes): generalize it + to set **both** the badge proof and the name `ClaimProof` onto the outgoing profile, + using `PHSimplexLink link` where the destination is a link, `PHTest` otherwise. + +The pure rename can land as a **standalone prep commit** ahead of the proof; the +`PHSimplexLink` wiring + name proof land with the proof work. diff --git a/plans/2026-06-27-namespace-ui-display-set.md b/plans/2026-06-27-namespace-ui-display-set.md new file mode 100644 index 0000000000..1f01cc4311 --- /dev/null +++ b/plans/2026-06-27-namespace-ui-display-set.md @@ -0,0 +1,243 @@ +# SimpleX name UI: display + verify + set (iOS + Android/desktop) + +Branch: `sh/namespace-ui` (rebased onto core `fc0582cf0` — the finalized verify API). This pass adds +displaying a contact's / channel's SimpleX name with verification state, verifying names (manual + auto), +and two screens for setting the user's own name and a channel's name. + +**Upstream sync (2026-06-27):** core `sh/namespace` is at `5008b4e62` ("refactor setting user name" + +test/comment cleanup, on top of the verify-API split `fc0582cf0`). UI branch rebased onto it (backup tag +`backup-namespace-ui-pre-rebase5`); pure-frontend, rebase clean. All dep-touching changes across these +syncs were **wire-neutral**: cosmetic StrJSON label on `Profile.contactDomain`; `APISetUserName` handler +refactor (constructor `{userId, simplexName}`, `/_set_name` parser, `CRUserProfileUpdated`/`NoChange` +response unchanged); `Internal.hs` record-field reordering in bot profiles; store/view/test cleanup. +`Types.hs` model fields/JSON, verify/set commands, responses, `NameVerifyOutcome`, `NameClaimProof`, and +`SimplexNameInfo` are unchanged. **No UI code change required.** + +## Scope + +Ships (both iOS and Android/desktop): +- Models: decode name / proof / verification fields, add `NameClaimProof` + `SimplexNameInfo` helpers. +- Name display + 3-state verification indicator on contact info and channel info. +- Verify API calls + new response handling. +- "Verify SimpleX names" privacy toggle (default ON). +- Two "Set SimpleX name" screens (own name; channel name). + +Out: no change to the core verify algorithm — the UI only triggers it and renders the result. + +## Decisions + +Single source of truth. The UX walkthrough shows the visuals; the implementation sections give file:line. + +### Product / UX (confirmed) +- **A. Title** — rename to "SimpleX address and name". +- **B. Screen body copy** — placeholders for now (final copy TBD): + - Own: *"Set a SimpleX name so people can connect to you using @yourname instead of a link. The name + must already be registered to your address."* + - Channel: *"Set a SimpleX name so people can find this channel as #name. The name must be registered + to this channel's address."* +- **C. Own name read-only** — No; the current own name appears only inside the set screen. +- **D. Rename scope** — rename the shared string everywhere (settings-menu row + screen title). +- **E. Auto-verify trigger** — with the toggle ON, auto-verify on open only if stored state is `null`; + verified/failed shows the stored result, and tapping the indicator re-verifies. +- **F. Failure reason** — shown on tap of the red cross (alert); an inconclusive result shows a brief + alert on completion. Not shown inline. + +### Technical approach +- **T1. Show the name only when a proof exists** (`contactDomainProof` / `groupDomainProof` != nil). +- **T2. One formatter + one parser.** `SimplexNameInfo.shortName` (display, mirrors `shortNameInfoStr`), + `editDomain` (prefix-less, prefills set fields), `SimplexNameInfo(parsing:)` (decode the encoded + `groupDomain` string). Contact display uses the decoded object; channel display parses `groupDomain`. +- **T3. Set screens send a name string; UI fixes only the type prefix.** `strP` reads `@`->contact / + `#`->group first, so the UI prepends `@` (own) / `#` (channel); the backend canonicalises the domain. + Empty input clears. +- **T4. Channel uses a raw `APIUpdateGroupProfile`** (documented at the call site): core has + `APISetUserName` but no `APISetGroupName`, so the channel name is set by re-sending the cloned + `GroupProfile` with `publicGroup.publicGroupAccess.groupDomain` updated. +- **T5. Gating.** Channel "Set SimpleX name" only when `useRelays && isOwner && publicGroup?.publicGroupAccess != nil`; + own "Set SimpleX name" lives in the existing-address branch (inherently requires an address). + +Minor defaults (flag if wrong): iOS toggle in the first "Chats" Section (PrivacySettings.swift:85); +spinner delay ~300ms; channel name cleared via empty input; set-name fields rely on backend rejection +for validation (+ helper text). + +## UX walkthrough + +Same on both platforms. Nothing existing moves. + +### Name + verification indicator (contact info / channel info) + +A new line under the name, above the description, shown per T1. + +``` + Contact info Channel info + +------------------+ +------------------+ + | [ photo ] | | [ photo ] | + | Alice | | My Team | + | @alice.simplex v| <- new | #myteam Verify | <- new + | "description..."| | "description..."| + +------------------+ +------------------+ + (v = check) (Verify = action) +``` + +Name on the left, indicator on the right; the indicator depends on state: + +| State (`*Verification`) | Name style | Indicator | +|---|---|---| +| Verified (`true`) | accent color | check mark in regular color | +| Failed (`false`) | code style | cross mark in red (tap -> failure reason, per F) | +| Not verified (`null`), toggle OFF | code style | "Verify name" action (accent) | +| Not verified (`null`), toggle ON | code style | auto-verify on open (per E) -> spinner -> result | +| Verifying (in-flight) | code style | delayed spinner (~300ms, so a fast result doesn't flash) | + +### Set screens (2 new buttons -> 2 new screens) + +1. **Own name** — on the "SimpleX address and name" screen, a new "Set SimpleX name" button just above + the "Or to share privately" section: + ``` + [ QR code ] + Share address + Business address (toggle) + Address settings + ------------------ + Set SimpleX name -> <- new + ------------------ + Or to share privately + Create 1-time link + ``` +2. **Channel name** — on channel info, in the existing "Advanced options" section (owners only): + ``` + Advanced options + Web access + Set SimpleX name -> <- new + ``` + +Each opens the same view (parameterised by prefix / body text / save action): explanation + a text field +with a fixed prefix adornment (`@` own -> user types `alice.simplex`; `#` channel -> user types `myteam`) ++ Save. Empty + Save clears the name. + +### Settings toggle +"Verify SimpleX names" toggle, default ON, in the privacy settings "Chats" section (iOS: +PrivacySettings.swift:85, no `MorePrivacyView`; Kotlin: `MorePrivacyView` Chats section, PrivacySettings.kt:118). + +## Backend reference (core @ fc0582cf0) + +Single source for wire/JSON facts. + +- Display: `shortNameInfoStr` (simplexmq Protocol.hs:1594) — public group on default `.simplex` TLD with + empty subdomain -> `#myteam`; else prefix (`@`/`#`) + full domain (`@alice.simplex`, `#myteam.testing`). +- Encoded form: `strEncode SimplexNameInfo = "simplex:/name" <> ("@"|"#") <> fullDomain` (Protocol.hs:1565). +- JSON shapes: `LocalProfile.contactDomain :: Maybe SimplexNameInfo` -> JSON **object**; + `PublicGroupAccess.groupDomain :: Maybe (StrJSON SimplexNameInfo)` -> JSON **string** (encoded form). +- Verification: `LocalProfile.contactDomainVerification` / `GroupInfo.groupDomainVerification :: Maybe Bool` + -> JSON `true`/`false`/absent (decodes as Swift `Bool?` / Kotlin `Boolean?`); null=not attempted, + false=failed, true=verified. +- Proof: `LocalProfile.contactDomainProof` / `PublicGroupAccess.groupDomainProof :: Maybe NameClaimProof` + -> JSON object `{linkOwnerId?: string, presHeader: string, signature: string}` (all strings). +- Verify commands (manual; network/resolver errors are retryable `ChatErrorAgent`): + - `APIVerifyContactName {contactId}` -> `/_verify name @`. + - `APIVerifyPublicGroupName {groupId}` -> `/_verify name #`. + - Outcome `NVOVerified | NVOFailed Text | NVOInconclusive Text` -> persists `Just True` / `Just False` / + leaves unchanged. Responses `CRContactNameVerified {user, contact, verificationResult :: Maybe Text}` / + `CRGroupNameVerified {user, groupInfo, verificationResult :: Maybe Text}` return the **updated** entity + plus `Nothing`=verified / `Just reason`=failure-or-inconclusive text. +- Set own name: `APISetUserName userId (Maybe SimplexNameInfo)` -> `/_set_name []` (parsed by + `strP`, NOT json; Commands.hs:5438). Rejects `name is not registered to your address`. Response + `CRUserProfileUpdated` / `CRUserProfileNoChange`. +- Set channel name: `APIUpdateGroupProfile` (`/_group_profile # `, Commands.hs:5571) with + `groupDomain` set. Rejects `name is not registered to this channel`. Response `CRGroupUpdated`. + +## Gotchas + +1. **iOS Codable (compile-blocking).** `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType` + are `Decodable`-only (ChatTypes.swift:5261-5281); adding `contactDomain` to the `Codable` `LocalProfile` + breaks `Encodable` synthesis -> make all four `Codable`. (Kotlin already `@Serializable`.) +2. **`groupDomain` string carries the `simplex:/name` prefix** -> strip it in `parsing`. +3. **`NameClaimProof` decoded for presence only.** UI just checks `!= nil`; if any field's JSON shape is + uncertain at implementation, decode permissively. +4. **Delayed spinner — no existing helper.** iOS: `@State var verifying` set true after `Task.sleep(~300ms)`, + guarded by an `inFlight` flag. Android: a small `LaunchedEffect(inFlight){ delay(300); show=true }` composable. +5. **Enum JSON parity** — backend `enumJSON (dropPrefix "TLD"/"NT")` lowercases -> matches the Swift/Kotlin + enum raw values. +6. **Navigation.** iOS `NavigationLink`; Android `ModalManager.start.showModalCloseable { ... }` + (template: PrivacySettings.kt:162). + +--- + +## iOS implementation + +### Models — `apps/ios/SimpleXChat/ChatTypes.swift` +- Make `SimplexNameInfo`/`SimplexNameDomain`/`SimplexTLD`/`SimplexNameType` (5261-5281) `Codable`. +- Add `NameClaimProof: Codable, Hashable { presHeader: String; signature: String; linkOwnerId: String? }`. +- `SimplexNameInfo` (5261): add `init?(parsing: String)`, `var shortName: String`, `var editDomain: String`. +- `LocalProfile` (153): add `contactDomain: SimplexNameInfo?`, `contactDomainProof: NameClaimProof?`, + `contactDomainVerification: Bool?`. +- `GroupInfo` (2506): add `groupDomainVerification: Bool?`. +- `PublicGroupAccess` (2616): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof?`. + +### API — `apps/ios/Shared/Model/{AppAPITypes,SimpleXAPI}.swift` +- `ChatCommand`: `apiSetUserName(userId: Int64, name: String?)` -> `"/_set_name \(userId)" + (name.map{" "+$0} ?? "")`; + `apiVerifyContactName(contactId: Int64)` -> `"/_verify name @\(contactId)"`; + `apiVerifyPublicGroupName(groupId: Int64)` -> `"/_verify name #\(groupId)"`. +- `ChatResponse1`: add `contactNameVerified(user:contact:verificationResult:)` / + `groupNameVerified(user:groupInfo:verificationResult:)` cases (+ `responseType`/`details` entries). + Templates: `contactUpdated` AppAPITypes.swift:1114 / responseType:1193 / details:1267; `groupUpdated`:1140. +- Wrappers: `apiSetUserName(_:)`; `apiVerifyContactName(_:)` (updated `Contact` + reason); + `apiVerifyPublicGroupName(_:)` (updated `GroupInfo` + reason). Channel set reuses `apiUpdateGroup(_:_:)`. + +### Views +- Reusable `SimplexNameView` subview rendering the 5-row state table (incl. delayed spinner + verify action). +- Contact: `ChatInfoView.swift` `contactInfoHeader()` — before the `shortDescr` block (~395), per T1 render + `SimplexNameView` from `contactDomain`/`contactDomainProof`/`contactDomainVerification`; onAppear auto-verify (E). +- Channel: `GroupChatInfoView.swift` `groupInfoHeader()` — before webPage (~334), same from parsed + `groupDomain` + `groupDomainProof` + `groupDomainVerification`; "Set SimpleX name" button in Advanced options (~248). +- Address view: `UserAddressView.swift` — "Set SimpleX name" Section above "Or to share privately" (194). +- New `SetSimplexNameView.swift` (own + channel modes). +- Title rename (A/D): the address screen's title is set by the presenter, not `UserAddressView` + (literal also at UserAddressLearnMore.swift:71) — locate the title source and rename everywhere. +- Toggle: `PrivacySettings.swift` main "Chats" Section (~85) `Toggle("Verify SimpleX names", isOn:)` + bound to `@AppStorage(DEFAULT_PRIVACY_VERIFY_SIMPLEX_NAMES)`; declare the constant + default `true` in + the `appDefaults` dict (SettingsView.swift:88-105). + +## Android/desktop implementation (multiplatform Kotlin) + +### Models — `model/ChatModel.kt` +- Add `@Serializable data class NameClaimProof(presHeader: String, signature: String, linkOwnerId: String? = null)`. +- `SimplexNameInfo` (4875): add `companion fun parse(encoded): SimplexNameInfo?`, `val shortName`, `val editDomain`. +- `LocalProfile` (2061): add `contactDomain: SimplexNameInfo? = null`, `contactDomainProof: NameClaimProof? = null`, + `contactDomainVerification: Boolean? = null`. +- `GroupInfo` (2181): add `groupDomainVerification: Boolean? = null`. +- `PublicGroupAccess` (2323): keep `groupDomain: String?`; add `groupDomainProof: NameClaimProof? = null`. + +### API — `model/SimpleXAPI.kt` +- `CC`: `ApiSetUserName(userId, name: String?)`, `ApiVerifyContactName(contactId)` -> `"/_verify name @$contactId"`, + `ApiVerifyPublicGroupName(groupId)` -> `"/_verify name #$groupId"` (+ cmdString entries). +- `CR`: `@SerialName("contactNameVerified") ContactNameVerified(user, contact, verificationResult: String?)`, + `GroupNameVerified(user, groupInfo, verificationResult: String?)` (+ `responseType`). Templates: + `ContactUpdated` SimpleXAPI.kt:6454 / responseType:6646; `GroupUpdated`:6500. +- Wrappers `apiSetUserName`, `apiVerifyContactName`, `apiVerifyPublicGroupName`; channel set reuses `apiUpdateGroupProfile`. +- Pref: `val privacyVerifySimplexNames = mkBoolPreference(SHARED_PREFS_PRIVACY_VERIFY_SIMPLEX_NAMES, true)` (~123). + +### Views +- Reusable `SimplexNameView` composable (the 5-row state table). +- Contact: `views/chat/ChatInfoView.kt` — after `ChatInfoDescription(...)` (~759), per T1; onAppear auto-verify (E). +- Channel: `views/chat/group/GroupChatInfoView.kt` — after `ChatInfoDescription(...)` (~947), per T1; + "Set SimpleX name" button in Advanced options `SectionView` (~805). +- New `views/usersettings/SetSimplexNameView.kt` (own + channel modes). +- `UserAddressView.kt`: "Set SimpleX name" button above the one-time-link section (~347). +- Toggle: `PrivacySettings.kt` `MorePrivacyView` "Chats" Section (~118) `SettingsPreferenceItem(..., appPrefs.privacyVerifySimplexNames)`. +- `strings.xml`: `set_simplex_name`, `verify_simplex_name`, `verify_simplex_names`, screen titles/body; + rename `simplex_address` -> "SimpleX address and name" (D). + +## Build / verify +- iOS: Xcode build of SimpleXChat + app (or swift build of the package targets). +- Android/desktop: `./gradlew` compile of `:common` (desktop target fastest). +- Manual: a peer with a verified name shows accent + check; tampered/failed shows red cross + reason on tap; + toggle OFF shows "Verify name"; toggle ON auto-verifies on open with a delayed spinner. Set own/channel + name; clearing works; core rejection surfaces as an alert. + +## Commit plan (conventional) +1. `feat(names): decode name/proof/verification; add SimplexNameInfo helpers + NameClaimProof` (models, both) +2. `feat(names): verify API (contact/group) + response handling` (CC/CR, wrappers) +3. `feat(names): show name + verification state on contact and channel info` (SimplexNameView + headers) +4. `feat(names): "Verify SimpleX names" privacy toggle + auto-verify on open` +5. `feat(names): set user and channel SimpleX name screens` diff --git a/plans/2026-06-29-desktop-tmpdir-missing-call-sound.md b/plans/2026-06-29-desktop-tmpdir-missing-call-sound.md new file mode 100644 index 0000000000..fba998c355 --- /dev/null +++ b/plans/2026-06-29-desktop-tmpdir-missing-call-sound.md @@ -0,0 +1,174 @@ +# Fix: desktop crash + blinking window when temp dir is missing during a call + +Date: 2026-06-29 +Area: `apps/multiplatform` — desktop (Compose) audio/recording + +## Symptom + +On Windows, when an incoming call arrives the window starts blinking continuously and +does not stop until the call ends or the app is killed. The logs show: + +``` +java.io.FileNotFoundException: C:\Users\\AppData\Local\Temp\simplex\ (The system cannot find the path specified) + at java.base/java.io.FileOutputStream.open0(Native Method) + ... + at chat.simplex.common.platform.SoundPlayer.start(RecAndPlay.desktop.kt:276) + at chat.simplex.common.views.call.IncomingCallAlertViewKt$IncomingCallAlertView$1$1.invokeSuspend(IncomingCallAlertView.kt:32) + ... + at androidx.compose.ui.scene.BaseComposeScene.render(BaseComposeScene.skiko.kt:171) +``` + +## Root cause + +`tmpDir` is declared once as a top-level `val`: + +```kotlin +// common/.../platform/Files.desktop.kt:13 +actual val tmpDir: File = + File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } +``` + +It registers `deleteOnExit()` but is **never created** at declaration. The directory is +created at startup by `Main.kt:30-31`, which does `tmpDir.deleteRecursively()` then +`tmpDir.mkdir()` on every launch — so on a normal session the directory exists by the +time the UI runs. A handful of features re-create it on demand (`NtfManager.desktop.kt`, +`DatabaseView.kt`), but none of those is on the incoming-call path. + +Three writers assume the directory already exists and write into it without creating it: + +- `SoundPlayer.start` — `RecAndPlay.desktop.kt:276` (the crash site; incoming-call ring) +- `CallSoundsPlayer.start` — `RecAndPlay.desktop.kt:297` (connecting / in-call sounds) +- `RecorderNative.start` — `RecAndPlay.desktop.kt:29` (voice messages) + +Because `Main.kt` creates the directory at startup, the realistic trigger is **not** a +cold start but **mid-session deletion**. The precise deleter is a **transient second +instance**, via the `deleteOnExit()` on the shared path: + +1. `deleteOnExit()` on `tmpDir` (Files.desktop.kt:13) registers `...\Temp\simplex` for + removal at JVM shutdown. +2. `acquireSingleInstance()` (SingleInstance.kt:33) touches `dataDir`. `dataDir` and + `tmpDir` are top-level `val`s in the **same file**, so one facade-class `` + initializes both — meaning any process that reaches `acquireSingleInstance` registers + `tmpDir`'s `deleteOnExit()`, even though it never uses `tmpDir`. +3. A second launch (`Main.kt:23`, `if (!acquireSingleInstance()) return`) sees the lock + held, signals the primary, and returns — the second JVM exits **normally**, firing the + shutdown hook: `File("...\Temp\simplex").delete()`. +4. `File.delete()` on a directory succeeds only if it is **empty**, which explains the + intermittency — the folder is removed when a second instance exits while `tmpDir` + happens to be empty (e.g. just after the primary's startup wipe). The **still-running + primary** now has no temp directory. + +The next writer then fails: `SoundPlayer.start` / `CallSoundsPlayer.start` at +`tmpFile.outputStream()` with `FileNotFoundException` (as in the stack trace); +`RecorderNative.start` at `File.createTempFile(..., tmpDir)` with the equivalent +`IOException`. + +An OS temp cleaner (Windows Storage Sense / Disk Cleanup) is a possible secondary cause, +but the second-instance path above is concrete and in-app. That the directory is deleted +at runtime is also acknowledged elsewhere: `DatabaseView.kt` lines 568/570 do +`tmpDir.deleteRecursively()` then `tmpDir.mkdir()`. + +### Why it blinks instead of just failing once + +Of the three writers, only `SoundPlayer.start` is on the Compose render path: it is +invoked synchronously inside a `LaunchedEffect` in `IncomingCallAlertView.kt:30-32`, with +no surrounding try/catch. The stack trace confirms the exception unwinds through the +render/flush path (`FlushCoroutineDispatcher.flush` -> `BaseComposeScene.render`) and +escapes into the AWT event-dispatch thread. The incoming-call alert's composition never +commits cleanly, so the window keeps repainting — the continuous blinking — until the +alert is dismissed (call stops) or the process is killed. + +The other two writers run off the render path — `CallSoundsPlayer.start` from background +coroutines in `CallView` (`withBGApi`), `RecorderNative.start` from a user-initiated +record action — so the same missing-dir bug there fails the sound/recording once rather +than producing a render loop. They are the same latent bug, guarded in the same pass. + +## Fix + +**Root cause — no destructive filesystem side effects in `Files.desktop` `val` +initializers.** Top-level `val` initializers run in *any* process that touches the facade +class, including a transient second instance (via `acquireSingleInstance()` → +`dataDir`). Two of them **delete** shared state and must not run there: + +- `tmpDir` had `.also { it.deleteOnExit() }` — a second instance's normal exit deleted + `...\Temp\simplex`. +- `preferencesTmpDir` had `.also { it.deleteRecursively() }` — a second instance's + `` wiped `configPath\tmp` (the same anti-pattern, firing even earlier). + +Make both declarations pure and move the destructive work into `Main`, past the +single-instance check, so only the owning instance performs it: + +```kotlin +// Main.kt +if (!acquireSingleInstance()) return +preferencesTmpDir.deleteRecursively() // was a val initializer; early, before settings writes +... +tmpDir.deleteRecursively() +tmpDir.mkdir() +tmpDir.deleteOnExit() // only the owning instance cleans up on exit +``` + +A transient second instance returns before these lines, so it no longer damages the +running primary's temp dirs. The remaining `val`-initializer side effects +(`wallpapersDir.mkdirs()`, `preferencesDir.parentFile.mkdirs()`) are **creations** — they +are idempotent and destroy nothing, so they are intentionally left in place. + +### Considered and not included: point-of-use `tmpDir.mkdirs()` guards + +An earlier version of this fix also added `tmpDir.mkdirs()` before each of the three +writers in `RecAndPlay.desktop.kt`, to recover from deletion by any *other* actor (a +Windows temp cleaner sweeping `%LOCALAPPDATA%\Temp`, or the `DatabaseView` delete/recreate +window). These were removed once the root cause was fixed: the reported failure was the +second-instance deletion, which no longer happens, and the directory is otherwise created +at startup. The guards were judged redundant against the remaining low-probability cases +and dropped in favour of the smaller, root-cause-only change. (`createTmpFileAndDelete` +still keeps its own `parentFile.mkdirs()`, so the preferences-temp path remains guarded.) + +### Related but NOT fixed here: `coreTmpDir` (core file transfers) + +A separate error in a **different** directory looks like the same bug but is not, and is +deliberately left out of this change: + +``` +error chat exception ...\AppData\Roaming\SimpleX\tmp\_snd.xftp: +CreateDirectory "\\?\C:\Users\\AppData\Roaming\SimpleX\tmp\_snd.xftp": does not exist +``` + +`coreTmpDir` (`Files.desktop.kt:17` = `...\AppData\Roaming\SimpleX\tmp`) is handed to the +core via `apiSetAppFilePaths` (`Core.kt:125`). The core **already** creates it recursively +at startup — `Commands.hs:546-554` (`APISetAppFilePaths` → `setFolder` → +`createDirectoryIfMissing True`). So a Kotlin-side `mkdirs()` on the declaration would be +**redundant** (duplicates the core) and **ineffective**: like the core's own startup +creation it runs once at init, so it cannot help when the directory is deleted +*mid-session* (its `Roaming` location points to a third-party cleaner / profile-sync / +manual deletion rather than OS temp cleanup). + +The real cause is that the XFTP agent creates the send work dir with a **non-recursive** +`createDirectory prefixPath` (simplexmq `FileTransfer/Agent.hs:357`, `:370`, rcv `:132`), +which throws when `coreTmpDir` is missing at send time. The surgical fix is +`createDirectory` → `createDirectoryIfMissing True` in the agent — an upstream `simplexmq` +change, out of scope for this repo-side PR. + +## Scope / what this does not change + +- Only these three writers are guarded. `SoundPlayer.start` is the one the bug report + exercises and the only one that fails catastrophically (Compose render loop); the other + two share the identical missing-dir bug on non-render paths and are guarded defensively + in the same change. +- The root `tmpDir` declaration is intentionally left unchanged, and a root-level + `mkdirs()` is deliberately **not** added. It would help nothing: cold start is already + covered by `Main.kt:30-31`, and a one-time `val` initializer cannot survive the + mid-session deletion that is the only real trigger. +- The other on-demand consumers (`Share.desktop.kt`, `PlatformTextField.desktop.kt`, + `Images.desktop.kt`, `ChatModel.kt`, `Utils.kt`) share the same latent exposure to + mid-session deletion, but each already wraps its temp write in `try/catch` and fails + gracefully (a logged error or a single error alert, feature no-ops) — none blinks or + crashes. They are left unguarded here; hardening them would require per-use `mkdirs()` + at each site and is low value given the graceful failure, so it is out of scope. + +## Verification + +Windows-only filesystem path; the root cause was established by reading the code and the +stack trace rather than reproduced on Linux. Each change is a single idempotent `mkdirs()` +call before an existing `RecAndPlay.desktop.kt` temp-file write, with no change to control +flow. diff --git a/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md b/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md new file mode 100644 index 0000000000..f75b962a4b --- /dev/null +++ b/plans/2026-06-29-fix-desktop-video-vlc-factory-race.md @@ -0,0 +1,77 @@ +# Fix desktop crash when opening a video (VLC factory init race) + +## Problem (user-facing) + +Opening a video in full screen on desktop can crash the app with: + +``` +java.util.NoSuchElementException + at java.base/java.lang.CompoundEnumeration.nextElement + ... java.util.ServiceLoader ... + at uk.co.caprica.vlcj.factory.discovery.provider.DirectoryProviderDiscoveryStrategy.getSupportedProviders + at uk.co.caprica.vlcj.factory.MediaPlayerFactory. + at chat.simplex.common.platform.RecAndPlay_desktopKt.vlcFactory_delegate$lambda$0(RecAndPlay.desktop.kt:16) +``` + +The crash is intermittent and originates from the lazy initialization of the shared +`MediaPlayerFactory` while a video full-screen view is being composed. + +## Cause + +Each `MediaPlayerFactory()` constructor runs VLC native-library discovery, which iterates a +JDK `ServiceLoader` over `DiscoveryDirectoryProvider`. `ServiceLoader` and the underlying +`CompoundEnumeration` are **not thread-safe**: when two factory constructions run concurrently +on different threads, one enumeration reports `hasNext() == true` and then throws +`NoSuchElementException` from `nextElement()`. + +There are two factories on the desktop: + +- `vlcFactory` — used by the real audio/video players. Its lazy init is triggered on the + AWT/Compose render thread when a video is opened full screen + (`VideoPlayer.initializeMediaPlayerComponent` -> `RecAndPlay.desktop.kt:16`). +- `vlcPreviewFactory` (`--avcodec-hw=none`) — used by preview snapshot helpers, whose lazy init + runs on the dedicated `previewThread` (`VideoPlayer.getOrCreateHelperPlayer`). + +The single-factory invariant established by #6739 ("use shared VLC media-player factory") was +the original protection against concurrent factory construction. #6924 reintroduced a second +factory (`vlcPreviewFactory`) for hardware-acceleration-free previews, reopening the race: the +render thread can construct `vlcFactory` while `previewThread` constructs `vlcPreviewFactory`, +producing two concurrent `ServiceLoader` discoveries and the crash. + +## Fix + +Serialize the two `MediaPlayerFactory()` constructions behind a shared lock so their +native-discovery / `ServiceLoader` runs can never overlap: + +```kotlin +private val vlcFactoryLock = Any() +internal val vlcFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory() } } +internal val vlcPreviewFactory: MediaPlayerFactory by lazy { synchronized(vlcFactoryLock) { MediaPlayerFactory("--avcodec-hw=none") } } +``` + +Both factories are preserved, including the preview factory's `--avcodec-hw=none` option. The +lock guards only the one-time construction of each factory, so there is no steady-state +contention once both are built. + +### Why this approach + +- **Minimal and intent-preserving.** Keeps both factories (preview still needs + `--avcodec-hw=none`) and only adds serialization, restoring the no-concurrent-construction + guarantee that #6739 relied on. +- **Lazy-preserving.** Each factory is still built strictly on demand; the lock only matters in + the rare window where both initialize at the same time. A smaller diff (forcing + `vlcFactory` first inside the preview initializer) was rejected because it would eagerly + construct the main factory whenever a preview is generated and reads as dead code. + +### Known trade-off + +Because `vlcFactory` is initialized on the AWT/render thread, if `previewThread` is mid-construction +of `vlcPreviewFactory` the render thread can briefly block on the lock until native discovery +finishes. This replaces an intermittent crash with a rare, short stall — an acceptable trade. +A more thorough follow-up would either collapse to a single factory (passing `:avcodec-hw=none` +as a per-media option on preview prepare) or eagerly initialize both factories off the render +thread at startup. + +## Scope + +- `apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt` diff --git a/plans/2026-07-07-signed-files-and-history.md b/plans/2026-07-07-signed-files-and-history.md new file mode 100644 index 0000000000..63084542c5 --- /dev/null +++ b/plans/2026-07-07-signed-files-and-history.md @@ -0,0 +1,64 @@ +# Signed file integrity + signed history preservation + +Extends [channel message signing](2026-06-04-channel-message-signing.md). Part A must land **before** Part B, because forwarding a signed file post is only meaningful once the signature actually binds the file bytes. Related: [roster catch-up subscribers](2026-06-22-roster-catchup-subscribers.md) (verification depends on the recipient holding the author's key). + +## Part A — Sign the file digest (bug, fix first) + +### Problem + +A "signed" XFTP file message signs nothing about the file itself. The signature covers `XMsgNew`, whose `FileInvitation` is built with `fileDigest = Nothing` (`Types.hs:1524`, `xftpFileInvitation`) and an empty embedded description (`dummyFileDescr`, `Internal.hs:389,410`) — because at send time the file is still uploading async. The real digest/key/servers live only in the later, **unsigned** `XMsgFileDescr` events. So the signature attests only `fileName + fileSize`. A malicious relay can pair the genuine signed `XMsgNew` with a substituted description pointing at different content of the same size, and the recipient displays it as "signed & verified". The file signature is currently meaningless. This affects **live** signed messages, not only history. + +### Fix + +Sign a **plaintext** content digest in `FileInvitation.fileDigest` (already a field, inside the signed `XMsgNew`), and verify the decrypted file against it on receive — **only for signed file messages** (when `sign` is passed to the send API). + +- Scope: signed file messages only. For an unsigned message a relay can rewrite both the file and the digest, so verifying it buys nothing; restricting to signed also avoids the extra hashing pass on ordinary sends. +- Sender: when a signed message carries an XFTP file, chat computes the sha512 of the unencrypted source and sets `fileDigest` before building `XMsgNew`, so it is covered by the signature. +- Receiver: for a signed file message, after download+decrypt, chat hashes the decrypted file and compares to the signed `fileDigest`; on mismatch, drop the file and surface a violation event. +- Compatibility: `fileDigest` is an optional field older clients already ignore — forward-compatible; verification runs only on clients that support it. + +### Why plaintext, and produced chat-side (verified in simplexmq) + +The XFTP description's `digest` is `sha512Hash` of the **encrypted** file (`Agent.hs:449`, `Client/Main.hs:290`), produced with a per-send random `key`+`nonce`; the recipient verifies the reassembled **ciphertext** against it (`Agent.hs:295`). It is therefore upload-specific — any re-encryption/re-upload changes it, so it can never be signed once and preserved. A **plaintext** digest is encryption-independent and verifies the content the recipient actually consumes. + +simplexmq computes **no** plaintext hash, and its only free (fused) read is the **async** encrypt pass (`encryptFileForUpload`, `Agent.hs:433`) — too late for the synchronous `FileInvitation` (built in `xftpSndFileTransfer_`, `Internal.hs:389`, before the async upload). So chat computes the plaintext sha512 itself, in the send path off the UI thread, before building the invitation, and hashes the decrypted file on receive to verify — **chat-only, no simplexmq change** (option A). It is one extra full read (decrypt-at-rest + sha512) per side, ~1-3 s for a file near the ~1 GB cap, one-time and only for signed files. Rejected alternative: fusing the digest into the async encrypt/decrypt and deferring the send until it is ready — single-read, but it couples the send to the file pipeline and risks an unresponsive send, which we will not do. + +### Threat model + +Author honest, relay/forwarder malicious. The relay controls the unsigned description but not the signed invitation. Signing the content digest binds the file end-to-end from author to recipient, independent of any relay. + +## Part B — Preserve signatures in history + +### Problem + +`sendHistory` → `processContentItem` (`Internal.hs:1370`) re-encodes each item's current content via `prepareGroupMsg` and sends it unsigned (`groupMsgSigning False`; the relay has no author key). Catch-up members therefore hold **all** history unsigned, so §7 enforcement (`requireVerifiedEdit`/`requireVerifiedDelete`) never protects their items — and can't heal later, because `updateGroupChatItem_` (`Messages.hs:2766`) does not write `msg_signed`, so a subsequent signed edit does not upgrade the item. Only delivering history signed at creation closes this. + +### Design + +- **Storage.** Two nullable columns on `chat_items`: `item_msg_body`, `item_signatures`. Written by relays only, for content items only (same `msg_content_tag` / `include_in_history` filter history uses). `chat_binding` is not stored — it is derived as `smpEncode(publicGroupId, authorMemberId)` at send time. +- **Capture.** Write the columns in `createNewChatItem_` when the item is signed; overwrite them in `updateGroupChatItem_` when a signed edit is applied. This keeps the stored bytes tracking the **latest** signed event, so history always forwards current content. Thread the raw `(msg_body, signatures)` onto `RcvMessage` (from `saveGroupFwdRcvMsg`'s `verifiedMsgParts`) and use `SndMessage`'s existing `signedMsg_`/`msgBody`; today both carry only the `msgSigned` status. +- **Forward.** In `sendHistory`, for a signed content item with stored bytes, forward the original bytes as a `VMSigned` `XGrpMsgForward` (reuse the live path's `encodeFwdElement` / `sendFwdMemberMessage`) instead of re-encoding; unsigned items keep the current re-encode path. +- **Edits need only the last event.** Forwarding the latest signed event — `XMsgNew` if never edited, else the latest `XMsgUpdate` — is sufficient: on the recipient a forwarded `XMsgUpdate` for a not-yet-existing item hits the create fallback in `groupMessageUpdate` (`Subscriber.hs:2234` `catchCINotFound` → `saveRcvChatItem'` → `createNewRcvChatItem`), which creates the item with the edit's `msgSigned` and content, marked edited. So one `(body, signatures)` per item, no original+edit replay. Self-consistent because a verified item only ever accepts verified edits (`requireVerifiedEdit`). +- **Files.** No special branch. The forwarded signed `XMsgNew` carries `name + size + digest` (from Part A); the description follows via the existing `XMsgFileDescr` path and may be re-forwarded/re-uploaded freely — it is not covered by the signature, and integrity now comes from the signed digest. +- **Compatibility.** Same wire format as live signed messages. Signing is tied to relay channels (`groupMsgSigning` gates on `useRelays'`, not a member version), so any relay subscriber already receives signed live messages; signed history is identical. No new version gate. + +### Result + +Catch-up members hold non-edited and edited signed posts as verified with current content, so §7 enforcement protects their edits/deletes — closing the residual documented in the signing plan. Retention is no longer bounded by the 30-day `messages` pruning. + +## Implementation steps + +Part A (chat-only, no simplexmq change): +1. On send, for a signed message with an XFTP file, hash the source off the UI thread and set `FileInvitation.fileDigest` before building `XMsgNew`. +2. On receive of a signed file message, hash the decrypted file and compare to the signed `fileDigest`; on mismatch, drop the file and surface a violation event. +3. Test (one, in the channel test harness): a signed file message verifies end-to-end; and — reusing the forged-message injection from the existing signature tests — a forged file (content not matching the signed digest) fails the check and is dropped. + +Part B: +4. Migration: add `item_msg_body`, `item_signatures` to `chat_items` (SQLite + Postgres modules; register in `Migrations.hs`; add to `.cabal`; schema files regenerate via tests). +5. Thread raw signed bytes onto `RcvMessage`; store/overwrite in `createNewChatItem_` and `updateGroupChatItem_` (relay + content-item + signed). +6. `sendHistory`: signed content item with stored bytes → forward `VMSigned`; else re-encode. +7. Test (one): a catch-up subscriber receives history exercising all transitions — a signed post that was edited (arrives signed, current content), another signed post that was deleted (excluded from history), and a signed post with a file (arrives signed, digest verified) — and a forged unsigned edit/delete of a catch-up-held signed item is rejected. + +## Docs to update on implementation + +Signing/files/history spec + product docs; move the digest gap from `product/gaps.md` to fixed; cross-link this plan from [channel message signing](2026-06-04-channel-message-signing.md). diff --git a/plans/2026-07-08-fix-image-empty-area-below.md b/plans/2026-07-08-fix-image-empty-area-below.md new file mode 100644 index 0000000000..d8990efda9 --- /dev/null +++ b/plans/2026-07-08-fix-image-empty-area-below.md @@ -0,0 +1,156 @@ +# Fix empty area below wide images (v7.0.0-beta.3 regression) + +## Problem + +Since **v7.0.0-beta.3**, wide/landscape images in the chat render with an +**empty strip below** them. The image is drawn correctly at the top of its box +but the box reserves more vertical space than the image occupies, leaving a gap. +Portrait/tall images are unaffected — hence "some images." + +Affects **Android and desktop** (the Compose `apps/multiplatform` UI). iOS is not +affected. + +## Cause + +The regression is commit `2c2337b07` (#7125) — the only image-sizing change +between beta.2 and beta.3. It replaced the framed image preview Box's +`aspectRatio` modifier with a directly-computed fixed height +(`apps/multiplatform/.../chat/item/CIImageView.kt`): + +```kotlin +// beta.2 (interim crash fix) +Modifier.width(w).aspectRatio((previewBitmap.width / previewBitmap.height).coerceIn(1f / 2.33f, 2.33f)) +// beta.3 (#7125) +Modifier.width(w).height(w * (previewBitmap.height / previewBitmap.width).coerceAtMost(2.33f)) +``` + +The box width is: + +```kotlin +val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH +``` + +- **Tall/portrait** images use `w = imageViewFullWidth() * 0.75f`, and + `imageViewFullWidth() = min(DEFAULT_MAX_IMAGE_WIDTH, window − 100.dp)` — so `w` + is always ≤ the available width. The box never has to shrink. No gap. +- **Wide/landscape** images use the **fixed** `w = DEFAULT_MAX_IMAGE_WIDTH = 500.dp`, + which is *not* reduced by the window width. + +On any screen or bubble narrower than ~500dp (all phones, many desktop windows), +`Modifier.width(500.dp)` is **clamped down** to the available width by Compose's +`SizeModifier` (it constrains the requested size into the incoming constraints; +`PriorityLayout` in `FramedItemView.kt` measures the image with `maxWidth` = the +bubble's available width). But `.height(w * ratio)` was computed from the +**un-clamped** `w = 500`, so it does **not** shrink. The inner image +(`ContentScale.FillWidth`, top-aligned via `Alignment.TopEnd`) fills the clamped +width and is therefore shorter than the too-tall box: + +``` +box height = 500 * ratio (from unclamped w) +image height = clampedWidth * ratio (FillWidth to the real width) +empty strip = (500 − clampedWidth) * ratio +``` + +**Why it's a regression and not present before:** the old `.width(w).aspectRatio(r)` +self-corrected. When the box width was clamped to the available width, +`aspectRatio` recomputed the height proportionally, so the box always matched the +image. Switching to a fixed `.height()` (computed once from the nominal `w`) broke +that coupling. + +They could not simply revert to `.aspectRatio()`: it reintroduces the original +#7123 crash. `FramedItemView`'s `Modifier.width(IntrinsicSize.Max)` triggers +`aspectRatio`'s `maxIntrinsicWidth = height × ratio`, which overflows Compose's +18-bit `Constraints` packing for extreme ratios (`4000×1` → `1165 × 4000` ≫ 262142). + +## Fix + +Keep `Modifier.width(w)` (this is what preserves the fixed intrinsic **width** `w` +that both avoids the crash and drives `FramedItemView`'s text-width adaptation), +but derive the box **height** from the *actual, already-clamped* width via a small +`Modifier.layout {}` — restoring the self-correcting behaviour without `aspectRatio` +(`CIImageView.kt`): + +```kotlin +val w = if (previewBitmap.width * 0.97 <= previewBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH +Modifier.width(w).layout { measurable, constraints -> + val width = constraints.maxWidth.coerceAtMost(w.roundToPx().coerceAtLeast(0)) + val height = (width * (previewBitmap.height.toFloat() / previewBitmap.width.toFloat()).coerceAtMost(2.33f)).roundToInt().coerceAtMost(constraints.maxHeight) + val placeable = measurable.measure(Constraints.fixed(width, height)) + layout(width, height) { placeable.place(0, 0) } +} +``` + +New imports: `androidx.compose.ui.layout.layout`, `androidx.compose.ui.unit.Constraints`, +`kotlin.math.roundToInt`. The change is deliberately surgical: the only semantic difference +from #7125 is that the height multiplies the **measured** `width` instead of the nominal `w`. +`roundToInt` (not truncation) matches the px rounding `.height(Dp)` did in #7125. + +### Why `coerceAtMost(w)` and `coerceAtLeast(0)` are load-bearing (crash-safety) + +`width = constraints.maxWidth.coerceAtMost(w.roundToPx().coerceAtLeast(0))` bounds the +width at `w` in every case, which is what keeps `Constraints.fixed(width, height)` inside +Compose's 18-bit (262142 px) packing limit and prevents the #7123 overflow from recurring: + +- **Real measure pass:** `width(w)` (a fixed-size `SizeNode`) already hands the block a + bounded `maxWidth ≤ w` — the screen-clamped width. `coerceAtMost(w)` is then a no-op and + `width` = that clamped width, exactly what removes the strip. +- **Unbounded intrinsic pass:** a plain `Modifier.layout {}` re-runs its lambda for intrinsic + queries, and `constraints.maxWidth` there is `Constraints.Infinity` (`Int.MAX_VALUE`). A + bare `constraints.maxWidth` would feed `Infinity` into `height = width × ratio` → + `Constraints.fixed` throws (the exact earlier-observed regression). `coerceAtMost(w)` + collapses `Infinity` back to `w`, so `height ≤ w × 2.33` — always finite. (In this tree the + outer `width(w)` masks intrinsics so the lambda is never actually invoked unbounded, but the + clamp makes the block correct regardless.) +- **`coerceAtLeast(0)`** mirrors what `Modifier.width()`'s `SizeNode` does internally: when the + desktop window is narrower than the 100dp padding in `imageViewFullWidth()` (or 0 at window + init), `w` is negative, and `Constraints.fixed` requires non-negative — so without the clamp + a portrait image in a tiny window would crash where `.width(w)` silently rendered 0-width. + +`width` and `height` are therefore always in `[0, w × 2.33]`. + +### Why this is correct at every aspect ratio + +- **Fits within `DEFAULT_MAX_IMAGE_WIDTH`** (wide window, or any tall image): the + measured width equals the nominal `w`, so `height = w × heightRatio` — **pixel-identical + to beta.3**. No visible change for the cases that already looked right. +- **Wider than the available width** (wide image on a phone/narrow window): + `width = constraints.maxWidth` is the clamped width, `height = clampedWidth × heightRatio` + — matches the `FillWidth` image exactly. **Empty strip gone.** +- **Tall images** (`h / w > 2.33`): `heightRatio` is capped at `2.33`, so the box is + `width × 2.33·width`; the `FillWidth` image is taller and is cropped by the box + (top-aligned) — same crop cap as before. `coerceAtMost(constraints.maxHeight)` keeps + the `PriorityLayout` height ceiling honoured. +- **No crash:** the intrinsic width is still `w` (from `Modifier.width(w)`, unaffected + by the inner `layout`), so `FramedItemView`'s `IntrinsicSize.Max` never multiplies by a + ratio. The `width = height × ratio` derivation that overflowed `Constraints` is gone. + +Both dimensions are always finite: `width(w)` gives the inner `layout` a bounded +`maxWidth`, and `height ≤ width × 2.33 ≤ maxHeight`. + +## Scope / non-goals + +- Only the `!smallView` framed image Box is changed. The chat-list `smallView` + preview is a fixed square, and all other image/video/link paths size with + `.width(...)` + `ContentScale` (no ratio-derived box), so none of them show the + gap or the crash. +- Same file is `commonMain`, so the one change covers **Android and desktop**. +- Not touched: the bitmap decoders' guards (`Images.android.kt` / `Images.desktop.kt`) + and the still-open "symmetric wide guard" defense-in-depth follow-up noted in + `plans/2026-06-23-wide-image-crash.md` — out of scope for this display fix. + +## iOS + +iOS already computes the height directly from the laid-out width +(`height = w × heightRatio`, `heightRatio = min(h / w, 2.33)`, +`apps/ios/SimpleXChat/ImageUtils.swift`) and lays out with `CGFloat` frames, so it +never had the clamped-width gap. No iOS change required; this brings Android/desktop +back into line. + +## Verification + +- Build Android arm64 debug APK (`bash ~/build/android.sh`) and Linux x86_64 + AppImage (`bash ~/build/linux.sh`) from branch `nd/fix-image-width`. +- Manual: send/view a landscape image (~3:1 and ~4:3) on a phone / narrow desktop + window — the empty strip below should be gone and the image should look as it did + before beta.3. Confirm a very wide panorama still renders as a natural thin strip + (no letterbox, no crash) and a very tall image is still cropped at 2.33. diff --git a/plans/2026-07-09-channel-sign-messages-preference.md b/plans/2026-07-09-channel-sign-messages-preference.md new file mode 100644 index 0000000000..3d6601ff16 --- /dev/null +++ b/plans/2026-07-09-channel-sign-messages-preference.md @@ -0,0 +1,62 @@ +# Channel SignMessages preference (recipient side, stage 1) + +## Problem + +Per-message opt-in signing (the current channel-message-signing PR) puts the signing decision on each sender, so a channel cannot express or enforce an expectation that its content is signed. The chosen direction is a channel-wide preference under which all content in the channel is expected to be signed. This is a two-stage rollout so clients have time to update and process the preference. Stage 1 ships **recipient support only**, default off, channel-only. Stage 2 (later) makes the preference affect sending. The existing Part-A signing/verification/§7 enforcement stays as-is (used for testing the recipient side until sending is built). + +## Design + +New group feature `GFSignMessages` (on/off, no-role, default off), channel-only. When on in a channel, the recipient marks a received **new content item** whose signature is absent as "signature required but missing" rather than plain unsigned. + +`CIMeta.msgSigned :: Maybe MsgSigStatus` becomes `msgVerified :: MsgVerified`: + +``` +data MsgVerified = MVSigned MsgSigStatus | MVSigMissing | MVUnsigned +``` + +- `MVSigned s` — a signature is present (verified or no-key), as today. +- `MVSigMissing` — pref requires a signature but it's absent (red warning in meta). +- `MVUnsigned` — pref doesn't require a signature and it's absent (legacy unsigned). + +Computed at content-item creation from `(RcvMessage.msgSigned, channel SignMessages pref)`: +- `Just s` → `MVSigned s` +- `Nothing` → `MVSigMissing` if the channel requires signing, else `MVUnsigned` + +Only **new content items** get this; edits/deletes keep today's behavior. `RcvMessage.msgSigned :: Maybe MsgSigStatus` (raw signature result) is unchanged. Sent items: `MVSigned MSSVerified` if signed, else `MVUnsigned` (no `MVSigMissing` for own items in stage 1). + +### Encoding + +- **DB (`msg_signed`, unchanged column):** `MVSigned MSSVerified`→`"verified"`, `MVSigned MSSSignedNoKey`→`"no_key"`, `MVSigMissing`→`"sig_missing"`, `MVUnsigned`→`"unsigned"`. Decode: those strings, plus **NULL → `MVUnsigned`** (legacy rows). No migration. +- **JSON (API→UI):** tagged encoding; `omittedField = MVUnsigned` for forward-compat (older clients / missing field). + +### Feature exclusion from regular groups + +`GFSignMessages` added with `groupFeatureInChannel = True`. New predicate `groupFeatureInRegularGroup :: GroupFeature -> Bool` (False for `GFSignMessages`, True otherwise); a `regularGroupFeatures = filter groupFeatureInRegularGroup allGroupFeatures` used where regular groups generate feature items / list features, so `SignMessages` is channel-only (no group items, not in group preference UI). + +### UI + +- `msgVerified` field + `MsgVerified` type on Kotlin/iOS. +- Meta badge: `MVSigned MSSVerified` → signature badge (as now, gated on file loaded); `MVSigMissing` → red `exclamationmark.triangle`, tap → alert "signature required but missing" (mirror the AUTH-error alert on the status X); `MVUnsigned` → nothing. +- Add the `SignMessages` channel preference to the channel preferences UI (will be default-off / hidden at release; shown for now for testing). + +## Steps + +1. `Types/Shared.hs`: `MsgVerified` + TextEncoding/ToField/FromField (NULL→MVUnsigned) + JSON (+omittedField). +2. `Types/Preferences.hs`: `GFSignMessages` (enum, SGADT, name, instances, allGroupFeatures, `groupFeatureInChannel`=True, `groupFeatureInRegularGroup`, preference plumbing, default off); update regular-group feature usage to `regularGroupFeatures`. +3. `Messages.hs`: rename `msgSigned`→`msgVerified`, type `MsgVerified`; `CIMeta` + `JCIMeta` + `mkCIMeta`. +4. Store: `createNewChatItem_` takes `MsgVerified`; write/read `msg_signed`. `createNewSndChatItem`/`createNewRcvChatItem` compute it; thread a `signMessagesRequired :: Bool` where needed. +5. `Subscriber.hs` `newGroupContentMessage`: compute the required flag from the channel pref; adapt §7 enforcement to `MsgVerified`. +6. `View.hs` + other Haskell usages of `msgSigned`. +7. Build core; iterate. +8. UI (Kotlin + iOS): type, field, badge + warning + alert, preference. +9. Tests (channel: pref on + unsigned content → MVSigMissing; pref off → MVUnsigned; signed → MVSigned). + +## Progress / divergences (2026-07-09) + +- **Haskell core: DONE, library builds clean.** Type `MsgVerified` + encodings; `GFSignMessages` feature (channel-only via `groupFeatureInRegularGroup`/`regularGroupFeatures`); `signMessagesRequired` + `toMsgVerified` threaded through send/receive/store; §7 enforcement adapted; CLI renderer `msgVerifiedStr`. +- **Kotlin: DONE (review-verified; gradle not run).** `MsgVerified` sealed class; `CIMeta.msgVerified`; badge + red `ic_warning` for `SigMissing`; tap-alert via `sigMissingInfo`; `GroupFeature.SignMessages` + all switches; `FullGroupPreferences`/`GroupPreferences.signMessages`; channel-gated toggle in `GroupPreferences.kt`; strings. +- **iOS: delegated (in progress).** +- **Divergence 1 (scope):** requirement applies to BOTH as-channel posts (`CDChannelRcv`) and member posts (`CDGroupRcv`); regular groups are never affected because the preference is off there by construction (default off, excluded from the group UI, business chats set it off). Rationale: "everything in the channel signed." Confirm if as-channel-only is wanted. +- **Divergence 2 (Kotlin pref placement):** toggle gated on `groupInfo.isChannel` at the top of the preferences screen (existing feature toggles there are gated on `!useRelays`, and relay-channels showed none). Confirm placement. +- **No migration:** `msg_signed` column reused. `MVUnsigned`→`"unsigned"`; decode `"unsigned"` and NULL → `MVUnsigned`. +- **Remaining:** iOS review; Haskell recipient test; product/spec doc updates (multiplatform + iOS doc protocol). diff --git a/plans/2026-07-10-remove-status-preset-contact.md b/plans/2026-07-10-remove-status-preset-contact.md new file mode 100644 index 0000000000..566cf11dd0 --- /dev/null +++ b/plans/2026-07-10-remove-status-preset-contact.md @@ -0,0 +1,41 @@ +# Remove the SimpleX Status preset contact + +Branch: `nd/remove-status-preset-contact` · PR #7231 (supersedes #7200) + +## 1. Problem statement + +Every new profile gets a "SimpleX Status" preset contact card. It is too confusing for non-technical users: it looks like a person or a chat, but it is an automated broadcast bot, and tapping it opens a connect dialog rather than content. On top of that, its stored contact link is the old `simplex:/contact/#/...` invitation address; the intended replacement is a channel, which a preset *contact* card cannot deliver (see §3). + +## 2. Solution summary + +Delete the preset entirely: + +- `simplexStatusContactProfile` removed from `Library/Internal.hs` (14 lines incl. inline logo). +- Its `createContact` call removed from `createPresetContactCards` in `Library/Commands.hs` (one line; the "Ask SimpleX Team" preset stays). +- Test expectations updated mechanically (see §5). + +No migration for existing profiles: preset cards are only ever created inside user record creation (`APICreateActiveUser` → `createPresetContactCards`), so existing profiles keep their stored card, matching how presets have always behaved. Users can still reach the bot via https://status.simplex.chat and the blog. + +## 3. Why not fix the link instead (the #7200 dead end) + +#7200 swapped the preset's link to a channel link (`https://smp5.simplex.im/c#...`). That cannot work: preset cards are contacts, and tapping one runs `APIConnectContactViaAddress` → `prepareContact`/`joinContact` — the direct-contact handshake. Channel short links (`/c#` decodes to `CCTChannel`) must be joined via `APIConnectPreparedGroup`; `APIConnect` explicitly rejects them ("channel links must be connected via APIConnectPreparedGroup"), but the contact-card path has no such guard and silently performs the wrong handshake. Making the status channel a preset would require a preset *prepared channel* — `APIPrepareGroup` needs a full connReq plus `GroupShortLinkData`, run after the user is active — a substantially larger change than wanted, for a card that confuses the users it is shown to. + +## 4. Scope of effect + +- **New profiles**: get only the "Ask SimpleX Team" card. +- **Existing profiles**: unchanged; their stored SimpleX Status contact remains until the user deletes it. +- **Discovery**: this removes the status bot's only in-app discovery path for new users — deliberate, since the card was doing more harm (confusion) than good (discovery). + +## 5. Test impact + +Removing the card shifts contact ids allocated after `/create user` down by one: + +- 10 chat-list / `hasContactProfiles` expectations lose the status entry (`Direct.hs`, `Profiles.hs`). +- 24 numeric contact refs shift: `@6`→`@5` in `Direct.hs`; `@5`→`@4` and `@6`→`@5` in `Profiles.hs`, plus the id-arithmetic comments. +- `configureTimedMessages alice bob "6" "3"` → `"5"` — the contact id travels as a bare string argument, invisible to `@N`-pattern search; caught only by running the tests. + +Verified unaffected: group ids (`#N`), pending-connection ids (`:N` — preset cards create no connection rows), user ids, unread/`/users` counts (cards create no chat items), `MobileTests`/`RemoteTests`/`Local.hs` (harness users bypass presets), display-name allocation (per-user). + +## 6. Verification + +All 8 non-timing-sensitive affected tests pass locally. The 3 TTL/timed-message tests (`testUsersDifferentCIExpirationTTL`, `testUsersRestartCIExpiration`, `testUsersTimedMessages`) fail identically on the base commit `6bb1da9e8` in the same environment — pre-existing cleanup-manager timing flakes (cf. `d7010d527`), not regressions. The packaged desktop build was verified to contain no "SimpleX Status" strings in `libsimplex.so` while retaining "Ask SimpleX Team". diff --git a/plans/2026-07-11-fix-desktop-clipboard-freeze.md b/plans/2026-07-11-fix-desktop-clipboard-freeze.md new file mode 100644 index 0000000000..59d357446e --- /dev/null +++ b/plans/2026-07-11-fix-desktop-clipboard-freeze.md @@ -0,0 +1,61 @@ +# Fix desktop UI freeze caused by clipboard polling; remove dead `clipboardHasText` state + +## Problem + +User report: the Linux desktop app becomes extremely slow — every scroll or button press takes ~9 seconds. The lag disappears while the clipboard is filled and returns when it is emptied, reproducibly triggered by KeePassXC's clipboard auto-clear (10s safety timeout). Manual clearing via desktop tools does not trigger it. + +## Root cause + +`SetupClipboardListener()` (desktop actual, `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt`) ran `chatModel.clipboardHasText.value = clipboard.hasText()` once at composition and then every 1 second in a `LaunchedEffect` — i.e. on the Compose main dispatcher, which on desktop is the AWT Event Dispatch Thread that processes all input and rendering. + +The call chain of `hasText()` (verified in Compose 1.8.2 / skiko 0.9.4 bytecode): + +``` +PlatformClipboardManager.hasText() + → org.jetbrains.skiko.ClipboardManager_hasText() + → ClipboardManager_getText() + → java.awt.datatransfer.Clipboard.getData(DataFlavor.stringFlavor) +``` + +So "does the clipboard have text" was answered by fetching the full clipboard contents over an X11 selection conversion, every second. On X11, `getData` blocks in `sun.awt.X11.XSelection.waitForSelectionNotify()` until the selection owner replies or `sun.awt.datatransfer.timeout` expires — **default 10 000 ms** (OpenJDK `UNIXToolkit.getDatatransferTimeout()`). + +When the X11 CLIPBOARD selection owner does not answer conversion requests — the state KeePassXC's auto-clear leaves behind — each poll blocked the EDT for the full 10 s, then the loop slept 1 s and blocked again. The EDT was therefore blocked ~10 of every ~11 seconds; a random click or scroll waited ~9 s on average, exactly as reported. Any responsive clipboard owner (any normal app copying text) made the polls instant again, which is why filling the clipboard "cured" it. + +## Why deletion (not a workaround) is correct + +`chatModel.clipboardHasText` has had **zero readers since July 2024**: its only ever consumer — a conditional paste icon over the chat list search field — was removed in #4398 (commit `3e623684b`). Everything else was write-only plumbing for that state: + +- desktop 1s poll (`Utils.desktop.kt`) — the freeze source; +- Android `addPrimaryClipChangedListener` (`Utils.android.kt`, added in #3529); +- Android `onResume` re-read of `hasPrimaryClip()` (`MainActivity.kt`, #3758) — a workaround for Android 10+ denying clipboard access to backgrounded apps, needed only to keep the state fresh for that same paste icon; +- the `expect`/`actual` declarations and the `App.kt` call site; +- the `ChatModel.clipboardHasText` field itself. + +Verified before removal: + +- No direct reads anywhere in the repo (all source sets, all modules), on any remote branch, and none in any upstream PR (GitHub code search: only the writers + one spec table row). +- No indirect access: no reflection over `ChatModel`, no `"clipboardHasText"` string literal, no wholesale snapshot observation, no serialization (`ChatModel` is `@Stable`, not `@Serializable`), nothing crosses the mobile↔desktop remote protocol, no Gradle/lint/ProGuard/codegen references, no tests. +- Removing the `App.kt` call cannot affect sibling composition: the composable emits no UI node (Android: bare `DisposableEffect`; desktop: state writes only), and there is no `key()`/`movableContentOf`/positional logic nearby. +- Android side effects: none. `hasPrimaryClip()` does not read clip contents (no Android 12+ access toast); this was the only primary-clip listener (no ordering concerns); the `onResume` coroutine `Job` was never captured or awaited. +- All remaining paste features are unaffected because they read the clipboard directly at the moment of user action, not via this state: "tap to paste link" (`NewChatView.kt`), Ctrl+V in the composer (`PlatformTextField.desktop.kt`), "paste desktop address" (`ConnectDesktopView.kt`), migration link paste (`MigrateToDevice.kt`). + +After this change the desktop app touches the X11 clipboard only on explicit user paste, so no background clipboard state can stall the UI. (A paste attempt while the selection owner is unresponsive can still block once, up to the AWT timeout — inherent to AWT's synchronous X11 clipboard, out of scope.) + +## Change + +Delete the dead mechanism end to end: + +- `common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt` — `expect fun SetupClipboardListener()` +- `common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt` — desktop actual (the polling loop) + unused imports +- `common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt` — Android actual (clip listener) + unused imports +- `common/src/commonMain/kotlin/chat/simplex/common/App.kt` — call site +- `common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt` — `clipboardHasText` field +- `android/src/main/java/chat/simplex/app/MainActivity.kt` — `onResume` clipboard block + unused imports +- `spec/state.md` — stale table row +- `spec/client/navigation.md` — stale "sets up clipboard listener" step in the MainScreen description + +## Verification + +- `:common:compileKotlinDesktop`, `:common:compileReleaseKotlinAndroid`, `:android:compileReleaseKotlin` pass. +- Repo-wide grep for `clipboardHasText`/`SetupClipboardListener` returns no references outside this document. +- Linux AppImage and Android APK built from this branch. diff --git a/plans/2026-07-11-simplex-name-ux.md b/plans/2026-07-11-simplex-name-ux.md new file mode 100644 index 0000000000..be3ec7e153 --- /dev/null +++ b/plans/2026-07-11-simplex-name-ux.md @@ -0,0 +1,157 @@ +# SimpleX name — UX improvements plan + +Date: 2026-07-11. Scope: **client-only** (iOS SwiftUI + Kotlin/Compose for Android & desktop); no core / simplexmq change. +Adversarially verified against the code; fixes are merged inline below. + +The contact-address and channel name editors are the **same shared view** per platform +(iOS `SetSimplexDomainView` in `apps/ios/.../UserSettings/UserAddressView.swift:718`; Kotlin `SetSimplexDomainView` in +`apps/multiplatform/.../usersettings/SetSimplexNameView.kt:22`), so fixing the editor once covers both call sites. + +Reusable pieces: +- Badge: iOS `SimplexNameView` (`ChatInfoView.swift:1377`), Kotlin `SimplexNameView` (`SimplexNameView.kt:28`). +- Inline name-error pattern: iOS `CreateProfile.profileNameField` (`CreateProfile.swift:310-333` — red `exclamationmark.circle` + in the field when invalid + disabled action button); Kotlin `ProfileNameField` (`WelcomeView.kt:413`, warning `IconButton` + + `isValid` predicate). See Task 3 for the reuse caveats. +- Warnings: iOS `showAlert` free function; Kotlin `AlertManager.shared`. String "Profile update will be sent to your SimpleX + contacts" exists on both platforms. +- `apiSetUserDomain` broadcasts the updated profile to contacts (same update+notify path as `APIUpdateProfile`) — CONFIRMED. + +## Resolved design decisions (maintainer) +- **Banner name = interactive** (reuse `SimplexNameView`). The iOS banner observes `chat` (`@ObservedObject`), so a verify + updates the model and refreshes via the observed `chat` — no real caveat. +- **Remove button = clears the input field only**; no save, no confirm. Saving an empty field is what removes the name. +- **Validation = disable Save while invalid + the inline warning-icon pattern**, with a SimpleX-name predicate (below). +- **Save-on-close = prompt Save / Don't save, ONLY when the name is valid AND changed** (invalid or unchanged → silent close). +- **Row display**: actual name (`@name.simplex` / `#name`) when set; original label ("Your SimpleX name" for the address) when + unset. [address rows DONE] +- **Task 5 warning**: pre-save confirm using the existing string, only when the name actually changed; user/contact path only. +- Copy copies the shown prefixed name. Kotlin editor title stays "Set SimpleX name". + +## Task 1 — SimpleX name in the chat-start banner (contact / channel / business) — *safe to build* +Reuse `SimplexNameView`, switching on chat type. +- **iOS** `ChatView.swift` `struct ChatBannerView` (1004-1065): insert between the shortDescr block (~1038) and the + `chatContext` block (~1040). `switch chat.chatInfo`: + - `.direct(let contact)` → contact block (mirror `ChatInfoView.swift:396-414`). NOT verbatim: the banner has no + `@State contact`, so the verify closure uses `ChatModel.shared.updateContact(ct)` and drops `contact = ct`. + - `.group(let groupInfo, _)` → nested here (both need `groupInfo`): `businessChat == nil` → channel block + (`GroupChatInfoView.swift:335-354`). NOT verbatim: the banner binds an immutable `let groupInfo`, so — like the contact + case — the verify closure uses `ChatModel.shared.updateGroup(gInfo)` and drops the `groupInfo = gInfo` reassignment + (`GroupChatInfoView.swift:346`, which only compiles at the source because `groupInfo` there is an `@Binding`). Else business + block (`:356-366`, verify `{ nil }`). + - `default` → `EmptyView()` (keep the switch exhaustive). +- **Kotlin** `ChatView.kt` `ChatBannerView` (2227-2358): insert between the descr `MarkdownText` (~2344) and `chatContext()` + (~2346). `when (chatInfo)`: `Direct` → contact block (`ChatInfoView.kt:760-773`); `Group` + `businessChat==null` → channel + (`GroupChatInfoView.kt:976-990`); `Group` + `businessChat!=null` → business (`:991-1001`). `remoteHostId` + `chatModel` are in + scope, so the verify bodies copy verbatim. + +## Task 2 — row shows the name; Copy + Remove buttons in the editor +- **Row (address)**: DONE (iOS `UserAddressView.swift`; Kotlin `UserAddressView.kt`). +- **Row (channel)**: show `#name` when set, keep the existing label when unset. iOS `GroupChatInfoView.swift:734` (value from + `:712`, already `#`-prefixed). Kotlin `GroupChatInfoView.kt:641`; the value at `:183` is NOT `#`-prefixed — render `"#$name"`. +- **Editor buttons** (shared editor, shown only when the ORIGINAL prefill is non-empty). Each platform gates on its own original: + iOS reuses the `original` captured in Task 4; Kotlin has no `original` var — it gates on the `simplexName` param, which IS the + immutable original (Task 4, `SetSimplexNameView.kt:26`). + - iOS `SetSimplexDomainView` (`UserAddressView.swift:718`): in the Save `Section`, gate on `if !original.isEmpty` and add + `Button("Copy") { UIPasteboard.general.string = }` and `Button("Remove") { simplexName = "" }` — clears the + existing `@State var simplexName` (NOT a var named `name`); no save, no confirm. + - Kotlin `SetSimplexNameView.kt` (63-75): below the Save `SectionItemView`, `if (simplexName.isNotBlank())` add a Copy + `SectionItemView` (clipboard + copied toast) and a Remove `SectionItemView { name.value = "" }`. + +## Task 3 — validate; block Save on invalid +Add a SimpleX-name `isValid` predicate that **normalizes internally** (trim, strip a leading `@`/`#`, add `.simplex` via +`addSimplexTLD`) then checks the grammar (dot-separated ASCII `[a-zA-Z0-9]` labels + internal hyphens, ≤63 bytes/label, ≤253 +total, TLD label present), mirroring simplexmq `SimplexName.hs` `nameLabelP` — note `isNameLetter` (`SimplexName.hs:71`) accepts +`A-Z` as well as `a-z` and the parser lowercases on accept (`:90`), so `isValid` MUST accept uppercase (or, equivalently, +lowercase the input inside `normalized()` before the grammar check); a literal `[a-z0-9]` predicate would flag a valid uppercase +entry that the core accepts-and-lowercases, wrongly disabling Save. **`isValid` returns true for empty** (a cleared field is +valid — it means "remove"), so the warning icon doesn't flash on Remove. +- **iOS**: mirror `profileNameField` — red `exclamationmark.circle` in the field when invalid; `Save.disabled(saving || !isValid || unchanged)` + (also fixes iOS not disabling Save when unchanged/empty). +- **Kotlin**: the editor field is currently `PlainTextEditor(name, placeholder)` (`SetSimplexNameView.kt:64`), NOT `ProfileNameField`, + so the warning icon is not there yet and both options below change which field the editor renders. `ProfileNameField` is also NOT + a clean drop-in — its invalid-tap hardcodes `showInvalidNameAlert(mkValidName(name.value), name)` (`WelcomeView.kt:454`, the + display-name correction, wrong for `@name.simplex`) and passes the RAW `name.value` to `isValid` (`:473`). Either (a) REPLACE + `PlainTextEditor` with `ProfileNameField` — passing a NON-EMPTY `placeholder` (its `trailingIcon` is gated on `!valid && placeholder != ""`, + `WelcomeView.kt:452`, so an empty placeholder hides the warning) AND adding an invalid-tap/correction-callback param to + `ProfileNameField` so it takes the SimpleX `isValid` and correction instead of the hardcoded display-name one — or (b) keep + `PlainTextEditor` but wrap it (Row/Box) with a separately-rendered warning icon computed from the SimpleX `isValid`. Either way, + `Save.disabled = unchanged || saving.value || !isValid`. + +## Task 4 — save-on-close prompt (valid && changed), consistent across contact + channel — *largest fix* +The editor currently stores no baseline, so first **capture the original**, then compute `changed` by comparing the normalized +entered value against the normalized original. But the existing `normalized()` takes NO argument — iOS `normalized()` +(`UserAddressView.swift:759`) reads `self.simplexName`, Kotlin `normalized()` (`SetSimplexNameView.kt:38`) reads `name.value` — +so each normalizes only the entered value and there is no way to normalize the original. **Refactor `normalized()` to accept the +string as a parameter** (iOS `private func normalized(_ s: String) -> String?`; Kotlin `fun normalized(s: String): String?`), +update its one existing call in `doSave` to pass the entered value, then reuse it for both sides: +`changed = normalized(entered) != normalized(original)`. +- iOS: add `@State private var original` set in `.onAppear` to the prefill; also `@State private var didSave = false`. + Compute `changed = normalized(simplexName) != normalized(original)` (and `unchanged = !changed`). +- Kotlin: the `simplexName` param IS the immutable original; redefine `unchanged` (`SetSimplexNameView.kt:32`) as + `normalized(name.value) == normalized(simplexName)`, comparing normalized values rather than raw-trimmed, so it matches iOS, + and add `val changed = !unchanged` alongside it (Kotlin parallel to the iOS `changed`, referenced by the close-prompt gate below). + **Ordering caveat**: `unchanged` is a `val` at `:32`, but the local `fun normalized` and its helper `fun addSimplexTLD` + are declared *below* it (`:34` and `:38`), and Kotlin does NOT hoist local functions — a `:32` initializer referencing + `normalized` fails to compile ("unresolved reference: normalized"). So first MOVE the `addSimplexTLD` + `normalized` + function declarations above the `unchanged` line (keeping their order, `addSimplexTLD` before `normalized`), then + redefine `unchanged`. (iOS is unaffected: there `normalized` is a struct member function, visible regardless of textual + order.) +Prompt only when `changed && isValid` (on the contact path this Save action carries the Task 5 broadcast warning and saves with +the nested confirm suppressed — see Task 5 "Close-prompt interaction"): +- **iOS**: `.onDisappear { if !didSave && changed && isValid { showAlert("Save SimpleX name?", Save/Don't-save) } }`. + **CRITICAL**: set `didSave = true` on a successful Save — Save calls `dismiss()` (`UserAddressView.swift:746`) which fires + `.onDisappear` with the edited value still set, so without `didSave` the prompt double-fires right after saving (cf. + `UserProfile.swift:157` `getCurrentProfile()` which resets its baseline instead). +- **Kotlin**: `ModalView(close = { onClose(close) })`; `onClose` shows the prompt only when `changed && isValid`. The desktop + background-click bypasses `ModalView.close` (`ModalView.kt:41,61`; cf. `UserAddressView.kt:527`), so on the **contact / + start-panel path** the close must also route through `onClose` via `chatModel.centerPanelBackgroundClickHandler`. Because + `SetSimplexDomainView` (`SetSimplexNameView.kt:22-58`) is ONE shared function serving both call sites, it cannot infer which + path it is on — so give it the signal explicitly: add a `registerBackgroundClose: Boolean = false` param. The contact call + site (`UserAddressView.kt`) passes `true`; the channel call site (`GroupChatInfoView.kt:182`) leaves it `false`. In a + `LaunchedEffect(Unit)` the editor registers the handler (→ `onClose(close)`) ONLY when `registerBackgroundClose` is true. Do + NOT register it for the channel editor: that editor opens via `ModalManager.end` (`GroupChatInfoView.kt:182`) while the desktop + background-click overlay is gated on start-panel modals and closes only `ModalManager.start` (`App.kt:449-456`), so registering + there is both a dead no-op for the channel path (already covered by the app-bar back button = `ModalView.close`) AND, because + `centerPanelBackgroundClickHandler` is a single global slot on `chatModel` (`ChatModel.kt:238`), it would cross-wire a + start-panel background click to the channel editor's close logic. The handler MUST be cleared to `null` on EVERY close path — + the save path (`doSave`, `SetSimplexNameView.kt:47-58`), don't-save/revert, and direct close. Clearing to `null` is idempotent, + so the editor clears it unconditionally on close regardless of `registerBackgroundClose` (a safe no-op on the channel path, + which never set it); cf. the precedent (`UserAddressView.kt:507,514,518`). The existing `showUnsavedChangesAlert` + (`UserAddressView.kt:786`) hardcodes auto-accept strings — write a local prompt / new SimpleX-name strings, don't call it directly. +Same pass: unify the editor prefill form (contact prefills full `@name.simplex`, channel prefills short `#name` — pick one). + +## Task 5 — warn when saving a *contact* name (profile broadcast) +Gate in the caller's save closure (shared editor; the channel path is behaviorally untouched — no broadcast confirm — though its +closure signature changes with the `confirmBroadcast` flag below; channel uses `apiSetPublicGroupAccess`). The save +closure is `(String?) async -> Bool` (iOS) / `suspend (String?) -> Boolean` (Kotlin) and the confirm is callback-based, so +**bridge the dialog** so the closure awaits the user's choice before calling `apiSetUserDomain`: +- **iOS** `UserAddressView.swift ~202-210`: `withCheckedContinuation` around `showAlert`. **MUST use the actions-based + `showAlert` overload** (`ShareSheet.swift:61`), building BOTH the confirm and the cancel action with handlers that resume the + continuation **exactly once** — the default `showAlert(title:message:buttonTitle:buttonAction:)` overload's Cancel action + (`cancelAlertAction`, `ShareSheet.swift:130`) has NO handler, so tapping Cancel would never resume and the async save closure + would hang forever with `saving` stuck true. Gate on a **client-side compare** (entered name vs + `currentUser.profile.contactDomain?.domain`) — not the response — because `apiSetUserDomain` collapses both changed and NoChange + to `return user` (`SimpleXAPI.swift:1380`). Confirm text = the existing "Profile update will be sent to your SimpleX contacts". +- **Kotlin** `UserAddressView.kt ~378-387`: `suspendCancellableCoroutine` around `AlertManager.shared.showAlertDialog`; string + exists. Wire `onConfirm`, `onDismiss`, AND `onDismissRequest` (`AlertManager.kt:126-133`) to each resume the continuation + **exactly once** — otherwise a dismissal (tap-outside / back) leaves the suspended save coroutine hung. (The editor already + disables Save when unchanged, so the "only when changed" gate is largely covered here.) + +**Close-prompt interaction (Task 4 ↔ Task 5).** Tapping *Save* in the Task 4 "Save SimpleX name?" close prompt runs the same +contact save closure that carries this Task 5 broadcast confirm, so without a decision two dialogs stack (Task 4 prompt → +then Task 5 confirm). Decision: the close prompt already captured intent to save, so the nested Task 5 confirm is SUPPRESSED +when the save originates from the close prompt, and the warning is surfaced exactly once by giving the **contact-path** close +prompt the broadcast-warning text ("Profile update will be sent to your SimpleX contacts") as its message body (the plain +"Save SimpleX name?" wording stays for the channel path, which has no broadcast). Mechanism: thread a `confirmBroadcast` flag +(default `true`) into the save call — the normal in-editor Save leaves it `true` (shows this confirm), the close-prompt Save +passes `false`. Note this CHANGES the shared save-closure signature — Kotlin `suspend (String?) -> Boolean` → +`suspend (String?, Boolean) -> Boolean`, iOS `(String?) async -> Bool` → `(String?, Bool) async -> Bool` — so BOTH call sites' +closure literals must accept the flag or the code won't compile (mismatched closure types). The contact call site acts on it; +the channel call site (`GroupChatInfoView`) must be updated to accept and IGNORE it (its closure has no broadcast to suppress). +The contact vs channel distinction is the same signal Task 4 already carries (`registerBackgroundClose` on +Kotlin; pass the equivalent flag to iOS's `SetSimplexDomainView`), so the close prompt picks its message from it. + +## Status +- DONE: address row shows dynamic `@name.simplex` when set / "Your SimpleX name" when unset (iOS + Kotlin). +- Task 1 is safe to build as-is. Tasks 2–5 have the verification fixes merged above (Task 4 is the largest; do the Task-4 + `original`/`didSave`/close plumbing first since Tasks 2 and 3 reuse the `original` field and the disabled-Save state). diff --git a/plans/2026-07-12-directory-business-bot-registration.md b/plans/2026-07-12-directory-business-bot-registration.md new file mode 100644 index 0000000000..6c57885ac8 --- /dev/null +++ b/plans/2026-07-12-directory-business-bot-registration.md @@ -0,0 +1,509 @@ +# Directory registration of businesses and service bots (via signed contact card) + +Status: draft plan for review. Grounded against the current tree (branch `ep/improve-names-2`). + +## 1. Goal + +Let a business or a service (chat bot) operator register their **contact `/a` address** in +the directory by **forwarding a signed contact card** to the directory bot — exactly the +UX we already have for channels (`/share chat #ch @'SimpleX Directory'`), but for a contact +address instead of a channel link. + +**Guiding principle: the flow is the channel registration flow verbatim — owner-signed card, admin +approval, re-approval on any profile change, the same periodic link-check loop — differing only in +the listing type (a contact `peerType`, not a group). Where a detail is unspecified here, the answer +is "whatever channels do."** + +Product decisions from the discussion, baked into this plan: + +- **The owner sends the card, and the signature is the authorization.** The `ownerSig` (signed + with the address key) is what proves the address owner authorized the listing — only the key + holder can produce it. We deliberately do NOT use a "directory connects and asks the owner to + confirm" double opt-in (it is a spam vector, like any mailing-list signup). "Submitter ≠ owner" + is handled not by letting non-owners submit, but by giving the owner's tooling a way to send (the + support-bot entry point that would cover the headless case is deferred — §B.4). +- **The directory does NOT connect to / join these addresses.** They are not groups. It verifies + the signature via a link-data *fetch* (not a connection) and records the address in a new table. +- **One table for both businesses and bots** — they are all contact `/a` addresses. The MVP types + each accepted registration by `peerType`: a **bot** requires `peerType == CPTBot`; a **business** + requires `peerType ∈ {CPTHuman, CPTBusiness}` (an unset `peerType` counts as `CPTHuman`); an + unrecognized `CPTUnknown` is rejected. The admin then manually verifies a business before + approving — as for channels. +- **Listing type = `ChatPeerType`.** Extend `ChatPeerType` (today `CPTHuman | CPTBot`, `Types.hs:710`) + with **`CPTBusiness`** and **`CPTUnknown Text`** (forward-compat, like `GTUnknown`), and make the + decoder **lenient** (unknown tag → `CPTUnknown`) so this version won't choke on future tags. + **Wire-compat caveat (verified):** `ChatPeerType` decodes strictly today + (`textDecode … _ -> Nothing`, `Profile` via `deriveJSON`), so a present-but-unknown `peerType` + makes an *already-deployed* app fail to parse the whole profile — it does **not** downgrade to + human. So a business must **not** publish `CPTBusiness` yet (old apps couldn't reach it); a + business's profile stays `CPTHuman` in practice, with `CPTBusiness` reserved for later. The MVP + types a **bot** from `peerType == CPTBot` and a **business** from `peerType ∈ {CPTHuman, + CPTBusiness}` (unset ≙ `CPTHuman`), rejecting `CPTUnknown`; it stores the resolved type + (`CPTBot`/`CPTBusiness`) on the listing. When the lenient version is broadly adopted, businesses + can publish `CPTBusiness` directly. +- **`peerType` and `businessAddress` are orthogonal, and the directory ignores `businessAddress`.** + `businessAddress` chooses the *conversation type* a connector gets (a business chat / group vs a + direct 1:1); it can be set by non-businesses, and a real business may run a plain direct-chat + address. The directory does **not** use it to classify — the type comes from the profile's + `peerType` (bot vs human/business, above), not from `businessAddress`. (App-side only, unchanged: + the connect-preview briefcase shows when **either** `businessAddress` or `peerType == CPTBusiness`; + bot cube from `peerType`, else person — in the MVP that briefcase comes from `businessAddress`. + Separate from directory classification.) +- **Description lives on the contact `Profile`** (new `description` field, parallel to + `GroupProfile.description`). In group-member profiles it is **redacted per the group's policy — + the same treatment `shortDescr` gets** (links/names stripped when the group prohibits them), not + removed wholesale. It is carried **full** in the direct contact view, the address link preview, + and the directory. See §G. +- **`peerType` + `description` are visible in the app independent of the directory** (that is why + owners will set them). `peerType` drives the type icon in the pre-connect alert + (`ConnectPlan.kt:698-713`) and a marker in the chat list / chat banner. The compact surfaces (the + alert, the shared-link card) are too small for the large `description`, so it appears via a + **"Read more"** affordance in the **chat banner** (`ChatView.kt` `ChatBannerView`) and the + **contact info page** (`ChatInfoView.kt:778`) that opens the full text in a sheet (iOS) / alert + (Kotlin). These are NOT the welcome/auto-reply message (`AddressSettings.autoReply`, transient + on-connect). Full details in §H. + +Deliverables: (a) an API + CLI to prepare and share the signed contact card; (b) the +`Profile.description` field; (c) directory handling that verifies and stores the address; (d) admin +approval, web listing, and search. (A support-bot entry point for headless businesses is out of +scope for now — §B.4.) + +## 2. End-to-end flow + +``` +Operator's client Directory bot +----------------- ------------- +/share address @'SimpleX Directory' + -> get own /a address (short link, + businessAddress flag, root key) + -> build MCChat { chatLink = + MCLContact {connLink, profile, business}, + ownerSig = sign(rootPrivKey, + chatBinding <> connLink) } ── card ──▶ DEChatLinkReceived (MCLContact, ownerSig) + -> APIConnectPlan (PLAN only, no connect) + fetches link data (opaque) + verifies sig + => CPContactAddress (CAPOk {ownerVerification}) + -> if OVVerified: + addContactReg (bot if CPTBot, + else business), status pending + notify admins with profile (admin verifies) +admins: /approve ... -> status active -> listingsUpdated + -> web listing.json + bot search include it +``` + +Nothing is connected or joined. The only network action on the directory side is a one-time, +opaque link-data fetch for signature verification (consistent with the established rule that +the directory may fetch link data, only name *resolution* leaks membership). + +## 3. What already exists (reuse map) + +All grounded in the current tree: + +- **Chat-link card type** — `MCLContact {connLink :: ShortLinkContact, profile :: Profile, business :: Bool}` + already exists (`src/Simplex/Chat/Protocol.hs:769`). `MCChat {text, chatLink, ownerSig}` and + `LinkOwnerSig {ownerId, chatBinding, ownerSig}` at `Protocol.hs:764,774`. +- **Owner-signature verification for contact addresses is already wired.** `connectPlan`'s + `CTShortContact CCTContact` path fetches `FixedLinkData {rootKey}` + `UserContactData {owners}` + and computes `ov = verifyLinkOwner rootKey owners l' sig_`, surfaced as + `CPContactAddress (CAPOk {contactSLinkData_, ownerVerification})` + (`src/Simplex/Chat/Library/Commands.hs:4287-4289,4518-4527`; `Controller.hs:1114-1121,1139-1142`). + For plain/business addresses `owners == []`, so `ownerId = Nothing` and verification uses the + link **root key** (`verifyLinkOwner` fallback). +- **The directory already receives any `MCChat` card as `DEChatLinkReceived`** — `Directory/Events.hs:108` + turns `(MCChat {chatLink, ownerSig}, Nothing)` into `DEChatLinkReceived`. Today `deChatLinkReceived` + only matches `MCLGroup` and otherwise replies "Only channels can be added to directory via link." + (`Directory/Service.hs:964-979`). We add an `MCLContact` case. +- **Card-sharing UI + API + signing** — `/share chat #g @to` → `SharePublicGroup` + (`Commands.hs:2437-2449`, parser `Commands.hs:5551`) → `APIShareChatMsgContent` + (`Commands.hs:1136-1170`) which builds the `MCChat` and signs with `mkLinkOwnerSig` + + `shareChatBinding` (binds the card to the recipient connection, anti-replay). +- **Address key + business flag storage** — `link_priv_sig_key` (the address root private key, + Ed25519) is stored in `user_contact_links` by `createUserContactLink` + (`src/Simplex/Chat/Store/Profiles.hs:429-439`); `businessAddress` lives in `AddressSettings` + (`Profiles.hs:497-502`) and is published as `ContactShortLinkData.business` + (`Commands.hs:4528-4533`, `Protocol.hs:1584-1592`). Note: `getUserAddress`/`UserContactLink` + do **not** currently read `link_priv_sig_key` back (`Profiles.hs:479-524`). +- **Directory store / listing / web infra** — `sx_directory_group_regs` table + (`Directory/Store/{SQLite,Postgres}/Migrations.hs`), `GroupReg`/`GroupRegStatus` + (`Directory/Store.hs:116-226`), `getAllListedGroups_` (`Store.hs:354-363`), `generateListing` + (`Directory/Listing.hs:148-170`), `DirectoryEntry`/`DirectoryEntryType = DETGroup` + (`Listing.hs:55-86`), website renderer `website/src/js/directory.jsc`. + +## 4. Work items + +### A. Protocol / types + +- `MCLContact` exists; no new protocol message for the card itself. +- **Extend `ChatPeerType`** (`Types.hs:710`, today `CPTHuman | CPTBot`) with `CPTBusiness` and + `CPTUnknown Text` (forward-compat, like `GTUnknown`). Update the `TextEncoding`/JSON instances + (`Types.hs:724-731`): encode `CPTBusiness` as `"business"` and `CPTUnknown t` back to `t` + (round-trips the original tag); make `textDecode` **lenient** — an unrecognized tag becomes + `CPTUnknown t` instead of `Nothing`, so this version never fails to parse a profile with a future + tag. **Verified constraint:** the *current* decoder is strict (`_ -> Nothing`) and `Profile` + is `deriveJSON`-parsed, so an already-deployed app fails the whole profile on an unknown `peerType`; + therefore `CPTBusiness` must not be published on profiles until the lenient version is broadly + adopted. **MVP:** the directory types a **bot** from `peerType == CPTBot` and a **business** from + `peerType ∈ {CPTHuman, CPTBusiness}` (unset ≙ `CPTHuman`; stored as `CPTBusiness`), and **rejects + `CPTUnknown`**. Businesses are then admin-verified — the admin is the gate, as for channels. +- **New optional `description :: Maybe Text` on `Profile`** (`Types.hs:693`), parallel to + `GroupProfile.description` (`Types.hs:867`). Additive/nullable — only businesses/bots set it. + It rides into the address link data automatically (`ContactShortLinkData` embeds the whole + `Profile`, `Protocol.hs:1584`), so the directory reads it from the fetched link data. It is + redacted per group policy in group-member profiles, on **both send and receive** (see §G). No + version bump is needed — `Profile` is `deriveJSON`-parsed and aeson ignores unknown keys, so old + apps just drop `description` (same as when `peerType`/`badge`/`contactDomain` were added). +- **Setting `peerType`/`description` (for tests + eventual UI).** Both are plain `Profile` fields, so + they ride through the existing profile-update path (`APIUpdateProfile` / the `/p` command); tests + drive them via `/_profile`. A small dedicated setter for the multi-line `description` is worth + adding for CLI ergonomics. The app-UI toggle to set `peerType = CPTBusiness` is deferred (per the + wire-compat caveat above). + +### B. Client: prepare + share the contact-address card + +1. **Signing key — from the agent, not the chat DB.** Sign the card with the address short-link key + via `getConnLinkPrivKey (aConnId addressConn)` (already in the agent, used at `Subscriber.hs:1649`; + `getUserAddressConnection` gives the connection). This is the authoritative key — the private half + of the short link's root key the directory verifies against — and it exists whenever the short link + does, **including right after an upgrade** (`setConnShortLink` provisions it). Do **not** read the + chat-DB `link_priv_sig_key` for signing: it is written only at `createUserContactLink` and never on + upgrade. *(Separate cleanup, off the signing path: persist `link_priv_sig_key` on upgrade too — + `setMyAddressData`/`setUserContactLinkShortLink` — reading it back via `getConnLinkPrivKey` so the + column stops being stale.)* +2. **Card-builder API — `APIShareMyAddress {toSendRef :: SendRef}`** (Controller) + handler in + `Commands.hs`, mirroring the group-share case (`APIShareChatMsgContent`, `Commands.hs:1136`): + - `getUserAddress` → `connLinkContact` (short link) + profile + `businessAddress`. + - `getUserAddressConnection` → conn; `getConnLinkPrivKey (aConnId conn)` → `rootPrivKey` + (`Nothing` ⇒ not upgraded → error; the UI pre-empts this via §B.5). + - hoist `shareChatBinding` to top-level; `binding <- shareChatBinding user toSendRef`. + - `ownerSig = LinkOwnerSig {ownerId = Nothing, chatBinding = B64UrlByteString cb, + ownerSig = C.sign' rootPrivKey (cb <> smpEncode connShortLink)}` (contact variant of + `mkLinkOwnerSig`, `ownerId = Nothing` so the directory verifies against the link root key). + - return `CRChatMsgContent user (MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig})`. + `SendRef` covers direct **and** group/channel targets. +3. **CLI command — `ShareMyAddress {toChatName}`**, parser `/share address @to` / `/share address #to` + (`Commands.hs:5551` neighborhood), handler mirroring `SharePublicGroup` (`Commands.hs:2437-2449`): + resolve `toChatName` → `SendRef` → `APIShareMyAddress` → `APISendMessages`. Shares to contacts and + groups/channels alike. +4. **Support-bot entry point — OUT OF SCOPE (deferred).** A headless business running + `apps/simplex-support-bot` (TypeScript, no app UI) will eventually need a way to trigger the + share — a bot admin/config command that calls `APIShareMyAddress` against the directory contact + once connected. Deferred; the core `APIShareMyAddress`/`/share address` path built here is exactly + what it will call. +5. **App UI — "Share via chat" (Phase 1; mirrors the channel share).** The receiving/rendering half + already exists from the channel work (`MsgChatLink.Contact`, `CIChatLinkHeader`, the compose + preview, and the `SharedContent → ShareListView → ComposeView` picker). New pieces: the entry + point, a `SharedContent.AddressLink` case, the `apiShareMyAddress` call, and the upgrade branch. + - **Entry point:** a **"Share via chat"** button (reuse the channel string) in the user's own + address screen (`UserAddressView.kt`), beside the existing OS-share "Share" button. Address + creation lands on this same screen (`createAddress` sets `userAddress`, `UserAddressView.kt:73-84` + — verified), so the button is visible immediately after creating an address. + - **Flow:** tap → if `userAddress.shouldBeUpgraded` (old full address) show an **upgrade alert** + ("To share your address in a chat it will be upgraded to a short link. All your contacts stay + connected."), buttons **[Upgrade & share]** / **[Cancel]** — on confirm: spinner → + `apiAddMyAddressShortLink`, **then** continue (two separate API calls, cleaner errors); no + "share old" option. Then set `SharedContent.AddressLink` → `ShareListView` (contacts + + groups/channels, with the simplex-link prohibition filtering) → pick destination → `ComposeView` + `LaunchedEffect` calls `apiShareMyAddress` → sets the existing `ChatLinkPreview` → optional + message text (same UX as the channel share) → **Send** → the recipient sees the existing + `CIChatLinkHeader` card and taps to connect. + - iOS mirrors this via the existing channel-share flow (`f49d98511`); Kotlin per + `plans/2026-04-17-kotlin-share-channel-link.md`. + +### C. Directory: verify + store (no connect) + +1. **`deChatLinkReceived` — add the `MCLContact` case** (`Directory/Service.hs:964`). + **No new verification code** — reuse the exact plan path channels use (resolved, Q1): + - `deChatLinkReceived ct (MCLContact {connLink, profile, business}) (Just ownerSig)`: + - `APIConnectPlan userId (contact link) PRMAll (Just ownerSig)` — **plan only**, no connect + (rename `PRMAllGroups` → `PRMAll` and make it work for contact links too, not just groups). + `connectPlan`'s contact-address path already computes `ov = verifyLinkOwner rootKey owners l' sig_` + (`Commands.hs:4288`), identical to the channel path at `Commands.hs:4346`; for a plain/business + address `owners == []`, so the card's `ownerId = Nothing` makes `verifyLinkOwner` verify against + the link **root key**. Expect `CPContactAddress (CAPOk {contactSLinkData_ = Just csld, ownerVerification})`. + Use the **fetched** `csld.profile` (`peerType`, `description`, name claim, …) as authoritative + — the card's copies are display-only / potentially stale; `csld.business` is not used for + typing. + - `OVVerified` → type from the fetched profile's `peerType`: **bot** if `CPTBot`, **business** + if `CPTHuman`/`CPTBusiness` (unset ≙ `CPTHuman`; stored as `CPTBusiness`) → `addContactReg` + status pending; **reject `CPTUnknown`** ("unsupported account type"). A business is then + admin-verified before approving (as for channels). `OVFailed reason` → "ownership verification + failed". `CAPKnown`/other → appropriate message. + - The fetch is intrinsic (the root public key isn't in the card, same as channels) and is an + opaque link-data read, not a connection — consistent with the established directory rule. + - Keep the existing `MCLGroup` and fall-through cases unchanged. +2. **New store: `sx_directory_contact_regs`.** Add a migration to + `Directory/Store/SQLite/Migrations.hs` and `Directory/Store/Postgres/Migrations.hs` (new named + migration appended to `schemaMigrations`). Proposed columns: + ``` + contact_reg_id PK autoincrement + user_contact_reg_id INTEGER -- per-submitter sequence (cf. user_group_reg_id) + submitter_contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE + conn_short_link TEXT NOT NULL -- the contact link; the LISTING IDENTITY (stable for + -- contacts). A link change ⇒ unlist + re-register. + -- Must be present in the fetched profile (contactLink). + display_name TEXT NOT NULL + full_name TEXT + short_descr TEXT + description TEXT -- long description (Profile.description) + image TEXT -- base64, optional + peer_type TEXT NOT NULL -- resolved listing type: "bot" or "business" (ChatPeerType) + simplex_name TEXT -- verified SimpleX name, optional (see Q5) + reg_status TEXT NOT NULL + promoted INTEGER NOT NULL DEFAULT 0 + created_at, updated_at TEXT + UNIQUE(conn_short_link); UNIQUE(submitter_contact_id, user_contact_reg_id) + ``` + Records store the profile inline (`display_name`, `description`, `image`, `peerType`, …) — + self-contained, no FK to a joined contact, since we never connect. Reuse **`GroupRegStatus`** + (resolved, Q3) for `reg_status` — same states as channels. New `Directory/Store.hs` data + + functions mirroring the `GroupReg` ones: `ContactReg`, `addContactReg`, `setContactRegStatus`, + `deleteContactReg`, `getContactRegBy{Id,Link}`, `getAllListedContacts`, and a search query. +3. **Registration lifecycle mirrors channels.** `proposed → pending approval → active`, plus + `suspended/removed`. On submission, notify admins with the profile + an approve command. The + directory **re-reads the address links in the same periodic loop as channels** (resolved, Q6 — + `deGroupLinkCheck`, `Service.hs:832`): re-fetch the link data, refresh the stored profile, and + a profile change triggers **re-approval** (hidden until re-approved), exactly like a channel + profile change (`reapprove`, `Service.hs:858`). The loop is the same as channels; a contact + address has a single key and no group membership, so the channel `checkValidOwner` owner-list + re-check has no analog and doesn't run — which also means the empty-`linkOwners` false-delist bug + can't arise, and `UNIQUE(conn_short_link)` (below) prevents the duplicate-row class that triggered + it. **Re-submission of an already-registered link (to research + propose):** mirror the channel + re-registration path (`deReregistration`) — upsert the existing reg and send it back to admin + review on any change, rather than erroring. +4. **Admin & user commands — same commands, extended with a chat type** (resolved, Q4). Reuse the + existing command constructors and syntax; carry a chat-type discriminator on the id token — `#` + for a group (existing), `@` for a contact address — e.g. `/approve @: `, + `/list @...`, `/suspend @...`, mirroring the group forms. The `@`/`#` prefix disambiguates the + overlapping id spaces (a `group_id` and a `contact_reg_id` both start at 1), so no parallel + command names are needed. Extend the command constructors with the chat type, extend + `Directory/Events.hs` `directoryCmdP` to parse the prefix, and branch on it in `Service.hs` + `deSuperUserCommand`/`deUserCommand`. +5. **Link identity + verified SimpleX names (resolved).** The contact **link is the listing + identity** (for contacts the link is expected to be stable). Two conditions: + - **Link present in the profile.** For listing, the fetched profile must declare this link — + `Profile.contactLink` present and equal to the registered link. If the link **changes** (or the + profile stops declaring it), the address is **unlisted** and must be re-registered — the link is + the anchor, not a mutable attribute. + - **Name↔link consistency, verified inline.** If the profile claims a SimpleX name + (`Profile.contactDomain`), resolve that name and confirm it points to **this** link, comparing + inline — the reverse direction of the existing by-name plan path (`Commands.hs:4272-4281`, + `contactDomain`/`nameResolvesTo`). A **name change** re-runs this check. On success, populate + `sx_directory_contact_regs.simplex_name` → flows to `DirectoryEntry.simplexName` and bot/web + search. Reuse `plans/2026-06-25-name-resolution.md` and the group-names work. + *(To research + propose: how the directory detects a link change on re-read, whether an address's + published profile actually carries `contactLink == its own link`, and the exact resolve-and-compare + call for the link → name-claim → link round-trip.)* + +### D. Listing + web + +1. **`DirectoryEntryType`** (`Listing.hs:55`): add `DETContact {peerType :: ChatPeerType}`. The + `taggedObjectJSON`/`dropPrefix "DET"` derivation already emits `{"type":"contact", ...}` for a new + constructor for free (single→multi constructor is transparent); `peerType` serializes as + `"business"`/`"bot"`/etc. +2. **`contactDirectoryEntry`** builder (analogue of `groupDirectoryEntry`, `Listing.hs:100`), from a + `ContactReg` row: `DirectoryEntry {entryType = DETContact peerType, displayName, simplexName, + groupLink = PublicLink Nothing (Just connShortLink), shortDescr` (from `Profile.shortDescr`)`, + welcomeMessage` (from the new `Profile.description`)`, imageFile, activeAt, createdAt}`. + `PublicLink` already models contact links (`Listing.hs:63-68`). Store the profile fields (incl. + `description`, `peerType`) on the reg row at registration so the entry is self-contained. +3. **`generateListing`** (`Listing.hs:148`): merge group entries + contact entries into the single + `DirectoryListing`. Feed the contact rows from `getAllListedContacts` (status active); build + `DirectoryEntry`s from both sources and serialize together. `listingsUpdated` triggers stay as-is, + plus fire on contact-reg status changes. +4. **Website `directory.jsc`**: branch `displayEntries` on `entryType.type` and, for contacts, on + `entryType.peerType`: + - business vs bot label/avatar from `peerType` (`business`/`bot`); non-group avatar fallback + instead of `/img/group.svg`; + - "Connect"/"Chat" affordance instead of the "N members/subscribers" line (`entryMemberCount` + already returns 0 for non-group — `directory.jsc:183-193`); + - join URI already works via `connShortLink` (`directory.jsc:331-348`). + Search/filter already reads generic fields (`displayName`, `shortDescr`, `welcomeMessage`, + `simplexName`), so text search works unchanged. + +### E. Bot search + +Include active contact regs in the bot's search results (`DCSearchGroup` path, +`Service.hs:1115`, backed by `searchListedGroups` in `Store.hs`) as **one unified result set** (not a +separate contact search); match on display name and SimpleX name. + +### F. Tests + +- **Client** (`tests/ChatTests/`): `/share address` produces an `MCChat`/`MCLContact` card with a + valid `ownerSig` (`ownerId = Nothing`); parser test for `/share address`. +- **Directory** (`tests/Bots/DirectoryTests.hs`, mirroring `testRegisterChannelViaCard` + `:2050` and `testDirectoryChannelName` `:2129`): register a business and a bot via card + (verified → pending → admin approve → listed), reject on bad/absent signature, search finds it, + and the generated `listing.json` contains a `"type":"contact"` entry with the right `peerType` + (`business`/`bot`). Wire under the names/SMP test harness as needed. +- **Profile description** (§G): a member's `description` is **redacted per the group's policy** in + the profile others receive in a group (send side) and when stored from an incoming member profile + (receive side) — links/names stripped when the group prohibits them, clean prose passing through; + a direct contact / address preview keeps it full. + +### G. `Profile.description` field + member-profile redaction (resolved) + +`description :: Maybe Text` is added to `Profile` (§A). In group-member profiles it is **redacted +per the group's policy — the same treatment `shortDescr` gets today** (not removed wholesale): +links and SimpleX names are stripped when the group prohibits them. + +1. **Send side** — in `redactedMemberProfile` (`Internal.hs:1266-1277`, which already redacts + `shortDescr`/`contactLink`/name-proof under the group's `SGFSimplexLinks`/`SGFDirectMessages`), + also redact `description` — with a **new inline-strip helper** (per G.3), not `shortDescr`'s + drop-whole `removeSimplexLink`. Adding `description` to `Profile` forces this output record to be + rebuilt here anyway. (Used on every member-profile-out path — `Internal.hs:1254,1262`, + `Subscriber.hs:851,3273`, `Commands.hs:4134`.) +2. **Receive side** — apply the same redaction when ingesting a member profile from the network, so + a peer can't inject a link/name-laden description. Chokepoints: `updateMemberProfile` + (`Store/Groups.hs:3407`) and member creation (`Store/Groups.hs:2510`, `1395`); prefer a single + helper mirroring the send-side redaction. +3. **Redaction granularity (RESOLVED).** **Inline-strip links and names** — drop the + `Uri`/`HyperLink`/`SimplexLink`/`SimplexName` (the `isLink` set, `Markdown.hs:184`) and `Mention` + spans via `parseMaybeMarkdownList`, re-concat the remaining `FormattedText`, keep the prose (empty + result ⇒ `Nothing`). **Exception:** if `hasObfuscatedSimplexLink` matches (a link that can't be + cleanly isolated as a token), drop the **whole** description. +4. **Kept full where wanted** — the address link data (`ContactShortLinkData` embeds the full, + unredacted profile), the direct contact profile view, and the directory listing all carry the + full `description`. Group redaction applies only to member-profile *delivery into a group*, a + separate code path. For the **directory** page, abuse is gated by **admin review** (Q7), not an + automatic filter. +5. **UI/UX** — add a multi-line "Description" field to the profile/address editor (app UI, follow-on + with §B.5). Because the field can carry into groups (redacted), an edit-time hint that links and + names won't show where a group prohibits them is worthwhile, mirroring `shortDescr`. + +### H. App visibility of `peerType` + `description` (why owners will set them) + +These are persistent profile identity shown to everyone who reaches the address — independent of the +directory. That is the reason to fill them in; the directory is a bonus channel. Existing surfaces +(multiplatform paths; iOS/Android mirror them): + +**`peerType` — type icon / badge** (small, already-present surfaces): +- Pre-connect "Open chat?" alert (`newchat/ConnectPlan.kt:698-713`) — type icon + verification; + briefcase when **either** the address `business` flag or `peerType == CPTBusiness`, bot cube from + `peerType`, else person (see §1). The alert holds no description (too small — `AlertManager.kt:289`). +- Chat list (`chatlist/ChatPreviewView.kt:188`, `isBot`) and the chat banner + (`chat/ChatView.kt:2227` `ChatBannerView`, which already has per-type captions — bot / business / + contact) — extend to a business marker from `peerType`. + +**`description` — shown via a "Read more" affordance, NOT inline** (the alert and the in-chat link +card `CIChatLinkHeader.kt` are too small — they carry only the short teaser). Rendered in **two +surfaces: the chat banner (`ChatBannerView`) and the contact info page (`ChatInfoView`, `:778`)**: +- Teaser text: if `shortDescr` is present → show `shortDescr`, then a clickable **"Read more"**; if + `shortDescr` is absent → show the first line of `description` truncated to 100 chars with ellipsis + (up to the first line break), then **"Read more"**. "Read more" appears only when a `description` + exists to reveal. +- **"Read more" is a general, extensible `Modal` markdown element.** Add an inline `Format` variant + `Modal {modalName :: Text}` to `Markdown.hs:51` (sibling to `Command`/`Mention`/`SimplexLink`) — + **no `showText`**: the app resolves both the tappable label and the modal content from the current + chat by `modalName` (e.g. `modalName = "description"` → renders "Read more", opens the contact's + `description`). `Format`'s existing `Unknown` fallback (`parseJSON … <|> pure (Unknown v)`, + `Markdown.hs:533`) makes it forward-compat — old apps decode it as `Unknown`. The **teaser is built + app-side** from the profile fields (d3), so the Haskell core just adds the variant + JSON so the + app's mirrored enum matches; each client renders the tap (iOS sheet / Android modal), reusing the + existing tappable-markdown mechanism (no iOS multiline-hit-test hack). +- This is NOT the welcome/auto-reply message (`AddressSettings.autoReply`, a transient on-connect + message), and NOT shown in the pre-connect alert or the shared-link card. + +**Profile editor** (`usersettings/UserProfileView.kt`) — add the multi-line description field and a +way to set the account type (`peerType`). Note: the editor exposes two separate "business" concepts +— `peerType` (identity) and the `businessAddress` conversation-type setting — which must use distinct +labels, since both otherwise read as "business." + +Note: before connecting, the only surface with room to read the full description is the directory web +page; in-app it is the banner/info "Read more" once the (prepared) chat is open. + +## 5. Files to touch (summary) + +- `src/Simplex/Chat/Types.hs` — extend `ChatPeerType` (`CPTBusiness`, `CPTUnknown`, lenient decode); + add `Profile.description`; JSON/TextEncoding derivations. +- `src/Simplex/Chat/Markdown.hs` — add the `Modal {modalName}` `Format` variant + JSON (§H). +- App views (Phase 1, §B.5/§H) — `UserAddressView.kt` ("Share via chat" button + upgrade branch), + `ChatInfoView.kt` + `ChatView.kt` `ChatBannerView` (description teaser + `Modal` "Read more"), the + Kotlin/Swift `Format` mirror (`Modal` case + tap → sheet/alert) (+ iOS equivalents). The `peerType` + badge/editor UI is deferred. +- `src/Simplex/Chat/Controller.hs` — `APIShareMyAddress`, `ShareMyAddress` command constructors. +- `src/Simplex/Chat/Library/Commands.hs` — handlers + parsers for the two new commands; reuse + `shareChatBinding`. +- `src/Simplex/Chat/Library/Internal.hs` — redact `description` per group policy in `redactedMemberProfile` (send side, §G). +- `src/Simplex/Chat/Store/Groups.hs` — redact `description` when ingesting a member profile (receive side, §G). +- `src/Simplex/Chat/Store/Profiles.hs` — persist `link_priv_sig_key` on short-link upgrade + (`setUserContactLinkShortLink`/`setMyAddressData`); card signing uses the agent's + `getConnLinkPrivKey`, not this column. +- `apps/simplex-directory-service/src/Directory/Service.hs` — `MCLContact` case in + `deChatLinkReceived`; contact-reg lifecycle + admin/user commands; listing trigger. +- `apps/simplex-directory-service/src/Directory/Store.hs` — `ContactReg` model + queries. +- `apps/simplex-directory-service/src/Directory/Store/{SQLite,Postgres}/Migrations.hs` — new table. +- `apps/simplex-directory-service/src/Directory/Events.hs` — extend `directoryCmdP` to parse the + `@`/`#` chat-type prefix and thread the chat type into the (shared) command constructors. +- `apps/simplex-directory-service/src/Directory/Listing.hs` — `DETContact`, `contactDirectoryEntry`, + merge in `generateListing`. +- `website/src/js/directory.jsc` (+ a contact/bot avatar asset) — non-group card rendering. +- `tests/Bots/DirectoryTests.hs`, `tests/ChatTests/*` — tests. + +## 6. Design decisions + +Resolved: + +- **Submission model (RESOLVED: identical to channels).** Submission is by the **link owner**, + **signed with the address key** — exactly the channel card flow, no extra requirement. The + `ownerSig` (only the key-holder can produce it) is the authorization. Admins then decide to list; + any profile change sends it back to admin review; the address is re-read on the same periodic loop + — all as for channels. The only "submitter ≠ owner" accommodation is giving the headless support + bot a way to send (§B.4). *(Earlier we explored open submission with the verified SimpleX name as + the authenticity signal, and an opt-in flag in the address link data; both dropped — the channel + model already answers authorization, and name-verification proves identity, not consent to list.)* +- **Description home (RESOLVED: `Profile.description`, redacted per group policy).** New profile + field. In group-member profiles it is redacted the same way `shortDescr` is (links/names stripped + under the group's policy, §G), not removed wholesale; carried full in the address link data / + direct view / directory. Directory abuse is gated by admin review (Q7). +- **Q1 — Verification (RESOLVED: reuse the plan).** Already verifiable via `APIConnectPlan` — the + same `verifyLinkOwner` path channels use, no new code. The intrinsic link-data fetch (to get the + root public key) is opaque and not a connection. No card/protocol extension. +- **Q4 — Command surface (RESOLVED: same commands, extended with chat type).** Reuse the existing + command constructors and syntax with a chat-type discriminator on the id token — `#` group + (existing), `@` contact (new) — e.g. `/approve @: `. The prefix disambiguates + the overlapping `group_id`/`contact_reg_id` spaces, so no parallel command names are needed. +- **Q5 — SimpleX names (RESOLVED: support now).** Verify name↔link consistency for addresses and + populate `simplex_name`; flows through to listing + search (see §C.5). + +- **Q2 — Entry type (RESOLVED: `ChatPeerType`, typed + admin-verified).** The listing type is a + `ChatPeerType`: **bot** from `peerType == CPTBot`; **business** from `peerType ∈ {CPTHuman, + CPTBusiness}` (unset ≙ `CPTHuman`; stored as `CPTBusiness`); `CPTUnknown` is rejected. Because + `CPTBusiness` can't be published on profiles yet (wire-compat, §A), a business's profile is + `CPTHuman` in practice; the admin verifies it. When profiles can carry `CPTBusiness`, it's read + directly. +- **Q3 — Reg status type (RESOLVED: reuse the group/channel type).** Use the same `GroupRegStatus` + the channel registrations use — no separate `ContactRegStatus`. The lifecycle mirrors channels. +- **Q6 — Updates (RESOLVED: re-read in the same loop as channels).** The directory re-reads the + registered address links in the same periodic link-check loop it runs for channels + (`deGroupLinkCheck`, `Service.hs:832`), re-fetching the address link data to pick up profile/link + changes and re-verify the name. Opaque fetch, no connection. +- **Q7 — Description screening (RESOLVED: two surfaces, two mechanisms).** *Directory page:* admin + approval is the gate — a profile change (incl. description) triggers re-approval, hiding the + address until re-approved, exactly like a channel profile change; no separate automatic content + filter on the directory description. *Group member profiles:* the group's own policy redacts the + description on delivery (links/names stripped like `shortDescr`, §G). The two are independent. + +## 7. Suggested sequencing + +**Phase 1 — UX prerequisites (self-contained; do these first — no registration work until they +land).** + +1. **`Profile.description` field** (§A) + member-profile redaction on send and receive (§G) + a test + that a member's description is redacted per group policy. +2. **Show the description in the app** — banner + contact-info "Read more" via the `Modal` markdown + element (§H). This is the "see how it looks" step; iterate on the UX here. +3. **`ChatPeerType` extension** (`CPTBusiness`, `CPTUnknown`, lenient decoder) (§A) — the type only, + **no UI** to set or display it yet. +4. **Share a contact link via chat** — core (`getUserAddressSignKey`, `APIShareAddress`, + `/share address`, §B.1–3) + the app share UI mirroring the channel share (§B.5) + a client test on + the signed `MCLContact` card. + +**Phase 2 — directory (only after Phase 1).** + +5. Directory store: migration + `ContactReg` model/queries (link-keyed). +6. `deChatLinkReceived` `MCLContact` case (verify + derive type + link-in-profile check + + `addContactReg`) + name↔link verification + admin approval + directory test through to "listed". +7. Listing merge (`DETContact` + `contactDirectoryEntry` + `generateListing`) + one unified + group+contact search + website rendering. + +**Deferred:** peerType setting/badge UI; the support-bot entry point (§B.4). diff --git a/plans/2026-07-13-fix-command-hover-cursor.md b/plans/2026-07-13-fix-command-hover-cursor.md new file mode 100644 index 0000000000..6f96abeee6 --- /dev/null +++ b/plans/2026-07-13-fix-command-hover-cursor.md @@ -0,0 +1,240 @@ +# Fix: hand cursor not appearing over clickable commands (desktop) + +## Problem + +On desktop, the mouse cursor should change to a hand over clickable commands in chat messages +(e.g. `/join 143556` in a bot's menu of commands) — the hand is what tells the user the command +can be clicked. This did not work reliably: hovering a command sometimes left the text/arrow +cursor in place, giving no indication that the command is clickable. It usually worked, but failed +intermittently — most often when clicking many commands in a row in a chat, because every click +inserts the sent message and shifts the list under the pointer — and once wrong, the cursor stayed +wrong even while moving within the same message; it only recovered after leaving the message text +and re-entering it. The same mechanism affects all clickable elements in message text (links, +simplex addresses, secrets), but command menus made it most visible. + +A second user-visible problem surfaced while testing this fix: a click on a command was sometimes +not registered at all when clicking two commands (e.g. two `/join`s) in short succession. That is +defect 3 below — two independent mechanisms in the click path, unrelated to the cursor layers. + +## Investigation + +The cursor is driven by two cooperating layers in `ClickableText` (TextItemView.kt): + +1. **Detection** — `Modifier.pointerInput { detectCursorMove { ... } }` maps the pointer position to + a character offset and checks for `COMMAND`/link annotations, storing `PointerIcon.Hand` or + `PointerIcon.Text` in per-item state (`onHover`). +2. **Display** — `Modifier.pointerHoverIcon(icon.value)` tells Compose which cursor to show. + +Both layers were traced against the Compose Multiplatform 1.8.2 sources (`ui-desktop`, +`foundation-desktop`). + +### Defect 1: detection layer lost events (commit "fix pointer cursor not changing to hand…") + +`detectCursorMove` (GestureDetector.kt) processed **one pointer event per +`awaitPointerEventScope` block**, exiting and re-entering the scope through `forEachGesture` for +every event. `forEachGesture` is deprecated in this exact Compose version with the message *"Use +awaitEachGesture instead. forEachGesture() can drop events between gestures."* — so the final Move +event, the one that should switch the cursor when the pointer comes to rest on a command, could be +silently dropped. It also ignored `Enter` events entirely, so when the chat list shifted under a +stationary cursor (every command click appends the sent message), the newly hovered item — which +receives `Enter`, not `Move` — never updated the icon state. + +**Fix:** one never-exiting `awaitPointerEventScope` loop reacting to `Move`, `Enter` and `Release` +(plus an optional `onExit` callback used by the cursor fix below). `Release` refreshes hover at +the final pointer position — after clicking a command the list shifts and the release is the only +event a stationary pointer gets. Button-held events are ignored (gated on *no* pointer pressed, +matching the old `forEachGesture` semantics which waited for all pointers up between events), and +so are out-of-bounds positions: while a button is pressed the hit path is locked, so the pressed +node keeps receiving events after the pointer leaves it — acting on those would apply hover +effects at clamped positions (e.g. a wrong cursor stuck after drag-releasing outside the text). +The other `detectCursorMove` call sites (scrollbar reveal in `ScrollableColumn.desktop.kt` and +`OperatorView.desktop.kt`) were audited: reacting to `Enter`/`Release` there is harmless or an +improvement, and `onExit` defaults to a no-op for them. + +### Defect 2: display layer silently swallows updates (commit "set hover cursor directly…") + +This made hovering reliable but the cursor could still get stuck, because Compose's +`pointerHoverIcon` display side is edge-triggered with several silent-drop guards +(all confirmed in the CMP 1.8.2 sources): + +- `HoverIconModifierNode` acts only on per-node `Enter`/`Exit` events (`PointerIcon.kt:253-259`); + Move events never refresh the displayed cursor. +- Its `icon` setter displays only if the value *changed* **and** the node is marked in-bounds + (`PointerIcon.kt:213-221`) — re-asserting the same value, or changing it while the node believes + the cursor is outside, is silently dropped. +- `setIcon` writes the AWT cursor immediately, last-write-wins, with no later reconciliation + (`RootNodeOwner.skiko.kt:738-744`); `onDetach`/`onCancelPointerInput` reset it to default from + recomposition, outside pointer dispatch (`PointerIcon.kt:278-288`). +- Whether a node gets `Enter` depends on `hasEntered` state stored separately in + `HitPathTracker.Node` (`HitPathTracker.kt:582-594`); confirmed windows exist where the two + desync (detach between `buildCache` and dispatch; `cleanUpHover` clearing `isIn` without + dispatching; Press/Release never remapping to Enter). Once desynced, the node receives only + Moves — which it ignores — so the hand can never be displayed again until the pointer physically + leaves and re-enters the text. +- Recovery after content shifts under a stationary cursor depends entirely on the synthetic Move + (`SyntheticEventSender`), which is skipped whenever the sender was reset or + `needUpdatePointerPosition` was not set for that frame. + +Clicking many commands maximizes exposure: each click inserts a message → recomposition burst + +list shift + (for `/join`) group-state changes — each transition can hit one of these edges. +Related upstream issues: JetBrains/compose-multiplatform #2091, #1314, #3750. + +**Fix (initial):** in `onHover` — which now fires reliably on every Move/Enter/Release over the +text — set the AWT cursor imperatively on desktop (`desktopSetHoverCursor`, writing to the same +Skia canvas component Compose writes to), and symmetrically reset both the cursor and the `icon` +state on the per-node `Exit` event. + +**Removed after defect 5 was fixed at the root.** The empirical justification for the imperative +layer (stage 1 under Testing) was contaminated: that test ran while defect 5 was live, i.e. while +every message insertion reset every hover handler in the viewport — which alone explains the +observed failure (a reset suspending handler restarts only on the next pointer event, leaving a +stationary cursor blind after every click). With defects 1 and 5 fixed, the full hover matrix +(rapid stationary-mouse command clicks, list shifting under a hovered pointer, fast sweeps across +commands/links/text, press-drag-out-release-re-enter, wheel-scroll over commands) passes without +the imperative layer, so it was deleted; `Modifier.kt`/`Modifier.desktop.kt`/`Modifier.android.kt` +are untouched by this PR again. What remains of this defect's fix is the `icon.value = Text` reset +on `Exit` in `MarkdownText` — without it, `pointerHoverIcon` re-displays a stale Hand on the next +Enter. The display-layer edges documented above are real in the sources, but with stable handlers +they are not practically reachable in these scenarios. + +### Residual limitation (known, strictly smaller than the fixed bug) + +If a hovered clickable text leaves the composition without the pointer ever getting an `Exit` +event (e.g. the hovered message is deleted, or a fast fling disposes the item within a frame), the +`onExit` reset does not run — the coroutine is simply cancelled. The retained `pointerHoverIcon` +modifier's `onDetach` reset covers this when its node is in the normal (non-desynced) state; in the +rare desynced state the cursor may stay a hand until the next hover write. This is the same event +class the fix reduces, bounded to one stale frame region, and self-corrects on any subsequent +hover. + +### Defect 3: command clicks intermittently lost (commit "multiplatform: fix command clicks lost on quick successive clicks") + +Clicking two commands in short succession sometimes lost a click. Two independent mechanisms, +each reproduced in an isolated harness before fixing and re-verified fixed with the same harness: + +1. **Press-scope race in `detectGesture`** (GestureDetector.kt — a 2022 copy of + `detectTapGestures` predating upstream's hardening; the file has not been resynced since). + The single `PressGestureScopeImpl` is shared across gestures, and its methods run in two + different lanes of the UI thread: `reset()`/`release()` synchronously inside pointer-event + dispatch, while the `onPress` handler (which awaits release, then calls `onClick`) runs in a + launched coroutine resumed through the dispatcher queue. With the old non-suspending + `reset()` (`mutex.tryLock()`), two fast clicks interleave as: up₁ `release()` sets + `isReleased` and unlocks — the first click's `tryAwaitRelease` resumption is only *queued* — + then down₂ `reset()` clears `isReleased` synchronously, so the queued resumption reads + `false` and the first click's `onClick` never fires. Reproduced deterministically in a + single-threaded coroutine harness replaying this ordering (`click1=false click2=true`). + + **Fix:** mirror the `detectTapGestures` serialization from Compose 1.8.2 (`foundation` + `TapGestureDetector.kt`): `reset()` is `suspend` and takes the mutex, so a new gesture cannot + clear the flags until the previous gesture's press handler finished; `release()`/`cancel()` + unlock guardedly (`if (mutex.isLocked)`) and are launched joining the reset job, so flag + writes cannot be reordered across gestures; `tryAwaitRelease()` releases the mutex after + acquiring it, which is what lets the next `reset()` proceed. The mutex becomes a + serialization token between consecutive gestures. `detectGesture` also runs for Android + touch, so the same quick-tap loss is fixed there. + +2. **`pointerInput` restart swallowing an in-flight click** (TextItemView.kt). `ClickableText` + keyed `pointerInput(onClick, onLongClick)` on lambdas that are new instances every + recomposition, so any recomposition of the message restarted the gesture coroutine and + destroyed a gesture in flight: the cancelled `onPress` never resumes, and the restarted + detector waits for a fresh down that never comes for the press already held. Clicking + command 1 sends a message whose insertion recomposes the item 50–300 ms later — exactly when + the second click tends to be pressed. Reproduced in a headless `ImageComposeScene` driving + the verbatim repo gesture code with synthetic pointer events: press → recomposition applies → + release lost the click, while control clicks on either side registered. + + **Fix:** key both `pointerInput` blocks on `Unit` and read the latest handlers through + `rememberUpdatedState` (the standard idiom for long-lived event coroutines). This also stops + recompositions from cancelling `detectCursorMove` mid-stream — the same event-loss class as + defect 1 — and gives all `ClickableText` callers latest-capture semantics for + `annotatedText` (previously the handlers could act on a stale capture until the restart). + +A theoretical residue remains: if three gestures' pointer events were all processed before the +dispatcher ran any of the second gesture's jobs, flags could still mispair. This exposure is +structurally identical in upstream Compose 1.8.2 (`launchAwaitingReset` joins only its own +gesture's reset job), is not reachable by human input, and fixing it would diverge from the +mirrored upstream — left as upstream parity. + +### Defect 4: press cancelled when the list shifts under the pointer (commit "multiplatform: don't cancel command click when chat list shifts under the pointer") + +Clicks were still lost after defect 3's fixes. `waitForUpOrCancellation` cancels a press when an +event's position is out of the node's bounds — correct for a pointer dragged away, wrong when the +*node* moved out from under a stationary pointer, which is exactly what happens when the sent +command's message inserts and shifts the list during the press. In node-local coordinates the two +cases are numerically identical; reproduced in a headless harness (60px shift under a held +pointer → synthetic `Exit` at out-of-bounds local position → press cancelled). + +**Fix:** the two cases separate in window coordinates. `ClickableText` tracks its +`LayoutCoordinates` (`onGloballyPositioned`) and passes a window-position lambda into +`detectGesture`; `waitForUpOrCancellation` exempts the out-of-bounds cancel when the pointer moved +≤ `touchSlop` in window space since the down. Real drag-aways (> slop) cancel as before; both +parameters default to null, reducing the check literally to the old expression for other callers. + +### Defect 5: every message insertion reset all pointer handlers (commit "multiplatform: stop pointer handler resets on chat item recomposition…") + +The dominant residual mechanism, found by instrumenting handler lifecycle: every insertion +produced a burst of gesture-handler cancellations for all visible clickable messages — with the +composables *not* disposed and the handler restarting on the *same* node. A press in flight during +such a burst died silently (`down` with no release/cancel), needing a second click; the hover +handler reset the same way, causing the residual hand-cursor flicker. + +Cause: `ChatViewListItem` provided `LocalViewConfiguration.current.bigTouchSlop()` — a **new +anonymous `ViewConfiguration` instance on every recomposition** of every item, and +`SuspendingPointerInputModifierNodeImpl.onViewConfigurationChange()` is +`resetPointerInputHandler()` (verified in the 1.8.2 sources). Insertions recompose all visible +items (shifting `index`), so each insertion reset every pointer handler in the viewport. + +**Fix:** provide a remembered instance — `remember(viewConfiguration) { +viewConfiguration.bigTouchSlop() }` — keyed on the parent `ViewConfiguration` so a real platform +change still regenerates it. Values are unchanged (the wrapper delegates); only the identity that +the reset machinery reacts to is stabilized. Verified in a harness: a press held across a +recomposition dies with the per-recomposition instance and survives with the remembered one. + +## Performance + +- `detectCursorMove` is now cheaper per event than before (no scope teardown/re-entry per event); + the pressed check allocates one list iterator per event. +- Up to two added recompositions per exit/re-enter cycle of a Hand region (the exit reset writes + `icon.value` Hand→Text, making the next re-enter a Text→Hand write where pre-fix both were + no-ops); otherwise the icon state logic is unchanged. Android is a no-op. +- The defect 3 fix launches two extra short-lived coroutines per gesture (reset job, release/cancel + job) — negligible at click rate. Not restarting `pointerInput` on every recomposition removes + per-recomposition coroutine churn that existed before. + +Framework line numbers cited above are from the official `ui-desktop-1.8.2`/`foundation-desktop-1.8.2` +sources jars on Maven Central; they will drift on upgrade. + +## Testing + +Verified on Linux (AppImage), in two stages (commit subjects name the defect layer each commit +fixes; the user-visible symptom needed both layers, per stage 1 below): + +1. A build with only the detection-layer fix (lossless `detectCursorMove`) was tested first and + the stale-cursor symptom **still reproduced** — this is the empirical justification for the + imperative display-layer workaround; the declarative-only fix is not sufficient. +2. With both layers (pre-hardening build, commit `df6b0655d`): hovering commands after clicking + several in a row, clicking stacked `/join` commands without moving the mouse, and sweeping + across commands quickly — the hand cursor tracks correctly in all cases. + +The later hardening commits (release refresh, pressed/bounds gating, exit resets, canvas cache) +are verified by compilation on both targets and multi-pass adversarial review; runtime +re-verification of the final build is pending. + +Defect 3 is verified by the two isolated repro harnesses described above (both mechanisms +reproduced pre-fix, both register every click post-fix — including a click held across a +recomposition and two fast clicks back-to-back through real Compose event dispatch in +`ImageComposeScene`), by compilation, and by multi-pass adversarial review (two clean passes +pre-commit, two post-commit). + +Stage 1's conclusion is retroactively invalidated by defect 5 (see defect 2's removal note): the +"detection fix alone is insufficient" evidence was gathered while insertions were resetting the +detection handler itself. After defect 5's fix, the imperative workaround was removed and the +hover scenarios re-verified passing on the framework path alone. + +Defects 4 and 5 were isolated with four rounds of temporary instrumentation in the real app +(logging every gesture exit path, composable disposal, and handler/node identity — removed before +commit), each mechanism reproduced and its fix verified in a headless harness, reviewed through +two clean adversarial passes, and confirmed fixed by runtime testing on Linux: fast successive +command clicks all register, and the residual hand-cursor flicker on message insertion is gone. +Known unfixable-at-this-layer residue: a click that lands where a command *was* before the list +shifted (pre-press shift — an aiming race, not an event-handling defect). diff --git a/plans/2026-07-14-fix-draft-message-loss-on-restricted.md b/plans/2026-07-14-fix-draft-message-loss-on-restricted.md new file mode 100644 index 0000000000..f001e38f73 --- /dev/null +++ b/plans/2026-07-14-fix-draft-message-loss-on-restricted.md @@ -0,0 +1,67 @@ +# Fix draft loss when switching to a chat where user cannot send messages + +**Branch:** `nd/fix-draft-message-loss-on-restricted` · **PR:** [#7239](https://github.com/simplex-chat/simplex-chat/pull/7239) (draft) · **Commit:** `9d4bb2188` + +## Problem + +A draft typed in the current chat is lost when another chat is opened where the user +cannot send messages (observer, channel subscriber, review by admins). Reproduces on +desktop only: the chat list is always visible, so `chat` and `chatId` change in the +same recomposition (`CenterPartOfScreen` passes `ChatModel.chatId` itself as +`currentChatId`, App.kt:389). Android is unaffected — `AndroidScreen` updates +`currentChatId` via a `snapshotFlow` collector, so the draft-saving effect fires a +frame before the chat swaps. + +## Root cause + +In `ComposeView.kt`, the effect clearing compose state of a non-sendable chat +(`LaunchedEffect(sendMsgEnabled)`) was composed *before* the draft-saving +`KeyChangeEffect(chatModel.chatId)`. `LaunchedEffect` bodies run in composition +order, so on a same-recomposition switch the clear effect wiped the live compose +state before it could be saved, and `KeyChangeEffect` then fell into its +else-branch, destroying any previously saved draft via `clearPrevDraft` and deleting +attachment files. Present since the effect was introduced (`7b362ff65`, #5922) — +not a recent regression. + +## Fix + +Move the clearing effect after `KeyChangeEffect` (6 lines moved + 2-line comment). +The draft is saved first; `clearCurrentDraft()` is then a no-op for it because +`draftChatId` no longer matches the opened chat. Preserved behavior: the opened +chat's own draft is still cleared when it cannot send, and the draft is still +cleared when the open chat itself becomes non-sendable (only the `sendMsgEnabled` +key changes). Incidental improvements: an active live message is now sent on switch +instead of dropped, and the `inProgress` branch no longer resurrects cleared text. + +## Verification + +- Headless Compose-runtime harness (scratchpad `TmpDraftLossEffectOrderTest.kt` / + `TmpDraftFixVerifyTest.kt`): reproduced the bug (clear-before-save order observed), + verified the fix across 5 scenarios (switch to non-sendable, in-chat demotion, + non-sendable chat's own saved draft cleared, Android staggered path, sendable + control). All pass; `:common:desktopTest` and `:common:compileKotlinDesktop` green. +- Manual desktop repro confirmed the bug and fix. +- Works with `privacySaveLastDraft` off: text discarded per the setting's contract, + never saved; disabling purges the slot, so no stale-state interactions. + +## iOS + +Not reproduced on device (desktop repro path is safe on iOS: back-navigation fires +`ComposeView.onDisappear`, which saves the draft). A code-traced fix for in-place +chat switches (member info → "message", notification tap, "forwarded from" — where +`onDisappear` never runs) was removed from the PR and parked on +`nd/ios-draft-save-on-chat-switch` (commit `7b9b8d987`) pending device verification. +Decisive test: type a draft in a group → member info → "message" → back to group. +If the draft survives without the change, delete the side branch. + +## Pre-existing issues found (not addressed, both platforms) + +- Secondary (member support/reports) chat views share the group's chat id and race + the primary on the single draft slot (Kotlin: unguarded `KeyChangeEffect` in + secondary instances can clear the primary's just-saved draft when a modal is open + during a switch; iOS: secondary `onDisappear` saves under the swapped chat id). +- Kotlin: drafts are not saved when a chat is closed to `chatId = null` on desktop + (ChatView is disposed before `KeyChangeEffect` runs; only the forwarding case is + saved via `DisposableEffect`). +- Switching between two non-sendable chats restores the target's draft into the + disabled composer (clear effect does not re-fire on `false→false`). diff --git a/plans/2026-07-14-self-hosted-server-roles-implementation.md b/plans/2026-07-14-self-hosted-server-roles-implementation.md new file mode 100644 index 0000000000..06c5064a99 --- /dev/null +++ b/plans/2026-07-14-self-hosted-server-roles-implementation.md @@ -0,0 +1,342 @@ +# Per-server roles for self-hosted servers — Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers-extended-cc:subagent-driven-development (if subagents available) or superpowers-extended-cc:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Give each individual self-hosted SMP server its own receive / private-routing / name-resolution toggles, stored per server and plumbed to the agent. + +**Architecture:** Add `roles :: Maybe ServerRoles` to simplex-chat's `UserServer'`; store it in three nullable `protocol_servers` columns; resolve `Maybe → ServerRoles` in `agentServerCfgs` (default `storage+proxy` on, `names` off). No simplexmq change. Add the three toggles to custom SMP server screens on Kotlin and Swift. + +**Tech Stack:** Haskell (simplex-chat), SQLite + PostgreSQL migrations, Kotlin (multiplatform), Swift (iOS). Tests via `cabal test` (HSpec). + +**Design doc:** `plans/2026-07-14-self-hosted-server-roles-product.md` + +**Companion checkout:** simplex-chat builds against simplexmq pinned in `cabal.project:24`; the working `../simplexmq` already contains `ServerRoles{storage,proxy,names}`. To build/test locally against it, uncomment `packages: . ../simplexmq` (`cabal.project:2`). No simplexmq edits are made by this plan. + +--- + +## Chunk 1: Haskell backend (types, resolution, DB, store, validation, tests) + +### Task 1: Add per-server `roles` to `UserServer'` and resolve it in `agentServerCfgs` + +**Files:** +- Modify: `src/Simplex/Chat/Operators.hs:242-250` (type), `:331-333` (constructor), `:438-448` (resolution), add constant near `operatorRoles` (`:174`) + +- [ ] **Step 1: Add the field to `UserServer'`** (after `enabled`, before `deleted`) + +```haskell +data UserServer' s (p :: ProtocolType) = UserServer + { serverId :: DBEntityId' s, + server :: ProtoServerWithAuth p, + preset :: Bool, + tested :: Maybe Bool, + enabled :: Bool, + roles :: Maybe ServerRoles, + deleted :: Bool + } +``` + +- [ ] **Step 2: Add the default constant** (next to `operatorRoles`, `:174`) + +```haskell +-- Default roles for a self-hosted server without stored roles: receive + private +-- routing on, name resolution off. Also the default for newly added servers. +defaultUserServerRoles :: ServerRoles +defaultUserServerRoles = ServerRoles {storage = True, proxy = True, names = False} +``` + +- [ ] **Step 3: Set `roles = Nothing` in the sole constructor** `newUserServer_` (`:333`) + +```haskell +newUserServer_ preset enabled server = + UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, roles = Nothing, deleted = False} +``` + +- [ ] **Step 4: Resolve per-server roles in `agentServerCfgs`** (`:442-448`). Bind the field via record pattern (no `OverloadedRecordDot`; bare `roles` selector is ambiguous with `ServerCfg.roles`). + +```haskell + agentServer srv@UserServer {server, enabled, roles = srvRoles} = + case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of + Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) + | opEnabled -> Just ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op} + | otherwise -> Nothing + Nothing -> + Just ServerCfg {server, enabled, operator = Nothing, roles = fromMaybe defaultUserServerRoles srvRoles} +``` + +- [ ] **Step 5: Drop the now-unused `allRoles` import.** `allRoles` (`Operators.hs:52`) had its only use at `:448`, which Step 4 replaces. Remove it from the import list to avoid an unused-import warning: + +```haskell +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) +``` + +- [ ] **Step 6: Build.** `fromMaybe` is already imported in `Operators.hs`. `ServerRoles(..)` is imported (`:52`). + +Run: `cabal build lib:simplex-chat --ghc-options -O0` +Expected: compile errors only at other `UserServer{...}` construction sites — none exist beyond `newUserServer_` and `getProtocolServers` (fixed in Task 3). Record *patterns* elsewhere (`Operators.hs:410,416,532,537,538,568`, `Profiles.hs:981`) are unaffected. + +- [ ] **Step 7: Commit** `feat(servers): add per-server roles field to UserServer` + +### Task 2: Database migrations (SQLite + Postgres) + schema + +**Files:** +- Create: `src/Simplex/Chat/Store/SQLite/Migrations/M20260714_server_roles.hs` +- Create: `src/Simplex/Chat/Store/Postgres/Migrations/M20260714_server_roles.hs` +- Modify: `src/Simplex/Chat/Store/SQLite/Migrations.hs` (import + list), `src/Simplex/Chat/Store/Postgres/Migrations.hs` (import + list) +- Modify: `src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql`, `src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql` (regenerated, see Step 5) + +- [ ] **Step 1: SQLite migration** (template: `M20260707_file_digest.hs`, `M20260603_simplex_name.hs`) + +```haskell +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260714_server_roles where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260714_server_roles :: Query +m20260714_server_roles = + [sql| +ALTER TABLE protocol_servers ADD COLUMN role_storage INTEGER; +ALTER TABLE protocol_servers ADD COLUMN role_proxy INTEGER; +ALTER TABLE protocol_servers ADD COLUMN role_names INTEGER; +|] + +down_m20260714_server_roles :: Query +down_m20260714_server_roles = + [sql| +ALTER TABLE protocol_servers DROP COLUMN role_storage; +ALTER TABLE protocol_servers DROP COLUMN role_proxy; +ALTER TABLE protocol_servers DROP COLUMN role_names; +|] +``` + +- [ ] **Step 2: Postgres migration** (template: Postgres `M20260707_file_digest.hs`; `[r|...|]` and `Text`) + +```haskell +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260714_server_roles where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260714_server_roles :: Text +m20260714_server_roles = + [r| +ALTER TABLE protocol_servers ADD COLUMN role_storage SMALLINT; +ALTER TABLE protocol_servers ADD COLUMN role_proxy SMALLINT; +ALTER TABLE protocol_servers ADD COLUMN role_names SMALLINT; +|] + +down_m20260714_server_roles :: Text +down_m20260714_server_roles = + [r| +ALTER TABLE protocol_servers DROP COLUMN role_storage; +ALTER TABLE protocol_servers DROP COLUMN role_proxy; +ALTER TABLE protocol_servers DROP COLUMN role_names; +|] +``` + +- [ ] **Step 3: Register SQLite migration** (`SQLite/Migrations.hs`): add `import ...M20260714_server_roles` after the `M20260707_file_digest` import (`:166`), and append to the list after the `20260707_file_digest` entry (`:330`) — add a comma to that line: + +```haskell + ("20260707_file_digest", m20260707_file_digest, Just down_m20260707_file_digest), + ("20260714_server_roles", m20260714_server_roles, Just down_m20260714_server_roles) +``` + +- [ ] **Step 4: Register Postgres migration** (`Postgres/Migrations.hs`): mirror Step 3 at the import (`:43`) and list (`:84`). + +- [ ] **Step 5: Regenerate reference schema.** The repo keeps `chat_schema.sql` in sync with migrations (see the "ci: update query plans" commits). Regenerate both SQLite and Postgres `chat_schema.sql` using the repo's schema-dump script rather than hand-editing. If no script is found, hand-add the three columns to the `protocol_servers` block in both `chat_schema.sql` files (SQLite `INTEGER`, Postgres `smallint`). + +Run: `cabal build lib:simplex-chat --ghc-options -O0` +Expected: PASS. + +- [ ] **Step 6: Commit** `feat(servers): add nullable role columns to protocol_servers` + +### Task 3: Store read/write of `roles` (`Profiles.hs`) + +**Files:** +- Modify: `src/Simplex/Chat/Store/Profiles.hs:639-654` (select/read), `:656-667` (insert), `:669-679` (update) + +- [ ] **Step 1: Read roles in `getProtocolServers`.** Extend the SELECT and `toUserServer`. Store roles as three nullable `BoolInt` columns; reconstruct `Maybe ServerRoles` (all present → `Just`, else `Nothing`). + +**MUST split with `:.`.** The existing select is 8 columns → an 8-tuple. Adding 3 gives 11, but the SQLite/Postgres `FromRow` instances cap flat tuples at **10** — an 11-element flat tuple has no instance and will not compile. Parse the 3 role columns as a trailing group via `:.`. + +```haskell + [sql| + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names + FROM protocol_servers + WHERE user_id = ? AND protocol = ? + |] +``` + +```haskell + toUserServer :: ((DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) :. (Maybe BoolInt, Maybe BoolInt, Maybe BoolInt)) -> UserServer p + toUserServer ((serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) :. (rStorage, rProxy, rNames)) = + let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + roles = ServerRoles <$> (unBI <$> rStorage) <*> (unBI <$> rProxy) <*> (unBI <$> rNames) + in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, roles, deleted = False} +``` + +`ServerRoles <$> mA <*> mB <*> mC :: Maybe ServerRoles` (Applicative on `Maybe`) yields `Just` only when all three are present, else `Nothing` — matching the all-or-none storage. Constructor arg order (`storage, proxy, names`) matches the column order. Ensure `:.` (from the DB simple package) and `ServerRoles(..)` are imported in `Profiles.hs`. + +- [ ] **Step 2: Write roles in `insertProtocolServer`.** Add the columns and values (extract `roles` via record pattern): + +```haskell +insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled, roles} = do + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled) :. roleColumns roles :. (userId, ts, ts)) +``` + +- [ ] **Step 3: Write roles in `updateProtocolServer`** similarly: + +```haskell +updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled, roles} = + DB.execute + db + [sql| + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, + role_storage = ?, role_proxy = ?, role_names = ?, updated_at = ? + WHERE smp_server_id = ? + |] + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled) :. roleColumns roles :. (ts, serverId)) +``` + +- [ ] **Step 4: Add the `roleColumns` helper** (private, in `Profiles.hs`). Map each selector over the `Maybe` — no two-branch `case`, so the all-or-none property is structural (a `Nothing` maps to three `Nothing`s automatically): + +```haskell +roleColumns :: Maybe ServerRoles -> (Maybe BoolInt, Maybe BoolInt, Maybe BoolInt) +roleColumns mr = (BI . storage <$> mr, BI . proxy <$> mr, BI . names <$> mr) +``` + +(`storage`/`proxy`/`names` are unambiguous selectors — unique to `ServerRoles`. If a future duplicate makes them ambiguous under `DuplicateRecordFields`, fall back to a punned `\case`.) + +- [ ] **Step 5: Build.** `cabal build lib:simplex-chat --ghc-options -O0` → PASS. +- [ ] **Step 6: Commit** `feat(servers): persist per-server roles` + +### Task 4: Update validation for the custom (self-hosted) bucket + +**Files:** +- Modify: `src/Simplex/Chat/Operators.hs:524-532` (`noServersErrs`, `hasRole`), `:564-569` (`noNamesServersWarns`, `namesEnabled`) + +This task **fuses two parallel role-coverage helpers into one.** `hasRole` (`:531`, storage/proxy) and `namesEnabled` (`:569`, names) are the same group-level check specialised to different role selectors, both returning `True` for the custom bucket. They cannot express per-server roles, so both are replaced by a single role-parameterised `hasRoleCoverage`. + +**Preserve** (do not touch): `noServers` + `srvEnabled` (still used for the `USENoServers` empty-check at `:526`) and `opEnabled` (also used by `noChatRelaysWarns`, `:560`). **Remove:** `hasRole` and `namesEnabled`. + +- [ ] **Step 1: Make coverage checks per-server-role-aware for the no-operator bucket.** Today `hasRole`/`namesEnabled` return `True` for the custom bucket (`operator' = Nothing`) and are group-level `u -> Bool` filters combined with `noServers cond = not $ any srvEnabled $ userServers p $ filter cond uss`. Group-level filtering **cannot** express per-server roles for the custom bucket — the evaluation must move to the per-server level. + +**Effective-role helper** — takes the protocol singleton (`operatorRoles` needs it; `noServersErrs` runs for BOTH SMP and XFTP at `:522-523`, so hardcoding `SPSMP` would read `smpRoles` for XFTP operator servers — a bug): + +```haskell +-- effective role for coverage: operator servers use operator roles (per protocol), +-- self-hosted servers use per-server roles (default when unset). +serverHasRole :: UserProtocol p => SProtocolType p -> (ServerRoles -> Bool) -> Maybe ServerOperator -> Maybe ServerRoles -> Bool +serverHasRole p roleSel op srvRoles = case op of + Just o@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p o) + Nothing -> roleSel (fromMaybe defaultUserServerRoles srvRoles) +``` + +**Restructure the checks to per-server evaluation.** For a role selector, "coverage exists" = any enabled, non-deleted server in any bucket whose effective role is on. Per bucket `u` the operator is `operator' u` and each server's roles come from `AUS _ UserServer{enabled, deleted, roles}` (the `roles` field is reachable — same pattern already used at `:532,:568`): + +```haskell + hasRoleCoverage :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> [u] -> Bool + hasRoleCoverage p roleSel = + any (\u -> any (srvOk (operator' u)) (map aUserServer' (servers' p u))) + where + srvOk op (AUS _ UserServer {enabled, deleted, roles}) = + enabled && not deleted && serverHasRole p roleSel op roles +``` + +Then rewrite `noServersErrs` (`:527`) storage/proxy branches as `[USEStorageMissing p' user | not (hasRoleCoverage p storage uss)] <> [USEProxyMissing p' user | not (hasRoleCoverage p proxy uss)]` (keep the `noServers opEnabled` empty-check at `:526` as-is), and `noNamesServersWarns` (`:565`) as `[USWNoNamesServers user | not (hasRoleCoverage SPSMP names uss)]`. Keep the exact constructors (`USEStorageMissing`, `USEProxyMissing`, `USWNoNamesServers`). XFTP `names` is never checked (names coverage is SMP-only). + +**Deliberate duplication (do not try to unify with `agentServerCfgs`).** Both this helper and `agentServerCfgs` (Task 1) resolve "operator-vs-self-hosted → effective roles", but their semantics for a *disabled operator* differ: `agentServerCfgs` drops the server entirely (`opEnabled … | otherwise -> Nothing`), whereas coverage treats it as contributing no roles while `USENoServers` handles existence. They also run over different shapes (`[ServerCfg]` per user vs. `[UserOperatorServers]` across users). Unifying them would grow the blast radius for no clarity gain; the shared logic is a 2-line `case` — leave it in both. (Flag for future: if a third consumer appears, extract `effectiveServerRoles :: SProtocolType p -> Maybe ServerOperator -> Maybe ServerRoles -> Maybe ServerRoles`.) + +- [ ] **Step 2: Build + reason through each call site.** Ensure `noServers`/`noNamesServers` now consult per-server roles for the custom bucket. `cabal build lib:simplex-chat --ghc-options -O0` → PASS. +- [ ] **Step 3: Commit** `fix(servers): validate coverage using per-server roles` + +### Task 5: Haskell tests + +**Files:** +- Modify/Create: server-config test module (locate the existing `agentServerCfgs` / operators test; e.g. under `tests/` — search `agentServerCfgs`, `validateUserServers`, `Operators`). If none, add `tests/ChatTests/ServerRolesTest.hs` and register it in the test runner. + +- [ ] **Step 1: Write failing tests.** + - `agentServerCfgs` for a self-hosted SMP server with `roles = Just (ServerRoles True False True)` → `ServerCfg.roles == ServerRoles True False True`, `operator == Nothing`. + - self-hosted with `roles = Nothing` → `ServerCfg.roles == defaultUserServerRoles` (`names == False`). + - operator-matched server ignores per-server `roles` → uses `operatorRoles`. + - store round-trip: insert a self-hosted server with `Just` roles, read back equal; insert with `Nothing`, read back `Nothing`. + - `validateUserServers`: custom-bucket-only user with all servers `names = False` → `USWNoNamesServers` present; enabling `names` on one → absent. +- [ ] **Step 2: Run, expect FAIL.** `cabal test --test-option=--match="/ServerRoles/"` +- [ ] **Step 3: (Implementation already done in Tasks 1–4.)** Run, expect PASS. +- [ ] **Step 4: Commit** `test(servers): cover per-server roles resolution and storage` + +--- + +## Chunk 2: UI (Kotlin + Swift) + +### Task 6: Kotlin (Desktop/Android) model + toggles + +**Files:** +- Modify: `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt:4787` (`UserServer`), `:4808,4818-4845` (sample/empty), and `.../networkAndServers/ScanProtocolServer.kt:22` +- Modify: `.../networkAndServers/ProtocolServerView.kt` (`UseServerSection` / `CustomServer`, ~`:161-234`) and `.../networkAndServers/NewServerView.kt` +- Reference: `.../networkAndServers/OperatorView.kt:260-320` (toggle pattern), `ServerRoles` (`SimpleXAPI.kt:4636`) + +- [ ] **Step 1: Add `roles` to `UserServer`** with default to avoid breaking constructors: + +```kotlin +val roles: ServerRoles? = null, +``` +Place it before `deleted`; update the primary constructor and the named-arg call sites (`empty` `:4808`, samples `:4818-4845`, `ScanProtocolServer.kt:22`). Since it has a default, unchanged call sites still compile. + +- [ ] **Step 2: Add the toggles to custom SMP servers.** In `ProtocolServerView.kt`, inside `CustomServer` / `UseServerSection`, when `server.protocol == smp && !server.preset`, add a `SectionView(MR.strings.operator_use_for_messages)` with three `PreferenceToggle` rows bound to the server's roles (receive → `storage`, private routing → `proxy`, names → `names`), mirroring `OperatorView.kt:261-320`. Editing writes back a new `ServerRoles` onto the edited `UserServer` state. Default the displayed roles to `ServerRoles(storage = true, proxy = true, names = false)` when `roles == null`. + - Strings: `operator_use_for_messages`, `operator_use_for_messages_receiving`, `operator_use_for_messages_private_routing`, `operator_use_for_names` (already exist, `strings.xml:2195-2198`). + - Do NOT show for XFTP or preset/operator servers. + +- [ ] **Step 3: New-server default.** In `NewServerView.kt`, construct the new SMP `UserServer` with `roles = ServerRoles(storage = true, proxy = true, names = false)` so the toggles show the default and persist on save. + +- [ ] **Step 4: Build.** Compile the multiplatform common module (repo's gradle/build command). +- [ ] **Step 5: Commit** `feat(servers): per-server role toggles on Android/desktop` + +### Task 7: Swift (iOS) model + toggles + +**Files:** +- Modify: `apps/ios/Shared/Model/AppAPITypes.swift:1953` (`UserServer` struct + `CodingKeys` `:2017`) +- Modify: `apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift` (`customServer()` / `useServerSection` `:88,123`) and `NewServerView.swift` +- Reference: `.../NetworkAndServers/OperatorView.swift:106-114` (toggle pattern), `ServerRoles` (`AppAPITypes.swift:1807`) + +- [ ] **Step 1: Add `roles` to `UserServer`.** + +```swift +var roles: ServerRoles? +``` +Add `case roles` to `CodingKeys` (`:2017`). Update initializers / sample data accordingly. + +- [ ] **Step 2: Add the toggles to custom SMP servers.** In `ProtocolServerView.swift`, inside `customServer()` / `useServerSection`, when the server is SMP and `!preset`, add a `Section("Use for messages")` with three `Toggle`s bound to the server's roles (`"To receive"` → `storage`, `"For private routing"` → `proxy`, `"To resolve names"` → `names`), mirroring `OperatorView.swift:106-114`. Bind to the edited server's `roles`, defaulting to `ServerRoles(storage: true, proxy: true, names: false)` when `nil`. + - iOS string keys are the literal English text (already used by `OperatorView.swift`); `"To resolve names"` may still need adding to `Localizable.strings` translations (English == key). + - Do NOT show for XFTP or preset/operator servers. + +- [ ] **Step 3: New-server default.** In `NewServerView.swift`, default the new SMP server's `roles` to `ServerRoles(storage: true, proxy: true, names: false)`. + +- [ ] **Step 4: Build** the iOS target (or type-check the changed files). +- [ ] **Step 5: Commit** `feat(servers): per-server role toggles on iOS` + +--- + +## Verification (whole feature) + +- [ ] `cabal test --test-option=--match="/ServerRoles/"` passes. +- [ ] Full backend build: `cabal build --ghc-options -O0`. +- [ ] Migration up/down: apply and roll back `20260714_server_roles` on a copy DB (SQLite + Postgres); confirm existing rows read as `roles = Nothing` → resolved to `defaultUserServerRoles`. +- [ ] Manual UI: add two self-hosted SMP servers, set different role toggles on each, save, reopen — toggles are independent and persisted; name resolution defaults off; toggles absent on XFTP and operator servers. +- [ ] Round-trip via API: `APIGetUserServers` returns `roles` for edited servers and omits it (Nothing) for untouched ones. diff --git a/plans/2026-07-14-self-hosted-server-roles-product.md b/plans/2026-07-14-self-hosted-server-roles-product.md new file mode 100644 index 0000000000..d91fdc2ff2 --- /dev/null +++ b/plans/2026-07-14-self-hosted-server-roles-product.md @@ -0,0 +1,138 @@ +# Per-server roles for self-hosted servers — Product / Design + +**Date:** 2026-07-14 +**Status:** Design (approved for planning) +**Scope:** simplex-chat backend + DB + Desktop/Android (Kotlin) + iOS (Swift). **No simplexmq changes.** + +## Problem + +Self-hosted SMP servers cannot be configured individually for what they are used +for. The three capabilities — **receiving** messages (`storage` role), **private +routing** (`proxy` role), and **name resolution** (`names` role) — are controlled +only per *operator* (SimpleX / Flux), via the operator toggles in `OperatorView`. +Servers the user adds themselves fall into the "custom" / no-operator bucket and +are hard-coded to `allRoles` (all three on) in `agentServerCfgs` +(`src/Simplex/Chat/Operators.hs:448`). There is no UI, storage, or type to give a +single self-hosted server a subset of roles. + +This is a problem because a self-hosted server is usually not a name resolver +(name resolution needs an Ethereum/SNRC endpoint via `[NAMES] enable: on` on the +smp-server). Today such a server is still offered to the agent as names-capable, +so a name lookup can be routed to it and fail. Users also may want a self-hosted +server used only for receiving, or only for private routing. + +## Goal + +Give **each individual added self-hosted SMP server** its own independent toggles: +**To receive** / **For private routing** / **To resolve names**, stored per server +and plumbed to the agent as that server's `ServerRoles`. + +## Current state (verified) + +- **Agent (simplexmq) — already complete.** `ServerCfg { server, operator :: Maybe + OperatorId, enabled, roles :: ServerRoles }` and `ServerRoles { storage, proxy, + names }` exist (`Simplex/Messaging/Agent/Env/SQLite.hs:98,106`). The agent + partitions servers into `storageSrvs / proxySrvs / nameSrvs` in `mkUserServers` + and consumes them (`getNextServer`, `getSMPProxyClient`, `getNextNameServer` / + `resolveName` / RSLV). The agent always receives **concrete** `ServerRoles` and + never applies defaults. +- **simplex-chat backend.** `UserServer'` (`Operators.hs:242`) has **no roles + field**. Roles are resolved only in `agentServerCfgs` (`Operators.hs:438-448`): + operator servers → `operatorRoles p op` (read from `server_operators` columns); + self-hosted servers → hard-coded `allRoles`. +- **DB.** `protocol_servers` (`chat_schema.sql:548`) stores all user servers with + only `enabled / preset / tested` — **no operator link and no role columns**. + Roles live only per-operator in `server_operators` + (`smp_role_storage/proxy/names`, `xftp_role_storage/proxy`). +- **UI.** Operator screens (`OperatorView.kt:260-320`, `OperatorView.swift:106-114`) + have the three toggles. The individual server screens + (`ProtocolServerView`, `NewServerView` on both platforms) show only + address / test / enabled / delete. Client `UserServer` + (`SimpleXAPI.kt:4787`, `AppAPITypes.swift:1953`) has no roles field. + +## Decisions + +1. **Where the logic lives — no simplexmq change.** The accurate, consistent + approach follows the existing layering: the agent always gets a concrete + `ServerRoles`; the only place roles are resolved/defaulted is simplex-chat's + `agentServerCfgs`. We keep the `Maybe ServerRoles → ServerRoles` resolution + there, alongside the existing `allRoles` / `operatorRoles` handling. No core + version bump, no wire/protocol change. +2. **SMP self-hosted only.** SMP has all three roles; XFTP has no `names` role. + The toggles are shown only for custom (non-preset) SMP servers. XFTP + self-hosted servers are functionally unchanged. +3. **Legacy default `NULL → names OFF`.** For rows without stored roles (existing + servers after upgrade, or servers not yet edited) the resolved value is + `defaultUserServerRoles = ServerRoles { storage = True, proxy = True, names = + False }`. This is also the default for newly added servers, so migrated and new + servers behave identically: receive + private routing on, name resolution off. + +## Design + +### Types (`Operators.hs`) + +- Add `roles :: Maybe ServerRoles` to `UserServer'` (per-server; `Nothing` = use + default). `DuplicateRecordFields` is already enabled (both `ServerCfg` and + `UserServer'` will have a `roles` field). +- Add `defaultUserServerRoles :: ServerRoles = ServerRoles True True False`. +- `agentServerCfgs`, self-hosted branch (`:448`): + `roles = fromMaybe defaultUserServerRoles (roles srv)`. Operator branch + unchanged — the per-server field is ignored for operator-matched servers, so + operator roles still win. + +Per-server, not global: `roles` is one value per `UserServer`; each server +resolves to its own `ServerCfg`. + +### Storage (`protocol_servers`) + +Three **nullable** columns (mirrors `server_operators`): `role_storage`, +`role_proxy`, `role_names` (INTEGER, no default → existing rows NULL). Read into +`Maybe ServerRoles` (all three present → `Just`, else `Nothing`); write all three +from `roles`. + +### chat↔UI API + +`UserServer'` JSON is derived with `defaultJSON` (`omitNothingFields = True`), so +the new `Maybe` `roles` field is omitted when `Nothing` — exactly like the +existing `tested :: Maybe Bool`. Older clients/servers remain compatible. + +### Validation (`Operators.hs:519-577`) + +`noNamesServersWarns` (`:564-569`) and `noServersErrs`'s `hasRole` (`:531`) +currently derive coverage from the *operator* and treat the no-operator (custom) +bucket as `True` for every role. With per-server roles and names-off default this +would be wrong (a self-hosted-only user would never see "no name servers"). These +must evaluate the custom bucket from each server's resolved per-server `roles`. + +### UI (Kotlin + Swift) + +- Add `roles: ServerRoles?` to the client `UserServer`. +- On custom (non-preset) **SMP** server screens only — `ProtocolServerView` and + `NewServerView`, both platforms — add a "Use for messages" section with the + three toggles, mirroring `OperatorView`. Reuse existing string keys: + `operator_use_for_messages`, `operator_use_for_messages_receiving`, + `operator_use_for_messages_private_routing`, `operator_use_for_names`. +- New server default: `storage = on, proxy = on, names = off`. + +## Behavior / backward compatibility + +- Existing self-hosted servers keep receive + private routing; name resolution is + off after upgrade (intentional — avoids routing lookups to non-resolver + servers). A user who wants a self-hosted resolver enables the toggle. +- No protocol / wire change; no simplexmq change; operator behavior unchanged. + +## Out of scope + +- Per-server roles for operator (SimpleX/Flux) servers — they keep operator-level + roles. +- XFTP per-server roles. +- Any change to the name-resolution protocol or the agent. + +## Testing strategy + +- Haskell: unit tests for `agentServerCfgs` (self-hosted `Just`/`Nothing` → + correct `ServerCfg.roles`; operator servers ignore per-server roles), store + round-trip of `roles` through `protocol_servers`, and `validateUserServers` + names/storage/proxy coverage for the custom bucket. +- UI: manual verification that toggles render only for custom SMP servers, persist + per server, default names-off, and survive save/reload. diff --git a/plans/2026-07-15-desktop-startup-error-window.md b/plans/2026-07-15-desktop-startup-error-window.md new file mode 100644 index 0000000000..ba88d2f893 --- /dev/null +++ b/plans/2026-07-15-desktop-startup-error-window.md @@ -0,0 +1,102 @@ +# Show desktop startup errors in a copyable window (#4146) + +## Problem + +When any exception escapes `main()` before the app window appears - a missing +DLL, a failed migration, broken AWT initialization - Windows users see only the +jpackage launcher's "Failed to launch JVM" box. The launcher runs without a +console, so stderr is lost, and no log file exists yet at that point. Every +report in #4146 stalled on exactly this ("no console output or log-files +whatsoever"); the same class of failure shipped once before as #5237 (wrong +OpenSSL DLL name, fixed by #5238) and was only diagnosable by rebuilding. + +The launcher reports any nonzero exit as "Failed to launch JVM" (jpackage +`JvmLauncher.cpp`, `JP_THROW` on nonzero `JLI_Launch` status), so the dialog is +generic for the whole class: bad `java-options` in `SimpleX.cfg`, poisoned +`_JAVA_OPTIONS`, DLL load failures, or any uncaught startup exception. + +## Change + +Catch `Throwable` around the startup portion of `main()`, show the error in a +native Win32 window, and on Windows exit cleanly afterwards. The window is laid +out like a message box: a system error icon and a message at the top, then two +clickable report links (the GitHub issue tracker and the support email +chat@simplex.chat) above a read-only, selectable, scrolling box holding the stack +trace (`EDIT`, `ES_READONLY | ES_MULTILINE | WS_VSCROLL`, sunken border), and an OK +button. The links are `SS_NOTIFY` static controls drawn blue via +`WM_CTLCOLORSTATIC`; clicking one sends `WM_COMMAND` (`STN_CLICKED`) and is opened +with `ShellExecute` - the URL in a browser, the email in the mail client (with a +`mailto:` scheme). Built with jna-platform's typed `User32` plus raw calls for the +stock icon/font, the link colouring and the link launch. Nothing is written to disk. + +Line endings are normalised to a single CRLF (`stackTraceToString().lines() +.joinToString("\r\n")`) because an `EDIT` control breaks lines only on CRLF - a +lone LF, or the CR-CR-LF produced by naively replacing LF with CRLF over the +JVM's already-CRLF Windows output, renders as merged lines. + +A plain `MessageBoxW` was tried first but rejected: its text cannot be selected +and it has no scrollbar, truncating the trace. An all-in-one read-only rich-edit +control was tried next - it made the message, links and trace all selectable with +`EM_AUTOURLDETECT` link detection - but its `EN_LINK` click notification proved +unreliable in the packaged runtime (links highlighted but did not open), and it +placed the links inside the trace box. Dedicated `SS_NOTIFY` link statics above a +plain edit control are reliably clickable and give the cleaner requested layout; +the trade-off is that the link text itself is not selectable (the addresses are +fixed and well-known, and the full trace remains selectable/copyable). + +Key placement detail: `showApp()` is inside the try block. Its first statements +(the `SystemTray.isSupported()` probe in `DesktopTray.kt`, then Compose setup) +are the process's first AWT initialization, which is itself a known startup +failure cause (#4146, fixed separately by bundling `jdk.accessibility`). +Verified empirically: a first build with `showApp()` outside the try showed no +dialog on a machine reproducing the AWT failure; moving it inside is required. + +## Why this design + +- Native window, not Swing: broken AWT initialization is one of the failure + causes, so a Swing dialog would crash the same way the app did. Win32 windowing + goes straight to `user32.dll` via JNA and is unaffected by the JVM's AWT state. +- The WndProc handles WM_COMMAND (OK -> close, or a link static -> ShellExecute + its URL/email by control id), WM_CTLCOLORSTATIC (white static backgrounds, plus + blue text for the two link statics) and WM_DESTROY (end the loop). The + `WindowProc` callback is held in a local for the loop's lifetime so it is not + garbage-collected while alive. +- `SS_NOTIFY` static links instead of a rich-edit control: no dependency on + Msftedit.dll or comctl32 v6, no `EN_LINK` reliability issue, and `STN_CLICKED` + is unambiguous. Simpler and it removes the rich-edit machinery entirely. +- On Windows, after the dialog is dismissed the process exits with `exitProcess(0)` + instead of rethrowing, so the launcher does not also show its own generic + "Failed to launch JVM" box on top of ours (it shows that box on any nonzero + exit). If the native window itself fails, we fall through to the caller's + `throw e`, and the launcher's box becomes the fallback. +- Windows-only dialog: on Linux/macOS the rethrown exception reaches stderr in + the terminal; the launcher-hides-everything problem is Windows-specific. +- The window icon is set to the app's own exe icon (falling back to the system + error icon) so the title bar is not the default "unknown" icon. +- try/catch in `main()`, not `Thread.setDefaultUncaughtExceptionHandler`: a + default handler would change crash handling for every thread for the app's + whole lifetime; the try/catch is scoped to startup only. +- Crashes after the window appears are out of scope: the existing + `WindowExceptionHandler` in `showApp()` (DesktopApp.kt) already surfaces + those in-app with a shareable stack trace and does not propagate to `main()`. + +## Impact + +- No behavior change when startup succeeds. +- On startup failure, Windows users get one error window with clickable report + links (issue tracker, email) above a selectable/scrollable stack trace - and no + longer the launcher's generic "Failed to launch JVM" box (suppressed via the + clean exit). +- The message text is hardcoded English: translated resources may not be + loadable in the failed state this code reports on. + +## Verification + +- Compiles on Linux; the Linux/macOS path (rethrow to stderr) is default JVM + behavior. +- Negative result that shaped the change: CI test MSI v1 (catch not covering + `showApp()`) showed no dialog on a Windows 10 machine reproducing the + assistive-technology startup failure - the crash fires at the SystemTray + probe inside `showApp()`. +- CI test MSI v2 (this change) built; confirmation on the same repro machine + that the dialog appears with the `AWTError` stack trace is pending. diff --git a/plans/2026-07-15-fix-windows-failed-to-launch-jvm-accessbridge.md b/plans/2026-07-15-fix-windows-failed-to-launch-jvm-accessbridge.md new file mode 100644 index 0000000000..40d32ea95a --- /dev/null +++ b/plans/2026-07-15-fix-windows-failed-to-launch-jvm-accessbridge.md @@ -0,0 +1,86 @@ +# Fix Windows "Failed to launch JVM" when assistive technologies are enabled (#4146) + +## Symptom + +On some Windows machines the desktop app fails on every launch with the jpackage +launcher's message box "Failed to launch JVM" and no log output anywhere. Reported +since v5.4 in #4146; #6547 shows the same dialog (cause not established). + +## Root cause + +1. Every JVM reads `%USERPROFILE%\.accessibility.properties` during AWT toolkit + initialization (JDK 17 `Toolkit.initAssistiveTechnologies`, Toolkit.java:407-422). + The file is created by enabling the Java Access Bridge: `jabswitch -enable` + (ships in Windows JREs/JDKs, including JRE 8), the "Enable Java Access Bridge" + checkbox in Ease of Access Center, or screen reader installers. It sets + `assistive_technologies=com.sun.java.accessibility.AccessBridge`. + +2. That provider lives in the `jdk.accessibility` module. Its Windows + implementation keeps the legacy name `com.sun.java.accessibility.AccessBridge` + specifically for compatibility with properties files written by pre-Java-9 + installers (JDK `ProviderImpl.java:38-43`). + +3. Our jlinked runtime did not include `jdk.accessibility`: the Compose gradle + plugin's (1.8.2) default modules are `java.base, java.desktop, java.logging, + jdk.crypto.ec`, plus our `modules("jdk.zipfs", "jdk.unsupported")` in + `desktop/build.gradle.kts`. + +4. With the property set and the module absent, `Toolkit.loadAssistiveTechnologies` + throws `AWTError: Assistive Technology not found: + com.sun.java.accessibility.AccessBridge` (Toolkit.java:491) at the first AWT + touch - before any window or logging exists. The launcher runs the JVM + in-process and reports any nonzero exit as "Failed to launch JVM" + (jpackage `JvmLauncher.cpp:212-214`), so the actual error is never seen. + +This also matches the user reports: a user in #4146 traced the exact `AWTError` +and could start the app after neutralizing the property in `SimpleX.cfg`; the +bug report that triggered this investigation had installed JRE 8 (whose Access +Bridge tooling creates the properties file when enabled). + +## Fix + +Add `jdk.accessibility` to the jlink modules in `desktop/build.gradle.kts`, +conditionally on the build host being Windows (desktop packages are always +built natively on the target OS, so the host OS is the package OS). The +conditional applies to both the release and the `debugJava` configurations. + +## Why this fix and not alternatives + +- Forcing `-Djavax.accessibility.assistive_technologies=` via jvmArgs would stop + the crash but permanently disable Java Access Bridge, breaking the app for + screen reader users (an affected screen reader user is asking for a solution + in #4146). +- Bundling the module makes the configuration the JDK explicitly supports work: + the app starts, and assistive technologies can attach. Screen reader support + additionally requires `WindowsAccessBridge-64.dll` on the machine, which real + assistive-technology users already have. + +## Impact assessment + +- Runtime size: +~112 KB (measured with jlink on Linux; same module set). + No new transitive modules are pulled in. + The size increase applies to the Windows package only. +- Machines without assistive technologies enabled: the provider-loading path is + gated on the `javax.accessibility.assistive_technologies` property being + non-blank (Toolkit.java:518), so the module stays dormant - no classes loaded, + no behavior change. +- Linux/macOS: unaffected - the module is only bundled when building on + Windows. (Its provider is Windows-only anyway, so bundling it elsewhere + would have had no functional effect, only the size cost.) + +## Verification + +- Reproduced on Windows 10 with the current release MSI: creating + `%USERPROFILE%\.accessibility.properties` with + `assistive_technologies=com.sun.java.accessibility.AccessBridge` makes every + launch fail with "Failed to launch JVM"; deleting the file restores launch. +- Test MSI with this change built via CI; confirmation that it launches on the + repro machine with the properties file present is pending. + +## Out of scope (follow-up) + +"Failed to launch JVM" is a generic dialog for any nonzero exit during startup; +at least one other cause exists in the wild (#4146 has a reporter unaffected by +the assistive technology workaround; #5237/#5238 was a DLL name mismatch). A +separate change adds a copyable startup error window so the remaining causes +become diagnosable from user reports. diff --git a/plans/2026-07-21-fix-video-preview-black-area.md b/plans/2026-07-21-fix-video-preview-black-area.md new file mode 100644 index 0000000000..b5643ecb6b --- /dev/null +++ b/plans/2026-07-21-fix-video-preview-black-area.md @@ -0,0 +1,70 @@ +# Fix: video preview expands into a long empty black area + +## Problem + +On Android, once a video message finishes downloading, its in-chat preview grows +into a tall empty black area below the thumbnail. It appears the moment the file +download completes (before playback), and is worst for landscape videos. + +## Root cause + +The video message renders into the shared `CHAT_IMAGE_LAYOUT_ID` box measured by +`PriorityLayout` (`FramedItemView.kt`). Unlike `CIImageView`, `CIVideoView` never +gave that box a definite size — it inherited the height of its tallest child. + +Once downloaded, the tallest child is the player surface. On Android that is a +`StyledPlayerView` with `RESIZE_MODE_FIXED_WIDTH`. Before playback the player is +unprepared (the media source is only set in `VideoPlayer.start()`, called from +`play()`), so it reports no video size and its `AspectRatioFrameLayout` keeps +aspect ratio 0 and does **not** shrink its height. + +This was latent until #6726 ("constrain image sizes for previews"). That change +bounded the box to `maxHeight = 2.33 × width` to tame extreme image aspect +ratios. Before it, the box was measured with unbounded height, so on Android the +unprepared surface collapsed to 0 (`getDefaultSize` with an `UNSPECIFIED` spec) +and contributed no height — accidentally masking the missing-size flaw. After +#6726 the surface expands to fill the now-bounded `2.33 × width`, showing as a +long black strip behind the shorter thumbnail. + +The later image fixes (#7123 / #7125 / #7223) all touched `CIImageView` only, so +video was never given a definite box size and kept the defect. + +## Fix + +Give `CIVideoView`'s `CHAT_IMAGE_LAYOUT_ID` box a definite size derived from the +preview aspect ratio, mirroring `CIImageView`. Height is computed from the +*measured* (clamped) width via `Modifier.width(w).layout { … }`, not the nominal +width, so a wide video whose nominal `w = DEFAULT_MAX_IMAGE_WIDTH` (500dp) is +clamped on a narrower screen does not leave an empty strip below — the same +correction #7223 applied to images. Applied only for `!smallView`; the small +chat-list thumbnail (`smallView = true`) keeps its existing sizing. + +With the box sized to the preview footprint, the player surface can no longer +expand past it (its bounded max height now equals the thumbnail height), and the +opaque preview drawn on top exactly covers it. When playing, `RESIZE_MODE_FIXED_WIDTH` +conforms the surface to the real video aspect, which matches the thumbnail. + +## Alternatives considered + +- **Force the Android `StyledPlayerView` aspect from the preview.** Android-only; + treats one render backend and leaves the box height still child-dependent — a + workaround at the wrong layer. +- **Only mount `PlayerView` while playing.** Larger behavioural change; risks the + play/preview transition blinking the code already guards against; does not + establish the container invariant. +- **`matchParentSize()` on the player.** Breaks: when playing, the preview is + removed, so no sibling defines the box and it collapses to 0. +- **Naive `Modifier.width(w).height(w * ratio)`.** Reintroduces the exact #7223 + empty-strip bug for wide videos on screens narrower than 500dp. + +The chosen fix restores the invariant "a media chat item occupies exactly the +media's aspect-ratio footprint" at the correct layer (the media container), for +all render states (pre-download / downloaded-idle / playing / encrypted) and both +platforms, using the same aspect source the player conforms to. + +## Verification + +- `android:assembleDebug` (arm64 debug APK) — BUILD SUCCESSFUL. +- Desktop `createDistributable` + AppImage (Linux x86_64) — BUILD SUCCESSFUL. +- Control APK built from the same base without the fix, to A/B the black area + against the fixed build (both `7.0-beta.4`, differing only by this change). diff --git a/plans/2026-07-23-fix-updater-download-deleted-by-chat-switch.md b/plans/2026-07-23-fix-updater-download-deleted-by-chat-switch.md new file mode 100644 index 0000000000..29690edd17 --- /dev/null +++ b/plans/2026-07-23-fix-updater-download-deleted-by-chat-switch.md @@ -0,0 +1,144 @@ +# Fix: chat switch deletes the in-app updater's download mid-transfer + +## Symptom + +Desktop in-app updater (Linux AppImage, v7.0.0-beta.5): the download progress reaches 100%, and then +nothing happens — no "App update is downloaded" dialog, no error, and `/tmp/simplex` is empty. +The update cannot proceed and there is no indication of why. + +Captured by starting the app from a terminal (the log goes nowhere otherwise — see *Why this was invisible*): + +``` +E: Failed to download the asset from release: java.nio.file.NoSuchFileException: /tmp/simplex/e859e2cb-b4c6-4ebf-812d-c327812db212 + at java.base/sun.nio.fs.UnixCopyFile.move(Unknown Source) + at java.base/java.nio.file.Files.move(Unknown Source) + at chat.simplex.common.views.helpers.AppUpdaterKt.downloadAsset$lambda$...(AppUpdater.kt:333) + at chat.simplex.common.views.helpers.UtilsKt.createTmpFileAndDelete(Utils.kt:401) + at chat.simplex.common.views.helpers.AppUpdaterKt.downloadAsset(AppUpdater.kt:323) +``` + +The exception names the **source** path, not the destination: the file being moved no longer exists. + +## Root cause + +The updater downloads into `createTmpFileAndDelete`, which registers every file it creates in a +chat-scoped cleanup set: + +```kotlin +fun createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T { + val tmpFile = File(dir, UUID.randomUUID().toString()) + tmpFile.parentFile.mkdirs() + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) // <-- kills the download + try { return onCreated(tmpFile) } finally { tmpFile.delete() } +} +``` + +`ComposeView.deleteUnusedFiles()` empties that set: + +```kotlin +chatModel.filesToDelete.forEach { it.delete() } +chatModel.filesToDelete.clear() +``` + +and it is called from `KeyChangeEffect(chatModel.chatId.value)` — on chat open, switch and close, +whenever the composer is empty and there is no draft (`ComposeView.kt:1320`), plus after sending a +live message (`:1301`) and a voice message (`:860`). + +`downloadAsset` writes the ~350 MB update into that temp file, which takes minutes, and the toast shown +during it ("Downloading app update, don't close the app") invites the user to keep using the app +meanwhile. Any chat switch in that window deletes the file out from under the running download. +`copyTo` keeps writing through the still-open file descriptor, so the transfer completes normally and +the progress indicator reaches 100%; only the subsequent `Files.move` fails. + +The failure is then swallowed by `downloadAsset`'s outer `catch`, which only calls `Log.e` — on desktop +that is a bare `println` (`Log.desktop.kt`), invisible unless the app was launched from a terminal. +Net effect: silence. + +This is not specific to the updater. The same window exists for the settings and themes writers +(`Resources.desktop.kt`, `Files.kt`), which use the same helper and then `Files.move` the temp file into +place; a chat switch landing between the write and the move surfaces there as an +"Error saving settings" alert and a rethrow. + +### Why this was invisible until v7.0-beta.1 + +Before #7104 the same line was `file.renameTo(newFile)`, whose `false` return was ignored, so the +dialog appeared regardless — over an empty folder (that was the bug #7104 fixed). Replacing it with +`Files.move` was correct, but it converted a silently-ignored failure into a thrown exception routed +into a stdout-only log, which turned the visible-but-wrong symptom into no symptom at all. The +underlying deletion predates both. + +## Fix + +`downloadAsset` stops using `createTmpFileAndDelete` and owns the file itself: + +```kotlin +val newFile = File(tmpDir, asset.name) +if (desktopPlatform.isWindows()) newFile.delete() +val partFile = File(tmpDir, "${asset.name}.part") +partFile.parentFile.mkdirs() +partFile.deleteOnExit() +try { + partFile.outputStream().use { output -> stream.copyTo(output) } + Files.move(partFile.toPath(), newFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + AlertManager.shared.showAlertDialogButtonsColumn(...) // unchanged +} finally { + partFile.delete() +} +``` + +The download is never registered in `ChatModel.filesToDelete`, so `deleteUnusedFiles()` cannot reach +it and a chat switch during the transfer is harmless. Cleanup is unchanged in substance: the `finally` +removes a partial file when the download fails or is cancelled (and is a no-op after a successful +move, exactly as the helper's own `finally` was), `deleteOnExit()` covers a clean exit, and `Main.kt` +wipes `tmpDir` at startup after a crash — a leftover `.part` is also truncated by the next download. + +This is the fix requested in review (#7295): the helper's contract is a scratch file that dies with +the lambda, and the updater's file is neither — it is moved to `asset.name` and kept for the user to +install, so the helper's `finally` never applied to it on the success path. `createTmpFileAndDelete` +is left as it is on master, with a comment warning that its file does not survive a chat switch. + +## Other callers + +Unchanged. The same trap still exists for anyone whose lambda outlives a chat switch, but no current +caller does: the settings/themes writers (`Resources.desktop.kt` ×2, `Files.kt`) write and `Files.move` +within milliseconds, and `CIFileView.kt`, `Utils.kt` and `Share.android.kt` finish with the file inside +the lambda. See "Follow-up" below. + +## Alternatives considered + +- **Removing `ChatModel.filesToDelete.add(tmpFile)` from the helper** (the first version of this PR) — + fixes the whole class rather than one caller, and the registration is redundant with the helper's + own `finally` at every call site. Rejected in review as too broad for this bug: it changes behaviour + at six unrelated call sites to fix the updater. Kept as a follow-up, see below. +- **`filesToDelete.remove(file)` at the updater's call site** — leaves a window between the `add` + inside the helper and the `remove` in the lambda, and adds a third concurrent mutation of + `ChatModel.filesToDelete` (a plain `mutableSetOf`, already mutated from both `Dispatchers.Default` + and the main thread). + +## Follow-up (not in this PR) + +`createTmpFileAndDelete` still registers its file in `ChatModel.filesToDelete` from whatever thread +calls it, including `Dispatchers.IO` (settings save, `saveFileFromUri`), while +`ComposeView.deleteUnusedFiles()` iterates that plain `mutableSetOf` on the UI thread — a concurrent +`add` during that `forEach` throws `ConcurrentModificationException`. The registration is also +redundant with the helper's `finally` at all six call sites. Removing it, or making the set +thread-safe, is a separate change. + +Note also that `preferencesTmpDir` on desktop is `File(configPath, "tmp")` — outside `tmpDir`, so it is +not covered by the startup wipe in `Main.kt`; its temp files rely on the `finally` and `deleteOnExit`. + +## Testing + +Repro and verification were done against the first version of this fix: with `desktop.version_name` +temporarily set to `7.0-beta.4` the updater offers v7.0.0-beta.5, and switching chats repeatedly during +the download reproduces the disappearing file and the missing dialog. This version compiles +(`:common:compileKotlinDesktop`); the manual download-and-switch run should be repeated before merge, +including the install path on Windows (where the app exits before deleting the file). + +## Out of scope + +`downloadAsset` and `checkForUpdate` report failures only via `Log.e`, which on desktop reaches nothing +but stdout — that is why this bug went unnoticed. Surfacing updater failures to the user is a separate +concern: it would not have prevented the deletion, and this change does not make future failures +visible. Left for a follow-up. diff --git a/plans/2026-07-25-fix-forward-moves-draft-to-target-chat.md b/plans/2026-07-25-fix-forward-moves-draft-to-target-chat.md new file mode 100644 index 0000000000..1ca21b7a16 --- /dev/null +++ b/plans/2026-07-25-fix-forward-moves-draft-to-target-chat.md @@ -0,0 +1,192 @@ +# Fix: forwarding a message moves the message draft to another chat (desktop) + +Branch: `nd/fix-forward-moves-draft-to-target-chat` (off `origin/master`) +Date: 2026-07-25 +PR: #7307 + +All line references are against `origin/master` at `cdef9f51e`, with this +fix applied. + +## Problem + +On desktop, forwarding a message out of a chat that has an unsent message +draft can move that draft to a **different** chat. It is most visible when +the draft lands on the chat that was forwarded to: after the forward is +sent, the source chat's draft text appears in the destination chat's input, +and the source chat no longer has a draft (no draft preview in the chat +list, empty input when reopened). + +Reported on Linux desktop. Android and iOS are not affected — see +"Cross-platform findings" below. + +Repro (desktop): + +1. Start the app (or press Esc so no chat is open), open chat **B**. +2. Click chat **A** in the chat list — without closing the chat in between. +3. Type text in A, do not send. +4. Right-click a message in A → Forward → pick **B**. +5. Send the forward → A's draft text appears in B's input, and is gone + from A. + +The condition for step 5 to show the draft is that the destination chat is +the chat that was open when the current `ComposeView` was first composed +(B, from step 1). With any other destination the draft is still misfiled — +it silently reappears in B later instead of in A. + +## Cause + +The draft is saved under a **stale chat id**. + +Forwarding closes the chat before the destination is picked +(`ChatView.kt:3656-3662`, `forwardContent`: `chatId = null`, then +`sharedContent = SharedContent.Forward(...)`). On desktop `ChatView` is +composed only while `chatId != null` (`App.kt:387-417`, +`CenterPartOfScreen`), so this disposes `ComposeView`, and the +`KeyChangeEffect(chatModel.chatId.value)` that normally moves the draft +(`ComposeView.kt:1311-1332`) never gets to run — the composable leaves +composition in the same recomposition that sets `chatId = null`. That is +what the desktop-only `DisposableEffect` at `ComposeView.kt:1354-1366` is +for: it saves the draft on dispose while a forward is pending. + +`DisposableEffect(Unit)` runs its effect lambda once and keeps the +`onDispose` it returned, so that closure captures the `chat` **parameter +value from the composition in which the effect was created** — later +recompositions with a different `chat` do not replace it. `composeState` +is a `MutableState` and reads fresh; `chat.id` does not. + +That capture is stale whenever the chat changed without `ComposeView` +being disposed, which on desktop is the normal way to switch chats: + +- `ChatItemsLoader.kt:71-73` and `ChatListNavLinkView.kt:238-244` + (`openLoadedChat`) set `chatModel.chatId.value` directly, with no + intermediate `null`, so `ChatView`/`ComposeView` are not disposed on a + chat switch. +- `ChatView.kt:134-144` remembers `composeState` with `rememberSaveable` + and **no keys**, i.e. the instance is deliberately shared across chat + switches. + +So `chat.id` in `onDispose` is the first chat opened after the last time +`chatId` was `null` — not the chat being forwarded from. + +Why the draft then surfaces in the destination chat: after the forward is +sent, `ComposeView.kt:930-943` restores a draft that belongs to the +current chat (`draftChatId == chat.chatInfo.id` and the forward did not +originate there, line 939). That is a deliberate feature — a draft in the +destination chat survives forwarding into it — and it is correct. It only +misbehaves because it was handed a draft misattributed to that chat. + +## Fix (2 lines, desktop-only) + +`ComposeView.kt:1354-1366` — read the current chat id in `onDispose` +through `rememberUpdatedState` instead of the captured parameter: + +```kotlin +if (appPlatform.isDesktop) { + // the same ComposeView is reused when switching chats, so `chat` captured by onDispose would be the chat opened first, not the current one + val currentChatId = rememberUpdatedState(chat.id) + // Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)` + DisposableEffect(Unit) { + onDispose { + if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = currentChatId.value + } + } + } +} +``` + +Blast radius: + +- Inside the existing `if (appPlatform.isDesktop)` block; Android never + executes it, iOS is a separate codebase. +- Only the value written to `draftChatId` changes, and only in the case + where it was already wrong. When `ComposeView` was first composed for + the chat being forwarded from — the case that already worked — + `currentChatId.value == chat.id` and behavior is identical. +- `rememberUpdatedState` is already used in this file + (`ComposeView.kt:1208-1210`, `1343`), so no new import. + +Alternatives considered and rejected: + +- `DisposableEffect(chat.id)` — restarts the effect on every chat switch, + so `onDispose` would also fire on plain chat switches and write a draft + there whenever a forward happens to be pending. Larger behavior change + for no benefit. +- Moving the desktop save into `KeyChangeEffect` — it cannot run at all in + this path, for the reason above. That is why the `DisposableEffect` + exists. +- Restructuring so `ComposeView` is keyed per chat — would fix the stale + capture but changes draft handling, focus and compose state for every + chat switch on both platforms. Out of proportion to the bug. + +## Cross-platform findings + +**Android — not affected.** + +- `ComposeView.kt:1354` — the `DisposableEffect` that writes + `draftChatId` is inside `if (appPlatform.isDesktop)`. +- `ComposeView.kt:1311-1332` — Android saves the draft in + `KeyChangeEffect(chatModel.chatId.value) { prevChatId -> ... }` + (`draftChatId = prevChatId`, line 1329); `prevChatId` comes from the + effect's own remembered previous key (`Utils.kt:683-699`), i.e. the chat + id that was actually open, so it cannot go stale. +- `App.kt:344-352` — `currentChatId` (what `ChatView` is composed from) is + set to `null` only after `onComposed(null)` finishes the slide-out + animation, so `ComposeView` is still composed when `chatId` becomes + `null` and that effect runs. +- `ComposeView.kt:1522-1524` — the `sharedContent` effect returns early + while `chatId == null`, so the draft saved for the source chat is the + plain text, without the forward context. + +Result: the draft stays with the source chat, the restore condition at +`ComposeView.kt:939` does not match in the destination, and +`clearCurrentDraft()` there is a no-op. + +**iOS — not affected by this symptom; has a different defect (kept +separate, not addressed here).** + +iOS does not use `sharedContent` or `chatId = null` for forwarding — the +picker is a sheet over the open chat that writes into the source chat's +compose binding: + +- `ChatItemForwardingView.swift:107-108` — forwarding to a different chat + does `composeState.wrappedValue = ComposeState.init(forwardingItems:fromChatInfo:)` + then `ItemsModel.shared.loadOpenChat(chat.id)`. +- `ComposeView.swift:95-101` — that initializer sets `message = ""`. +- `ComposeView.swift:682-707` — the only writer of `draftChatId` is + `saveCurrentDraft()` (line 699, defined at `1852-1855`) in + `.onDisappear`, and `ChatView.swift:355-368` keeps the same `ChatView` + instance across a chat switch (it reassigns `chat`), so no disappear + fires during a forward. + +So on iOS `draftChatId` is never written with a stale id — the restore at +`ComposeView.swift:1548-1554` only fires for a draft that genuinely +belongs to the destination chat. Instead, text typed in the source chat +and not yet persisted as a draft is **discarded** when forwarding out of +that chat: loss, not migration, and a divergence from Android/desktop, +which preserve it as a draft. Two lower-confidence iOS leads, noted but +not verified: + +- `ComposeView.swift:1551` lacks the + `forwardingFromChatId != chat.chatInfo.id` guard that + `ComposeView.kt:939` has, so forwarding into the same chat could + re-populate the input from a stale saved draft (`ChatView.swift:783` + restores a draft on chat open without clearing it). +- other in-chat `loadOpenChat` calls that do not overwrite compose state + (e.g. `ChatItemInfoView.swift:386`) would carry typed text into the + newly opened chat, since nothing saves or clears it on that path. + +## Verification + +- `./gradlew :common:compileKotlinDesktop` — passes. +- Manual, desktop AppImage built from this branch: + 1. R1 (was broken): app start → open B → click A → type in A → forward + from A to B → send. Expected: B's input holds only the forwarded + item; A still shows its draft in the chat list and when reopened. + 2. R2 (regression guard, was already correct): app start → open A + directly → type in A → forward from A to B → send. A keeps its + draft. + 3. R3 (the restore feature at `ComposeView.kt:939` must still work): + draft in B → open A → forward a message from A into B → send. B's own + draft is restored in the input after sending. diff --git a/plans/2026-07-25-fix-inflight-send-writes-other-chat-compose.md b/plans/2026-07-25-fix-inflight-send-writes-other-chat-compose.md new file mode 100644 index 0000000000..7a27fdd9d1 --- /dev/null +++ b/plans/2026-07-25-fix-inflight-send-writes-other-chat-compose.md @@ -0,0 +1,244 @@ +# Fix: message being sent leaks into another chat's compose/draft, and erases what is typed there + +Branch: `nd/fix-inflight-send-writes-other-chat-compose` (off `origin/stable`) +Date: 2026-07-25 +PR: #7308 + +Line references are against `origin/stable` at `8dc387cb5`, with this fix +applied. Android and desktop only +(`multiplatform/.../views/chat/ComposeView.kt`); iOS has the same defect +but is not addressed here. + +## Problem + +Two reported symptoms, one cause. Both need a send that is still in +flight when the chat is switched (slow network, large file, or the send +just hanging with the progress circle showing): + +1. **The message ends up in another chat's draft.** Reply to a message + (or just type), press send, switch to another chat while it is + sending: the text *and the reply context* appear in that chat's input, + and leaving it saves them as that chat's draft. No forwarding + involved. +2. **A late send erases what you typed.** Press send, the progress circle + keeps spinning, switch to another chat and back, type a new message — + when the original send finally succeeds, the newly typed message is + erased. + +## Cause + +The compose state is shared, and the send outlives the chat: + +- `ChatView.kt:134` — one `MutableState` per `ChatView` + instance, `rememberSaveable` with no keys, reused for every chat that + the view displays. +- `Utils.kt:43-46` — `withLongRunningApi` launches on + `CoroutineScope(Dispatchers.Default)`, a standalone scope with no tie + to the composition or to the chat, and `sendMessage` + (`ComposeView.kt:972-976`) uses it. Leaving the chat never cancels an + in-flight send. + +Two writes then act on the wrong chat: + +- **On the chat switch** — `ComposeView.kt:1343-1347`: the `cs.inProgress` + branch used to keep the message in the shared compose state + (`composeState.value = cs.copy(inProgress = false, progressByTimeout = false)`) + and only cleared the *previous* chat's saved draft. The text and the + quote were therefore sitting in the input of the chat opened next, and + `ComposeView.kt:1348-1358` (`!cs.empty`) then saved them as *that* + chat's draft on the next switch. Symptom 1. +- **When the send completes** — `ComposeView.kt:943-968`, running in the + detached coroutine after the switch: `clearState(live)` on success, or + `composeState.value = lastFailed` on failure, where `lastFailed = + cs.copy(inProgress = false, preview = preview)` + (`ComposeView.kt:729`) **keeps `contextItem`, i.e. the reply**. On + success this wipes whatever is in the input now — including a message + typed after coming back (symptom 2); on failure it dumps the old + message into whichever chat is open (symptom 1 again). + +The same function was already inconsistent about which chat it acts on: +its draft bookkeeping (`clearCurrentDraft()`, and the forwarding +condition) uses the **captured** `chat` — the chat the message was +composed in — while its `composeState` writes hit whatever chat is +displayed at that moment. + +## Fix + +Two changes, both in `ComposeView.kt`. + +**1. Do not keep the message being sent in the shared compose state** +(`ComposeView.kt:1343-1347`). On switching away with a send in flight the +compose state is cleared, so nothing leaks into the chat opened next: + +```kotlin +} else if (cs.inProgress) { + clearPrevDraft(prevChatId) + // the message being sent must not be kept in the compose state, it is shared with the chat opened next; + // if it fails to send it is restored in this chat or saved as its draft + clearState() +} +``` + +`clearState()` is used rather than assigning an empty `ComposeState` so that +the link preview state is reset too (`pendingLinkUrl` still points at the +sent message's link, and its fetch would otherwise set a preview on the +input of the chat opened next), and so that the attachment size limit is +carried over the same way as everywhere else. + +In-flight content is deliberately **not** saved as a draft here: the +message has been submitted and will most likely be sent, and a draft is +for messages that are not sent yet. + +`clearState()` alone would leave the chat opened next with an empty input +even when it has a draft: this branch, like the live message one above it, +returns before the branch that loads a draft +(`chatModel.draftChatId.value == draftChatId(chatModel.chatId.value, chatScope)`), +so that draft was never shown - and, being still in the slot but not in +any compose state, it was then dropped by `clearPrevDraft` on the next +chat switch. It is loaded here instead. This is not the one-slot +limitation below: nothing else is competing for the slot, the draft is +simply lost. + +**2. Only touch the compose state if it still holds the message that was +sent** (`ComposeView.kt:936-968`): + +```kotlin +withContext(Dispatchers.Main) { + val chatIsOpen = chatModel.chatId.value == chat.id + val liveSend = live || cs.liveMessage != null + val sentMessageInCompose = chatIsOpen && (liveSend || composeState.value.inProgress) + if (sentMessageInCompose) { + if (lastFailed == null) { + clearState(live) + } else { + composeState.value = lastFailed + } + } + val draft = chatModel.draft.value + if (wasForwarding && chatModel.draftChatId.value == draftChatId(chat.chatInfo.id, chatScope) && forwardingFromChatId != chat.chatInfo.id && draft != null) { + if (sentMessageInCompose) composeState.value = draft + } else { + clearCurrentDraft() + if (!sentMessageInCompose && lastFailed != null) { + // the message was not sent, so it is restored in the chat it was composed in, or kept as its draft if another chat is open + if (chatIsOpen && composeState.value.empty) { + composeState.value = lastFailed + } else if (saveLastDraft) { + chatModel.draft.value = lastFailed + chatModel.draftChatId.value = draftChatId(chat.id, chatScope) + } + } + } +} +``` + +Both the checks and the changes run on `Dispatchers.Main` (the block has +no suspension points), so they cannot be interleaved with the user +switching chats or typing - `KeyChangeEffect`, which does change 1, runs +there too. + +`inProgress` is the marker that the compose state is still the submitted +message: it is set by `sending()` (`ComposeView.kt:596-598`), preserved by +`copy` while sending (the only other write during a send is +`progressByTimeout` at `ComposeView.kt:1610-1617`), reset when switching +away (change 1), and never set by typing a new message. So a chat switch +*or* newly typed text both make the guard false. + +A **failed** send is different from an in-flight one - the message was not +sent, so it is an unsent message. It is put back into the input if that +chat is open and nothing else is being composed there, and kept as that +chat's draft otherwise, so it never appears in another chat (see the +limitations below for when it is still dropped). Staying in the chat is +unaffected: the guard is true there +and the failed message is restored into the input as before, keeping +"preserving long message when failed to send" (`e61babdc8`) working. + +Deliberately unchanged: + +- The condition of the forwarding branch. Gating the whole branch would + send a forward that completed after the user left to `clearCurrentDraft()` + instead, **deleting** the destination chat's draft that the branch + exists to preserve - only the compose write inside it is gated. +- Live message sends (`live`, or `cs.liveMessage != null` for the send + that finalises a live message when leaving the chat, `ComposeView.kt:1338-1342`), + as long as their chat is the one open. They never call `sending()`, so a + guard based on `inProgress` would change their behaviour: failed live + sends would stop restoring and would write a draft on every failing + keystroke send. That is why `liveSend` is an alternative to `inProgress` + inside the guard, and why it is excluded from the restore/draft branch - + not gating it there would produce exactly that draft-per-keystroke. + + What they are **not** exempt from is `chatIsOpen`. An earlier revision + had `live || cs.liveMessage != null` outside it, which holds only while + a live message is always sent to the chat that is open. #7323 removes + that: the live message committed by a chat switch is sent to the chat it + was composed in, while this view already shows another one, so an + unguarded clause here would clear *that* chat's compose state - the leak + this fix exists to prevent. Standalone this changes nothing except a + live send that completes after its chat was left, which now leaves the + opened chat alone. `sendMessageAsync` reads `composeState` inside the + coroutine (`ComposeView.kt:684`), so that branch cannot clear the state + itself without racing the send; #7323 adds the `composed` parameter that + makes the captured state explicit. + +**3. The same check where the flag is shared** (`ComposeView.kt:600-602` +and the three senders that connect a prepared chat). They call the same +`sending()`, so an unguarded `clearState()` or `inProgress` reset from +one of them corrupts the state of a send started in the chat opened next. + +## Behaviour after the fix + +| situation | before | after | +| --- | --- | --- | +| send, stay in chat, succeeds | input cleared | input cleared (unchanged) | +| send, stay in chat, fails | message restored in input | message restored in input (unchanged) | +| send, switch chats, succeeds | message left in the other chat's input, saved as its draft | other chat untouched | +| send, switch chats, fails | message dumped into the other chat's input | message restored in the chat it was composed in, or kept as its draft | +| send hangs, switch away and back, type, then it succeeds | typed message erased | typed message kept | +| forward send, still in destination chat | destination chat's draft restored | unchanged | +| live message sent on leaving the chat | compose state cleared by the send | unchanged | + +## Limitations + +Kept deliberately, to not grow the change: + +- A message that failed to send is dropped, rather than kept, when the + "Message draft" privacy setting is off, when the destination chat of a + failed forward already has a draft (its own draft is preserved + instead), and when the single draft slot is later taken by another + chat - drafts are one global slot, so the last write wins. +- The three senders that connect a prepared chat share the same + `sending()` flag, so they use the same check (`ComposeView.kt:604-616`, + `618-640`, `659-685`). Without it a connect completing after the chat + was switched would clear `inProgress` for a send started in the chat + opened next, and that sent message would then stay in the input. They + have no failed-message restore, so their typed message is dropped when + the chat is switched instead of being carried into the next chat. +- Typing in the same chat while its own send is in flight is still + cleared when the send completes: `inProgress` is preserved by `copy`, + so the guard stays true. Unchanged from before, and different from the + reported symptom, which needs the chat to be switched. +## Verification + +- `./gradlew :common:compileKotlinDesktop` — passes. +- Manual (needs a slow or failing send — e.g. airplane mode, or a large + file). On desktop any chat switch exercises it; on Android only an + in-place switch does (member info → open chat), because leaving to the + chat list destroys the view: + 1. Reply + type in A, send, switch to B while sending. B's input must + stay empty; leaving B must not create a draft in B. If the send + failed, A must hold the message (with the reply) as its draft. + 2. Send in A with the network off so the circle keeps spinning, switch + to B and back to A, type a new message, restore the network. The + typed message must survive the old send completing. + 3. Regression: ordinary send in A (input clears), failed send while + staying in A (message comes back in the input), forward into a chat + that has a draft (draft restored after sending). + +Rebased onto the scope-aware draft ids introduced by #7309: the draft +written here for a message that failed to send uses +`draftChatId(chat.id, chatScope)`, like every other draft write. + +Related: `plans/2026-07-25-fix-forward-moves-draft-to-target-chat.md` +(PR #7307) — different cause (stale `chat` captured by the desktop +`onDispose`), same shared-compose-state design. diff --git a/plans/2026-07-27-fix-accept-request-non-active-profile.md b/plans/2026-07-27-fix-accept-request-non-active-profile.md new file mode 100644 index 0000000000..43b28d6563 --- /dev/null +++ b/plans/2026-07-27-fix-accept-request-non-active-profile.md @@ -0,0 +1,98 @@ +# Accepting a contact request from a notification for a non-active profile + +## Problem + +With two profiles on one device, a contact request that arrives for the profile that is **not** +currently active shows a notification, and tapping **Accept** in it fails with: + +``` +ERROR accepting contact request: error store: error store userContactLinkNotFound +``` + +Repro: create an address in profile 1, create profile 2, connect to profile 2's address from +profile 1, then accept the resulting request from the notification while profile 1 is active. + +## Cause + +`APIAcceptContact` is scoped to the **active** user (`Library/Commands.hs`): + +```haskell +APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do + uclData_ <- withFastStore $ \db -> do + uclId_ <- getUserContactLinkIdByCReq db connReqId -- NOT user-scoped + forM uclId_ $ \uclId -> do + uclGLinkInfo <- getUserContactLinkById db userId uclId -- user-scoped -> throws +``` + +`getUserContactLinkIdByCReq` (`Store/Direct.hs`) has no `user_id` filter, so it returns the address +id of the *other* profile; `getUserContactLinkById` (`Store/Profiles.hs`) then filters on +`user_id = ?` and throws `SEUserContactLinkNotFound`. Every chat-preview/chat-item query is +unaffected — only the accept fails. + +The client never compensated: `NtfManager.acceptContactRequestAction` computed `isCurrentUser` +only to decide whether to update the chat model, and called the API without switching profile — +unlike its neighbours `openChatAction` and `showChatsAction`, which both call `changeActiveUser`. +iOS is not affected: `processNotificationResponse` has switched the active user since +`06a0dbd0f` (2023). + +The core-side scoping is itself a regression. `7dd4dc3b4` ("core: support accepting contact +requests for non active users (for accepting via notification)", #1809) deliberately made this +command use the request's own user via `getContactRequest'`. `7f6bc3089` (#5978, first released in +v6.4.0-beta.4) reverted it to `withUser $ \user@User {userId}` + user-scoped `getContactRequest` +as a side effect of unrelated short-link work, leaving `getUserByContactRequestId` +(`Store/Direct.hs`) as dead code. So the bug predates the v7 line. + +## Fix + +Client-side, in `NtfManager.acceptContactRequestAction`: switch to the profile the request was +sent to before calling the API, mirroring `openChatAction`/`showChatsAction` and iOS. + +- `changeActiveUser` is called only when the target profile differs from the active one; the + accept then runs in the right profile, and `isCurrentUser` — computed *after* the switch — is + true, so the accepted contact is inserted into the chat list the user is now looking at instead + of being silently dropped. +- The body moves into `withLongRunningApi` and gains `awaitChatStartedIfNeeded`, which the two + sibling actions already had. This is required, not incidental: `APISetActiveUser` starts with + `unlessM (lift chatStarted) $ throwChatError CEChatNotStarted`, so without the wait a tap during + cold start would fail the switch, and the switch is the whole fix. +- `clearOverlays` is set when a switch happened, so a modal left open by the previous profile does + not end up rendering the new profile's data. It is scoped to the switch branch on purpose: the + siblings clear unconditionally because they *navigate*, which accepting does not. + +## Alternative considered and rejected + +Restoring the core behaviour — deriving the user from the request via the already-present +`getUserByContactRequestId` instead of `withUser` — was implemented, built, and covered by a test +(`accept contact request for non active user`, passing), then dropped. It fixes the API for all +callers (terminal `/_accept`, bots, the python/nodejs SDKs) and does not depend on a client-side +switch that swallows its own errors. It was rejected for this fix because the UI has to switch +profiles anyway for the result to be visible, so the core capability would never be exercised by +the app, and the client change alone resolves every path reachable from a notification. The core +API therefore remains active-user-scoped, and `getUserByContactRequestId` remains unused. + +## Known gaps not addressed here + +- `acceptContactRequestAction` passes `rhId = null` (its own long-standing TODO), so accepting from + a notification always targets the local core. A request that arrived on a *remote host* produces + a notification carrying a remote user id, and the new `changeActiveUser(null, userId, null)` will + switch the **local** profile. `openChatAction` — the notification's default tap action — already + has this flaw, so this extends an existing pattern rather than introducing one; the real fix is + carrying the remote host id in the notification. +- On desktop the Accept action is not clickable: `NtfManager.desktop.kt` passes the action to + two-slices, whose Linux backend does not render action buttons, and it passes + `NotificationAction.ACCEPT_CONTACT_REQUEST.name` as the label instead of + `generalGetString(MR.strings.accept)`. The bug is therefore Android-only in practice. +- Accepting from a notification still does not open the new contact's chat, whereas iOS dismisses + sheets and calls `loadOpenChat` from inside `acceptContactRequest` when `contact.sndReady`. The + Kotlin equivalent is the existing `close` callback of `acceptContactRequest`, which the + notification path passes as `null`. +- `APIRejectContact` is also active-user-scoped and fails the same way. It is left alone: unlike + accept, it never supported non-active users (#1809 changed only accept), and the notification has + no Reject action. + +## Testing + +- `:common:compileKotlinDesktop` and `:android:assembleDebug` build clean. +- Manual, Android: profile 1 active, request lands on inactive profile 2, tap Accept in the + notification — the app switches to profile 2, the contact appears in the list, no error. +- No automated coverage: the changed path is reachable only from a notification action. diff --git a/plans/2026-07-29-fix-live-message-sent-to-wrong-chat.md b/plans/2026-07-29-fix-live-message-sent-to-wrong-chat.md new file mode 100644 index 0000000000..bd37361dc5 --- /dev/null +++ b/plans/2026-07-29-fix-live-message-sent-to-wrong-chat.md @@ -0,0 +1,248 @@ +# Fix: live message is sent to the chat opened after switching chats + +Branch: `nd/fix-live-message-sent-to-wrong-chat` (off `origin/stable`) +Date: 2026-07-29 + +Line references are against `origin/stable` at `970ef8932`, with this fix +applied. Android and desktop (`commonMain/ComposeView.kt`). + +## Problem + +Typing a live message and switching to another chat sends that message to +the chat that was opened, without the user sending anything. Reported on +desktop, where every chat switch reuses the same view. + +## Cause + +A live message is committed when the chat is switched +(`ComposeView.kt:1353-1369`), which before this change was: + +``` + if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) { + sendMessage(null) +``` + +`KeyChangeEffect` is `LaunchedEffect(key1) { block(prev) }` +(`Utils.kt:683-698`), so when the key changes, `remember(key1)` rebuilds +it from the lambda of the composition that is running *now* - and by then +`chatModel.chatId` is already the new chat, `ChatView` has recomposed +`ComposeView` with the new `chat`, and the block that runs captured that +one. + +`sendMessage(null)` → `sendMessageAsync` → `send(chat, ...)` uses the +captured `chat`, while the message content comes from `composeState`, +which is shared between the chats opened in this view. So the content of +the chat that was left is sent to the chat that was opened: + +- `liveMessage.sent == false` - a new message is created in the wrong + chat, which is what is seen; +- `liveMessage.sent == true` - `apiUpdateChatItem` is called with the new + chat's type and id and the item id from the previous chat, which the + backend cannot resolve. + +The same mismatch made the post-send `clearCurrentDraft()` clear the +draft of the chat opened after the switch, deleting a draft that was +never sent. + +## Fix + +`sendMessageAsync` and `sendMessage` take the chat the message was +composed in, defaulting to the chat this view shows +(`ComposeView.kt:685-694`, `988-993`). Only what a live message can reach +uses it: the message send, the update of an already sent live message, +and the two places that clear the draft after sending. Live messages have +no context item (`SendMsgView.kt:156-165` only offers the button when the +compose is empty and has none), so the forwarding, editing and reporting +branches cannot run with a chat other than the view's and keep using +`chat` - the parameter is not threaded through them. + +The chat switch resolves the chat by the id it had before the switch: + +```kotlin +val liveMessageChat = if (prevChatId == null || prevChatId == chat.id) chat else chatsCtx.getChat(prevChatId) +// if that chat is gone there is nowhere to send it, and it must not be sent to the chat opened instead +if (liveMessageChat != null) sendMessage(null, toChat = liveMessageChat, composed = cs) else clearState() +``` + +`prevChatId == chat.id` keeps the view's own chat, which is what secondary +(member support) chat views need - they share the group's chat id, and +only their `chat` carries the scope. + +If the previous chat can no longer be found the message is not sent at +all, and the compose state is cleared so it does not leak into the chat +that was opened. Sending it to the chat that is open now is the defect +being fixed, so it is not used as a fallback. + +### Handing the compose state over to the opened chat + +Sending to the right chat is not enough on its own: `composeState` is +shared between the chats opened in this view, and this is the only branch +of `KeyChangeEffect` that neither resets it nor loads the opened chat's +draft - the branch that loads a draft (`else if (chatModel.draftChatId +.value == draftChatId(chatModel.chatId.value, chatScope) ...)`) is later +in the same `if` chain and cannot be reached. So the live message stayed +in the compose state of a view that now shows another chat, and that +chat's draft was never read. + +`sendMessageAsync` then made it visible. It runs on `Dispatchers.Default`, +so its writes land after the switch: + +```kotlin +val liveMessage = cs.liveMessage +if (!live) { + if (liveMessage != null) composeState.value = cs.copy(liveMessage = null) // the whole composed state + sending() // and its spinner +} +``` + +The opened chat's input showed the text composed in the previous one until +the send completed and `clearState()` emptied it; the draft it should have +shown was still in the model, and the next switch away dropped it. This +predates this fix - without it the same writes happen, and there +`clearCurrentDraft()` resolves to the opened chat and deletes its draft +outright. + +Four changes, all following from "this send no longer owns the compose +state": + +- `sendMessageAsync` takes its `cs` as a parameter defaulting to + `composeState.value`, and `sendMessage` takes `composed: ComposeState? = + null`, so only the chat switch passes a state and every other sender + still reads it inside the coroutine, exactly where the send read it + before. The chat switch + captures it on the main thread before replacing it - without that the + send would read the compose state of the chat that was opened and send + *its draft* to the previous chat. +- `checkLinkPreview` takes that state too. It re-read `composeState` + rather than what was passed in, and it is reached by every text live + message through `updateMsgContent`, so with the compose state handed + over it would have rebuilt the message from the opened chat's draft, or + from nothing - overwriting the live message instead of committing it. + Only the calls a live message can reach pass the state. The forwarding + call site keeps reading the current one - it is unreachable from the + chat switch, and `forwardItem` suspends before it, so passing the + captured state there would drop what was typed while the forward was in + flight. The three senders that connect a prepared chat keep reading the + current one too. +- Every `composeState` write in `sendMessageAsync` is guarded by + `composeIsForSend()` (`toChat.id == chat.id`): directly for the two at + the start, and through `chatIsOpen` for the clear/restore at the end, + which #7308 already routes through `sentMessageInCompose`. It compares + the two chats rather than checking which one is open, so the send made + by a chat switch never takes the compose state back, not even if that + chat is opened again before the send completes. + `clearCurrentDraft(toChat)` is already keyed on the chat and needs no + guard. Whether the *view's own* send may still write when its chat has + been switched away is #7308's question, not this one's. +- The chat-switch branch then resets `composeState` to the opened chat's + draft, or to an empty state, like the branches below it do. + +## Blast radius + +`toChat` defaults to the chat this view shows, so every other send passes +no chat: the send button (`SendMsgView.kt`), the live updates while typing +(`sendMessageAsync(live = true)`), forwarding, editing and reporting. For +all of them `composeIsForSend()` is true, so every guard added here is a +no-op and they behave exactly as before. Only the send started by the chat +switch passes a different chat, and only the branches it can reach were +changed. + +The one change not behind that guard is `checkLinkPreview` reading the +state passed in. It matters only where the two can differ, which is after +a suspension: the forwarding branch waits on `forwardItem`, so that call +site deliberately keeps reading the current state (it is unreachable from +the chat switch anyway). The other call sites are reached with nothing +suspending since the state was captured. + +The live message update loop is not affected: it is started once +(`SendMsgView.kt:523-559`) with the `::updateLiveMessage` reference of the +composition in which live mode started, so its updates already go to the +chat the message belongs to. It exits because the chat switch replaces the +compose state with the opened chat's, which has no `liveMessage` - on the +main thread, as the chat is switched, rather than when the send completes +as before. Only that send was created fresh on every composition, which is +why it was the one going to the wrong chat. + +Not covered, and unchanged: a live message in a member support chat that +is closed without changing the chat id is never committed - the effect +that commits it is keyed on the chat id, which does not change when that +view is closed. + +`chatsCtx.getChat` searches the context's own list, and a secondary +context (member support, reports) is built with an empty one, so there it +can only return null. That branch is not reached from a support chat in +practice - it shares the group's chat id, so `prevChatId == chat.id` holds +and the view's own `chat` is used - and if it ever were, the message is +discarded rather than sent to the chat that was opened, which is the +behaviour intended for "the chat is gone" anyway. + +## Verification + +- `./gradlew :common:compileKotlinDesktop` — passes. +- Manual: + 1. Start a live message in **A**, type, and switch to **B** while + typing. The message must appear in **A**; nothing is sent in **B**, + and B's input and draft are untouched. + 2. Repeat with a draft already saved in **B** - it must still be there + after the switch. This is the case that was found failing: B showed + the text composed in A, then emptied when the send completed, and B's + draft was dropped on the next switch. Watch B's input from the moment + of the switch, not only after the send finishes. + 3. Slow or failing send (network off) while doing 1 and 2, so the window + between the switch and the send completing is long enough to type in + **B** - what is typed there must survive the send completing. + 4. The live message must carry a **link preview**: type a URL in **A**, + let the preview load, then switch. The message committed to A must be + the text that was composed - not the opened chat's draft, and not + empty. Every text live message is rebuilt through + `updateMsgContent` -> `checkLinkPreview`, so this is what breaks if + that one stops reading the state it was given. + 5. Switch **back**: live message in A, switch to B, return to A and type + something new before the send completes. What is typed in A must + survive - the send handed the compose state over at the switch and + must not take it back. + 6. Regressions: an ordinary send goes to the chat it was typed in; + forwarding still targets the chat it was forwarded to, and text typed + while a forward is in flight is still appended to it; reporting a + message still reports it in the chat it belongs to; sending in a + member support chat still goes to that scope. + +## Merged with #7308 + +#7308 (a send that is still in flight when the chat is switched) landed in +`stable` first, so this branch was merged with it. Both changed the end of +`sendMessageAsync`, and the two guards are **not** the same rule - the +merge keeps both: + +- here, `composeIsForSend()` = `toChat.id == chat.id` - is this send for + the chat this view shows, or for another one; +- in #7308, `chatIsOpen` = `chatModel.chatId.value == chat.id` - is the + chat this view shows still the one open. + +`chatIsOpen` becomes the conjunction, +`composeIsForSend() && chatModel.chatId.value == chat.id`. Where `toChat` +is `chat` - every send but the one made by a chat switch - that reduces to +#7308's own check, so its behaviour is unchanged. + +Nothing else in that block had to move. #7308 already routes both compose +writes through `sentMessageInCompose`, which derives from `chatIsOpen`, so +guarding `chatIsOpen` guards them; the rest of the change there is one +call site taking `toChat`, `clearCurrentDraft`. The draft id a failed +message is saved under keeps using `chat`: that branch is behind +`!liveSend`, which the send made by a chat switch never satisfies, so +`toChat` is always `chat` where it is read. + +An earlier revision of this note said that #7308's `cs.liveMessage != null` +clause "already covers the send made by the chat switch". **It did not.** +At the time that clause sat outside the `chatIsOpen` check: + +```kotlin +val sentMessageInCompose = live || cs.liveMessage != null || (chatIsOpen && composeState.value.inProgress) +``` + +which is correct only while a live message is always sent to the chat that +is open - the assumption this fix removes. Read as written, the clause +*exempts* the chat-switch send from the very guard that protects the +opened chat, and a merge that followed it reintroduced the leak described +above. #7308 shipped with the live clauses moved inside `chatIsOpen`, +which was a no-op on its own branch and is what makes this merge work. diff --git a/plans/2026-07-31-fix-db-passphrase-toggle-clipped.md b/plans/2026-07-31-fix-db-passphrase-toggle-clipped.md new file mode 100644 index 0000000000..16767d62aa --- /dev/null +++ b/plans/2026-07-31-fix-db-passphrase-toggle-clipped.md @@ -0,0 +1,124 @@ +# Fix "Save passphrase in settings" toggle unreachable on desktop + +## Symptom + +On 7.0 desktop (reproduced on the Linux AppImage and on Windows), in +Chat data → Database passphrase & export → Database passphrase, the +"Save passphrase in settings" toggle cannot be switched off. The reporter sees +the switch sitting slightly past the right edge of the section card. Because the +setting stays on, the passphrase remains stored in `settings.properties` and the +app never prompts for it on start — on desktop that file holds the passphrase in +clear text, since `Cryptor.desktop.kt` is an identity implementation. + +This is distinct from the case where the switch is *rendered disabled* +(`DatabaseEncryptionView.kt:127`, `enabled = (!initialRandomDBPassphrase && !progressIndicator) || migration`), +which is intended behaviour for a database still using the initial random +passphrase. The reports here are from users who set their own passphrase, so +`initialRandomDBPassphrase == false` and the switch is enabled — just not +reachable. + +## Root cause + +1. `SavePassphraseSetting` is hand-rolled in both platform actuals + (`DatabaseEncryptionView.desktop.kt:43-53`, `.android.kt:43-53`) and is the + only toggle row in the app whose label carries no `weight`: + + ```kotlin + Text(stringResource(MR.strings.save_passphrase_in_settings), Modifier.padding(end = 24.dp)) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch(checked = useKeychain, onCheckedChange = onCheckedChange, enabled = enabled) + ``` + + `Row` measures unweighted children first against the full available width, then + divides what is left among weighted ones. A label that does not comfortably fit + consumes the remainder, the weighted `Spacer` collapses to zero, and + `DefaultSwitch` is placed past the row's right edge. + +2. Every other toggle row goes through `SettingsActionItemWithContent` + (`SettingsView.kt:380`), which gives the label `Modifier.weight(1f)`. There the + label truncates and the trailing control keeps its size and position, so the + same string length is harmless. + +3. Before #6777 this row had enough slack and no clipping. `SectionView` was a + plain `Column` with no horizontal inset, and `SectionItemView` used + `DEFAULT_PADDING` (20.dp) per side — 348.dp of content width on the desktop + start pane (`DEFAULT_START_MODAL_WIDTH` = 388.dp). Any overflow still drew and + still received pointer events. + +4. #6777 introduced `LocalCardScreen` / `CardColumnLayout` (`Section.kt`), which + wraps section content in `Modifier.padding(horizontal = CARD_PADDING /* 18.dp */)` + … `.clip(SectionCardShape)`, and switches `itemHPadding` from `DEFAULT_PADDING` + to `CARD_PADDING`. `DatabaseEncryptionView` is opened with `cardScreen = true` + (`DatabaseView.kt:235`), so its row content width drops 348.dp → **316.dp**. + +5. `Modifier.clip` clips pointer input as well as drawing. The displaced switch is + therefore both cut off visually and unhittable — the toggle stops working rather + than merely looking wrong. + +Budget arithmetic on the desktop start pane: fixed cost in the row is ~96.dp +(24 icon + 8 spacer + 24 label end-padding + ~40 switch), leaving ~220.dp for a +27-character label at 16.sp, which needs ~215.dp in English. Borderline at 100% +font scale and over budget as soon as the label is longer — a longer localization, +or a larger font size, since the label scales with `fontSizeSqrtMultiplier` while +`CARD_PADDING` does not. + +The widths above are derived from the layout constants, not measured against a +running client; the reporter's observation that the switch sits slightly past the +card edge is what confirms the row overflows in practice. + +## Fix + +Move the weight onto the label and drop the weighted spacer, in both actuals: + +```kotlin +Text(stringResource(MR.strings.save_passphrase_in_settings), Modifier.weight(1f).padding(end = 24.dp)) +DefaultSwitch(checked = useKeychain, onCheckedChange = onCheckedChange, enabled = enabled) +``` + +The label now truncates instead of displacing the switch, matching what +`SettingsActionItemWithContent` does for every other toggle row. + +The spacer has to go: leaving both the label and the spacer weighted would split +the remaining space between them and starve the label instead, which trades one +layout bug for another. + +## Why this fix and not alternatives + +- **Widening the card or shrinking `CARD_PADDING`** would buy back the ~32.dp lost + in #6777, but only until the next longer localization or font-size step. The row + would stay the one place in the app where a long label can push a control out of + reach. +- **Removing `clip` from `CardColumnLayout`** would restore clickability of + overflowing content, but the clip is what gives section cards their rounded + corners; dropping it would regress the design and leave the switch drawn outside + its card. +- **Shortening the string** is a translation-wide problem, not a fix, and does not + help at larger font sizes. + +## Impact + +- Desktop and Android only. Both actuals carry the identical defect; Android's row + is in fact narrower still (~288.dp on a 360.dp-wide screen), so it is affected at + least as much — it simply has not been reported. +- iOS is unaffected: `DatabaseEncryptionView.swift` uses a SwiftUI `Toggle` inside + `settingsRow`, where the label truncates and the toggle cannot be displaced. The + `initialRandomDBPassphrase` disabled-state logic is the same on iOS + (`DatabaseEncryptionView.swift:80`) and is unchanged by this fix. +- Users already stuck in the bad state have `StoreDBPassphrase=true` in + `settings.properties` with the passphrase stored alongside it. After this fix + they can turn the setting off in the UI, which removes the stored passphrase via + `removePassphraseFromKeyChain` and restores the prompt on start. +- No behaviour change beyond the row layout: no logic, preference, or string was + touched. + +## Verification + +- `bash ~/build/linux.sh` on this branch: cold `dist-newstyle`, `libsimplex.so` + rebuilt from master's sources, `:common:compileKotlinDesktop` executed, + `BUILD SUCCESSFUL`, AppImage produced. +- `bash ~/build/android.sh` on this branch: `BUILD SUCCESSFUL`, arm64-v8a debug APK + produced (native libs are the prebuilt ones, so this exercises the Kotlin change + only). +- Not done: the rendered row has not been checked in a running client. Worth + confirming at a raised font size and in a locale with a longer label, which is + the case that made the overflow visible in the first place. diff --git a/plans/2026-08-07-webm-video-detection.md b/plans/2026-08-07-webm-video-detection.md new file mode 100644 index 0000000000..e8d4ff1e08 --- /dev/null +++ b/plans/2026-08-07-webm-video-detection.md @@ -0,0 +1,19 @@ +# Send dropped `.webm` as video only when it has a video track + +## Problem + +Dragging a `.webm` file onto the desktop compose area attaches it as a plain file instead of embedding it as a video with a preview frame and duration. Every other video container the app recognises (`.mov`, `.avi`, `.mp4`, `.mpg`, `.mpeg`, `.mkv`) embeds. The same omission hides `.webm` from the "Attach → video" file picker, so the only way to send one is "Choose file", which sends it as a document. + +## Cause + +`isVideoUri` (`apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt:298`) classifies attachments by file extension and does not list `.webm`; the desktop picker filter `isVideo` (`apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt:5`) repeats the same list with the same omission. `onFilesAttached` groups the dropped URIs by `isImage(it) || isVideoUri(it)`, so a `.webm` fails both predicates, falls into the files group and reaches `processPickedFile`, which builds a `ComposePreview.FilePreview`. + +Adding the extension to both lists is not sufficient on its own. Unlike the other containers, `.webm` is used about as often for audio alone as for video — it is `MediaRecorder`'s default audio container, and Opus/Vorbis in WebM is widespread on the web. An audio-only file classified as video reaches the video branch of `processPickedMedia`, where `getBitmapFromVideo` finds no video track, returns a null preview and raises the "video decoding" alert; the item is then skipped and nothing is attached at all (`ComposeView.kt:366-376`). That is strictly worse than the file attachment the same drop produced before. + +## Fix + +Add `.webm` to both extension lists, and for `.webm` alone decide from the file's content rather than its name. A new `expect suspend fun hasVideoTrack(uri)` (`views/helpers/Utils.kt`) reports whether the container declares a video track, reading metadata only and never decoding a frame. On desktop it is implemented with libvlc's media parse (`platform/VideoPlayer.desktop.kt`), which signals completion with an event rather than a poll, so no frame-decoding budget is needed; measured at 12-346 ms across VP8, VP9, AV1, alpha and a 42 MB file, with a 3 s timeout as a guard against a stuck parse. On Android it uses `MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO`. Either implementation answering "no", or failing, attaches the file as a file, which is always safe. + +`onFilesAttached` consults it only when a `.webm` is actually among the dropped URIs; every other attachment keeps the original synchronous code path on the caller thread, so the change adds no latency and no threading difference to images, documents or the other video containers. Files with a video track are sent as video, the rest as files. + +The content check is applied only where the user has not said how the file should be sent — drag & drop and paste. An explicitly picked video is still trusted: selecting an audio-only `.webm` through "Attach → video" raises the existing decoding error, which matches how the other containers already behave. diff --git a/plans/delete-leave-dialog-with-profile-impl.md b/plans/delete-leave-dialog-with-profile-impl.md new file mode 100644 index 0000000000..860d555d36 --- /dev/null +++ b/plans/delete-leave-dialog-with-profile-impl.md @@ -0,0 +1,323 @@ +# Implementation plan — chat name on its own line in delete/leave/clear dialogs + +Follows the product spec in +[`delete-leave-dialog-with-profile.md`](./delete-leave-dialog-with-profile.md). + +Pure code change — zero string additions, zero new helpers, zero +signature changes. Each call site edits one argument: the `text =` / +`message:` value gains `"${displayName}\n\n"` prepended to the +existing localized warning (or, where there is no current body, the +chat name becomes the new body). + +One commit per platform. + +## Commit 1 — Kotlin + +**Files touched:** +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt` + — adds `parseHtml: Boolean = true` to `showAlertDialog` and + `showAlertDialogButtonsColumn`. When `false`, the body text is wrapped + as `AnnotatedString` and routed through the existing AnnotatedString + `AlertContent` overload, which does NOT call + `escapedHtmlToAnnotatedString`. Default stays `true` so existing + callers are unaffected. +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt` +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt` + — adds the previously-missed `deleteContactConnectionAlert` + dispatcher to the coverage (pending contact connections). + +Every Kotlin call site that prepends the chat name sets +`parseHtml = false`, so `displayName` is never HTML-interpreted. + +### 1.1 — `deleteGroupDialog` (`GroupChatInfoView.kt:182`) + +```diff + fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val chatInfo = chat.chatInfo + val titleId = /* unchanged */ + val messageId = /* unchanged */ + AlertManager.shared.showAlertDialog( + title = generalGetString(titleId), +- text = generalGetString(messageId), ++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + confirmText = generalGetString(MR.strings.delete_verb), + onConfirm = { /* unchanged */ }, + destructive = true, + ) + } +``` + +### 1.2 — `leaveGroupDialog` (`GroupChatInfoView.kt:222`) + +```diff + fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val titleId = /* unchanged */ + val messageId = /* unchanged */ + AlertManager.shared.showAlertDialog( + title = generalGetString(titleId), +- text = generalGetString(messageId), ++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + confirmText = generalGetString(MR.strings.leave_group_button), + onConfirm = { /* unchanged */ }, + destructive = true, + ) + } +``` + +Signature unchanged. No caller updates. `groupInfo.displayName` is +already available on the existing parameter (`ChatModel.kt:2142`). + +### 1.3 — `clearChatDialog` (`ChatInfoView.kt:492`) + +```diff + fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.clear_chat_question), +- text = generalGetString(MR.strings.clear_chat_warning), ++ text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}", + confirmText = generalGetString(MR.strings.clear_verb), + onConfirm = { controller.clearChat(chat, close) }, + destructive = true, + ) + } +``` + +### 1.4 — Contact-delete dispatchers (`ChatInfoView.kt`) + +Four functions. `deleteContactOrConversationDialog` (line 248) has +no existing `text =`, so the chat name becomes the new body. The +other three already have a `text =`, so the name is prepended. + +All four already have `contact: Contact` as a parameter, so +`contact.displayName` is used directly (same value as +`chat.chatInfo.displayName` for a direct chat, shorter expression). + +```diff + // deleteContactOrConversationDialog — line 248 + private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), ++ text = contact.displayName, + buttons = { /* unchanged */ } + ) + } +``` + +```diff + // deleteActiveContactDialog — line 304 + private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) { + val contactDeleteMode = mutableStateOf(ContactDeleteMode.Full()) + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), +- text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), ++ text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + buttons = { /* unchanged */ } + ) + } +``` + +Same diff for `deleteContactWithoutConversation` (line 361) and +`deleteNotReadyContact` (line 417) — both use +`delete_contact_cannot_undo_warning`. Neither takes `contact` as +a parameter, so the name is read via `chat.chatInfo.displayName` +(which resolves to `contact.displayName` because these dispatchers +are only reached for `ChatInfo.Direct` chats). Their titles +(`confirm_delete_contact_question`) stay unchanged — the +not-ready / no-conversation paths keep their distinct title. + +## Commit 2 — iOS + +**Files touched:** +- `apps/ios/Shared/Views/ChatList/ChatListNavLink.swift` +- `apps/ios/Shared/Views/Chat/ChatInfoView.swift` +- `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift` + +### 2.1 — `deleteGroupAlert` (two locations) + +`Views/Chat/Group/GroupChatInfoView.swift:835` and +`Views/ChatList/ChatListNavLink.swift:567` get the same diff. +`deleteGroupAlertMessage(_:)` already returns a `Text` containing +the localized warning — concatenate to it. + +```diff + private func deleteGroupAlert() -> Alert { + let label: LocalizedStringKey = /* unchanged */ + return Alert( + title: Text(label), +- message: deleteGroupAlertMessage(groupInfo), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + deleteGroupAlertMessage(groupInfo), + primaryButton: .destructive(Text("Delete")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +`Text(chat.chatInfo.displayName)` resolves to `Text(_ content: some StringProtocol)` +(the runtime-string overload — no localization lookup, matches +codebase convention: `ChatView.swift:984`, `ChatInfoToolbar.swift:49`, +`SettingsView.swift:540`). `Text(verbatim: "\n\n")` is the literal +separator, matching the codebase convention that reserves +`verbatim:` for fixed punctuation (`ContextItemView.swift:88` is +the textbook example: `Text(chatLink.displayName) + Text(verbatim: " - ")`). +The third term `Text(messageLabel)` keeps the existing +`LocalizedStringKey` lookup. + +### 2.2 — `leaveGroupAlert` (two locations) + +`Views/Chat/Group/GroupChatInfoView.swift:872` and +`Views/ChatList/ChatListNavLink.swift:622`: + +```diff + private func leaveGroupAlert() -> Alert { + let titleLabel: LocalizedStringKey = /* unchanged */ + let messageLabel: LocalizedStringKey = /* unchanged */ + return Alert( + title: Text(titleLabel), +- message: Text(messageLabel), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text(messageLabel), + primaryButton: .destructive(Text("Leave")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +### 2.3 — `clearChatAlert` (three locations) + +`Views/Chat/ChatInfoView.swift:577`, +`Views/Chat/Group/GroupChatInfoView.swift:858`, +`Views/ChatList/ChatListNavLink.swift:600`: + +```diff + private func clearChatAlert() -> Alert { + Alert( + title: Text("Clear conversation?"), +- message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + primaryButton: .destructive(Text("Clear")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +### 2.4 — Contact-delete action sheets + +Three functions in `Views/Chat/ChatInfoView.swift`. None currently +pass `message:` to `ActionSheet`; we add it. `ActionSheet`'s +`message:` is an optional second parameter that SwiftUI already +supports. + +All three functions have `contact: Contact` in scope. Use bare +`Text(contact.displayName)` (resolves to the `StringProtocol` +overload, no localization lookup, matches codebase convention). +Add only the name as `message:` — these ActionSheets had no +message before, so adding any additional warning would be new +behavior beyond the stated goal. + +**`deleteContactOrConversationDialog`** (line 1177): + +```diff + private func deleteContactOrConversationDialog( + _ chat: Chat, _ contact: Contact, _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void, + _ showSheetContent: @escaping (SomeSheet) -> Void + ) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Delete contact?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteContactOrConversationDialog" + )) + } +``` + +**`deleteContactWithoutConversation`** (line 1324): + +```diff + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteContactWithoutConversation" + )) +``` + +**`deleteNotReadyContact`** (line 1348) — same: + +```diff + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteNotReadyContact" + )) +``` + +### 2.5 — `DeleteActiveContactDialog` sheet (line 1282) unchanged + +The secondary multi-option sheet is reached only after the user +confirms "Delete contact" in the previous action sheet — which now +shows the name. The sheet itself remains as-is. + +## Verification + +For each platform, exercise every entry point and confirm the +body reads `` on its own line followed by the existing +warning: + +- Android & Desktop: + - Chat list swipe — direct contact, group, channel, business chat + → delete / clear / leave. (Note folder's clear dialog is + intentionally unchanged — `clearNoteFolderDialog` excluded.) + - Chat info screens — "Delete contact" / "Delete group" / "Delete + channel" / "Clear conversation" / "Leave …" rows. + - Contact list (`ContactListNavView.kt:148`) — "Delete contact" + action shows the name in entry-point dialog and toggle dialog. + - Multi-option contact-delete path: entry dialog (now has a name + body where it had none) → toggle dialog (name above the + warning) → success. +- iOS: + - Same matrix from chat list swipe and chat info screens. + - Action-sheet contact-delete paths show the name as the + `message:` line on iPhone and iPad. + +Edge cases: + +- Long chat name — alert containers wrap automatically; the body + occupies 3+ lines. Confirm with a chat renamed to ~40 characters. +- Special characters (emoji, RTL, double quotes) — render literally + via string interpolation, no format-substitution involved. +- Empty `displayName` — does not occur in practice (`NamedChat` + enforces non-empty via `localAlias.ifEmpty { profile.displayName }`). + +Diff-level checks: + +- `git diff '*strings.xml' '*Localizable.strings'` returns zero + hunks. Pure code change. +- `git diff --stat` shows ~5 files total: two Kotlin dispatcher + files, three iOS view files. +- Cancel/confirm flows behave exactly as before — same API calls, + same model updates, same navigation. + +## Out of scope + +- Profile picture / avatar in dialogs — excluded by product decision. +- Refactoring the iOS duplication between `ChatListNavLink` and + `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO` at + `GroupChatInfoView.swift:834`). +- Pre-existing wording divergence between Kotlin's "Clear chat?" + and iOS's "Clear conversation?". Both platforms keep their + titles. +- "Delete invitation" at `ChatListNavLink.swift:236` — has no + confirmation dialog (direct call to `deleteChat(chat)`); nothing + to modify. +- Bolding the chat name. SwiftUI `Text + Text` supports `.bold()` + on the first term, but Jetpack Compose `AlertDialog` text is a + single unstyled string — keeping both unstyled preserves parity. diff --git a/plans/delete-leave-dialog-with-profile.md b/plans/delete-leave-dialog-with-profile.md new file mode 100644 index 0000000000..a05fb66532 --- /dev/null +++ b/plans/delete-leave-dialog-with-profile.md @@ -0,0 +1,249 @@ +# Show chat name in delete / leave / clear confirmation dialogs + +## Goal + +The current delete-contact, delete-group, delete-channel, leave-group, +leave-channel and clear-chat confirmations are generic. From a long +chat list, swiping on a row and triggering one of these actions opens +a dialog whose title is "Delete group?", "Leave channel?", "Clear +conversation?" — with no indication of *which* chat is the target. A +user can easily act on the wrong chat. + +The fix: include the chat's display name in the dialog body, on a line +of its own above the existing warning text. Nothing else changes — +same title, same warning text, same buttons, same colors, same dialog +shape. No profile picture, no layout changes, no new helpers, no new +translation strings. + +We deliberately do NOT reuse the open-chat-link alert layout (centered +profile image + name + open-chat button). That layout is the *invite* +flow's identity; repurposing it for destructive confirmations would +confuse the two flows visually. The minimum change that solves the +"which chat?" problem is putting the name in the body text. + +## Why body, not title; why no new strings + +The title carries the action ("Delete group?", "Leave channel?"). The +body carries the consequences ("Group will be deleted for all +members…"). The chat name belongs with the body — it is the subject +of the consequence, not part of the question. + +Adding the name to the title would require new format-string variants +(`delete_group_named_question` etc.) and per-locale re-translation. +Putting the name on its own line in the body is a pure code change — +the existing translated warnings are concatenated with the chat name +in code: + +``` +Tech Talk + +Group will be deleted for all members - this cannot be undone! +``` + +The display name appears first because the user wants to confirm +*which* chat before reading *what* will happen. The blank line between +the name and the warning makes the name visually distinct. + +## Current state + +### Multiplatform (Kotlin / Android / Desktop) + +All eight dialogs go through `AlertManager.shared.showAlertDialog` or +`showAlertDialogButtonsColumn`: + +- `deleteGroupDialog` — `views/chat/group/GroupChatInfoView.kt:182` +- `leaveGroupDialog` — `views/chat/group/GroupChatInfoView.kt:222` +- `clearChatDialog` — `views/chat/ChatInfoView.kt:492` +- `deleteContactOrConversationDialog` — `views/chat/ChatInfoView.kt:248` +- `deleteActiveContactDialog` — `views/chat/ChatInfoView.kt:304` +- `deleteContactWithoutConversation` — `views/chat/ChatInfoView.kt:361` +- `deleteNotReadyContact` — `views/chat/ChatInfoView.kt:417` +- `deleteContactConnectionAlert` — `views/chatlist/ChatListNavLinkView.kt:772` + (deletes a pending contact connection; takes a `PendingContactConnection` + whose `displayName` reflects any custom name the user set) + +Call sites (chat-info screens, chat-list swipe / overflow, contact +list) funnel through these dispatcher functions. + +### iOS (Swift) + +Two SwiftUI patterns are used: + +- SwiftUI `Alert` with `primaryButton: .destructive` / `.cancel()`: + - `deleteGroupAlert` — `Views/ChatList/ChatListNavLink.swift:567`, + `Views/Chat/Group/GroupChatInfoView.swift:835` + - `leaveGroupAlert` — `Views/ChatList/ChatListNavLink.swift:622`, + `Views/Chat/Group/GroupChatInfoView.swift:872` + - `clearChatAlert` — `Views/ChatList/ChatListNavLink.swift:600`, + `Views/Chat/ChatInfoView.swift:577`, + `Views/Chat/Group/GroupChatInfoView.swift:858` +- SwiftUI `ActionSheet`: + - `deleteContactOrConversationDialog` — + `Views/Chat/ChatInfoView.swift:1177` + - `deleteContactWithoutConversation` — + `Views/Chat/ChatInfoView.swift:1324` + - `deleteNotReadyContact` — `Views/Chat/ChatInfoView.swift:1348` + +`Alert(message:)` accepts `Text`, and `ActionSheet(message:)` (an +existing optional parameter not used today) accepts `Text` too — so +the name can be added by composing the existing message string with +`"\n\n"` and the chat name. No widget changes. + +## Design + +| Dialog | Body today | Body after | +|---|---|---| +| Delete group | `Group will be deleted for all members – this cannot be undone!` | `Tech Talk` + blank line + existing text | +| Delete channel | `Channel will be deleted for all subscribers – this cannot be undone!` | `SimpleX news` + blank line + existing text | +| Leave group | `You will stop receiving messages from this group. …` | `Tech Talk` + blank line + existing text | +| Clear chat | `All messages will be deleted – this cannot be undone! …` | `Alice` + blank line + existing text | +| Delete contact (entry sheet) | *(no body today — title only + buttons)* | `Alice` (becomes the body) | +| Delete contact (active variant) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text | +| Confirm contact deletion (not-ready / no-conversation) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text | + +Title text is unchanged in every case. Existing titles +(`delete_contact_question`, `confirm_delete_contact_question`, etc.) +keep their semantic distinction — the "Confirm contact deletion?" +title still appears for the not-ready / no-conversation paths. + +### Which name to use: `displayName`, not `chatViewName` + +The chat list row labels chats with `cInfo.chatViewName` +(`ChatPreviewView.kt:87`), defined as: + +```kotlin +val chatViewName: String + get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } +``` + +The dialog uses `chatInfo.displayName` (and `groupInfo.displayName` +for the leave dialog). For most chats these are identical: + +- If `localAlias` is set, both resolve to the alias. +- If `displayName == fullName` (or `fullName` is empty), both resolve + to `displayName`. + +For a contact with distinct display name and full name (no alias), +the row would show `alice / Alice Smith` while the dialog shows +`alice`. Acceptable: `displayName` is the recognizable identifier, +shorter, and the dialog format (single line above the warning) +benefits from concision. Two-part identifiers in the dialog would +crowd the layout. + +### `clearNoteFolderDialog` is excluded + +The local notes folder is a single-instance object — there is only +one per user — and its existing warning text already names it +unambiguously. Adding the display name on its own line would be +pure redundancy. Skipped. + +## Changes + +### Multiplatform (Kotlin) + +Each dispatcher function changes one argument: the `text =` parameter +passed to `AlertManager.shared.showAlertDialog` / +`showAlertDialogButtonsColumn`. The new value is the chat name + two +newlines + the existing message text: + +```kotlin +text = "${chatInfo.displayName}\n\n${generalGetString(messageId)}", +parseHtml = false, +``` + +`parseHtml = false` is a new boolean parameter added to both alert +helpers. It bypasses `escapedHtmlToAnnotatedString` so the +user-controlled `displayName` is rendered as literal text, never +interpreted as HTML markup (``, ``, `&`, etc.). The default +remains `true`; only our delete-confirmation dispatchers opt out. + +For `leaveGroupDialog` the source is `groupInfo.displayName` (the +function already takes `groupInfo` — no signature change needed, +no caller updates needed). + +For `deleteGroupDialog`, also `groupInfo.displayName`, for consistency +with `leaveGroupDialog` (both have `groupInfo` already in scope). + +For `deleteContactOrConversationDialog`, which has no `text =` +parameter today, add `text = chatInfo.displayName` (no concatenation +needed — the dialog had no body text before). + +### iOS + +Each of the eight call sites changes one argument: the `message:` +parameter passed to `Alert(…)` or `ActionSheet(…)`. The new value +composes the chat name with the existing localized message string: + +```swift +message: Text("\(chat.chatInfo.displayName)\n\n\(existingMessage)"), +``` + +For the three `ActionSheet` sites that have no `message:` today, add +`message: Text(chat.chatInfo.displayName)`. + +## Out of scope + +- Profile picture / avatar in any of these dialogs — excluded by + decision: the open-chat-link alert owns that layout, and reusing + it for destructive confirmations conflates two semantically + different flows. +- The pre-existing wording divergence between Kotlin's + `clear_chat_question` ("Clear chat?") and iOS's "Clear + conversation?". Both platforms keep their existing titles. +- Refactoring the iOS duplication between `ChatListNavLink` and + `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO reuse + this and clearChatAlert with ChatInfoView` at + `GroupChatInfoView.swift:834`). +- "Delete invitation" at `ChatListNavLink.swift:236` — goes through + `deleteChat(chat)` directly with no confirmation dialog. No dialog + to modify. +- Bolding the chat name on its own line. SwiftUI `Text` concatenation + supports `.bold()`; Jetpack Compose `AlertDialog` text is a single + string. Keep both platforms unstyled for parity. + +## Verification + +Per platform, exercise every entry point and confirm the dialog body +reads `` on its own line followed by a blank line followed +by the existing warning: + +- Android & Desktop: + - Chat list swipe — direct contact, group, channel, business chat + → delete / clear / leave actions. (Note folder's clear dialog + is intentionally unchanged.) + - Chat info screens — "Delete contact" / "Delete group" / "Delete + channel" / "Clear conversation" / "Leave …" rows. + - Contact list (`ContactListNavView.kt:148`) — "Delete contact" + action. + - The multi-option contact-delete path: entry dialog (now has a + name body where it had none) → toggle dialog (name above the + warning) → success. +- iOS: + - Same matrix from chat list swipe and chat info screens. + - Action-sheet contact-delete paths show the name as the + `message:` line. + +Edge cases: + +- Long chat name (40+ chars) — alert containers wrap automatically; + body now occupies 3+ lines (name on 2, blank line, warning on 1+). + Confirm via a chat renamed to a long string. +- Special characters in name (emoji, RTL text, double quotes) — + render literally because the substitution is string concatenation, + not format expansion. A contact named `Bob "the builder"` displays + as `Bob "the builder"` on its own line. No quoting/escaping issue. +- Empty `displayName` would render an empty first line above the + warning. In practice `displayName` is non-empty (the `NamedChat` + interface enforces it via `localAlias.ifEmpty { profile.displayName }`); + no defensive trimming added. + +Diff-level checks: + +- `git diff strings.xml` and `git diff '*Localizable.strings'` show + zero hunks. The change is pure code. +- `git diff --stat` shows each platform touched in 2–4 files: + the dispatcher file(s) on Kotlin (`ChatInfoView.kt`, + `GroupChatInfoView.kt`), and the SwiftUI views holding the + alert/sheet builders on iOS. +- Behavior is unchanged. Cancel returns to the prior screen; + confirm performs the same destructive API call as before. diff --git a/scripts/android/build-android-bundle.sh b/scripts/android/build-android-bundle.sh index b784da2aad..972fb0ee72 100755 --- a/scripts/android/build-android-bundle.sh +++ b/scripts/android/build-android-bundle.sh @@ -23,5 +23,8 @@ unzip -o "$tmp/libsimplex.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/s curl -sSf "$libsup" -o "$tmp/libsupport.zip" unzip -o "$tmp/libsupport.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a" -gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean build -cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/release/android-release-unsigned.apk" "$PWD/simplex-chat.apk" +# Build only the arch the libs were downloaded for +sed -i.bak 's/include(.*/include("arm64-v8a")/' "$tmp/simplex-chat/apps/multiplatform/android/build.gradle.kts" + +gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleFossRelease +cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/foss/release/android-foss-arm64-v8a-release-unsigned.apk" "$PWD/simplex-chat.apk" diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index 7edee9c304..267db9f243 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -101,7 +101,7 @@ build() { sed -i.bak 's/${extract_native_libs}/true/' "$folder/apps/multiplatform/android/src/main/AndroidManifest.xml" sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" - sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts" + sed -i.bak '/^tasks {/Q' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak "s/android.version_code=.*/android.version_code=${vercode}/" "$folder/apps/multiplatform/gradle.properties" for arch in $arches; do @@ -119,7 +119,7 @@ build() { arch_map "$arch" android_tmp_folder="${tmp}/android-${arch}" - android_apk_output="${folder}/apps/multiplatform/android/build/outputs/apk/release/android-${android_arch}-release-unsigned.apk" + android_apk_output="${folder}/apps/multiplatform/android/build/outputs/apk/foss/release/android-foss-${android_arch}-release-unsigned.apk" android_apk_output_final="simplex-chat-${android_arch}.apk" libs_folder="${folder}/apps/multiplatform/common/src/commonMain/cpp/android/libs" @@ -134,7 +134,7 @@ build() { # Build only one arch sed -i.bak "s/include(.*/include(\"${android_arch}\")/" "$folder/apps/multiplatform/android/build.gradle.kts" - gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleRelease + gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleFossRelease mkdir -p "$android_tmp_folder" unzip -oqd "$android_tmp_folder" "$android_apk_output" diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index b527720e5b..e6ecc7478d 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,23 @@ + + https://simplex.chat/blog/20260722-simplex-public-names.html + +

New in v7.0.

+

SimpleX public names for channels and businesses (BETA).

+

Better channels:

+
    +
  • Promote subscribers to contributors.
  • +
  • Publish your channel on your website.
  • +
  • Verify security code with contributors.
  • +
  • Wider messages, easier to read.
  • +
  • Host your own chat relays.
  • +
+

Add a longer description to your profile.

+

Simplified app settings.

+
+
https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html diff --git a/scripts/ios/update-pbxproj.sh b/scripts/ios/update-pbxproj.sh new file mode 100755 index 0000000000..fd48cdfa60 --- /dev/null +++ b/scripts/ios/update-pbxproj.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +# Updates libHSsimplex-chat-*.a references in project.pbxproj to match the +# libraries currently in apps/ios/Libraries/ios (populated by prepare.sh). +# Handles both the plain .a and the -ghc*.a variant. + +set -e + +PBXPROJ=./apps/ios/SimpleX.xcodeproj/project.pbxproj +LIB_DIR=./apps/ios/Libraries/ios + +if [ ! -f "$PBXPROJ" ]; then + echo "Error: $PBXPROJ not found. Run from repo root." >&2 + exit 1 +fi +if [ ! -d "$LIB_DIR" ]; then + echo "Error: $LIB_DIR not found. Run prepare.sh first." >&2 + exit 1 +fi + +# New filenames from the prepared Libraries directory. +NEW_PLAIN= +NEW_GHC= +for f in "$LIB_DIR"/libHSsimplex-chat-*.a; do + [ -f "$f" ] || continue + base=$(basename "$f") + case "$base" in + *-ghc*) NEW_GHC=$base ;; + *) NEW_PLAIN=$base ;; + esac +done +if [ -z "$NEW_PLAIN" ] || [ -z "$NEW_GHC" ]; then + echo "Error: expected libHSsimplex-chat-*.a and -ghc*.a in $LIB_DIR." >&2 + echo "Run prepare.sh first." >&2 + exit 1 +fi + +# Current filenames referenced in project.pbxproj. +OLD_PLAIN= +OLD_GHC= +for ref in $(grep -hoE 'libHSsimplex-chat-[^ "/]+\.a' "$PBXPROJ" | sort -u); do + case "$ref" in + *-ghc*) OLD_GHC=$ref ;; + *) OLD_PLAIN=$ref ;; + esac +done +if [ -z "$OLD_PLAIN" ] || [ -z "$OLD_GHC" ]; then + echo "Error: no libHSsimplex-chat references found in $PBXPROJ." >&2 + exit 1 +fi + +if [ "$OLD_PLAIN" = "$NEW_PLAIN" ] && [ "$OLD_GHC" = "$NEW_GHC" ]; then + echo "Already up to date: $NEW_PLAIN" + exit 0 +fi + +# Sanity check before mutating: pbxproj must have exactly 4 lines per variant. +OLD_PLAIN_LINES=$(grep -cF "$OLD_PLAIN" "$PBXPROJ" || true) +OLD_GHC_LINES=$(grep -cF "$OLD_GHC" "$PBXPROJ" || true) +if [ "$OLD_PLAIN_LINES" -ne 4 ] || [ "$OLD_GHC_LINES" -ne 4 ]; then + echo "Error: expected 4 + 4 lines, found $OLD_PLAIN_LINES plain and $OLD_GHC_LINES ghc." >&2 + exit 1 +fi + +echo "Replacing in $PBXPROJ:" +echo " $OLD_PLAIN -> $NEW_PLAIN" +echo " $OLD_GHC -> $NEW_GHC" + +# Escape regex metachar '.' so versions match literally (no other metachars present). +escape_dots() { printf '%s' "$1" | sed 's/\./\\./g'; } +OLD_PLAIN_RE=$(escape_dots "$OLD_PLAIN") +OLD_GHC_RE=$(escape_dots "$OLD_GHC") + +# Put TMP next to PBXPROJ so the final mv is an atomic rename (same filesystem). +TMP=$(mktemp "$PBXPROJ.XXXXXX") +trap 'rm -f "$TMP"' EXIT +# Replace ghc variant first (longer, more specific), then plain. +sed -e "s|$OLD_GHC_RE|$NEW_GHC|g" -e "s|$OLD_PLAIN_RE|$NEW_PLAIN|g" "$PBXPROJ" > "$TMP" +mv "$TMP" "$PBXPROJ" + +# Verify result: exactly 4 plain lines and 4 ghc lines. +NEW_PLAIN_LINES=$(grep -cF "$NEW_PLAIN" "$PBXPROJ" || true) +NEW_GHC_LINES=$(grep -cF "$NEW_GHC" "$PBXPROJ" || true) +if [ "$NEW_PLAIN_LINES" -ne 4 ] || [ "$NEW_GHC_LINES" -ne 4 ]; then + echo "Error: post-replacement: $NEW_PLAIN_LINES plain + $NEW_GHC_LINES ghc (expected 4+4)." >&2 + exit 1 +fi +echo "Updated 8 lines (4 plain + 4 ghc)." diff --git a/scripts/ios/update-version.sh b/scripts/ios/update-version.sh new file mode 100755 index 0000000000..87097f9d27 --- /dev/null +++ b/scripts/ios/update-version.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +# Bumps CURRENT_PROJECT_VERSION (build number) and MARKETING_VERSION in +# apps/ios/SimpleX.xcodeproj/project.pbxproj. Each appears in 10 places. +# +# Usage: ./scripts/ios/update-version.sh +# Example: ./scripts/ios/update-version.sh 333 6.5.3 + +set -e + +if [ $# -ne 2 ]; then + echo "Usage: $0 " >&2 + echo "Example: $0 333 6.5.3" >&2 + exit 1 +fi + +NEW_BUILD=$1 +NEW_MARKETING=$2 + +if ! echo "$NEW_BUILD" | grep -qE '^[0-9]+$'; then + echo "Error: build_number must be a positive integer (got: $NEW_BUILD)." >&2 + exit 1 +fi +if ! echo "$NEW_MARKETING" | grep -qE '^[0-9]+(\.[0-9]+)+$'; then + echo "Error: marketing_version must be like 6.5.3 (got: $NEW_MARKETING)." >&2 + exit 1 +fi + +PBXPROJ=./apps/ios/SimpleX.xcodeproj/project.pbxproj +if [ ! -f "$PBXPROJ" ]; then + echo "Error: $PBXPROJ not found. Run from repo root." >&2 + exit 1 +fi + +# Detect current values; head -1 covers the (unexpected) mixed-values case, +# which the 10-line sanity check below will reject. +OLD_BUILD=$(grep -hoE 'CURRENT_PROJECT_VERSION = [^;]+;' "$PBXPROJ" \ + | sed -E 's/^.*= ([^;]+);$/\1/' | sort -u | head -1) +OLD_MARKETING=$(grep -hoE 'MARKETING_VERSION = [^;]+;' "$PBXPROJ" \ + | sed -E 's/^.*= ([^;]+);$/\1/' | sort -u | head -1) +if [ -z "$OLD_BUILD" ] || [ -z "$OLD_MARKETING" ]; then + echo "Error: CURRENT_PROJECT_VERSION or MARKETING_VERSION not found in $PBXPROJ." >&2 + exit 1 +fi + +if [ "$OLD_BUILD" = "$NEW_BUILD" ] && [ "$OLD_MARKETING" = "$NEW_MARKETING" ]; then + echo "Already up to date: build $NEW_BUILD, version $NEW_MARKETING" + exit 0 +fi + +# Each field must appear in exactly 10 lines with a single uniform value. +OLD_BUILD_LINES=$(grep -cF "CURRENT_PROJECT_VERSION = $OLD_BUILD;" "$PBXPROJ" || true) +OLD_MARKETING_LINES=$(grep -cF "MARKETING_VERSION = $OLD_MARKETING;" "$PBXPROJ" || true) +if [ "$OLD_BUILD_LINES" -ne 10 ] || [ "$OLD_MARKETING_LINES" -ne 10 ]; then + echo "Error: expected 10 + 10 lines, found $OLD_BUILD_LINES CURRENT_PROJECT_VERSION and $OLD_MARKETING_LINES MARKETING_VERSION (mixed values?)." >&2 + exit 1 +fi + +echo "Bumping in $PBXPROJ:" +if [ "$OLD_BUILD" != "$NEW_BUILD" ]; then + echo " CURRENT_PROJECT_VERSION: $OLD_BUILD -> $NEW_BUILD" +fi +if [ "$OLD_MARKETING" != "$NEW_MARKETING" ]; then + echo " MARKETING_VERSION: $OLD_MARKETING -> $NEW_MARKETING" +fi + +# Escape '.' in OLD_MARKETING so version dots match literally. +OLD_MARKETING_RE=$(printf '%s' "$OLD_MARKETING" | sed 's/\./\\./g') + +TMP=$(mktemp "$PBXPROJ.XXXXXX") +trap 'rm -f "$TMP"' EXIT +sed \ + -e "s|CURRENT_PROJECT_VERSION = $OLD_BUILD;|CURRENT_PROJECT_VERSION = $NEW_BUILD;|g" \ + -e "s|MARKETING_VERSION = $OLD_MARKETING_RE;|MARKETING_VERSION = $NEW_MARKETING;|g" \ + "$PBXPROJ" > "$TMP" +mv "$TMP" "$PBXPROJ" + +NEW_BUILD_LINES=$(grep -cF "CURRENT_PROJECT_VERSION = $NEW_BUILD;" "$PBXPROJ" || true) +NEW_MARKETING_LINES=$(grep -cF "MARKETING_VERSION = $NEW_MARKETING;" "$PBXPROJ" || true) +if [ "$NEW_BUILD_LINES" -ne 10 ] || [ "$NEW_MARKETING_LINES" -ne 10 ]; then + echo "Error: post-replacement: $NEW_BUILD_LINES CURRENT_PROJECT_VERSION + $NEW_MARKETING_LINES MARKETING_VERSION (expected 10+10)." >&2 + exit 1 +fi +echo "Updated 20 lines (10 + 10)." diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8519d08c3c..5d70ed7b94 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."92598c2ddb06cfc2c19797a2c900cffcb8af4d5c" = "0x98w50vcb2qwdxgkgfvygn1vzclb12zg1k6kcs9rrw54rhmvfmp"; + "https://github.com/simplex-chat/simplexmq.git"."efaad8e73436d60f5052f07dda6b71151ad5039b" = "1jczm6baqz34sn13jgp5srqjk3gnx90brsfrsqw29al769ssrvkv"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/scripts/simplex-chat-reproduce-builds-android.sh b/scripts/simplex-chat-reproduce-builds-android.sh index 20c2fdf116..4bd7262d17 100755 --- a/scripts/simplex-chat-reproduce-builds-android.sh +++ b/scripts/simplex-chat-reproduce-builds-android.sh @@ -118,9 +118,14 @@ check_apk() { verify_apk() { apk_name="$1" + # Release APKs are packaged by AGP (gradle :android:assembleFossRelease; AGP version is + # gradle.plugin.version in apps/multiplatform/gradle.properties), which zero-pads ZIP + # alignment. Do NOT add --pad-like-apksigner (standalone apksigner >= 35.0.0-rc1 uses + # the 0xd935 extra-field padding) unless AGP is bumped to a packager that uses it — + # otherwise apksigcopier aborts with "APK Signing Block offset < central directory offset". # https://github.com/obfusk/apksigcopier?tab=readme-ov-file#what-about-signatures-made-by-apksigner-from-build-tools--3500-rc1 - docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --pad-like-apksigner --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ - "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" + docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" docker exec "${CONTAINER_NAME}" mv "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" \ "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 74a96c1371..bf4dc7e10b 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.5.6.1 +version: 7.0.0.11 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -40,6 +40,7 @@ library Simplex.Chat.AppSettings Simplex.Chat.Badges Simplex.Chat.Badges.CLI + Simplex.Chat.Names Simplex.Chat.Call Simplex.Chat.Controller Simplex.Chat.Delivery @@ -93,6 +94,7 @@ library Simplex.Chat.Types.Shared Simplex.Chat.Types.UITheme Simplex.Chat.Util + Simplex.Chat.Web if !flag(client_library) exposed-modules: Simplex.Chat.Bot @@ -138,6 +140,18 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index Simplex.Chat.Store.Postgres.Migrations.M20260515_public_group_access Simplex.Chat.Store.Postgres.Migrations.M20260516_supporter_badges + Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders + Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services + Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at + Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain + Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster + Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name + Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup + Simplex.Chat.Store.Postgres.Migrations.M20260707_file_digest + Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code + Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description + Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history + Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles else exposed-modules: Simplex.Chat.Archive @@ -295,13 +309,25 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index Simplex.Chat.Store.SQLite.Migrations.M20260515_public_group_access Simplex.Chat.Store.SQLite.Migrations.M20260516_supporter_badges + Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders + Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services + Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at + Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain + Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster + Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name + Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup + Simplex.Chat.Store.SQLite.Migrations.M20260707_file_digest + Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code + Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description + Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history + Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles other-modules: Paths_simplex_chat hs-source-dirs: src default-extensions: StrictData - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -378,7 +404,7 @@ executable simplex-bot apps/simplex-bot default-extensions: StrictData - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded build-depends: base >=4.7 && <5 , directory ==1.3.* @@ -397,7 +423,7 @@ executable simplex-bot-advanced apps/simplex-bot-advanced default-extensions: StrictData - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded build-depends: async ==2.2.* , base >=4.7 && <5 @@ -428,7 +454,7 @@ executable simplex-broadcast-bot Broadcast.Bot Broadcast.Options Paths_simplex_chat - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded build-depends: async ==2.2.* , base >=4.7 && <5 @@ -458,7 +484,7 @@ executable simplex-chat apps/simplex-chat default-extensions: StrictData - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded build-depends: aeson ==2.2.* , base >=4.7 && <5 @@ -501,7 +527,7 @@ executable simplex-directory-service Directory.Store.Migrate Directory.Util Paths_simplex_chat - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded -rtsopts build-depends: aeson ==2.2.* , async ==2.2.* @@ -568,12 +594,14 @@ test-suite simplex-chat-test ChatTests.Forward ChatTests.Groups ChatTests.Local + ChatTests.Names ChatTests.Profiles ChatTests.Utils JSONFixtures JSONTests MarkdownTests MemberRelationsTests + NameResolver MessageBatching OperatorTests ProtocolTests @@ -622,7 +650,7 @@ test-suite simplex-chat-test default-extensions: StrictData -- add -fhpc to ghc-options below to run tests with coverage - ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -Werror=name-shadowing -threaded build-depends: QuickCheck ==2.14.* , aeson ==2.2.* @@ -654,6 +682,8 @@ test-suite simplex-chat-test , unicode-transforms ==0.4.* , unliftio ==0.2.* , uri-bytestring >=0.3.3.1 && <0.4 + , wai ==3.2.* + , warp ==3.3.* default-language: Haskell2010 if flag(client_postgres) other-modules: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 5adaaca150..b795ba9b9c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -128,6 +128,7 @@ defaultChatConfig = highlyAvailable = False, deliveryWorkerDelay = 0, deliveryBucketSize = 10000, + webPreviewConfig = Nothing, channelSubscriberRole = GRObserver, relayChecksInterval = 15 * 60, -- 15 minutes relayInactiveTTL = nominalDay, @@ -147,18 +148,16 @@ createChatDatabase chatDbOpts migrationConfig = runExceptT $ do agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False []) migrationConfig pure ChatDatabase {chatStore, agentStore} -newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController +newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO (Either AgentErrorType ChatController) newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, webPreviewConfig, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} - firstTime = dbNew chatStore - currentUser <- newTVarIO user + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, webPreviewConfig, highlyAvailable, confirmMigrations = confirmMigrations'} randomPresetServers <- chooseRandomServers presetServers' let rndSrvs = L.toList randomPresetServers operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op @@ -166,90 +165,95 @@ newChatController agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP} - currentRemoteHost <- newTVarIO Nothing servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers - smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode - agentAsync <- newTVarIO Nothing - random <- liftIO C.newRandom - eventSeq <- newTVarIO 0 - inputQ <- newTBQueueIO tbqSize - outputQ <- newTBQueueIO tbqSize - subscriptionMode <- newTVarIO SMSubscribe - chatLock <- newEmptyTMVarIO - entityLocks <- TM.emptyIO - sndFiles <- newTVarIO M.empty - rcvFiles <- newTVarIO M.empty - currentCalls <- TM.emptyIO - localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName - multicastSubscribers <- newTMVarIO 0 - remoteSessionSeq <- newTVarIO 0 - remoteHostSessions <- TM.emptyIO - remoteHostsFolder <- newTVarIO Nothing - remoteCtrlSession <- newTVarIO Nothing - filesFolder <- newTVarIO optFilesFolder - chatStoreChanged <- newTVarIO False - deliveryTaskWorkers <- TM.emptyIO - deliveryJobWorkers <- TM.emptyIO - relayRequestWorkers <- TM.emptyIO - chatRelayTests <- TM.emptyIO - expireCIThreads <- TM.emptyIO - expireCIFlags <- TM.emptyIO - cleanupManagerAsync <- newTVarIO Nothing - relayGroupLinkChecksAsync <- newTVarIO Nothing - timedItemThreads <- TM.emptyIO - chatActivated <- newTVarIO True - showLiveItems <- newTVarIO False - encryptLocalFiles <- newTVarIO False - tempDirectory <- newTVarIO optTempDirectory - assetsDirectory <- newTVarIO Nothing - contactMergeEnabled <- newTVarIO True - pure - ChatController - { firstTime, - currentUser, - randomPresetServers, - randomAgentServers, - currentRemoteHost, - smpAgent, - agentAsync, - chatStore, - chatStoreChanged, - random, - eventSeq, - inputQ, - outputQ, - subscriptionMode, - chatLock, - entityLocks, - sndFiles, - rcvFiles, - currentCalls, - localDeviceName, - multicastSubscribers, - remoteSessionSeq, - remoteHostSessions, - remoteHostsFolder, - remoteCtrlSession, - config, - filesFolder, - deliveryTaskWorkers, - deliveryJobWorkers, - relayRequestWorkers, - chatRelayTests, - expireCIThreads, - expireCIFlags, - cleanupManagerAsync, - relayGroupLinkChecksAsync, - timedItemThreads, - chatActivated, - showLiveItems, - encryptLocalFiles, - tempDirectory, - assetsDirectory, - logFilePath = logFile, - contactMergeEnabled - } + runExceptT (getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode) + >>= mapM (mkChatController config randomPresetServers randomAgentServers) where + mkChatController config randomPresetServers randomAgentServers smpAgent = do + currentUser <- newTVarIO user + currentRemoteHost <- newTVarIO Nothing + agentAsync <- newTVarIO Nothing + random <- liftIO C.newRandom + eventSeq <- newTVarIO 0 + inputQ <- newTBQueueIO tbqSize + outputQ <- newTBQueueIO tbqSize + subscriptionMode <- newTVarIO SMSubscribe + chatLock <- newEmptyTMVarIO + entityLocks <- TM.emptyIO + sndFiles <- newTVarIO M.empty + rcvFiles <- newTVarIO M.empty + currentCalls <- TM.emptyIO + localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName + multicastSubscribers <- newTMVarIO 0 + remoteSessionSeq <- newTVarIO 0 + remoteHostSessions <- TM.emptyIO + remoteHostsFolder <- newTVarIO Nothing + remoteCtrlSession <- newTVarIO Nothing + filesFolder <- newTVarIO optFilesFolder + chatStoreChanged <- newTVarIO False + deliveryTaskWorkers <- TM.emptyIO + deliveryJobWorkers <- TM.emptyIO + relayRequestWorkers <- TM.emptyIO + relayGroupLinkChecksAsync <- newTVarIO Nothing + webPreviewState <- forM webPreviewConfig $ \_ -> newWebPreviewState + chatRelayTests <- TM.emptyIO + expireCIThreads <- TM.emptyIO + expireCIFlags <- TM.emptyIO + cleanupManagerAsync <- newTVarIO Nothing + timedItemThreads <- TM.emptyIO + chatActivated <- newTVarIO True + showLiveItems <- newTVarIO False + encryptLocalFiles <- newTVarIO False + tempDirectory <- newTVarIO optTempDirectory + assetsDirectory <- newTVarIO Nothing + contactMergeEnabled <- newTVarIO True + pure + ChatController + { firstTime = dbNew chatStore, + currentUser, + randomPresetServers, + randomAgentServers, + currentRemoteHost, + smpAgent, + agentAsync, + chatStore, + chatStoreChanged, + random, + eventSeq, + inputQ, + outputQ, + subscriptionMode, + chatLock, + entityLocks, + sndFiles, + rcvFiles, + currentCalls, + localDeviceName, + multicastSubscribers, + remoteSessionSeq, + remoteHostSessions, + remoteHostsFolder, + remoteCtrlSession, + config, + filesFolder, + deliveryTaskWorkers, + deliveryJobWorkers, + relayRequestWorkers, + relayGroupLinkChecksAsync, + webPreviewState, + chatRelayTests, + expireCIThreads, + expireCIFlags, + cleanupManagerAsync, + timedItemThreads, + chatActivated, + showLiveItems, + encryptLocalFiles, + tempDirectory, + assetsDirectory, + logFilePath = logFile, + contactMergeEnabled + } presetServers' :: PresetServers presetServers' = presetServers {operators = operators', netCfg = netCfg'} where @@ -283,7 +287,8 @@ newChatController ops <- getUpdateServerOperators db presetOps (null users) let opDomains = operatorDomains $ mapMaybe snd ops (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users - pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, presetDomains, presetServers = L.toList allPresetServers} + let useServices = M.fromList $ map (\User {agentUserId = AgentUserId uId, clientService} -> (uId, isTrue clientService)) users + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, useServices, presetDomains, presetServers = L.toList allPresetServers} where optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] optServers srvs overrides_ = case L.nonEmpty overrides_ of diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index e861d27f11..6e35517ed3 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -1,6 +1,7 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} @@ -27,8 +28,8 @@ module Simplex.Chat.Badges maxXFTPFileSize, maxFileSizeSupporter, maxFileSizeLegend, - BadgePresHeaderTag (..), - BadgePresHeader (..), + ProofPresHeaderTag (..), + ProofPresHeader (..), BadgePurchase (..), BadgeMasterKey (..), BadgeRequest (..), @@ -197,9 +198,9 @@ maxXFTPFileSize = \case -- presentation, not bound to any context; the 'T' tag marks it so master rejects it. -- PHUnknown is the forward-compat catch-all for tags this version does not interpret. -data BadgePresHeaderTag = PHTestTag | PHUnknownTag Char +data ProofPresHeaderTag = PHTestTag | PHUnknownTag Char -instance StrEncoding BadgePresHeaderTag where +instance StrEncoding ProofPresHeaderTag where strEncode = B.singleton . \case PHTestTag -> 'T' PHUnknownTag c -> c @@ -209,11 +210,13 @@ instance StrEncoding BadgePresHeaderTag where 'T' -> PHTestTag c -> PHUnknownTag c -data BadgePresHeader +data ProofPresHeader = PHTest ByteString | PHUnknown Char ByteString + deriving (Eq, Show) + deriving (ToJSON, FromJSON) via (StrJSON "ProofPresHeader" ProofPresHeader) -instance StrEncoding BadgePresHeader where +instance StrEncoding ProofPresHeader where strEncode = \case PHTest nonce -> strEncode PHTestTag <> nonce PHUnknown c b -> strEncode (PHUnknownTag c) <> b @@ -223,8 +226,8 @@ instance StrEncoding BadgePresHeader where PHUnknownTag c -> PHUnknown c <$> A.takeByteString -- v6.5.x accepts both; v7 will reject PHTest/PHUnknown -badgePresHeaderAccepted :: BadgePresHeader -> Bool -badgePresHeaderAccepted = \case +proofPresHeaderAccepted :: ProofPresHeader -> Bool +proofPresHeaderAccepted = \case PHTest _ -> True PHUnknown _ _ -> True @@ -311,7 +314,7 @@ generateBadgeProof pk (BadgeCredential keyIdx masterKey signature badgeInfo) ph fmap (\p -> BadgeProof keyIdx ph p badgeInfo) <$> bbsProofGen pk signature bbsBadgeHeader ph bbsBadgeDisclosedIndexes (badgeMessages masterKey badgeInfo) -- application-level proof generation with a semantic presentation header -badgeProof :: BBSPublicKey -> BadgeCredential -> BadgePresHeader -> IO (Either String BadgeProof) +badgeProof :: BBSPublicKey -> BadgeCredential -> ProofPresHeader -> IO (Either String BadgeProof) badgeProof pk cred ph = generateBadgeProof pk cred (BBSPresHeader $ strEncode ph) -- Recipient-side: verify a badge proof with the configured key its index points to. @@ -324,7 +327,7 @@ verifyBadge keys b@(BadgeProof keyIdx _ _ _) = case M.lookup keyIdx keys of verifyBadgeWith :: BBSPublicKey -> BadgeProof -> IO Bool verifyBadgeWith pk (BadgeProof _ ph@(BBSPresHeader phBytes) proof badgeInfo) - | either (const False) badgePresHeaderAccepted (strDecode phBytes) = + | either (const False) proofPresHeaderAccepted (strDecode phBytes) = bbsProofVerify pk proof bbsBadgeHeader ph bbsBadgeDisclosedIndexes bbsBadgeMessageCount (badgeInfoMessages badgeInfo) | otherwise = pure False diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 3aca687ec5..7284b72d62 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -86,14 +86,14 @@ sendComposedMessages cc sendRef = sendComposedMessages_ cc sendRef . L.map (Noth sendComposedMessages_ :: ChatController -> SendRef -> NonEmpty (Maybe ChatItemId, MsgContent) -> IO () sendComposedMessages_ cc sendRef qmcs = do let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs - sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case + sendChatCmd cc (APISendMessages sendRef False Nothing False cms) >>= \case Right (CRNewChatItems {}) -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef r -> putStrLn $ "unexpected send message response: " <> show r sendComposedMessageFile :: ChatController -> SendRef -> Maybe ChatItemId -> MsgContent -> CryptoFile -> IO () sendComposedMessageFile cc sendRef qiId mc file = do let cm = ComposedMessage {fileSource = Just file, quotedItemId = qiId, msgContent = mc, mentions = M.empty} - sendChatCmd cc (APISendMessages sendRef False Nothing (cm :| [])) >>= \case + sendChatCmd cc (APISendMessages sendRef False Nothing False (cm :| [])) >>= \case Right (CRNewChatItems {}) -> printLog cc CLLInfo $ "sent file message to " <> show sendRef r -> putStrLn $ "unexpected send message response: " <> show r diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index fbb8536fcf..6a80f51a1e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -39,6 +39,7 @@ import Data.Char (ord) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) +import Data.Set (Set) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) import Data.String @@ -89,7 +90,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SMPServerWithAuth, SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, TransportPeer (..), simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -162,6 +163,7 @@ data ChatConfig = ChatConfig ciExpirationInterval :: Int64, -- microseconds deliveryWorkerDelay :: Int64, -- microseconds deliveryBucketSize :: Int, + webPreviewConfig :: Maybe WebPreviewConfig, channelSubscriberRole :: GroupMemberRole, -- TODO [relays] starting role should be communicated in protocol from owner to relays relayChecksInterval :: NominalDiffTime, relayInactiveTTL :: NominalDiffTime, @@ -173,6 +175,43 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } +data WebPreviewConfig = WebPreviewConfig + { webDomain :: Text, + webJsonDir :: FilePath, + webCorsFile :: Maybe FilePath, + webUpdateInterval :: Int, -- seconds + webPreviewItemCount :: Int + } + +data PublishableGroup = PublishableGroup + { pgFileName :: FilePath, + pgCorsEntry :: Maybe (Text, CorsOrigin) + } + +data CorsOrigin = CorsAny | CorsOrigins [Text] + deriving (Show) + +data WebPreviewState = WebPreviewState + { publishableGroupIds :: TVar (Map Int64 PublishableGroup), + priorityRender :: TQueue Int64, + filesToRemove :: TQueue FilePath, + corsNeeded :: TVar Bool, + routinePending :: TVar (Set Int64), + wakeSignal :: TMVar (), + webPreviewWorkerAsync :: TVar (Maybe (Async ())) + } + +newWebPreviewState :: IO WebPreviewState +newWebPreviewState = do + publishableGroupIds <- newTVarIO mempty + priorityRender <- newTQueueIO + filesToRemove <- newTQueueIO + corsNeeded <- newTVarIO False + routinePending <- newTVarIO mempty + wakeSignal <- newEmptyTMVarIO + webPreviewWorkerAsync <- newTVarIO Nothing + pure WebPreviewState {publishableGroupIds, priorityRender, filesToRemove, corsNeeded, routinePending, wakeSignal, webPreviewWorkerAsync} + -- | Builds the read-only context threaded through store functions from chat config. -- The single construction point, so new store-wide config (e.g. server keys) is added in one place. mkStoreCxt :: ChatConfig -> StoreCxt @@ -265,11 +304,12 @@ data ChatController = ChatController deliveryTaskWorkers :: TMap DeliveryWorkerKey Worker, deliveryJobWorkers :: TMap DeliveryWorkerKey Worker, relayRequestWorkers :: TMap Int Worker, -- single global worker with key 1 is used to fit into existing worker management framework + relayGroupLinkChecksAsync :: TVar (Maybe (Async ())), + webPreviewState :: Maybe WebPreviewState, chatRelayTests :: TMap ConnId RelayTest, expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), - relayGroupLinkChecksAsync :: TVar (Maybe (Async ())), chatActivated :: TVar Bool, timedItemThreads :: TMap (ChatRef, ChatItemId) (TVar (Maybe (Weak ThreadId))), showLiveItems :: TVar Bool, @@ -304,6 +344,7 @@ data ChatCommand | UnhideUser UserPwd | MuteUser | UnmuteUser + | SetClientService UserId ContactName Bool | APIDeleteUser {userId :: UserId, delSMPQueues :: Bool, viewPwd :: Maybe UserPwd} | DeleteUser UserName Bool (Maybe UserPwd) | StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True @@ -338,7 +379,7 @@ data ChatCommand | APIGetChatContentTypes ChatRef | APIGetChatItems {chatPagination :: ChatPagination, search :: Maybe Text} | APIGetChatItemInfo {chatRef :: ChatRef, chatItemId :: ChatItemId} - | APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessages :: NonEmpty ComposedMessage} + | APISendMessages {sendRef :: SendRef, liveMessage :: Bool, ttl :: Maybe Int, signMessages :: Bool, composedMessages :: NonEmpty ComposedMessage} | APICreateChatTag ChatTagData | APISetChatTags ChatRef (Maybe (NonEmpty ChatTagId)) | APIDeleteChatTag ChatTagId @@ -357,6 +398,7 @@ data ChatCommand | APIPlanForwardChatItems {fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId} | APIForwardChatItems {toChatRef :: ChatRef, sendAsGroup :: ShowGroupAsSender, fromChatRef :: ChatRef, chatItemIds :: NonEmpty ChatItemId, ttl :: Maybe Int} | APIShareChatMsgContent {shareChatRef :: ChatRef, toSendRef :: SendRef} + | APIShareMyAddress {toSendRef :: SendRef} | APIUserRead UserId | UserRead | APIChatRead {chatRef :: ChatRef} @@ -376,6 +418,7 @@ data ChatCommand | APIGetCallInvitations | APICallStatus ContactId WebRTCCallStatus | APIUpdateProfile {userId :: UserId, profile :: Profile} + | APISetUserDomain {userId :: UserId, simplexDomain :: Maybe SimplexDomain} | APISetContactPrefs {contactId :: ContactId, preferences :: Preferences} | APISetContactAlias {contactId :: ContactId, localAlias :: LocalAlias} | APISetGroupAlias {groupId :: GroupId, localAlias :: LocalAlias} @@ -401,6 +444,7 @@ data ChatCommand | APILeaveGroup {groupId :: GroupId} | APIListMembers {groupId :: GroupId} | APIUpdateGroupProfile {groupId :: GroupId, groupProfile :: GroupProfile} + | APISetPublicGroupAccess GroupId PublicGroupAccess | APICreateGroupLink {groupId :: GroupId, memberRole :: GroupMemberRole} | APIGroupLinkMemberRole {groupId :: GroupId, memberRole :: GroupMemberRole} | APIDeleteGroupLink {groupId :: GroupId} @@ -486,22 +530,24 @@ data ChatCommand | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to - | APIConnectPlan {userId :: UserId, connectionLink :: Maybe AConnectionLink, resolveKnown :: Bool, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectionLink is used to report link parsing failure as special error - | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData - | APIPrepareGroup UserId CreatedLinkContact DirectLink GroupShortLinkData + | APIConnectPlan {userId :: UserId, connectTarget :: Maybe AConnectTarget, resolveMode :: PlanResolveMode, linkOwnerSig :: Maybe LinkOwnerSig} -- Maybe AConnectTarget is used to report parsing failure as special error + | APIPrepareContact UserId ACreatedConnLink (Maybe SimplexDomain) ContactShortLinkData + | APIPrepareGroup UserId CreatedLinkContact DirectLink (Maybe SimplexDomain) GroupShortLinkData | APIChangePreparedContactUser ContactId UserId | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} | APIConnectPreparedGroup {groupId :: GroupId, incognito :: IncognitoEnabled, ownerContact :: Maybe GroupOwnerContact, msgContent_ :: Maybe MsgContent} | APIConnect {userId :: UserId, incognito :: IncognitoEnabled, preparedLink_ :: Maybe ACreatedConnLink} -- Maybe is used to report link parsing failure as special error - | Connect {incognito :: IncognitoEnabled, connLink_ :: Maybe AConnectionLink} + | Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe AConnectTarget} + | APIVerifyContactDomain {contactId :: ContactId} + | APIVerifyGroupDomain {groupId :: GroupId} | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName ChatDeleteMode | ClearContact ContactName | APIListContacts {userId :: UserId} | ListContacts - | APICreateMyAddress {userId :: UserId} + | APICreateMyAddress {userId :: UserId, server_ :: Maybe SMPServerWithAuth} | CreateMyAddress | APIDeleteMyAddress {userId :: UserId} | DeleteMyAddress @@ -518,6 +564,7 @@ data ChatCommand | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} | SharePublicGroup {shareGroupName :: GroupName, toChatName :: ChatName} + | ShareMyAddress {toChatName :: ChatName} | SendMessage SendName Text | SendMemberContactMessage GroupName ContactName Text | AcceptMemberContact ContactName @@ -554,6 +601,7 @@ data ChatCommand | ShowGroupProfile GroupName | UpdateGroupDescription GroupName (Maybe Text) | ShowGroupDescription GroupName + | SetPublicGroupAccess GroupName PublicGroupAccess | CreateGroupLink GroupName GroupMemberRole | GroupLinkMemberRole GroupName GroupMemberRole | DeleteGroupLink GroupName @@ -579,6 +627,7 @@ data ChatCommand | SetBotCommands [ChatBotCommand] | UpdateProfile ContactName (Maybe Text) -- UserId (not used in UI) | UpdateProfileImage (Maybe ImageData) -- UserId (not used in UI) + | UpdateProfileImageFromFile FilePath -- set profile image from a .png/.jpg/.jpeg file | AddBadge BadgeCredential -- attach an issued badge credential (testing; credential from `simplex-chat badge sign`) | ShowProfileImage | SetUserFeature AChatFeature FeatureAllowed -- UserId (not used in UI) @@ -624,6 +673,22 @@ data ChatCommand CustomChatCommand ByteString deriving (Show) +data PlanResolveMode + = PRMAllGroups -- resolve all known groups and all unknown chats + | PRMUnknown -- only resolve if chat is unknown (default) + | PRMNever -- do not resolve links and names, only do local search + deriving (Eq, Show) + +planResolveModeP :: A.Parser PlanResolveMode +planResolveModeP = + A.takeTill (== ' ') >>= \case + "allGroups" -> pure PRMAllGroups + "on" -> pure PRMAllGroups + "unknown" -> pure PRMUnknown + "off" -> pure PRMUnknown + "never" -> pure PRMNever + _ -> fail "bad PlanResolveMode" + allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal allowRemoteCommand = \case StartChat {} -> False @@ -732,6 +797,8 @@ data ChatResponse | CRContactCode {user :: User, contact :: Contact, connectionCode :: Text} | CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text} | CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text} + | CRContactDomainVerified {user :: User, contact :: Contact, verificationFailure :: Maybe Text} + | CRGroupDomainVerified {user :: User, groupInfo :: GroupInfo, verificationFailure :: Maybe Text} | CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]} | CRNewChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemUpdated {user :: User, chatItem :: AChatItem} @@ -772,7 +839,7 @@ data ChatResponse | CRInvitation {user :: User, connLinkInvitation :: CreatedLinkInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} - | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan} + | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, planSimplexName :: Maybe SimplexNameInfo, otherSimplexName :: Maybe SimplexNameInfo, connectionPlan :: ConnectionPlan} | CRNewPreparedChat {user :: User, chat :: AChat} | CRContactUserChanged {user :: User, fromContact :: Contact, newUser :: User, toContact :: Contact} | CRGroupUserChanged {user :: User, fromGroup :: GroupInfo, newUser :: User, toGroup :: GroupInfo} @@ -906,6 +973,7 @@ data ChatEvent | CEvtConnectionsDiff {userIds :: DatabaseDiff AgentUserId, connIds :: DatabaseDiff AgentConnId} | CEvtSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} | CEvtSubscriptionStatus {server :: SMPServer, subscriptionStatus :: SubscriptionStatus, connections :: [AgentConnId]} + | CEvtServiceSubStatus {server :: SMPServer, serviceSubEvent :: ServiceSubEvent} | CEvtHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CEvtHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CEvtReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} @@ -1060,7 +1128,7 @@ data GroupLinkPlan | GLPOwnLink {groupInfo :: GroupInfo} | GLPConnectingConfirmReconnect | GLPConnectingProhibit {groupInfo_ :: Maybe GroupInfo} - | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: BoolDef, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} + | GLPKnown {groupInfo :: GroupInfo, groupUpdated :: Bool, ownerVerification :: Maybe OwnerVerification, linkOwners :: ListDef GroupLinkOwner} | GLPNoRelays {groupSLinkData_ :: Maybe GroupShortLinkData} | GLPUpdateRequired {groupSLinkData_ :: Maybe GroupShortLinkData} deriving (Show) @@ -1322,6 +1390,13 @@ data ChatItemDeletion = ChatItemDeletion } deriving (Show) +data ServiceSubEvent + = ServiceSubUp {serviceError :: Maybe Text, queueCount :: Int64} + | ServiceSubDown {queueCount :: Int64} + | ServiceSubAll + | ServiceSubEnd {queueCount :: Int64} + deriving (Show) + data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant deriving (Eq, Ord, Show) @@ -1349,13 +1424,18 @@ data ChatError | ChatErrorRemoteHost {rhKey :: RHKey, remoteHostError :: RemoteHostError} deriving (Show, Exception) +-- why a resolved SimpleX name could not be used (the name itself resolved; an unregistered name is the agent's NAME NOT_FOUND) +data SimplexDomainError + = SDENoValidLink -- the name's record has no usable contact/channel link + | SDEUnknownDomain -- the resolved link's profile has no name, or a different name + deriving (Eq, Show) + data ChatErrorType = CENoActiveUser | CENoConnectionUser {agentConnId :: AgentConnId} | CENoSndFileUser {agentSndFileId :: AgentSndFileId} | CENoRcvFileUser {agentRcvFileId :: AgentRcvFileId} | CEUserUnknown - | CEActiveUserExists -- TODO delete | CEUserExists {contactName :: ContactName} | CEChatRelayExists | CEDifferentActiveUser {commandUserId :: UserId, activeUserId :: UserId} @@ -1371,6 +1451,8 @@ data ChatErrorType | CEChatNotStopped | CEChatStoreChanged | CEInvalidConnReq + | CESimplexDomainNotReady {simplexDomain :: SimplexDomain, simplexDomainError :: SimplexDomainError} + | CENotResolvedLocally -- a name or link is not a known chat in the local store and online resolution is off (PRMNever) | CEUnsupportedConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEConnReqMessageProhibited @@ -1445,6 +1527,9 @@ data SQLiteError = SQLiteErrorNotADatabase | SQLiteError {dbError :: String} throwDBError :: DatabaseError -> CM () throwDBError = throwError . ChatErrorDatabase +chatErrorAgent :: AgentErrorType -> ChatError +chatErrorAgent e = ChatErrorAgent e (AgentConnId B.empty) Nothing + -- TODO review errors, some of it can be covered by HTTP2 errors data RemoteHostError = RHEMissing -- No remote session matches this identifier @@ -1676,7 +1761,7 @@ withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a withAgent action = asks smpAgent >>= liftIO . runExceptT . action - >>= liftEither . first (\e -> ChatErrorAgent e (AgentConnId "") Nothing) + >>= liftEither . first chatErrorAgent withAgent' :: (AgentClient -> IO a) -> CM' a withAgent' action = asks smpAgent >>= liftIO . action @@ -1699,6 +1784,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SDE") ''SimplexDomainError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHE") ''RemoteHostError) @@ -1741,6 +1828,8 @@ $(JQ.deriveJSON defaultJSON ''ParsedServerAddress) $(JQ.deriveJSON defaultJSON ''ChatItemDeletion) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "ServiceSub") ''ServiceSubEvent) + $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) #if !defined(dbPostgres) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index e51f7a40e8..c8cf201421 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -19,6 +19,7 @@ import Control.Monad.Except import Control.Monad.Reader import qualified Data.ByteString.Char8 as B import Data.List (find) +import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (getCurrentTime) @@ -43,7 +44,7 @@ import Text.Read (readMaybe) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath, maintenance}, createBot} chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {dbOptions, logAgent, yesToUpMigrations, migrationBackupPath, maintenance}, createBot, userDisplayName, userImageFile} chat = case logAgent of Just level -> do setLogLevel level @@ -59,21 +60,38 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha users <- withTransaction chatStore getUsers u_ <- selectActiveUser coreOptions chatStore users let backgroundMode = maintenance - cc <- newChatController db u_ cfg opts backgroundMode - forM_ (preStartHook chatHooks) ($ cc) - u <- maybe (noMaintenance >> createActiveUser cc coreOptions createBot) pure u_ - unless testView $ putStrLn $ "Current user: " <> userStr u - runSimplexChat cfg opts u cc chat + newChatController db u_ cfg opts backgroundMode >>= \case + Left e -> do + putStrLn $ "Error starting chat: " <> show e + exitFailure + Right cc -> do + forM_ (preStartHook chatHooks) ($ cc) + u <- case u_ of + Nothing -> do + noMaintenance + img_ <- mapM loadImageFile userImageFile + createActiveUser cc coreOptions createBot userDisplayName img_ + Just u@User {localDisplayName} -> do + forM_ userDisplayName $ \name -> + when (localDisplayName /= name) $ do + putStrLn $ "Active user display name " <> show localDisplayName <> " does not match --user-display-name " <> show name + exitFailure + -- --user-image-file only applies when the profile is created; ignore it for an existing user + forM_ userImageFile $ \_ -> + putStrLn "Note: --user-image-file is ignored for an existing user (it only sets the image on profile creation); use \"/set profile image file \" to change it" + pure u + unless testView $ putStrLn $ "Current user: " <> userStr u + runSimplexChat cfg opts u cc chat noMaintenance = when maintenance $ do putStrLn "exiting: no active user in maintenance mode" exitFailure runSimplexChat :: ChatConfig -> ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO () -runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatRelay, maintenance}} u cc@ChatController {config = ChatConfig {chatHooks}} chat +runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatRelay, chatRelayServer, headless, maintenance}} u cc@ChatController {config = ChatConfig {chatHooks}} chat | maintenance = wait =<< async (chat u cc) | otherwise = do a1 <- runReaderT (startChatController True True) cc - when (chatRelay && not testView) $ askCreateRelayAddress cc u + when (chatRelay && not testView) $ askCreateRelayAddress cc u chatRelayServer headless forM_ (postStartHook chatHooks) ($ cc) a2 <- async $ chat u cc waitEither_ a1 a2 @@ -116,36 +134,38 @@ selectActiveUser CoreChatOpts {chatRelay} st users let user = users !! (n - 1) in Just <$> withTransaction st (`setActiveUser` user) -createActiveUser :: ChatController -> CoreChatOpts -> Maybe CreateBotOpts -> IO User -createActiveUser cc CoreChatOpts {chatRelay} = \case - Just CreateBotOpts {botDisplayName, allowFiles} -> do +createActiveUser :: ChatController -> CoreChatOpts -> Maybe CreateBotOpts -> Maybe Text -> Maybe ImageData -> IO User +createActiveUser cc CoreChatOpts {chatRelay, headless} createBot_ userDisplayName_ img_ = case createBot_ of + Just CreateBotOpts {botDisplayName, allowFiles, clientService} -> do let preferences = if allowFiles then Nothing else Just emptyChatPrefs {files = Just FilesPreference {allow = FANo}} - createUser exitFailure $ (mkProfile botDisplayName) {peerType = Just CPTBot, preferences} - Nothing - | chatRelay -> do - putStrLn - "No chat relay user profile found, it will be created now.\n\ - \Please choose chat relay display name." - loop - | otherwise -> do - putStrLn - "No user profiles found, it will be created now.\n\ - \Please choose your display name.\n\ - \It will be sent to your contacts when you connect.\n\ - \It is only stored on your device and you can change it later." - loop + createUser exitFailure clientService $ (mkProfile botDisplayName) {peerType = Just CPTBot, preferences} + Nothing -> case userDisplayName_ of + Just displayName -> createUser exitFailure False $ (mkProfile displayName :: Profile) {image = img_} + Nothing + | headless -> putStrLn "No user profile found and no --user-display-name provided (required with --headless)" >> exitFailure + | otherwise -> putStrLn prompt >> loop + where + prompt + | chatRelay = + "No chat relay user profile found, it will be created now.\n\ + \Please choose chat relay display name." + | otherwise = + "No user profiles found, it will be created now.\n\ + \Please choose your display name.\n\ + \It will be sent to your contacts when you connect.\n\ + \It is only stored on your device and you can change it later." where loop = do displayName <- T.pack <$> withPrompt "display name: " getLine - createUser loop $ mkProfile displayName - mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} - createUser onError p = - execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = chatRelay}) 0 `runReaderT` cc >>= \case + createUser loop False $ mkProfile displayName + mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + createUser onError clientService p = + execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user r -> printResponseEvent (Nothing, Nothing) (config cc) r >> onError -askCreateRelayAddress :: ChatController -> User -> IO () -askCreateRelayAddress cc@ChatController {chatStore} user = +askCreateRelayAddress :: ChatController -> User -> Maybe SMPServerWithAuth -> Bool -> IO () +askCreateRelayAddress cc@ChatController {chatStore} user@User {userId} server_ headless = withTransaction chatStore (\db -> runExceptT $ getUserAddress db user) >>= \case Right _ -> pure () Left SEUserContactLinkNotFound -> promptCreate @@ -153,9 +173,9 @@ askCreateRelayAddress cc@ChatController {chatStore} user = where promptCreate :: IO () promptCreate = do - ok <- onOffPrompt "Create relay address" True + ok <- if headless then pure True else onOffPrompt "Create relay address" True when ok $ - execChatCommand' CreateMyAddress 0 `runReaderT` cc >>= \case + execChatCommand' (APICreateMyAddress userId server_) 0 `runReaderT` cc >>= \case Right (CRUserContactLinkCreated _ address) -> do putStrLn "Chat relay address is created:" putStrLn $ addressStr address @@ -190,6 +210,9 @@ onOffPrompt prompt def = "N" -> pure False _ -> putStrLn "Invalid input, please enter 'y' or 'n'" >> onOffPrompt prompt def +loadImageFile :: FilePath -> IO ImageData +loadImageFile path = loadImageData path >>= either (\e -> putStrLn ("--user-image-file: " <> e) >> exitFailure) pure + userStr :: User -> String userStr User {localDisplayName, profile = LocalProfile {fullName}} = T.unpack $ localDisplayName <> if T.null fullName || localDisplayName == fullName then "" else " (" <> fullName <> ")" diff --git a/src/Simplex/Chat/Delivery.hs b/src/Simplex/Chat/Delivery.hs index 822ee5efb9..a6ccc74247 100644 --- a/src/Simplex/Chat/Delivery.hs +++ b/src/Simplex/Chat/Delivery.hs @@ -161,7 +161,7 @@ instance TextEncoding DeliveryTaskStatus where data MessageDeliveryJob = MessageDeliveryJob { jobId :: Int64, jobScope :: DeliveryJobScope, - singleSenderGMId_ :: Maybe GroupMemberId, -- Just for single-sender deliveries, Nothing for multi-sender deliveries + senderGMIds :: [GroupMemberId], body :: ByteString, cursorGMId_ :: Maybe GroupMemberId } diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index 59e7a2c941..56ed65fb4b 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -187,8 +187,6 @@ contactsHelpInfo = indent <> highlight "/verify @ " <> " - clear security code verification", indent <> highlight "/info @ " <> " - info about contact connection", indent <> highlight "/switch @ " <> " - switch receiving messages to another SMP relay", - indent <> highlight "/pq @ on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for a contact", - indent <> " " <> " (both have to enable for quantum resistance)", "", green "Contact chat preferences:", indent <> highlight "/set voice @ yes/no/always " <> " - allow/prohibit voice messages with the contact", @@ -324,16 +322,13 @@ settingsInfo = map styleMarkdown [ green "Chat settings:", - indent <> highlight "/pq on/off " <> " - [BETA] toggle quantum resistant / standard e2e encryption for the new contacts", indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/smp " <> " - show / set configured SMP servers", indent <> highlight "/xftp " <> " - show / set configured XFTP servers", indent <> highlight "/info " <> " - information about contact connection", indent <> highlight "/info # " <> " - information about member connection", indent <> highlight "/(un)mute " <> " - (un)mute contact, the last messages can be printed with /tail command", - indent <> highlight "/(un)mute # " <> " - (un)mute group", - indent <> highlight "/get stats " <> " - get usage statistics", - indent <> highlight "/reset stats " <> " - reset usage statistics" + indent <> highlight "/(un)mute # " <> " - (un)mute group" ] databaseHelpInfo :: [StyledString] diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index e11a8bd523..2226f80fab 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -13,6 +13,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Library.Commands where @@ -56,6 +57,7 @@ import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), maxXFTPFileSize, mkBadgeStatus, verifyCredential) +import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) @@ -90,7 +92,8 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Util (liftIOEither, zipWith3') import qualified Simplex.Chat.Util as U -import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSize, maxFileSizeHard) +import Simplex.Chat.Web (webPreviewWorker) +import Simplex.FileTransfer.Description (FileDescriptionURI (..), maxFileSizeHard) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Protocol @@ -104,11 +107,12 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.ShortLink as SL import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (NAME), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) +import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth) @@ -128,7 +132,7 @@ import qualified UnliftIO.Exception as E import UnliftIO.IO (hClose) import UnliftIO.STM #if defined(dbPostgres) -import Data.Bifunctor (bimap, second) +import Data.Bifunctor (bimap, first, second) import Simplex.Messaging.Agent.Client (SubInfo (..), getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, temporaryOrHostError) #else import Data.Bifunctor (bimap, first, second) @@ -150,9 +154,55 @@ _defaultNtfServers = maxImageSize :: Integer maxImageSize = 261120 * 2 -- auto-receive on mobiles +-- matches the cap mobile and desktop UIs pass to resizeImageToStrSize for profile images +maxProfileImageSize :: Int +maxProfileImageSize = 12500 + +checkProfileImageSize :: Maybe ImageData -> CM () +checkProfileImageSize = mapM_ $ \(ImageData t) -> + let size = T.length t + in when (size > maxProfileImageSize) $ throwCmdError $ "Profile image is too large " <> show size + +checkProfileSize :: Profile -> CM () +checkProfileSize p = checkInfoSize "Profile" (XInfo p) + +checkGroupProfileSize :: GroupProfile -> CM () +checkGroupProfileSize p = checkInfoSize "Group profile" (XGrpInfo p) + +-- validates that the profile update event fits into the connection info sent to peers +checkInfoSize :: String -> ChatMsgEvent 'Json -> CM () +checkInfoSize what event = do + vr <- chatVersionRange + let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent = event} + case encodeChatMessage maxEncodedInfoLength info of + ECMEncoded _ -> pure () + ECMLarge -> throwCmdError $ what <> " is too large" + imageExtensions :: [String] imageExtensions = [".jpg", ".jpeg", ".png", ".gif"] +-- read a .png/.jpg/.jpeg image file and encode it as a data: URL ImageData, or return an error message +loadImageData :: FilePath -> IO (Either String ImageData) +loadImageData path = case map toLower (takeExtension path) of + ".png" -> readImage "image/png" + ".jpg" -> readImage "image/jpg" + ".jpeg" -> readImage "image/jpg" + _ -> pure $ Left $ "unsupported image extension in " <> path <> " (only .png, .jpg, .jpeg)" + where + readImage mime = do + exists <- doesFileExist path + if not exists + then pure $ Left $ "image file not found: " <> path + else do + bs <- B.readFile path + pure $ + if B.null bs + then Left $ "image file is empty: " <> path + else Right $ ImageData $ "data:" <> mime <> ";base64," <> safeDecodeUtf8 (B64.encode bs) + +readProfileImageFile :: FilePath -> CM ImageData +readProfileImageFile path = liftIO (loadImageData path) >>= either throwCmdError pure + fixedImagePreview :: ImageData fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==" @@ -201,6 +251,7 @@ startChatController mainApp enableSndFiles = do startCleanupManager void $ forkIO $ mapM_ startExpireCIs users startRelayChecks users + startWebPreview users else when enableSndFiles $ startXFTP xftpStartSndWorkers pure a1 startXFTP startWorkers = do @@ -232,6 +283,20 @@ startChatController mainApp enableSndFiles = do a <- Just <$> async (void $ runExceptT $ runRelayGroupLinkChecks relayUser) atomically $ writeTVar relayAsync a _ -> pure () + startWebPreview users = do + let relayUsers = filter (\User {userChatRelay} -> isTrue userChatRelay) users + ChatConfig {webPreviewConfig = cfg_} <- asks config + case (relayUsers, cfg_) of + (_ : _, Just cfg) -> do + wps_ <- asks webPreviewState + forM_ wps_ $ \WebPreviewState {webPreviewWorkerAsync} -> + readTVarIO webPreviewWorkerAsync >>= \case + Nothing -> do + cc <- ask + a <- Just <$> async (liftIO $ webPreviewWorker cfg cc relayUsers) + atomically $ writeTVar webPreviewWorkerAsync a + _ -> pure () + _ -> pure () startExpireCIs user = whenM shouldExpireChats $ do startExpireCIThread user setExpireCIFlag user True @@ -349,30 +414,33 @@ parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace processChatCommand :: StoreCxt -> NetworkRequestMode -> ChatCommand -> CM ChatResponse processChatCommand cxt nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay} -> do - forM_ profile $ \Profile {displayName} -> checkValidName displayName + CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay, clientService} -> do + forM_ profile $ \p@Profile {displayName, image} -> do + checkValidName displayName + checkProfileImageSize image + checkProfileSize p p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash, userChatRelay = userChatRelay'} -> do when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} - when (userChatRelay && isTrue userChatRelay') $ throwChatError CEChatRelayExists + when (isTrue userChatRelay && isTrue userChatRelay') $ throwChatError CEChatRelayExists (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u - auId <- withAgent $ \a -> createUser a smp' xftp' + let service = isTrue clientService + auId <- withAgent $ \a -> createUser a service smp' xftp' ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> do - user <- createUserRecordAt db (AgentUserId auId) p userChatRelay True ts + user <- createUserRecordAt db (AgentUserId auId) (isTrue userChatRelay) service p True ts mapM_ (setUserServers db user ts) uss - createPresetContactCards db cxt user `catchAllErrors` \_ -> pure () + createPresetContactCards db user `catchAllErrors` \_ -> pure () createNoteFolder db user pure user atomically . writeTVar u $ Just user pure $ CRActiveUser user where - createPresetContactCards :: DB.Connection -> StoreCxt -> User -> ExceptT StoreError IO () - createPresetContactCards db cxt user = do - createContact db cxt user simplexStatusContactProfile + createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO () + createPresetContactCards db user = do createContact db cxt user simplexTeamContactProfile chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP))) chooseServers user_ = do @@ -461,6 +529,19 @@ processChatCommand cxt nm = \case UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUnhideUser userId viewPwd MuteUser -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIMuteUser userId UnmuteUser -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUnmuteUser userId + SetClientService userId' name enable -> checkChatStopped $ withUser' $ \currUser@User {userId} -> do + user@User {agentUserId = AgentUserId auId, clientService, profile = LocalProfile {displayName}} <- + if userId == userId' then pure currUser else privateGetUser userId' + unless (name == displayName) $ throwChatError CEUserUnknown + if enable == isTrue clientService + then ok user + else do + withStore' $ \db -> updateClientService db userId' enable + withAgent $ \a -> setUserService a auId enable + let user' = user {clientService = BoolDef enable} :: User + when (userId == userId') $ chatWriteVar currentUser $ Just user' + setStoreChanged + ok user' APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' viewPwd_ @@ -611,7 +692,8 @@ processChatCommand cxt nm = \case (SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId) _ -> pure Nothing forwardedFromChatItem <- getForwardedFromItem user ci - pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem} + fileXftpServers <- getChatItemFileServers user dir ci + pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem, fileXftpServers} where getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of @@ -621,7 +703,7 @@ processChatCommand cxt nm = \case -- TODO [knocking] getAChatItem doesn't differentiate how to read based on scope - it should, instead of using group filter Just <$> withFastStore (\db -> getAChatItem db cxt user (ChatRef CTGroup gId Nothing) fwdItemId) _ -> pure Nothing - APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of + APISendMessages sendRef live itemTTL sign cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of SRDirect chatId -> do mapM_ assertNoMentions cms withContactLock "sendMessage" chatId $ @@ -634,17 +716,19 @@ processChatCommand cxt nm = \case (gInfo, cmrs) <- withFastStore $ \db -> do g <- getGroupInfo db cxt user chatId (g,) <$> mapM (composedMessageReqMentions db user g) cms - sendGroupContentMessages user gInfo gsScope asGroup live itemTTL cmrs + sendGroupContentMessages user gInfo gsScope asGroup live itemTTL sign cmrs APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do _ <- createChatTag db user emoji text CRChatTags user <$> getUserChatTags db user APISetChatTags (ChatRef cType chatId scope) tagIds -> withUser $ \user -> case cType of - CTDirect -> withFastStore' $ \db -> do - updateDirectChatTags db chatId (maybe [] L.toList tagIds) - CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId - CTGroup | isNothing scope -> withFastStore' $ \db -> do - updateGroupChatTags db chatId (maybe [] L.toList tagIds) - CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId + CTDirect -> withFastStore $ \db -> do + Contact {contactId} <- getContact db cxt user chatId + liftIO $ updateDirectChatTags db contactId (maybe [] L.toList tagIds) + CRTagsUpdated user <$> liftIO (getUserChatTags db user) <*> liftIO (getDirectChatTags db contactId) + CTGroup | isNothing scope -> withFastStore $ \db -> do + GroupInfo {groupId} <- getGroupInfo db cxt user chatId + liftIO $ updateGroupChatTags db groupId (maybe [] L.toList tagIds) + CRTagsUpdated user <$> liftIO (getUserChatTags db user) <*> liftIO (getGroupChatTags db groupId) _ -> throwCmdError "not supported" APIDeleteChatTag tagId -> withUser $ \user -> do withFastStore' $ \db -> deleteChatTag db user tagId @@ -663,7 +747,7 @@ processChatCommand cxt nm = \case gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId let mc = MCReport reportText reportReason cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} - sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False False Nothing [composedMessageReq cm] + sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False False Nothing False [composedMessageReq cm] ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage @@ -704,7 +788,7 @@ processChatCommand cxt nm = \case -- TODO [knocking] check chat item scope? cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId case cci of - CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender}, content = ciContent} -> do + CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender, msgVerified}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of (CISndMsgContent oldMC, Just itemSharedMId, True) -> do chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope @@ -716,7 +800,8 @@ processChatCommand cxt nm = \case let msgScope = toMsgScope gInfo <$> chatScopeInfo mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions event = XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) msgScope (Just showGroupAsSender) - SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients event + reuseSign = case msgVerified of Just (MVSigned _) -> True; _ -> False + SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients reuseSign event ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ @@ -775,16 +860,14 @@ processChatCommand cxt nm = \case recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion assertDeletable items assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier - let msgIds = itemsMsgIds items - events = L.nonEmpty $ map (\msgId -> XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) False) msgIds - mapM_ (sendGroupMessages user gInfo Nothing False recipients) events + let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned gInfo chatScopeInfo False) items + mapM_ (sendGroupSignedMessages user gInfo Nothing False recipients) signedEvents delGroupChatItems user gInfo chatScopeInfo items False CIDMHistory -> do unless (publicGroupEditor gInfo (membership gInfo)) $ throwChatError CEInvalidChatItemDelete recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion - let msgIds = itemsMsgIds items - events = L.nonEmpty $ map (\msgId -> XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) True) msgIds - mapM_ (sendGroupMessages user gInfo Nothing False recipients) events + let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned gInfo chatScopeInfo True) items + mapM_ (sendGroupSignedMessages user gInfo Nothing False recipients) signedEvents delGroupChatItems user gInfo chatScopeInfo items False pure $ CRChatItemsDeleted user deletions True False CTLocal -> do @@ -806,6 +889,15 @@ processChatCommand cxt nm = \case SMDRcv -> False itemsMsgIds :: [CChatItem c] -> [SharedMsgId] itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) + -- history delete always signs (attributable owner action); self-delete signs iff the target was held signed (deniability) + delEventSigned :: GroupInfo -> Maybe GroupChatScopeInfo -> Bool -> CChatItem 'CTGroup -> Maybe (Maybe MsgSigning, ChatMsgEvent 'Json) + delEventSigned gInfo chatScopeInfo onlyHistory (CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId, msgVerified}}) = + delEvent <$> itemSharedMsgId + where + delEvent msgId = + let evt = XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) onlyHistory + in (groupMsgSigning (onlyHistory || itemSigned) gInfo evt, evt) + itemSigned = case msgVerified of Just (MVSigned _) -> True; _ -> False APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds -- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?) @@ -873,7 +965,7 @@ processChatCommand cxt nm = \case let itemMemberId = memberId' <$> chatItemMember g ci rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs - SndMessage {msgId} <- sendGroupMessage user g scope recipients (XMsgReact itemSharedMId itemMemberId (toMsgScope g <$> chatScopeInfo) reaction add) + SndMessage {msgId} <- sendGroupMessage user g scope recipients False (XMsgReact itemSharedMId itemMemberId (toMsgScope g <$> chatScopeInfo) reaction add) createdAt <- liftIO getCurrentTime reactions <- withFastStore' $ \db -> do setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt @@ -959,7 +1051,7 @@ processChatCommand cxt nm = \case Just cmrs' -> withGroupLock "forwardChatItem, to group" toChatId $ do gInfo <- withFastStore $ \db -> getGroupInfo db cxt user toChatId - sendGroupContentMessages user gInfo toScope sendAsGroup False itemTTL cmrs' + sendGroupContentMessages user gInfo toScope sendAsGroup False itemTTL False cmrs' Nothing -> pure $ CRNewChatItems user [] CTLocal -> do cmrs <- prepareForward user @@ -1101,30 +1193,23 @@ processChatCommand cxt nm = \case _ -> Nothing ownerSig <- pure signingKeys $>>= \GroupKeys {memberPrivKey} -> - mkLinkOwnerSig memberPrivKey groupLink memberId <$$> shareChatBinding user toSendRef + mkLinkOwnerSig memberPrivKey groupLink (Just memberId) <$$> shareChatBinding user toSendRef let text = safeDecodeUtf8 $ strEncode groupLink pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig} - where - mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig - mkLinkOwnerSig privKey connLink MemberId {unMemberId} (cbTag, bindingData) = - let ownerId = Just $ B64UrlByteString unMemberId - cb = encodeChatBinding cbTag bindingData - ownerSig = C.sign' privKey $ cb <> smpEncode connLink - in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} - shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) - shareChatBinding u = \case - SRDirect contactId -> do - ct <- withFastStore $ \db -> getContact db cxt u contactId - forM (contactConn ct) $ \conn -> - (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) - SRGroup toGroupId _ asGroup -> do - GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId - pure $ mkBinding m <$> publicGroup - where - mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} - | asGroup = (CBChannel, smpEncode pgId) - | otherwise = (CBGroup, smpEncode (pgId, memberId)) APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" + APIShareMyAddress toSendRef -> withUser $ \user -> do + UserContactLink {connLinkContact = CCLink _ sl_, addressSettings} <- withFastStore (`getUserAddress` user) + case sl_ of + Nothing -> throwCmdError "your address has no short link to share" + Just connLink -> do + conn <- withFastStore $ \db -> getUserAddressConnection db cxt user + ownerSig <- + withAgent (`getConnLinkPrivKey` aConnId conn) $>>= \privKey -> + mkLinkOwnerSig privKey connLink Nothing <$$> shareChatBinding user toSendRef + let business = businessAddress addressSettings + profile = userProfileDirect user Nothing Nothing True + text = safeDecodeUtf8 $ strEncode connLink + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig} APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -1257,11 +1342,13 @@ processChatCommand cxt nm = \case filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "deleteChat group" chatId $ do deleteCIFiles user filesInfo + -- the roster blob file has no chat item, so it is missed by getGroupFileInfo above + cleanupGroupRosterFile user gInfo (members, recipients) <- getRecipients gInfo let doSendDel = memberActive membership && isOwner msgSigned <- if doSendDel - then isJust . signedMsg_ <$> sendGroupMessage' user gInfo recipients XGrpDel + then (\SndMessage {signedMsg_} -> isJust signedMsg_) <$> sendGroupMessage' user gInfo recipients XGrpDel else pure False deleteGroupLinkIfExists user gInfo deleteMembersConnections' user members doSendDel @@ -1457,6 +1544,24 @@ processChatCommand cxt nm = \case withCurrentCall contactId $ \user ct call -> updateCallItemStatus user ct call receivedStatus Nothing $> Just call APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) + APISetUserDomain userId domain_ -> withUserId userId $ \user@User {profile = p@LocalProfile {contactLink, contactDomain}} -> + if (claimDomain <$> contactDomain) == domain_ + then pure $ CRUserProfileNoChange user + else do + cl' <- case domain_ of + Nothing -> pure contactLink + Just domain -> do + UserContactLink {shortLinkDataSet, connLinkContact = CCLink _ sl_} <- withFastStore (`getUserAddress` user) + case sl_ of + Just sl | shortLinkDataSet -> do + NameRecord {nrSimplexContact} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain + unless (nameResolvesTo sl nrSimplexContact) $ throwChatError $ CESimplexDomainNotReady domain SDENoValidLink + pure $ Just (CLShort sl) + _ -> throwCmdError "create the address short link and add it to name" + let p' = (fromLocalProfile p :: Profile) {contactDomain = mkDomainClaim <$> domain_, contactLink = cl'} + updateProfile_ user p' True $ withFastStore $ \db -> do + user' <- updateUserProfile db user p' + liftIO $ setUserSimplexDomain db user' domain_ APISetContactPrefs contactId prefs' -> withUser $ \user -> do ct <- withFastStore $ \db -> getContact db cxt user contactId updateContactPrefs user ct prefs' @@ -1679,8 +1784,11 @@ processChatCommand cxt nm = \case CRServerOperatorConditions <$> getServerOperators db APISetChatTTL userId (ChatRef cType chatId scope) newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatTTL" $ do - (oldTTL_, globalTTL, ttlCount) <- withStore' $ \db -> - (,,) <$> getSetChatTTL db <*> getChatItemTTL db user <*> getChatTTLCount db user + (oldTTL_, globalTTL, ttlCount) <- withStore $ \db -> do + oldTTL <- getSetChatTTL db user + globalTTL <- liftIO $ getChatItemTTL db user + ttlCount <- liftIO $ getChatTTLCount db user + pure (oldTTL, globalTTL, ttlCount) let newTTL = fromMaybe globalTTL newTTL_ oldTTL = fromMaybe globalTTL oldTTL_ when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do @@ -1689,9 +1797,13 @@ processChatCommand cxt nm = \case lift $ setChatItemsExpiration user globalTTL ttlCount ok user where - getSetChatTTL db = case cType of - CTDirect -> getDirectChatTTL db chatId <* setDirectChatTTL db chatId newTTL_ - CTGroup | isNothing scope -> getGroupChatTTL db chatId <* setGroupChatTTL db chatId newTTL_ + getSetChatTTL db currentUser = case cType of + CTDirect -> do + Contact {contactId} <- getContact db cxt currentUser chatId + liftIO $ getDirectChatTTL db contactId <* setDirectChatTTL db contactId newTTL_ + CTGroup | isNothing scope -> do + GroupInfo {groupId} <- getGroupInfo db cxt currentUser chatId + liftIO $ getGroupChatTTL db groupId <* setGroupChatTTL db groupId newTTL_ _ -> pure Nothing expireChat user globalTTL = do currentTs <- liftIO getCurrentTime @@ -1729,7 +1841,7 @@ processChatCommand cxt nm = \case pure $ CRChatItemTTL user (Just ttl) GetChatItemTTL -> withUser' $ \User {userId} -> do processChatCommand cxt nm $ APIGetChatItemTTL userId - APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ + APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> CRNetworkConfig <$> lift getNetworkConfig SetNetworkConfig simpleNetCfg -> do @@ -1788,13 +1900,13 @@ processChatCommand cxt nm = \case APIGroupInfo gId -> withUser $ \user -> CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db cxt user gId) APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId + gInfo@GroupInfo {groupProfile = p} <- withFastStore $ \db -> getGroupInfo db cxt user groupId case p of GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData gInfo' <- case groupSLinkData_ of - Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData + Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData Nothing _ -> pure gInfo when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $ withGroupLock "syncSubscriberRelays" groupId $ @@ -1871,28 +1983,41 @@ processChatCommand cxt nm = \case Nothing -> throwChatError $ CEContactNotActive ct APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId - case activeConn of - Just conn@Connection {connId} -> do - code <- getConnectionCode $ aConnId conn + if useRelays' g + then do + code <- getChannelMemberCode g m m' <- case memberSecurityCode m of Just SecurityCode {securityCode} | sameVerificationCode code securityCode -> pure m | otherwise -> do - withFastStore' $ \db -> setConnectionVerified db user connId Nothing - pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} + withFastStore' $ \db -> setGroupMemberVerified db user (groupMemberId' m) Nothing + pure m {memberVerifiedCode = Nothing} _ -> pure m pure $ CRGroupMemberCode user g m' code - _ -> throwChatError CEGroupMemberNotActive + else case activeConn of + Just conn@Connection {connId} -> do + code <- getConnectionCode $ aConnId conn + m' <- case memberSecurityCode m of + Just SecurityCode {securityCode} + | sameVerificationCode code securityCode -> pure m + | otherwise -> do + withFastStore' $ \db -> setConnectionVerified db user connId Nothing + pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} + _ -> pure m + pure $ CRGroupMemberCode user g m' code + _ -> throwChatError CEGroupMemberNotActive APIVerifyContact contactId code -> withUser $ \user -> do ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of Just conn -> verifyConnectionCode user conn code Nothing -> throwChatError $ CEContactNotActive ct APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do - GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db cxt user gId gMemberId - case activeConn of - Just conn -> verifyConnectionCode user conn code - _ -> throwChatError CEGroupMemberNotActive + (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId + if useRelays' g + then verifyChannelMemberCode user g m code + else case activeConn of + Just conn -> verifyConnectionCode user conn code + _ -> throwChatError CEGroupMemberNotActive APIEnableContact contactId -> withUser $ \user -> do ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of @@ -1942,11 +2067,11 @@ processChatCommand cxt nm = \case -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode + -- TODO [badges] bind link and badge to handshake context linkProfile <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True - let userData = contactShortLinkData linkProfile Nothing + let userData = contactShortLinkData linkProfile {contactDomain = Nothing} Nothing userLinkData = UserInvLinkData userData - -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode + (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -1988,8 +2113,7 @@ processChatCommand cxt nm = \case if short then Just . UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser Nothing (userProfileDirect newUser Nothing Nothing True) else pure Nothing - -- TODO [certs rcv] - (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn subMode + (agConnId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -1998,10 +2122,11 @@ processChatCommand cxt nm = \case createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' - APIConnectPlan userId (Just cLink) resolveKnown linkOwnerSig_ -> withUserId userId $ \user -> - uncurry (CRConnectionPlan user) <$> connectPlan user cLink resolveKnown linkOwnerSig_ + APIConnectPlan userId (Just ct) resolveMode linkOwnerSig_ -> withUserId userId $ \user -> do + (ccLink, planSimplexName, otherSimplexName, plan) <- connectPlan user ct resolveMode linkOwnerSig_ Nothing + pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq - APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do + APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do let ContactShortLinkData {profile, message, business} = contactSLinkData welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId case accLink of @@ -2011,11 +2136,11 @@ processChatCommand cxt nm = \case groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences groupProfile = businessGroupProfile profile groupPreferences gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing + (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing verifiedDomain hostMember <- maybe (throwCmdError "no host member") pure hostMember_ - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDGroupRcv gInfo Nothing hostMember - createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing + createItem sharedMsgId content = createChatItem user cd True content sharedMsgId Nothing Nothing cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo aci <- mapM (createItem welcomeSharedMsgId . CIRcvMsgContent) message @@ -2024,10 +2149,10 @@ processChatCommand cxt nm = \case _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTGroup chat ACCL _ (CCLink cReq _) -> do - ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId (True <$ verifiedDomain) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) let cd = CDDirectRcv ct - createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing + createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing cInfo = DirectChat ct void $ createItem Nothing $ CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ connRequestPQEncryption cReq void $ createFeatureEnabledItems_ user ct @@ -2036,19 +2161,15 @@ processChatCommand cxt nm = \case Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTDirect chat - APIPrepareGroup userId ccLink direct groupSLinkData -> withUserId userId $ \user -> do - let GroupShortLinkData {groupProfile = gp@GroupProfile {description}, publicGroupData = publicGroupData_} = groupSLinkData - publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ + APIPrepareGroup userId ccLink direct verifiedDomain groupSLinkData -> withUserId userId $ \user -> do + let GroupShortLinkData {groupProfile = GroupProfile {description}} = groupSLinkData welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId - let useRelays = not direct - subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember - gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + (gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId verifiedDomain + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_ cInfo = GroupChat gInfo Nothing void $ createGroupFeatureItems_ user cd True CIRcvGroupFeature gInfo - aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing + aci <- forM description $ \descr -> createChatItem user cd True (CIRcvMsgContent $ MCText descr) welcomeSharedMsgId Nothing Nothing let chat = case aci of Just (AChatItem SCTGroup dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} _ -> Chat cInfo [] emptyChatStats @@ -2116,7 +2237,7 @@ processChatCommand cxt nm = \case -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msg_ $ \(sharedMsgId, mc) -> do - ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing + ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToContact user ct' customUserProfile CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' @@ -2209,7 +2330,7 @@ processChatCommand cxt nm = \case liftIO $ setPreparedGroupStartedConnection db groupId getGroupInfo db cxt user groupId forM_ msg_ $ \(sharedMsgId, mc) -> do - ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToGroup user gInfo' customUserProfile [] CVRConnectedContact _ct -> throwChatError $ CEException "contact already exists when connecting to group" @@ -2226,12 +2347,35 @@ processChatCommand cxt nm = \case CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq - Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do - -- TODO [relays] member: /c api to support groups with relays - -- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup - (ccLink, plan) <- connectPlan user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e - connectWithPlan user incognito ccLink plan + Connect incognito (Just ct) -> withUser $ \user -> do + let con m cReq = pure (ACCL m (CCLink cReq Nothing), Nothing, Nothing, CPInvitationLink (ILPOk Nothing Nothing)) + (ccLink, planSimplexName, otherSimplexName, plan) <- connectPlan user ct PRMUnknown Nothing Nothing `catchAllErrors` \e -> case ct of + ACTarget m (CTFullContact cReq) -> con m cReq + ACTarget m (CTInv (CLFull cReq)) -> con m cReq + _ -> throwError e + connectWithPlan user incognito ccLink planSimplexName otherSimplexName plan Connect _ Nothing -> throwChatError CEInvalidConnReq + APIVerifyContactDomain contactId -> withUser $ \user -> do + ct@Contact {profile = LocalProfile {contactDomain}, preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId + let connLink_ = preparedContact >>= \PreparedContact {connLinkToConnect = ACCL m (CCLink _ sLnk_)} -> ACSL m <$> sLnk_ + domain <- maybe (throwCmdError "contact has no name to verify") pure contactDomain + (verified, reason) <- verifyEntityDomain user nm NTContact domain connLink_ + ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v) verified + pure $ CRContactDomainVerified user ct' reason + APIVerifyGroupDomain groupId -> withUser $ \user -> do + g@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId + PublicGroupProfile {groupLink, publicGroupAccess} <- maybe (throwCmdError "not a public group") pure publicGroup + claim <- maybe (throwCmdError "group has no name to verify") pure $ publicGroupAccess >>= groupDomainClaim + -- checks the profile link, not the link we joined through (which may have rotated) + (verified, reason) <- + tryAllErrors (withAgent $ \a -> resolveSimplexName a nm (aUserId user) (claimDomain claim)) >>= \case + Right NameRecord {nrSimplexChannel} + | nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing) + | otherwise -> pure (False, Just "the name does not resolve to the link in the group profile") + Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered") + Left e -> throwError e + g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified + pure $ CRGroupDomainVerified user g' reason APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId ccLink <- case contactLink of @@ -2248,33 +2392,36 @@ processChatCommand cxt nm = \case throwError e ConnectSimplex incognito -> withUser $ \user -> do plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing)) - connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan + connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) Nothing Nothing plan DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing APIListContacts userId -> withUserId userId $ \user -> CRContactsList user <$> withFastStore' (\db -> getUserContacts db cxt user) ListContacts -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user@User {userChatRelay} -> do + APICreateMyAddress userId server_ -> withUserId userId $ \user@User {userChatRelay} -> do withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case Left SEUserContactLinkNotFound -> pure () Left e -> throwError $ ChatErrorStore e Right _ -> throwError $ ChatErrorStore SEDuplicateContactLink subMode <- chatReadVar subscriptionMode + gVar <- asks random + rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar + let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing server_ + ccLink' <- shortenCreatedLink ccLink -- TODO [relays] relay: add identity, key to link data? userData <- if isTrue userChatRelay then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True) else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} - -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) Nothing IKPQOn subMode - ccLink' <- shortenCreatedLink ccLink + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOn subMode let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' - withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode + withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode rootPrivKey pure $ CRUserContactLinkCreated user ccLink'' CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand cxt nm $ APICreateMyAddress userId + processChatCommand cxt nm $ APICreateMyAddress userId Nothing APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do conn <- withFastStore $ \db -> getUserAddressConnection db cxt user withChatLock "deleteMyAddress" $ do @@ -2353,7 +2500,19 @@ processChatCommand cxt nm = \case _ -> throwCmdError "unsupported share target" processChatCommand cxt nm (APIShareChatMsgContent (ChatRef CTGroup groupId Nothing) sendRef) >>= \case CRChatMsgContent _ mc -> - processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] + r -> pure r + ShareMyAddress toChatName -> withUser $ \user -> do + toChatRef <- getChatRef user toChatName + sendRef <- case toChatRef of + ChatRef CTDirect ctId _ -> pure $ SRDirect ctId + ChatRef CTGroup gId scope_ -> do + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId + pure $ SRGroup gId scope_ (useRelays' gInfo) + _ -> throwCmdError "unsupported share target" + processChatCommand cxt nm (APIShareMyAddress sendRef) >>= \case + CRChatMsgContent _ mc -> + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg @@ -2362,7 +2521,7 @@ processChatCommand cxt nm = \case withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let sendRef = SRDirect ctId - processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] Left _ -> withFastStore' (\db -> runExceptT $ getActiveMembersByName db cxt user name) >>= \case Right [(gInfo, member)] -> do @@ -2382,7 +2541,7 @@ processChatCommand cxt nm = \case GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_ (gInfo, cScope_,) <$> liftIO (getMessageMentions db user gId msg) let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo cScope_) - processChatCommand cxt nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [ComposedMessage Nothing Nothing mc mentions] SNLocal -> do folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand cxt nm $ APICreateChatItems folderId [composedMessage Nothing mc] @@ -2402,7 +2561,7 @@ processChatCommand cxt nm = \case cr -> pure cr Just ctId -> do let sendRef = SRDirect ctId - processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage Nothing mc] AcceptMemberContact cName -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName processChatCommand cxt nm $ APIAcceptMemberContact contactId @@ -2410,7 +2569,7 @@ processChatCommand cxt nm = \case (chatRef, mentions) <- getChatRefAndMentions user chatName msg withSendRef user chatRef $ \sendRef -> do let mc = MCText msg - processChatCommand cxt nm $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand cxt nm $ APISendMessages sendRef True Nothing False [ComposedMessage Nothing Nothing mc mentions] SendMessageBroadcast mc -> withUser $ \user -> do contacts <- withFastStore' $ \db -> getUserContacts db cxt user withChatLock "sendMessageBroadcast" $ do @@ -2455,7 +2614,7 @@ processChatCommand cxt nm = \case contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg - processChatCommand cxt nm $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] + processChatCommand cxt nm $ APISendMessages (SRDirect contactId) False Nothing False [ComposedMessage Nothing (Just quotedItemId) mc M.empty] DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg @@ -2502,7 +2661,7 @@ processChatCommand cxt nm = \case then throwError e else do let relayResults = map toRelayResult results - toRelayResult (r, Left e) = AddRelayResult r (Just e) + toRelayResult (r, Left e') = AddRelayResult r (Just e') toRelayResult (r, Right _) = AddRelayResult r Nothing pure $ CRPublicGroupCreationFailed user relayResults where @@ -2520,7 +2679,7 @@ processChatCommand cxt nm = \case let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with entityId as linkEntityId (no server request) - (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) Nothing ccLink' <- setShortLinkType CCTChannel <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl @@ -2599,8 +2758,7 @@ processChatCommand cxt nm = \case Nothing -> do gVar <- asks random subMode <- chatReadVar subscriptionMode - -- TODO [certs rcv] - (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode + (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2681,7 +2839,7 @@ processChatCommand cxt nm = \case modMs <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo let rcpModMs' = filter memberCurrent modMs msg = XGrpLinkAcpt GAAccepted role (memberId' m) - void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg + void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') False msg when (maxVersion (memberChatVRange m) < groupKnockingVersion) $ forM_ (memberConn m) $ \mConn -> do let msg2 = XMsgNew $ mcSimple (MCText acceptedToGroupMessage) @@ -2721,34 +2879,51 @@ processChatCommand cxt nm = \case -- TODO [relays] possible optimization is to read only required members + relays g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId when (selfSelected gInfo) $ throwCmdError "can't change role for self" - let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members + let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, anyRelay, anyRosterChange, finalPrivilegedCount) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $ throwCmdError "can't change role of multiple members when admins selected, or new role is admin" when anyPending $ throwCmdError "can't change role of members pending approval" + when (anyRelay || newRole == GRRelay) $ throwCmdError "relay role can't be changed" + -- TODO allow moderators (needs UI) - relay is rejected above (anyRelay), so drop the GRAdmin floor: + -- TODO assertUserGroupRole gInfo (roleRequiredToChange maxRole newRole) assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole]) + -- in relay groups the roster has a single signer, so only the owner may change member/moderator/admin roles + when (useRelays' gInfo && (isRosterRole newRole || anyPrivilegedTarget) && memberRole' (membership gInfo) /= GROwner) $ + throwCmdError "only the group owner can change moderator and admin roles" + when (useRelays' gInfo && isRosterRole newRole && finalPrivilegedCount > maxGroupRosterSize) $ + throwCmdError $ "the number of members, moderators and admins would exceed the limit of " <> show maxGroupRosterSize (errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems - (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g currentMems + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterChange + -- roster (with the change projected in) before the delta, so a relay stores the blob at this version before forwarding the delta + rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRoleChanged newRole currentMems) else pure Nothing + (errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g rosterVer currentMems unless (null acis) $ toView $ CEvtNewChatItems user acis let errs = errs1 <> errs2 unless (null errs) $ toView $ CEvtChatErrors errs pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole, msgSigned} -- same order is not guaranteed where selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds - selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool) - selectMembers = foldr' addMember ([], [], [], GRObserver, False, False) + -- anyPrivilegedTarget: a target currently member/moderator/admin (gates the owner-only check); anyRosterChange: + -- a current member's role change that alters the roster blob - the only case that bumps the version, since a + -- bump with no delta reads as a gap to subscribers; finalPrivilegedCount: moderators + admins after the change. + selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Bool, Bool, Int) + selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, False, False, 0) where - addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending) + addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRelay, anyRosterChange, privCount) | groupMemberId `elem` memberIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin anyPending' = anyPending || memberPending m - in - if - | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending') - | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending') - | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending') - | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending) + anyPrivTarget' = anyPrivTarget || isRosterRole memberRole + anyRelay' = anyRelay || memberRole == GRRelay + privCount' = if isRosterRole newRole then privCount + 1 else privCount + in if + | memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange, privCount') + | memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange, privCount') + -- a current member's role actually changes here; it alters the roster iff the old or new role is on it + | otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange || isRosterRole newRole || isRosterRole memberRole, privCount') + | otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRelay, anyRosterChange, if isRosterRole memberRole then privCount + 1 else privCount) changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember]) changeRoleInvitedMems user gInfo memsToChange = do -- not batched, as we need to send different invitations to different connections anyway @@ -2763,19 +2938,20 @@ processChatCommand cxt nm = \case withFastStore' $ \db -> updateGroupMemberRole db user m newRole pure (m :: GroupMember) {memberRole = newRole} _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName - changeRoleCurrentMems :: User -> Group -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) - changeRoleCurrentMems user (Group gInfo members) memsToChange = case L.nonEmpty memsToChange of + changeRoleCurrentMems :: User -> Group -> Maybe VersionRoster -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) + changeRoleCurrentMems user (Group gInfo members) rosterVer memsToChange = case L.nonEmpty memsToChange of Nothing -> pure ([], [], [], False) Just memsToChange' -> do - let events = L.map (\GroupMember {memberId} -> XGrpMemRole memberId newRole) memsToChange' + let mKey m = if isJust rosterVer then MemberKey <$> memberPubKey m else Nothing + events = L.map (\m@GroupMember {memberId} -> XGrpMemRole memberId newRole (mKey m) rosterVer) memsToChange' recipients = filter memberCurrent members - (msgs_, _gsr) <- sendGroupMessages user gInfo Nothing False recipients events - let signed = any (either (const False) (isJust . signedMsg_)) msgs_ + (msgs_, _gsr) <- sendGroupMessages user gInfo Nothing False recipients False events + let signed = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_ itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) False itemsData Nothing False when (length cis_ /= length memsToChange) $ logError "changeRoleCurrentMems: memsToChange and cis_ length mismatch" - (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing)) $ rights cis_ + (errs, changed) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updMember db) memsToChange) pure (errs, changed, acis, signed) where sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c @@ -2817,8 +2993,8 @@ processChatCommand cxt nm = \case let mrs = if blockFlag then MRSBlocked else MRSUnrestricted events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems' recipients = filter memberCurrent remainingMems - (msgs_, _gsr) <- sendGroupMessages_ user gInfo recipients events - let msgSigned = any (either (const False) (isJust . signedMsg_)) msgs_ + (msgs_, _gsr) <- sendGroupMessages_ user gInfo recipients False events + let msgSigned = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_ itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) False itemsData Nothing False when (length cis_ /= length blockMems) $ logError "blockMembers: blockMems and cis_ length mismatch" @@ -2839,15 +3015,20 @@ processChatCommand cxt nm = \case withGroupLock "removeMembers" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId - let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers gmIds members + let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin, anyPrivilegedRemoved, anyRosterRemoved) = selectMembers gmIds members gmIds = S.fromList $ L.toList groupMemberIds memCount = length groupMemberIds when (count /= memCount) $ throwChatError CEGroupMemberNotFound when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected" assertUserGroupRole gInfo $ max GRAdmin maxRole + when (useRelays' gInfo && anyPrivilegedRemoved && memberRole' (membership gInfo) /= GROwner) $ + throwCmdError "only the group owner can remove members, moderators and admins" (errs1, deleted1) <- deleteInvitedMems user invitedMems let recipients = filter memberCurrent members - (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing recipients currentMems + let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterRemoved + -- roster (excluding the removed members) before the delta, so a relay stores the blob at this version before forwarding the delta + rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRemoved currentMems) else pure Nothing + (errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing rosterVer recipients currentMems (errs3, deleted3, acis3, signed3) <- foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], [], False) pendingApprvMems let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members @@ -2865,22 +3046,26 @@ processChatCommand cxt nm = \case let acis' = map (updateACIGroupInfo gInfo') acis unless (null acis') $ toView $ CEvtNewChatItems user acis' unless (null errs) $ toView $ CEvtChatErrors errs - when withMessages $ deleteMessages user gInfo' deleted pure $ CRUserDeletedMembers user gInfo' deleted withMessages msgSigned -- same order is not guaranteed where - selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) - selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False) + -- anyPrivilegedRemoved: any removed member is member/moderator/admin (gates the owner-only check); anyRosterRemoved: + -- a current roster member is removed - the only case that alters the blob and so bumps the version (pending/ + -- invited members aren't on the roster, and a bump with no delta reads as a gap to subscribers). + selectMembers :: S.Set GroupMemberId -> [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool) + selectMembers gmIds = foldl' addMember (0, [], [], [], [], GRObserver, False, False, False) where - addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin) m@GroupMember {groupMemberId, memberStatus, memberRole} + addMember acc@(n, invited, pendingApprv, pendingRvw, current, maxRole, anyAdmin, anyPrivRemoved, anyRosterRemoved) m@GroupMember {groupMemberId, memberStatus, memberRole} | groupMemberId `S.member` gmIds = let maxRole' = max maxRole memberRole anyAdmin' = anyAdmin || memberRole >= GRAdmin + anyPrivRemoved' = anyPrivRemoved || isRosterRole memberRole n' = n + 1 in case memberStatus of - GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin') - GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin') - GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin') - _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin') + GSMemInvited -> (n', m : invited, pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + GSMemPendingApproval -> (n', invited, m : pendingApprv, pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + GSMemPendingReview -> (n', invited, pendingApprv, m : pendingRvw, current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved) + -- removed current member: alters the roster blob iff it currently holds a roster role + _ -> (n', invited, pendingApprv, pendingRvw, m : current, maxRole', anyAdmin', anyPrivRemoved', anyRosterRemoved || isRosterRole memberRole) | otherwise = acc deleteInvitedMems :: User -> [GroupMember] -> CM ([ChatError], [GroupMember]) deleteInvitedMems user memsToDelete = do @@ -2893,16 +3078,16 @@ processChatCommand cxt nm = \case deletePendingMember :: ([ChatError], [GroupMember], [AChatItem], Bool) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem], Bool) deletePendingMember (accErrs, accDeleted, accACIs, accSigned) user gInfo recipients m = do (m', scopeInfo) <- mkMemberSupportChatInfo m - (errs, deleted, acis, signed) <- deleteMemsSend user gInfo (Just scopeInfo) recipients [m'] + (errs, deleted, acis, signed) <- deleteMemsSend user gInfo (Just scopeInfo) Nothing recipients [m'] pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs, accSigned || signed) - deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) - deleteMemsSend user gInfo chatScopeInfo recipients memsToDelete = case L.nonEmpty memsToDelete of + deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe VersionRoster -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool) + deleteMemsSend user gInfo chatScopeInfo rosterVer recipients memsToDelete = case L.nonEmpty memsToDelete of Nothing -> pure ([], [], [], False) Just memsToDelete' -> do let chatScope = toChatScope <$> chatScopeInfo - events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages) memsToDelete' - (msgs_, _gsr) <- sendGroupMessages user gInfo chatScope False recipients events - let signed = any (either (const False) (isJust . signedMsg_)) msgs_ + events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages rosterVer) memsToDelete' + (msgs_, _gsr) <- sendGroupMessages user gInfo chatScope False recipients False events + let signed = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_ itemsData_ = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_) skipUnwantedItem = \case Right Nothing -> Nothing @@ -2910,11 +3095,14 @@ processChatCommand cxt nm = \case Left e -> Just $ Left e itemsData = mapMaybe skipUnwantedItem itemsData_ cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) False itemsData Nothing False + -- MUST run before delMember so getGroupMemberFileInfo can still resolve file info under fullDelete. + when withMessages $ deleteMessages user gInfo memsToDelete deleteMembersConnections' user memsToDelete True (errs, deleted) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (delMember db) memsToDelete) let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo chatScopeInfo)) $ rights cis_ pure (errs, deleted, acis, signed) where + fullDelete = withMessages && groupFeatureUserAllowed SGFFullDelete gInfo sndItemData :: GroupMember -> SndMessage -> Maybe (NewSndChatItemData c) sndItemData GroupMember {groupMemberId, memberProfile, memberStatus} msg | memberStatus == GSMemRemoved || memberStatus == GSMemLeft = Nothing @@ -2927,10 +3115,12 @@ processChatCommand cxt nm = \case -- voided result (updated group info) may have incorrect state of membersRequireAttention. -- To avoid complicating code by chaining group info updates, -- instead we re-read it once after deleting all members before response. - void $ deleteOrUpdateMemberRecordIO db user gInfo m + if fullDelete + then void $ fullyDeleteMemberRecordIO db user gInfo m + else void $ deleteOrUpdateMemberRecordIO db user gInfo m pure m {memberStatus = GSMemRemoved} deleteMessages user gInfo@GroupInfo {membership} ms - | groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms membership + | groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms | otherwise = markGroupMembersCIsDeleted user gInfo ms membership APILeaveGroup groupId -> withUser $ \user@User {userId} -> do gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user groupId @@ -2957,12 +3147,12 @@ processChatCommand cxt nm = \case -- Relay leaving channel: create delivery job for cursor-based sending and async connection cleanup. leaveChannelRelay gInfo = do msg@SndMessage {msgBody, signedMsg_} <- - liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning gInfo XGrpLeave, XGrpLeave)) + liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning False gInfo XGrpLeave, XGrpLeave)) let body = encodeBatchElement signedMsg_ msgBody withFastStore' $ \db -> do deleteGroupDeliveryTasks db gInfo deleteGroupDeliveryJobs db gInfo - createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) Nothing body + createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) [] body lift . void $ getDeliveryJobWorker True (groupId, DWSGroup) pure msg leaveGroupSendMsg user gInfo = do @@ -3028,7 +3218,7 @@ processChatCommand cxt nm = \case processChatCommand cxt nm $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId - runUpdateGroupProfile user gInfo p' + runUpdateGroupProfile user gInfo p' False UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} -> updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr} ShowGroupProfile gName -> withUser $ \user -> @@ -3037,6 +3227,17 @@ processChatCommand cxt nm = \case updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName) + APISetPublicGroupAccess gId access@PublicGroupAccess {groupDomainClaim = newClaim} -> withUser $ \user -> do + gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> getGroupInfo db cxt user gId + case publicGroup of + Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do + let domainChanged = (claimDomain <$> newClaim) /= (claimDomain <$> (existingAccess >>= groupDomainClaim)) + forM_ (claimDomain <$> newClaim) $ \newDomain -> + when domainChanged $ do + NameRecord {nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) newDomain + unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink + runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} (isJust newClaim && domainChanged) + Nothing -> throwChatError $ CECommandError "not a public group" APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId assertUserGroupRole gInfo GRAdmin @@ -3046,8 +3247,7 @@ processChatCommand cxt nm = \case let userData = encodeShortLinkData $ GroupShortLinkData {groupProfile, publicGroupData = Nothing} userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} crClientData = encodeJSON $ CRDataGroup groupLinkId - -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode + (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode @@ -3087,11 +3287,10 @@ processChatCommand cxt nm = \case when (isJust $ memberContactId m) $ throwCmdError "member contact already exists" subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? - -- TODO [certs rcv] - (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode + (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) -- TODO not sure it is correct to set connections status here? pure $ CRNewMemberContact user ct g m _ -> throwChatError CEGroupMemberNotActive @@ -3149,9 +3348,12 @@ processChatCommand cxt nm = \case -- [incognito] send membership incognito profile p <- presentUserBadge user (incognitoMembershipProfile gInfo) $ 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 + sqSecured <- 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 + SetPublicGroupAccess gName access -> withUser $ \user -> do + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand cxt nm $ APISetPublicGroupAccess groupId access CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand cxt nm $ APICreateGroupLink groupId mRole @@ -3172,7 +3374,7 @@ processChatCommand cxt nm = \case qiId <- getGroupChatItemIdByText db user gId cName quotedMsg (gInfo, qiId,) <$> liftIO (getMessageMentions db user gId msg) let mc = MCText msg - processChatCommand cxt nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + processChatCommand cxt nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing False [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand cxt nm $ APIClearChat (ChatRef CTLocal folderId Nothing) @@ -3213,7 +3415,7 @@ processChatCommand cxt nm = \case chatRef <- getChatRef user chatName case chatRef of ChatRef CTLocal folderId _ -> processChatCommand cxt nm $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] - _ -> withSendRef user chatRef $ \sendRef -> processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] + _ -> withSendRef user chatRef $ \sendRef -> processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage (Just f) (MCFile "")] SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName withSendRef user chatRef $ \sendRef -> do @@ -3222,7 +3424,7 @@ processChatCommand cxt nm = \case fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing False [composedMessage (Just f) (MCImage "" fixedImagePreview)] ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> throwCmdError "TODO" @@ -3261,7 +3463,7 @@ processChatCommand cxt nm = \case (gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getSharedMsgIdByFileId db userId fileId chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion - void . sendGroupMessage user gInfo scope recipients $ XFileCancel sharedMsgId + void . sendGroupMessage user gInfo scope recipients False $ XFileCancel sharedMsgId pure $ CRSndFileCancelled user (Just aci) ftm fts (Just _, _) -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" where @@ -3309,6 +3511,10 @@ processChatCommand cxt nm = \case UpdateProfileImage image -> withUser $ \user@User {profile} -> do let p = (fromLocalProfile profile :: Profile) {image} updateProfile user p + UpdateProfileImageFromFile path -> withUser $ \user@User {profile} -> do + img <- readProfileImageFile path + let p = (fromLocalProfile profile :: Profile) {image = Just img} + updateProfile user p ShowProfileImage -> withUser $ \user@User {profile} -> pure $ CRUserProfileImage user $ fromLocalProfile profile SetUserFeature (ACF f) allowed -> withUser $ \user@User {profile} -> do let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user} @@ -3318,10 +3524,10 @@ processChatCommand cxt nm = \case let prefs' = setPreference f allowed_ $ Just userPreferences updateContactPrefs user ct prefs' SetGroupFeature (AGFNR f) gName enabled -> - updateGroupProfileByName gName $ \p -> + updateGroupProfileByName_ (Just $ toGroupFeature f) gName $ \p -> p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} SetGroupFeatureRole (AGFR f) gName enabled role -> - updateGroupProfileByName gName $ \p -> + updateGroupProfileByName_ (Just $ toGroupFeature f) gName $ \p -> p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} SetGroupMemberAdmissionReview gName reviewAdmissionApplication -> updateGroupProfileByName gName $ \p@GroupProfile {memberAdmission} -> @@ -3457,11 +3663,11 @@ processChatCommand cxt nm = \case (chatRef,) <$> case cType of CTGroup -> withFastStore' $ \db -> getMessageMentions db user chatId msg _ -> pure [] -#if !defined(dbPostgres) checkChatStopped :: CM ChatResponse -> CM ChatResponse checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped) setStoreChanged :: CM () setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True) +#if !defined(dbPostgres) withStoreChanged :: CM () -> CM ChatResponse withStoreChanged a = checkChatStopped $ a >> setStoreChanged >> ok_ #endif @@ -3477,6 +3683,11 @@ processChatCommand cxt nm = \case getGroupAndMemberId user gName mName >>= processChatCommand cxt nm . uncurry cmd getConnectionCode :: ConnId -> CM Text getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) + getChannelMemberCode :: GroupInfo -> GroupMember -> CM Text + getChannelMemberCode GroupInfo {membership} m = + case (memberPubKey membership, memberPubKey m) of + (Just ownKey, Just memKey) -> pure $ channelMemberCode ownKey memKey + _ -> throwCmdError "no member key to compute security code" verifyConnectionCode :: User -> Connection -> Maybe Text -> CM ChatResponse verifyConnectionCode user conn@Connection {connId} (Just code) = do code' <- getConnectionCode $ aConnId conn @@ -3487,6 +3698,16 @@ processChatCommand cxt nm = \case code' <- getConnectionCode $ aConnId conn withFastStore' $ \db -> setConnectionVerified db user connId Nothing pure $ CRConnectionVerified user False code' + verifyChannelMemberCode :: User -> GroupInfo -> GroupMember -> Maybe Text -> CM ChatResponse + verifyChannelMemberCode user g m (Just code) = do + code' <- getChannelMemberCode g m + let verified = sameVerificationCode code code' + when verified . withFastStore' $ \db -> setGroupMemberVerified db user (groupMemberId' m) $ Just code' + pure $ CRConnectionVerified user verified code' + verifyChannelMemberCode user g m _ = do + code' <- getChannelMemberCode g m + withFastStore' $ \db -> setGroupMemberVerified db user (groupMemberId' m) Nothing + pure $ CRConnectionVerified user False code' getSentChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId _scope) msg = case cType of CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg @@ -3527,7 +3748,7 @@ processChatCommand cxt nm = \case joinPreparedConn conn incognitoProfile chatV = do profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode + sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined conn' <- withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus pure (conn', incognitoProfile) @@ -3570,13 +3791,18 @@ processChatCommand cxt nm = \case where cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat} + -- relay-group joins (only via connectToRelay) carry the target relay member in preparedEntity_; + -- its memberId binds the join signature so a sibling relay can't replay it + relayMemberId_ = case preparedEntity_ of + Just (PCEGroup gInfo m) | useRelays' gInfo -> Just (memberId' m) + _ -> Nothing joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" -- TODO [relays] member: refactor joinContact and up avoiding parallel ifs, xContactId is not used xContactId <- mkXContactId xContactId_ localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ PQSupportOn + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ PQSupportOn pure $ CVRSentInvitation conn' incognitoProfile connect' groupLinkId xContactId_ gInfo_ = do let inGroup = isJust groupLinkId @@ -3591,7 +3817,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk 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_ gInfo_ pqSup + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse connectContactViaAddress user@User {userId} incognito ct@Contact {contactId, activeConn} (CCLink cReq shortLink) = @@ -3606,7 +3832,7 @@ processChatCommand cxt nm = \case subMode <- chatReadVar subscriptionMode let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup - void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup + void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing Nothing pqSup ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of @@ -3615,7 +3841,7 @@ processChatCommand cxt nm = \case xContactId <- mkXContactId xContactId_ localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile - void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing PQSupportOn + void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing Nothing PQSupportOn ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile _ -> throwCmdError "contact already has connection" @@ -3627,13 +3853,14 @@ processChatCommand cxt nm = \case r <- tryAllErrors $ do (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink relayLinkData_ <- liftIO $ decodeLinkUserData cData - case (relayLinkData_, linkEntityId) of - (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + relayMemberId <- case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do withFastStore $ \db -> updateRelayMemberData db cxt user relayMember (MemberId entityId) (MemberKey relayKey) p + pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" let cReq = linkConnReq fd relayLinkToConnect = CCLink cReq (Just relayLink) - void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing + void $ connectViaContact user (Just $ PCEGroup gInfo (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing relayMember' <- withFastStore $ \db -> getGroupMember db cxt user (groupId' gInfo) (groupMemberId' relayMember) pure (relayLink, relayMember', r) syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM () @@ -3669,24 +3896,18 @@ processChatCommand cxt nm = \case pure (connId, chatV) mkXContactId :: Maybe XContactId -> CM XContactId mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure - joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> PQSupport -> CM Connection - joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ pqSup = do + joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> Maybe MemberId -> PQSupport -> CM Connection + joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do -- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of - Just gInfo_' -> - let allowSimplexLinks = maybe True groupUserAllowSimplexLinks gInfo_' - in userProfileInGroup' user allowSimplexLinks incognitoProfile + Just gInfo_' -> userProfileInGroup' user gInfo_' incognitoProfile Nothing -> userProfileDirect user incognitoProfile Nothing True - chatEvent <- case gInfo_ of - Just (Just gInfo) | useRelays' gInfo -> do - let GroupInfo {membership = GroupMember {memberId}} = gInfo - memberPubKey <- case groupKeys gInfo of - Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey - Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" - pure $ XMember profileToSend memberId (MemberKey memberPubKey) - _ -> pure $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ - dm <- encodeConnInfoPQ pqSup chatV chatEvent + dm <- case gInfo_ of + Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of + Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend + Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId" + _ -> encodeConnInfoPQ pqSup chatV $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ subMode <- chatReadVar subscriptionMode void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined @@ -3704,10 +3925,12 @@ processChatCommand cxt nm = \case updateProfile :: User -> Profile -> CM ChatResponse updateProfile user p' = updateProfile_ user p' True $ withFastStore $ \db -> updateUserProfile db user p' updateProfile_ :: User -> Profile -> Bool -> CM User -> CM ChatResponse - updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} shouldUpdateAddressData updateUser + updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n', image = img'} shouldUpdateAddressData updateUser | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user | otherwise = do when (n /= n') $ checkValidName n' + checkProfileImageSize img' + checkProfileSize p' -- read contacts before user update to correctly merge preferences contacts <- withFastStore' $ \db -> getUserContacts db cxt user user' <- updateUser @@ -3756,16 +3979,16 @@ processChatCommand cxt nm = \case -- non-incognito (filtered above), so the user's badge is presented; a profile update keeps the badge instead of clearing it ctSndEvent :: ChangedProfileContact -> CM (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json) ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = do - p <- presentUserBadge user' Nothing mergedProfile' - pure (ConnectionId connId, Nothing, XInfo p) + p'' <- presentUserBadge user' Nothing mergedProfile' + pure (ConnectionId connId, Nothing, XInfo p'') ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> (conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId])) setMyAddressData :: User -> UserContactLink -> CM UserContactLink - setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do + setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _, addressSettings} = do conn <- withFastStore $ \db -> getUserAddressConnection db cxt user - shortLinkProfile <- presentUserBadge user Nothing $ userProfileDirect user Nothing Nothing True + shortLinkProfile <- presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) -- TODO [short links] do not save address to server if data did not change, spinners, error handling let userData | isTrue userChatRelay = relayShortLinkData shortLinkProfile @@ -3792,11 +4015,16 @@ processChatCommand cxt nm = \case void (sendDirectContactMessage user ct' $ XInfo p) `catchAllErrors` eToView lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' - runUpdateGroupProfile :: User -> GroupInfo -> GroupProfile -> CM ChatResponse - runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n'} = do + runUpdateGroupProfile :: User -> GroupInfo -> GroupProfile -> Bool -> CM ChatResponse + runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n', image = img'} domainVerified = do assertUserGroupRole gInfo GROwner when (n /= n') $ checkValidName n' - gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo p' + checkProfileImageSize img' + checkGroupProfileSize p' + -- updateGroupProfile clears domain verification; re-set it when the caller already re-resolved the name + gInfo' <- withStore $ \db -> do + g <- updateGroupProfile db user gInfo p' + if domainVerified then liftIO $ setGroupDomainVerified db user g True else pure g msg <- case businessChat of Just BusinessChatInfo {businessId} -> do ms <- withStore' $ \db -> getGroupMembers db cxt user gInfo' @@ -3807,14 +4035,14 @@ processChatCommand cxt nm = \case withStore $ \db -> getGroupMemberByMemberId db cxt user gInfo' businessId let p'' = p' {displayName, fullName, shortDescr, image} :: GroupProfile recipients = filter memberCurrentOrPending oldMs - void $ sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p'') + void $ sendGroupMessage user gInfo' Nothing recipients False (XGrpInfo p'') let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' recipients = filter memberCurrentOrPending newMs - sendGroupMessage user gInfo' Nothing recipients $ XGrpPrefs ps' + sendGroupMessage user gInfo' Nothing recipients False $ XGrpPrefs ps' Nothing -> do void $ setGroupLinkData' nm user gInfo' recipients <- getRecipients - sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p') + sendGroupMessage user gInfo' Nothing recipients False (XGrpInfo p') where getRecipients | useRelays' gInfo' = withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo' @@ -3826,7 +4054,7 @@ processChatCommand cxt nm = \case ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' Nothing) ci] createGroupFeatureChangedItems user cd CISndGroupFeature gInfo gInfo' - pure $ CRGroupUpdated user gInfo gInfo' Nothing (isJust $ signedMsg_ msg) + pure $ CRGroupUpdated user gInfo gInfo' Nothing ((\SndMessage {signedMsg_} -> isJust signedMsg_) msg) checkValidName :: GroupName -> CM () checkValidName displayName = do when (T.null displayName) $ throwChatError CEInvalidDisplayName {displayName, validName = ""} @@ -3843,8 +4071,9 @@ processChatCommand cxt nm = \case assertDeletable gInfo items assertUserGroupRole gInfo GRModerator let msgMemIds = itemsMsgMemIds gInfo items - events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId memId (toMsgScope gInfo <$> chatScopeInfo) False) msgMemIds - mapM_ (sendGroupMessages_ user gInfo ms) events + -- moderation deletes always sign (attributable; avoids the catch-up-moderator divergence) + signedEvents = L.nonEmpty $ map (\(msgId, memId) -> let evt = XMsgDel msgId memId (toMsgScope gInfo <$> chatScopeInfo) False in (groupMsgSigning True gInfo evt, evt)) msgMemIds + mapM_ (sendGroupSignedMessages_ gInfo ms) signedEvents delGroupChatItems user gInfo chatScopeInfo items True where assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () @@ -3878,10 +4107,17 @@ processChatCommand cxt nm = \case then deleteGroupCIs user gInfo chatScopeInfo items m deletedTs else markGroupCIsDeleted user gInfo chatScopeInfo items m deletedTs updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse - updateGroupProfileByName gName update = withUser $ \user -> do + updateGroupProfileByName = updateGroupProfileByName_ Nothing + updateGroupProfileByName_ :: Maybe GroupFeature -> GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse + updateGroupProfileByName_ feature_ gName update = withUser $ \user -> do gInfo@GroupInfo {groupProfile = p} <- withStore $ \db -> getGroupIdByName db user gName >>= getGroupInfo db cxt user - runUpdateGroupProfile user gInfo $ update p + forM_ feature_ $ \feature -> do + let channel = useRelays' gInfo + applicable = if channel then groupFeatureInChannel feature else groupFeatureInRegularGroup feature + unless applicable $ + throwCmdError $ T.unpack (groupFeatureNameText feature) <> " is not available in " <> (if channel then "channels" else "groups") + runUpdateGroupProfile user gInfo (update p) False withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse withCurrentCall ctId action = do (user, ct) <- withStore $ \db -> do @@ -3936,8 +4172,10 @@ processChatCommand cxt nm = \case groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName pure (groupId, groupMemberId) newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> CM GroupInfo - newGroup user incognito gProfile@GroupProfile {displayName} useRelays memberId groupKeys_ publicMemberCount_ = do + newGroup user incognito gProfile@GroupProfile {displayName, image} useRelays memberId groupKeys_ publicMemberCount_ = do checkValidName displayName + checkProfileImageSize image + checkGroupProfileSize gProfile -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_ @@ -3988,9 +4226,8 @@ processChatCommand cxt nm = \case conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode pure (relayMember, conn, groupRelay) let GroupMember {memberRole = userRole, memberId = userMemberId} = membership - allowSimplexLinks = groupUserAllowSimplexLinks gInfo GroupMember {memberId = relayMemberId} = relayMember - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership let relayInv = GroupRelayInvitation { fromMember = MemberIdRole userMemberId userRole, fromMemberProfile = membershipProfile, @@ -3998,7 +4235,7 @@ processChatCommand cxt nm = \case groupLink = groupSLink } dm <- encodeConnInfo $ XGrpRelayInv relayInv - (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode + sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode let newConnStatus = if sqSecured then ConnSndReady else ConnJoined withFastStore' $ \db -> do void $ updateConnectionStatusFromTo db conn ConnPrepared newConnStatus @@ -4066,13 +4303,13 @@ processChatCommand cxt nm = \case pure (gId, chatSettings) _ -> throwCmdError "not supported" processChatCommand cxt nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings - connectPlan :: User -> AConnectionLink -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) - connectPlan user (ACL SCMInvitation cLink) _ sig_ = case cLink of + connectPlan :: User -> AConnectTarget -> PlanResolveMode -> Maybe LinkOwnerSig -> Maybe (Either ChatError NameRecord) -> CM (ACreatedConnLink, Maybe SimplexNameInfo, Maybe SimplexNameInfo, ConnectionPlan) + connectPlan user (ACTarget SCMInvitation (CTInv cLink)) _ sig_ _ = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing Nothing CLShort l -> do let l' = serverShortLink l knownLinkPlans l' >>= \case - Just r -> pure r + Just (createdLink, p) -> pure (createdLink, Nothing, Nothing, p) Nothing -> do (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) @@ -4087,52 +4324,112 @@ processChatCommand cxt nm = \case Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db cxt user l' invitationReqAndPlan cReq sLnk_ cld ov = do plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) - pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) - connectPlan user (ACL SCMContact cLink) resolveKnown sig_ = case cLink of - CLFull cReq -> do + pure (ACCL SCMInvitation (CCLink cReq sLnk_), Nothing, Nothing, plan) + connectPlan user (ACTarget SCMContact ct) resolveMode sig_ nameRec = case ct of + CTDomain d + -- local search only: look up #d then @d in the store, without online name resolution + | resolveMode == PRMNever -> connectPlanNoName $ ChatError CENotResolvedLocally + | otherwise -> + tryAllErrors (withAgent $ \a -> resolveSimplexName a nm (aUserId user) d) >>= \case + Right nr + | isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) -> + (addOther nr <$> connectPlanName NTPublicGroup (Right nr)) `catchAllErrors` \e -> + (addOther nr <$> connectPlanName NTContact (Right nr) `catchAllErrors` \_ -> throwError e) + | isJust (firstNameLink CCTContact (nrSimplexContact nr)) -> + addOther nr <$> connectPlanName NTContact (Right nr) + | otherwise -> connectPlanNoName $ ChatError $ CESimplexDomainNotReady d SDENoValidLink + Left e -> connectPlanNoName e + where + connectPlanName nameType nr_ = connectPlan user connTarget resolveMode sig_ (Just nr_) + where + connTarget = ACTarget SCMContact $ CTShortContact $ CTName $ SimplexNameInfo nameType d + connectPlanNoName e = + connectPlanName NTPublicGroup (Left e) `catchAllErrors` \e' -> + (connectPlanName NTContact (Left e) `catchAllErrors` \_ -> throwError e') + -- the same domain can resolve to both an @ name (contact or business) and a # channel; + -- keyed off the resolved name's type, so a contact name returning a business group still offers the channel + addOther nr (l, planName, _, p) = (l, planName, otherName, p) + where + otherName = case planName of + Just (SimplexNameInfo NTContact _) | isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) -> Just $ SimplexNameInfo NTPublicGroup d + Just (SimplexNameInfo NTPublicGroup _) | isJust (firstNameLink CCTContact (nrSimplexContact nr)) -> Just $ SimplexNameInfo NTContact d + _ -> Nothing + CTFullContact cReq -> do plan <- contactOrGroupRequestPlan user cReq `catchAllErrors` (pure . CPError) - pure (ACCL SCMContact $ CCLink cReq Nothing, plan) - CLShort l@(CSLContact _ ct _ _) -> - case ct of + pure (ACCL SCMContact $ CCLink cReq Nothing, Nothing, Nothing, plan) + CTShortContact nl -> + (\(l, p) -> (l, simplexName_, Nothing, p)) <$> case ctType of CCTContact -> knownLinkPlans >>= \case Just r -> pure r Nothing -> do + when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally + l' <- resolveSLink (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' + contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) + let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_ + linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain + planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing + refreshContact ct' = case (planDomain, linkProfile_) of + (Just _, Just p) -> updateContactFromLinkData user ct' p + _ -> pure ct' + forM_ planDomain $ \nameDomain -> + unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case - Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) + Just ct' | not (contactDeleted ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'')) _ -> do - contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) let ContactLinkData _ UserContactData {owners} = cData ov = verifyLinkOwner rootKey owners l' sig_ plan <- contactRequestPlan user cReq contactSLinkData_ ov - pure (con cReq, plan) + case plan of + CPContactAddress (CAPKnown ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPKnown ct'')) + CPContactAddress (CAPContactViaAddress ct') -> do + ct'' <- refreshContact ct' + pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'')) + _ -> pure (con l' cReq, plan) where + knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan)) knownLinkPlans = withFastStore $ \db -> - liftIO (getUserContactLinkViaShortLink db user l') >>= \case - Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink) + liftIO (getUserContactLinkViaTarget db user nl') >>= \case + Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink) Nothing -> - getContactViaShortLinkToConnect db cxt user l' >>= \case - Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct')) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' + getContactToConnect db cxt user nl' >>= \case + Just (ccl, ct') -> pure $ if contactDeleted ct' then Nothing else Just (ACCL SCMContact ccl, CPContactAddress (CAPKnown ct')) + Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl' CCTGroup -> groupShortLinkPlan CCTChannel -> groupShortLinkPlan CCTRelay -> throwCmdError "chat relay links are not supported in this version" where - l' = serverShortLink l - con cReq = ACCL SCMContact $ CCLink cReq (Just l') - gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) + (nl', simplexName_) = case nl of + CTLink sl -> (CTLink (serverShortLink sl), Nothing) + CTName ni -> (nl, Just ni) + ctType = case nl of + CTLink (CSLContact _ t _ _) -> t + CTName SimplexNameInfo {nameType = NTContact} -> CCTContact + CTName SimplexNameInfo {nameType = NTPublicGroup} -> CCTChannel + resolveSLink = case nl' of + CTLink l' -> pure l' + CTName n -> serverShortLink <$> resolveNameLink n + con l' cReq = ACCL SCMContact $ CCLink cReq (Just l') + gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g False Nothing (ListDef []))) + groupShortLinkPlan :: CM (ACreatedConnLink, ConnectionPlan) groupShortLinkPlan = knownLinkPlans >>= \case Just (_, CPGroupLink (GLPKnown g _ _ _)) - | resolveKnown -> resolveKnownGroup g + | resolveMode == PRMAllGroups -> resolveKnownGroup g Just r -> pure r Nothing -> do + when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally + l' <- resolveSLink (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData if - | not direct && unsupportedGroupType groupSLinkData_ -> pure (con (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) - | not direct && null relays -> pure (con (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + | not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) + | not direct && null relays -> pure (con l' (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) | otherwise -> do let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} @@ -4143,34 +4440,88 @@ processChatCommand cxt nm = \case (Nothing, Nothing) -> pure () _ -> throwChatError CEInvalidConnReq let ov = verifyLinkOwner rootKey owners l' sig_ - plan <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov - pure (con cReq, plan) + glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners + planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing + plan0 <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov glOwners + -- a joined channel is found by link but not by name (its domain is not verified locally, + -- e.g. an un-upgraded relay dropped the claim); refresh its profile from the fresh link + -- data and mark it verified, so the check below passes and future by-name lookups match + plan <- case (planDomain, plan0, groupSLinkData_) of + (Just nameDomain, CPGroupLink (GLPKnown g u o os), Just sLinkData) -> + (\(g', _) -> CPGroupLink (GLPKnown g' u o os)) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain) + _ -> pure plan0 + forM_ planDomain $ \nameDomain -> + let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of + CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _) -> Just groupProfile + CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) -> Just groupProfile + CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) -> Just groupProfile + CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) -> Just groupProfile + _ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_ + in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain + pure (con l' cReq, plan) where unsupportedGroupType = \case Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel _ -> False + knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan)) knownLinkPlans = withFastStore $ \db -> - liftIO (getGroupInfoViaUserShortLink db cxt user l') >>= \case - Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' + liftIO (getGroupInfoViaUserTarget db cxt user nl') >>= \case + Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g)) + Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl' resolveKnownGroup g = do + l' <- resolveSLink (fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData let ov = verifyLinkOwner rk owners l' sig_ glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners (g', updated) <- case groupSLinkData_ of - Just sLinkData -> updateGroupFromLinkData user g sLinkData + Just sLinkData -> updateGroupFromLinkData user g sLinkData Nothing _ -> pure (g, False) - pure (con (linkConnReq fd), CPGroupLink (GLPKnown g' (BoolDef updated) ov (ListDef glOwners))) - connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse - connectWithPlan user@User {userId} incognito ccLink plan + pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' updated ov (ListDef glOwners))) + -- resolve a name to its first contact/channel short link + resolveNameLink :: SimplexNameInfo -> CM (ConnShortLink 'CMContact) + resolveNameLink SimplexNameInfo {nameType, nameDomain} = do + NameRecord {nrSimplexContact, nrSimplexChannel} <- maybe (withAgent $ \a -> resolveSimplexName a nm (aUserId user) nameDomain) (ExceptT . pure) nameRec + let (candidates, ctType') = case nameType of + NTContact -> (nrSimplexContact, CCTContact) + NTPublicGroup -> (nrSimplexChannel, CCTChannel) + maybe (throwChatError $ CESimplexDomainNotReady nameDomain SDENoValidLink) pure $ firstNameLink ctType' candidates + connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> Maybe SimplexNameInfo -> Maybe SimplexNameInfo -> ConnectionPlan -> CM ChatResponse + connectWithPlan user@User {userId} incognito ccLink planSimplexName otherSimplexName plan | connectionPlanProceed plan = do case plan of CPError e -> eToView e; _ -> pure () case plan of CPContactAddress (CAPContactViaAddress Contact {contactId}) -> processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId + CPContactAddress (CAPOk (Just sld) _) | isJust vName -> connectContactViaName sld + CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _) + | ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld _ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink - | otherwise = pure $ CRConnectionPlan user ccLink plan + | otherwise = pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan + where + vName = nameDomain <$> planSimplexName + joinChannelViaRelays :: CreatedLinkContact -> GroupShortLinkData -> CM ChatResponse + joinChannelViaRelays ccl gld = do + GroupInfo {groupId} <- prepareChannelGroup + processChatCommand cxt nm APIConnectPreparedGroup {groupId, incognito, ownerContact = Nothing, msgContent_ = Nothing} + `catchAllErrors` \e -> do + deletePreparedChannel groupId `catchAllErrors` eToView + throwError e + where + prepareChannelGroup = + processChatCommand cxt nm (APIPrepareGroup userId ccl False vName gld) >>= \case + CRNewPreparedChat _ (AChat SCTGroup (Chat (GroupChat gInfo _) _ _)) -> pure gInfo + _ -> throwChatError $ CEException "joinChannelViaRelays: unexpected response from APIPrepareGroup" + deletePreparedChannel groupId = do + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId + deleteGroupConnections user gInfo False + withFastStore' $ \db -> deleteGroup db user gInfo + connectContactViaName :: ContactShortLinkData -> CM ChatResponse + connectContactViaName sld = + processChatCommand cxt nm (APIPrepareContact userId ccLink vName sld) >>= \case + CRNewPreparedChat _ (AChat SCTDirect (Chat (DirectChat Contact {contactId}) _ _)) -> + processChatCommand cxt nm (APIConnectPreparedContact contactId incognito Nothing) + _ -> throwChatError $ CEException "connectContactViaName: unexpected response from APIPrepareContact" invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan invitationRequestPlan user cReq cld ov = do maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov) @@ -4198,54 +4549,58 @@ processChatCommand cxt nm = \case groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of Nothing -> contactRequestPlan user cReq Nothing Nothing - Just _ -> groupJoinRequestPlan user cReq Nothing Nothing Nothing + Just _ -> groupJoinRequestPlan user cReq Nothing Nothing Nothing [] contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan contactRequestPlan user (CRContactUri crData) cld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas + plan p = pure $ CPContactAddress p withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case - Just _ -> pure $ CPContactAddress CAPOwnLink + Just _ -> plan $ CAPOwnLink Nothing -> withFastStore' (\db -> getContactConnEntityByConnReqHash db cxt user cReqHashes) >>= \case Nothing -> withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case - Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) - _ -> pure $ CPContactAddress (CAPOk cld ov) + Just ct | not (contactDeleted ct) -> plan $ CAPContactViaAddress ct + _ -> plan $ CAPOk cld ov Just (RcvDirectMsgConnection Connection {connStatus} Nothing) - | connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk cld ov) - | otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect + | connStatus == ConnPrepared -> plan $ CAPOk cld ov + | otherwise -> plan CAPConnectingConfirmReconnect Just (RcvDirectMsgConnection _ (Just ct)) - | not (contactReady ct) && contactActive ct -> pure $ CPContactAddress (CAPConnectingProhibit ct) - | contactDeleted ct -> pure $ CPContactAddress (CAPOk cld ov) - | otherwise -> pure $ CPContactAddress (CAPKnown ct) + | not (contactReady ct) && contactActive ct -> plan $ CAPConnectingProhibit ct + | contactDeleted ct -> plan $ CAPOk cld ov + | otherwise -> plan $ CAPKnown ct -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing [] Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" - groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov = do + groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan + groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov glOwners = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas + plan p = pure $ CPGroupLink p withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas) >>= \case - Just g -> pure $ CPGroupLink (GLPOwnLink g) + Just g -> plan $ GLPOwnLink g Nothing -> do connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes case (gInfo_, connEnt_) of - (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov) + (Nothing, Nothing) -> plan $ GLPOk linkInfo gld ov -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) - (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect + (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> plan $ GLPConnectingConfirmReconnect (Nothing, Just (RcvDirectMsgConnection _ (Just ct))) - | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) - | otherwise -> pure $ CPGroupLink (GLPOk linkInfo gld ov) + | not (contactReady ct) && contactActive ct -> plan $ GLPConnectingProhibit gInfo_ + | otherwise -> plan $ GLPOk linkInfo gld ov (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov - groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} linkInfo gld ov - | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) + (Just gInfo, _) -> groupPlan gInfo linkInfo gld ov glOwners + groupPlan :: GroupInfo -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} linkInfo gld ov glOwners + | memberStatus membership == GSMemRejected = plan $ GLPKnown gInfo False ov (ListDef glOwners) | not (memberActive membership) && not (memberRemoved membership) = - pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) - | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo (BoolDef False) ov (ListDef [])) - | otherwise = pure $ CPGroupLink (GLPOk linkInfo gld ov) + plan $ GLPConnectingProhibit $ Just gInfo + | memberActive membership = plan $ GLPKnown gInfo False ov (ListDef glOwners) + | otherwise = plan $ GLPOk linkInfo gld ov + where + plan p = pure $ CPGroupLink p contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -4257,6 +4612,25 @@ processChatCommand cxt nm = \case serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> Maybe MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig + mkLinkOwnerSig privKey connLink ownerMemberId (cbTag, bindingData) = + let ownerId = (\MemberId {unMemberId} -> B64UrlByteString unMemberId) <$> ownerMemberId + cb = encodeChatBinding cbTag bindingData + ownerSig = C.sign' privKey $ cb <> smpEncode connLink + in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} + shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) + shareChatBinding u = \case + SRDirect contactId -> do + ct <- withFastStore $ \db -> getContact db cxt u contactId + forM (contactConn ct) $ \conn -> + (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + SRGroup toGroupId _ asGroup -> do + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId + pure $ mkBinding m <$> publicGroup + where + mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} + | asGroup = (CBChannel, smpEncode pgId) + | otherwise = (CBGroup, smpEncode (pgId, memberId)) verifyLinkOwner :: ConnectionModeI m => C.PublicKeyEd25519 -> [OwnerAuth] -> ConnShortLink m -> Maybe LinkOwnerSig -> Maybe OwnerVerification verifyLinkOwner rootKey owners connLink = fmap $ \LinkOwnerSig {ownerId, chatBinding = B64UrlByteString bindingBytes, ownerSig} -> @@ -4362,21 +4736,22 @@ processChatCommand cxt nm = \case quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) quoteData _ = throwError SEInvalidQuote - sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages user gInfo scope showGroupAsSender live itemTTL cmrs = do + sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages user gInfo scope showGroupAsSender live itemTTL sign cmrs = do assertMultiSendable live cmrs chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope recipients <- getGroupRecipients cxt user gInfo chatScopeInfo modsCompatVersion - sendGroupContentMessages_ user gInfo scope showGroupAsSender chatScopeInfo recipients live itemTTL cmrs + sendGroupContentMessages_ user gInfo scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs where hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs modsCompatVersion = if hasReport then contentReportsVersion else groupKnockingVersion - sendGroupContentMessages_ :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse - sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} scope showGroupAsSender chatScopeInfo recipients live itemTTL cmrs = do + sendGroupContentMessages_ :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse + sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs = do forM_ allowedRole $ assertUserGroupRole gInfo assertGroupContentAllowed processComposedMessages where + signMsgs = sign || groupFeatureAllowed SGFSignMessages gInfo allowedRole :: Maybe GroupMemberRole allowedRole = case scope of Nothing -> Just GRAuthor @@ -4401,7 +4776,7 @@ processChatCommand cxt nm = \case (fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length recipients) timed_ <- sndGroupCITimed live gInfo itemTTL (chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_ - (msgs_, gsr) <- sendGroupMessages user gInfo Nothing showGroupAsSender recipients chatMsgEvents + (msgs_, gsr) <- sendGroupMessages user gInfo Nothing showGroupAsSender recipients signMsgs chatMsgEvents let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_) cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) showGroupAsSender itemsData timed_ live when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" @@ -4420,7 +4795,11 @@ processChatCommand cxt nm = \case let User {profile = LocalProfile {localBadge}} = user fileSize <- checkSndFile (if incognitoMembership gInfo then Nothing else localBadge) file (fInv, ciFile) <- xftpSndFileTransfer user file fileSize n $ CGGroup gInfo recipients - pure (Just fInv, Just ciFile) + fInv' <- + if signMsgs && useRelays' gInfo + then (\d -> (fInv :: FileInvitation) {fileDigest = Just d}) <$> cryptoFileDigest file + else pure fInv + pure (Just fInv', Just ciFile) Nothing -> pure (Nothing, Nothing) prepareMsgs :: NonEmpty (ComposedMessageReq, Maybe FileInvitation) -> Maybe CITimed -> CM (NonEmpty (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))) prepareMsgs cmsFileInvs timed_ = withFastStore $ \db -> @@ -4583,11 +4962,56 @@ processChatCommand cxt nm = \case gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId a $ SRGroup gId scope (sendAsGroup' gInfo scope) _ -> throwCmdError "not supported" + preparedGroupFromLink :: User -> CreatedLinkContact -> DirectLink -> GroupShortLinkData -> Maybe SharedMsgId -> Maybe SimplexDomain -> CM (GroupInfo, Maybe GroupMember) + preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId verifiedDomain = do + let GroupShortLinkData {groupProfile = gp, publicGroupData = publicGroupData_} = groupSLinkData + publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ + useRelays = not direct + subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember + gVar <- asks random + withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ verifiedDomain + getSharedMsgId :: CM SharedMsgId getSharedMsgId = do gVar <- asks random liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12 +firstNameLink :: ContactConnType -> [Text] -> Maybe (ConnShortLink 'CMContact) +firstNameLink ctType = foldr (\t r -> nameLink t <|> r) Nothing + where + nameLink t = case strDecode @(ConnShortLink 'CMContact) (encodeUtf8 t) of + Right sl@(CSLContact _ ct _ _) | ct == ctType -> Just sl + _ -> Nothing + +nameResolvesTo :: ConnShortLink 'CMContact -> [Text] -> Bool +nameResolvesTo sLnk = any (either (const False) (sameShortLinkContact sLnk) . strDecode . encodeUtf8) + +verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text) +verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, proof = proof_} connLink_ = case (proof_, connLink_) of + (Nothing, _) -> pure (Nothing, Just "no name proof to verify") + (_, Nothing) -> pure (Nothing, Just "no connection link to check the name against") + (Just proof, Just (ACSL SCMContact profileSLnk)) -> do + NameRecord {nrSimplexContact, nrSimplexChannel} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain + let resolvedLinks = case nameType of + NTContact -> nrSimplexContact + NTPublicGroup -> nrSimplexChannel + if not (nameResolvesTo profileSLnk resolvedLinks) + then pure (Just False, Just "the name does not resolve to this address") + else do + ok <- verifyDomainProof proof profileSLnk + pure (Just ok, if ok then Nothing else Just "the name proof was not signed by this address's owner") + (Just _, Just _) -> pure (Nothing, Just "unexpected connection link type for name verification") + where + verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool + verifyDomainProof SimplexDomainProof {linkOwnerId, presHeader, signature} sLnk@(CSLContact _ ct srv key) = do + (FixedLinkData {rootKey}, ContactLinkData _ UserContactData {owners}) <- getShortLinkConnReq nm user sLnk + let ownerKey_ = case linkOwnerId of + Nothing -> Just rootKey + Just (StrJSON oid) -> ownerKey <$> find (\OwnerAuth {ownerId} -> ownerId == oid) owners + pure $ maybe False (\k -> C.verify' k signature proofPayload) ownerKey_ + where + proofPayload = strEncode (Str "simplex_domain_v1", presHeader, domain, ct, srv, key) + data ConnectViaContactResult = CVRConnectedContact Contact | CVRSentInvitation Connection (Maybe Profile) @@ -4736,7 +5160,7 @@ agentSubscriber = do q <- asks $ subQ . smpAgent forever (atomically (readTBQueue q) >>= process) `catchOwn` \e -> do - eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) (AgentConnId "") Nothing + eToView' $ chatErrorAgent $ CRITICAL True $ "Message reception stopped: " <> show e E.throwIO e where process :: (ACorrId, AEntityId, AEvt) -> CM' () @@ -4748,7 +5172,7 @@ agentSubscriber = do where run action = action `catchAllOwnErrors'` eToView' -type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) +type AgentSubResult = Map ConnId (Either AgentErrorType ()) cleanupManager :: CM () cleanupManager = do @@ -4787,6 +5211,8 @@ cleanupManager = do liftIO $ threadDelay' stepDelay cleanupStaleRelayTestConns user `catchAllErrors` eToView liftIO $ threadDelay' stepDelay + cleanupRemovedMembers user `catchAllErrors` eToView + liftIO $ threadDelay' stepDelay cleanupTimedItems cleanupInterval user = do ts <- liftIO getCurrentTime let startTimedThreadCutoff = addUTCTime cleanupInterval ts @@ -4813,6 +5239,13 @@ cleanupManager = do forM_ staleConns $ \acId -> do deleteAgentConnectionAsync acId withStore' $ \db -> deleteConnectionByAgentConnId db user acId + cleanupRemovedMembers user = do + cxt <- chatStoreCxt + ts <- liftIO getCurrentTime + let cutoffTs = addUTCTime (-nominalDay) ts + removedMembers <- withStore' $ \db -> getRemovedMembersToCleanup db cxt user cutoffTs + forM_ removedMembers $ \m -> + withStore' (\db -> deleteGroupMember db user m) `catchAllErrors` eToView cleanupMessages = do ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-(30 * nominalDay)) ts @@ -4862,10 +5295,11 @@ runRelayGroupLinkChecks user = do then do -- TODO [relays] emit event to UI when relay own status promoted to RSActive -- CEvtGroupRelayUpdated requires GroupRelay (owner-side), not available on relay side - void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSAccepted RSActive + void $ withStore' $ \db -> updateRelayOwnStatus_ db gInfo RSActive else void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSActive RSInactive _ -> pure () _ -> pure () + sendRelayCapIfNeeded user gInfo checkRelayInactiveGroups = do cxt <- chatStoreCxt ttl <- asks (relayInactiveTTL . config) @@ -4963,6 +5397,7 @@ chatCommandP = "/unhide user " *> (UnhideUser <$> pwdP), "/mute user" $> MuteUser, "/unmute user" $> UnmuteUser, + "/set client service " *> (SetClientService <$> A.decimal <* A.char ':' <*> displayNameP <* A.space <*> onOffP), "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), "/delete user " *> (DeleteUser <$> displayNameP <*> pure True <*> optional (A.space *> pwdP)), ("/user" <|> "/u") $> ShowActiveUser, @@ -5015,7 +5450,7 @@ chatCommandP = "/_get content types " *> (APIGetChatContentTypes <$> chatRefP), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> textP)), "/_get item info " *> (APIGetChatItemInfo <$> chatRefP <* A.space <*> A.decimal), - "/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), + "/_send " *> (APISendMessages <$> sendRefP <*> liveMessageP <*> sendMessageTTLP <*> signMessagesP <*> (" json " *> jsonP <|> " text " *> composedMessagesTextP)), "/_create tag " *> (APICreateChatTag <$> jsonP), "/_tags " *> (APISetChatTags <$> chatRefP <*> optional _strP), "/_delete tag " *> (APIDeleteChatTag <$> A.decimal), @@ -5034,6 +5469,7 @@ chatCommandP = "/_forward plan " *> (APIPlanForwardChatItems <$> chatRefP <*> _strP), "/_forward " *> (APIForwardChatItems <$> chatRefP <*> (" as_group=" *> onOffP <|> pure False) <* A.space <*> chatRefP <*> _strP <*> sendMessageTTLP), "/_share chat content " *> (APIShareChatMsgContent <$> chatRefP <* A.space <*> sendRefP), + "/_share address " *> (APIShareMyAddress <$> sendRefP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP), @@ -5053,6 +5489,7 @@ chatCommandP = "/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP), "/_call get" $> APIGetCallInvitations, "/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP), + "/_set domain " *> (APISetUserDomain <$> A.decimal <*> optional (A.space *> strP)), "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias #" *> (APISetGroupAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), @@ -5188,10 +5625,12 @@ chatCommandP = "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayNameP <* A.space <*> groupProfile), ("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayNameP), + "/public group access " *> char_ '#' *> (SetPublicGroupAccess <$> displayNameP <*> publicGroupAccessP), "/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> optional (A.space *> msgTextP)), "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)), "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), + "/_public group access #" *> (APISetPublicGroupAccess <$> A.decimal <* A.space <*> jsonP), "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), @@ -5208,9 +5647,9 @@ chatCommandP = (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayNameP <* A.space <* char_ '@' <*> (Just <$> displayNameP) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> onOffP) <|> pure False) <*> optional (" sig=" *> jsonP)), - "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), - "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <* A.space <*> jsonP), + "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> ((" resolve=" *> planResolveModeP) <|> pure PRMUnknown) <*> optional (" sig=" *> jsonP)), + "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <*> optional (" domain=" *> strP) <* A.space <*> jsonP), + "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <*> (" direct=" *> onOffP <|> pure True) <*> optional (" domain=" *> strP) <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal), "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), @@ -5221,11 +5660,14 @@ chatCommandP = "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), ("/connect" <|> "/c") *> (AddContact <$> incognitoP), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), + "/_verify domain @" *> (APIVerifyContactDomain <$> A.decimal), + "/_verify domain #" *> (APIVerifyGroupDomain <$> A.decimal), ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardLocalMessage <$> chatNameP <* " <- * " <*> msgTextP, "/share chat #" *> (SharePublicGroup <$> displayNameP <* A.space <*> chatNameP), + "/share address " *> (ShareMyAddress <$> chatNameP), SendMessage <$> sendNameP <* A.space <*> msgTextP, "@#" *> (SendMemberContactMessage <$> displayNameP <* A.space <* char_ '@' <*> displayNameP <* A.space <*> msgTextP), "/accept_member_contact @" *> (AcceptMemberContact <$> displayNameP), @@ -5255,7 +5697,7 @@ chatCommandP = ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), - "/_address " *> (APICreateMyAddress <$> A.decimal), + "/_address " *> (APICreateMyAddress <$> A.decimal <*> optional (A.space *> strP)), ("/address" <|> "/ad") $> CreateMyAddress, "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), ("/delete_address" <|> "/da") $> DeleteMyAddress, @@ -5270,6 +5712,7 @@ chatCommandP = ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP), ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, ("/welcome" <|> "/w") $> Welcome, + "/set profile image file " *> (UpdateProfileImageFromFile <$> filePath), "/set profile image " *> (UpdateProfileImage . Just . ImageData <$> imageP), "/delete profile image" $> UpdateProfileImage Nothing, "/show profile image" $> ShowProfileImage, @@ -5296,6 +5739,7 @@ chatCommandP = "/set disappear @" *> (SetContactTimedMessages <$> displayNameP <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), "/set reports #" *> (SetGroupFeature (AGFNR SGFReports) <$> displayNameP <*> _strP), + "/set signatures #" *> (SetGroupFeature (AGFNR SGFSignMessages) <$> displayNameP <*> _strP), "/set support #" *> (SetGroupFeature (AGFNR SGFSupport) <$> displayNameP <*> (A.space *> strP)), "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayNameP <*> _strP <*> optional memberRole), "/set admission review #" *> (SetGroupMemberAdmissionReview <$> displayNameP <*> (A.space *> memberCriteriaP)), @@ -5388,12 +5832,19 @@ chatCommandP = pure [composedMessage Nothing text] updatedMessagesTextP = (`UpdatedMessage` []) <$> mcTextP liveMessageP = " live=" *> onOffP <|> pure False + signMessagesP = " sign=" *> onOffP <|> pure False sendMessageTTLP = " ttl=" *> ((Just <$> A.decimal) <|> ("default" $> Nothing)) <|> pure Nothing receiptSettings = do enable <- onOffP clearOverrides <- (" clear_overrides=" *> onOffP) <|> pure False pure UserMsgReceiptSettings {enable, clearOverrides} onOffP = ("on" $> True) <|> ("off" $> False) + publicGroupAccessP = do + groupWebPage <- optional (" web=" *> (safeDecodeUtf8 <$> A.takeTill A.isSpace)) + groupDomain <- optional (" domain=" *> strP) + domainWebPage <- (" domain_page=" *> onOffP) <|> pure False + allowEmbedding <- (" embed=" *> onOffP) <|> pure False + pure PublicGroupAccess {groupWebPage, groupDomainClaim = mkDomainClaim <$> groupDomain, domainWebPage, allowEmbedding} profileNameDescr = (,) <$> displayNameP <*> shortDescrP -- 'Help with bot':'link ','Menu of commands':[...] botCommandsP :: Parser [ChatBotCommand] @@ -5411,18 +5862,20 @@ chatCommandP = k : ws -> pure (k, if null ws then Nothing else Just $ T.unwords ws) pure CBCCommand {label, keyword, params} quoted = A.char '\'' *> A.takeTill (== '\'') <* A.char '\'' - newUserP userChatRelay = do + newUserP relay = do (cName, shortDescr) <- profileNameDescr - let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} - pure NewUser {profile, pastTimestamp = False, userChatRelay} + service <- (" service=" *> onOffP) <|> pure False + let profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service} newBotUserP = do files_ <- optional $ "files=" *> onOffP <* A.space + service <- ("service=" *> onOffP <* A.space) <|> pure False (cName, shortDescr) <- profileNameDescr let preferences = case files_ of Just True -> Nothing _ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}} - profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing} - pure NewUser {profile, pastTimestamp = False, userChatRelay = False} + profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing} + pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do @@ -5533,9 +5986,9 @@ chatCommandP = srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',') where srvRoles = \case - "off" -> Right $ ServerRoles False False - "proxy" -> Right ServerRoles {storage = False, proxy = True} - "storage" -> Right ServerRoles {storage = True, proxy = False} + "off" -> Right $ ServerRoles False False False + "proxy" -> Right ServerRoles {storage = False, proxy = True, names = False} + "storage" -> Right ServerRoles {storage = True, proxy = False, names = False} "on" -> Right allRoles _ -> Left "bad ServerRoles" netCfgP = do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 79fff87e5b..6fb002eaee 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -53,13 +53,14 @@ import Data.Text.Encoding (encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Calendar (fromGregorian) import Data.Time.Clock (UTCTime (..), diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds, secondsToDiffTime) -import Simplex.Chat.Badges (BadgeCredential (..), BadgePresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Badges (BadgeCredential (..), ProofPresHeader (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), badgeProof, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Files import Simplex.Chat.Markdown import Simplex.Chat.Messages -import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBinaryBatch, encodeFwdElement) +import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchElements, batchMessages, encodeBatchElement, encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Operators @@ -80,6 +81,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Util (encryptFile, shuffle) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD +import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.Messaging.Agent @@ -90,7 +92,7 @@ import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..)) -import Simplex.Messaging.Compression (compressionLevel) +import Simplex.Messaging.Compression (compressionLevel, limitDecompress') import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -341,7 +343,7 @@ prohibitedGroupContent :: GroupInfo -> GroupMember -> Maybe GroupChatScopeInfo - prohibitedGroupContent gInfo@GroupInfo {membership = mem@GroupMember {memberRole = userRole}} m scopeInfo mc ft file_ sent | not supportAllowed = Just GFSupport | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) && not hostApprovalVoice = Just GFVoice - | isNothing scopeInfo && not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles + | isNothing scopeInfo && not (isVoice mc) && (isJust file_ || isMedia mc) && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles | isNothing scopeInfo && isReport mc && (badReportUser || not (groupFeatureAllowed SGFReports gInfo)) = Just GFReports | isNothing scopeInfo && prohibitedSimplexLinks gInfo m mc ft = Just GFSimplexLinks | otherwise = Nothing @@ -395,6 +397,12 @@ xftpSndFileTransfer_ user file@(CryptoFile filePath cfArgs) fileSize n contactOr ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} pure (fInv, ciFile, ft) +cryptoFileDigest :: CryptoFile -> CM FD.FileDigest +cryptoFileDigest (CryptoFile filePath cfArgs) = do + fsPath <- lift $ toFSFilePath filePath + r <- liftIO $ runExceptT $ CF.readFile (CryptoFile fsPath cfArgs) + either (throwChatError . CEInternalError . show) (pure . FD.FileDigest . LC.sha512Hash) r + xftpSndFileRedirect :: User -> FileTransferId -> ValidFileDescription 'FRecipient -> CM FileTransferMeta xftpSndFileRedirect user ftId vfd = do let fileName = "redirect.yaml" @@ -516,22 +524,20 @@ updateACIGroupInfo gInfo' = \case AChatItem SCTGroup dir (GroupChat gInfo' chatScopeInfo) ci aci -> aci -deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM () -deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do - deletedTs <- liftIO getCurrentTime - filesInfo <- withStore' $ \db -> deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs +deleteGroupMemberCIs :: User -> GroupInfo -> GroupMember -> CM () +deleteGroupMemberCIs user gInfo member = do + filesInfo <- withStore' $ \db -> deleteGroupMemberCIs_ db user gInfo member deleteCIFiles user filesInfo -deleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () -deleteGroupMembersCIs user gInfo members byGroupMember = do - deletedTs <- liftIO getCurrentTime - filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> deleteGroupMemberCIs_ db user gInfo m byGroupMember SMDRcv deletedTs +deleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> CM () +deleteGroupMembersCIs user gInfo members = do + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ deleteGroupMemberCIs_ db user gInfo deleteCIFiles user filesInfo -deleteGroupMemberCIs_ :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO [CIFileInfo] -deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs = do +deleteGroupMemberCIs_ :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO [CIFileInfo] +deleteGroupMemberCIs_ db user gInfo member = do fs <- getGroupMemberFileInfo db user gInfo member - updateMemberCIsModerated db user gInfo member byGroupMember msgDir deletedTs + deleteMemberCIs db user gInfo member pure fs deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse @@ -702,7 +708,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI ci <- xftpAcceptRcvFT db cxt user fileId filePath userApproved rfd <- getRcvFileDescrByRcvFileId db fileId pure (ci, rfd) - receiveViaCompleteFD user fileId rfd userApproved cryptoArgs + receiveViaCompleteFD user fileId rfd fileSize userApproved cryptoArgs pure ci (Nothing, Just _fileConnReq) -> throwChatError $ CEException "accepting file via a separate connection is deprecated" -- group & direct file protocol @@ -744,14 +750,21 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) ) -receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs = +receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Integer -> Bool -> Maybe CryptoFileArgs -> CM () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} expectedFileSize userApprovedRelays cfArgs = when fileDescrComplete $ do rd <- parseFileDescription fileDescrText + let FD.ValidFileDescription FD.FileDescription {size = FD.FileSize encSize, redirect} = rd + redirectSize = maybe 0 (\FD.RedirectFileInfo {size = FD.FileSize s} -> toInteger s) redirect + -- for a redirect, encSize is the description blob and redirectSize the final file; take the larger + rcvSize = max (toInteger encSize) redirectSize + -- 10 MB margin: encryption and chunk-size rounding make the transfer larger than the advertised size + maxRcvSize = min expectedFileSize (toInteger FD.maxFileSizeHard) + toInteger (FD.mb 10 :: Int64) + when (rcvSize > maxRcvSize) $ throwChatError $ CEFileRcvChunk "declared file size exceeds the file invitation size" if userApprovedRelays then receive' rd True else do - let srvs = fileServers rd + let srvs = fileDescrServers rd unknownSrvs <- getUnknownSrvs srvs let approved = null unknownSrvs ifM @@ -764,9 +777,6 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved startReceivingFile user fileId withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) - fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer] - fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) = - S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do knownSrvs <- L.map protoServer' <$> getKnownAgentServers SPXFTP user @@ -909,8 +919,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId pure (ct, conn, ExistingIncognito <$> incognitoProfile) profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - -- TODO [certs rcv] - (ct,conn,) . fst <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode) + (ct,conn,) <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact acceptContactRequestAsync @@ -923,17 +932,19 @@ acceptContactRequestAsync profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True cxt <- chatStoreCxt let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - (cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV + (cmdId, acId) <- prepareAgentAccept user True cReqInvId cReqPQSup currentTs <- liftIO getCurrentTime - withStore $ \db -> do + ct' <- withStore $ \db -> do forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId getContact db cxt user contactId + agentAcceptContactAsync cmdId acId True cReqInvId (XInfo profileToSend) cReqPQSup chatV subMode + pure ct' -acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember acceptGroupJoinRequestAsync - user + user@User {userId} uclId gInfo@GroupInfo {groupProfile, membership, businessChat} cReqInvId @@ -945,12 +956,22 @@ acceptGroupJoinRequestAsync gAccepted gLinkMemRole incognitoProfile - memberKey_ = do + memberKey_ + existingMem_ = do gVar <- asks random let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted + -- a roster-established privileged member attaches a connection to its existing record (keeping + -- owner-authoritative role + key); everyone else is created fresh with the group-link role cxt <- chatStoreCxt - (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_ + (groupMemberId, memberId) <- case existingMem_ of + Just m -> do + -- refresh the hash placeholder name from the authenticated join profile; role + key stay roster-authoritative + withStore $ \db -> do + liftIO $ updateGroupMemberStatus db userId m initialStatus + void $ updateMemberProfile db cxt user m cReqProfile + pure (groupMemberId' m, memberId' m) + Nothing -> withStore $ \db -> + createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_ let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile) GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -967,10 +988,12 @@ acceptGroupJoinRequestAsync } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff + m <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + pure m acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember acceptGroupJoinSendRejectAsync @@ -997,10 +1020,12 @@ acceptGroupJoinSendRejectAsync } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user False cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user False cReqInvId PQSupportOff + m <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId + agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff chatV subMode + pure m acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember) acceptBusinessJoinRequestAsync @@ -1024,15 +1049,16 @@ acceptBusinessJoinRequestAsync -- This refers to the "title member" that defines the group name and profile. -- This coincides with fromMember to be current user when accepting the connecting user, -- but it will be different when inviting somebody else. - business = Just $ BusinessChatInfo {chatType = BCBusiness, businessId = userMemberId, customerId = memberId}, + business = Just $ BusinessChatInfo {chatType = BCBusiness, businessId = userMemberId, customerId = memberId, businessDomain = Nothing}, groupSize = Just 1 } subMode <- chatReadVar subscriptionMode let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff withStore' $ \db -> do forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId - createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode let cd = CDGroupSnd gInfo Nothing -- TODO [short links] move to profileContactRequest? createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing @@ -1049,17 +1075,20 @@ acceptRelayJoinRequestAsync cReqInvId cReqChatVRange relayLink = do - -- TODO [channel web] derive RelayCapabilities from relay config (RelayWebOptions) - let msg = XGrpRelayAcpt relayLink defaultRelayCapabilities + ChatConfig {webPreviewConfig} <- asks config + let webDomain_ = (\WebPreviewConfig {webDomain} -> webDomain) <$> webPreviewConfig + msg = XGrpRelayAcpt relayLink RelayCapabilities {webDomain = webDomain_} subMode <- chatReadVar subscriptionMode cxt <- chatStoreCxt let chatV = vr cxt `peerConnChatVersion` cReqChatVRange - connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV - withStore $ \db -> do - liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + (cmdId, acId) <- prepareAgentAccept user True cReqInvId PQSupportOff + r <- withStore $ \db -> do + liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted ownerMember' <- getGroupMemberById db cxt user groupMemberId pure (gInfo', ownerMember') + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + pure r rejectRelayInvitationAsync :: User @@ -1079,13 +1108,14 @@ rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange init subMode <- chatReadVar subscriptionMode chatVR <- chatVersionRange let chatV = chatVR `peerConnChatVersion` reqChatVRange - connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV + (cmdId, acId) <- prepareAgentAccept user False invId PQSupportOff withStore' $ \db -> - createJoiningMemberConnection db user uclId connIds chatV reqChatVRange groupMemberId subMode + createJoiningMemberConnection db user uclId (cmdId, acId) chatV reqChatVRange groupMemberId subMode + agentAcceptContactAsync cmdId acId False invId msg PQSupportOff chatV subMode businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile -businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = - GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} +businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences = + GroupProfile {displayName, fullName, description, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} introduceToModerators :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators cxt user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do @@ -1164,26 +1194,61 @@ memberIntroEvt gInfo reMember = mRestrictions = memberRestrictions reMember in XGrpMemIntro mInfo mRestrictions +-- Forward the saved owner-signed roster verbatim (reusing its signed shared_msg_id), then the +-- blob chunks, so the recipient verifies the owner signature. +serveRoster :: User -> GroupInfo -> GroupMember -> CM () +serveRoster user gInfo member = + when (member `supportsVersion` groupRosterVersion) $ do + cxt <- chatStoreCxt + withStore' (\db -> getStoredGroupRoster db gInfo) >>= \case + Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_, storedVer_) -> + case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of + Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e + Right chatMsg@ChatMessage {msgId} -> + withStore' (\db -> runExceptT $ getGroupMemberById db cxt user ownerGMId) >>= \case + Right owner -> do + let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' owner) (memberShortenedName owner), fwdBrokerTs = brokerTs} + sendFwdMemberMessage member fwd (VMSigned MSSVerified sm chatMsg) + forM_ ((,) <$> msgId <*> blob_) $ \(sid, blob) -> + sendInlineBlobChunks user gInfo [member] sid blob + -- record the blob's own stored version as served, not roster_version (the gate): a delta can + -- advance the gate past the stored blob on a failed blob send, and recording the gate would + -- over-claim what this member was actually served, suppressing legitimate catch-up + forM_ storedVer_ $ \v -> withStore' $ \db -> setMemberRosterServedVersion db member v + Left e -> logError $ "serveRoster: roster owner not found: " <> tshow e + Nothing -> pure () + -- Used in groups with relays to introduce moderators and above to a new member, -- and to announce the new member to moderators and above. -- This doesn't create introduction records in db, compared to above methods. introduceInChannel :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" -introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn} = do - modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo +introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do + (owners, adminsMods) <- withStore' $ \db -> + (,) <$> getGroupOwners db cxt user gInfo <*> getGroupAdminsMods db cxt user gInfo + let modMs = owners <> adminsMods void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing - let introEvts = map (memberIntroEvt gInfo) modMs - forM_ (L.nonEmpty introEvts) $ \introEvts' -> - sendGroupMemberMessages user gInfo conn introEvts' + withStore' $ \db -> + setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs] + -- owner intros first so the joiner has the owner profile loaded before applying the saved roster (signed by the owner) + sendIntros owners + serveRoster user gInfo subscriber + sendIntros adminsMods + withStore' $ \db -> + setMembersVectorsNewRelation db modMs subscriberIdx IDSubjectIntroduced MRIntroduced + where + sendIntros ms = forM_ (L.nonEmpty $ map (memberIntroEvt gInfo) ms) $ \evts -> + sendGroupMemberMessages user gInfo conn evts userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile -userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks +userProfileInGroup user g = userProfileInGroup' user (Just g) {-# INLINE userProfileInGroup #-} -userProfileInGroup' :: User -> Bool -> Maybe Profile -> Profile -userProfileInGroup' User {profile = p} allowSimplexLinks incognitoProfile = +-- Nothing group ⇒ no redaction (e.g. joining via a link with no group profile yet). +userProfileInGroup' :: User -> Maybe GroupInfo -> Maybe Profile -> Profile +userProfileInGroup' User {profile = p} mg incognitoProfile = let p' = fromMaybe (fromLocalProfile p) incognitoProfile - in redactedMemberProfile allowSimplexLinks p' + in maybe p' (\g -> redactedMemberProfile g (membership g) p') mg memberInfo :: GroupInfo -> GroupMember -> MemberInfo memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn} = @@ -1191,40 +1256,92 @@ memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, a { memberId, memberRole, v = ChatVersionRange . peerChatVRange <$> activeConn, - profile = redactedMemberProfile allowSimplexLinks $ fromLocalProfile memberProfile, + profile = redactedMemberProfile g m $ fromLocalProfile memberProfile, memberKey = MemberKey <$> memberPubKey } - where - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && groupFeatureMemberAllowed SGFDirectMessages m g -redactedMemberProfile :: Bool -> Profile -> Profile -redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDescr, image, peerType, badge} = - Profile {displayName, fullName, shortDescr = removeSimplexLink =<< shortDescr, image, contactLink = Nothing, preferences = Nothing, peerType, badge} +redactedMemberProfile :: GroupInfo -> GroupMember -> Profile -> Profile +redactedMemberProfile g m Profile {displayName, fullName, shortDescr, description, image, contactLink = lnk, peerType, badge, contactDomain} = + Profile {displayName, fullName, shortDescr = removeSimplexLink True =<< shortDescr, description = removeSimplexLink False =<< description, image, contactLink, preferences = Nothing, peerType, badge, contactDomain = redactedDomain} where - removeSimplexLink s + contactLink = if allowSimplexLinks then lnk else Nothing + redactedDomain = if allowDirect then (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain else Nothing + allowDirect = groupFeatureMemberAllowed SGFDirectMessages m g + allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m g && allowDirect + removeSimplexLink dropOnLink s | allowSimplexLinks = Just s - | hasObfuscatedSimplexLink s = Nothing - | otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s + | otherwise = case parseMaybeMarkdownList s of + Nothing -> dropObfuscated + Just fts + | not (any ftIsSimplexLink fts) -> dropObfuscated + | dropOnLink || T.null (T.strip kept) || hasObfuscatedSimplexLink kept -> Nothing + | otherwise -> Just kept + where + kept = T.concat $ map (\(FormattedText _ t) -> t) $ filter (not . ftIsSimplexLink) fts + where + dropObfuscated = if hasObfuscatedSimplexLink s then Nothing else Just s + +-- Roles carried by the roster; owners are on the link, not the roster. +isRosterRole :: GroupMemberRole -> Bool +isRosterRole r = r == GRMember || r == GRModerator || r == GRAdmin + +isPrivilegedRole :: GroupMemberRole -> Bool +isPrivilegedRole r = r >= GRMember + +-- Minimum role allowed to change a member's role from `from` to `to` (moderators only up to member; relay checked separately). +roleRequiredToChange :: GroupMemberRole -> GroupMemberRole -> GroupMemberRole +roleRequiredToChange from to + | from <= GRMember && to <= GRMember = GRModerator + | otherwise = maximum ([GRAdmin, from, to] :: [GroupMemberRole]) + +-- Drop non-privileged-role entries and de-duplicate by memberId, keeping the first. +-- Runs on the parsed roster blob. +validateGroupRoster :: [RosterMember] -> [RosterMember] +validateGroupRoster entries = + dedup S.empty $ filter (\RosterMember {role} -> isRosterRole role) entries + where + dedup _ [] = [] + dedup seen (rm@RosterMember {memberId} : rms) + | memberId `S.member` seen = dedup seen rms + | otherwise = rm : dedup (S.insert memberId seen) rms + +-- Privileged members without a known key are skipped (recipients can't verify them). +buildGroupRoster :: [GroupMember] -> [RosterMember] +buildGroupRoster mods = take maxGroupRosterSize $ mapMaybe rosterMember mods + where + rosterMember GroupMember {memberId, memberPubKey, memberRole} + | isRosterRole memberRole = (\k -> RosterMember {memberId, key = MemberKey k, role = memberRole, privileges = 0}) <$> memberPubKey + | otherwise = Nothing sendHistory :: User -> GroupInfo -> GroupMember -> CM () sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just conn} = when (m `supportsVersion` batchSendVersion) $ do (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) - (errs', events) <- partitionEithers <$> mapM (tryAllErrors . itemForwardEvents) items + (errs', fwdMsgsByItem) <- partitionEithers <$> mapM (tryAllErrors . itemForwardMsgs) items let errors = map ChatErrorStore errs <> errs' unless (null errors) $ toView $ CEvtChatErrors errors - let events' = concat events - events_ <- case descrEvent_ of - Just descr -> mkEvents <$> withStore' (\db -> getMemberJoinRequest db user gInfo m) - where - mkEvents = \case - Just (_, Just _welcomeMsgId) -> events' - _ -> events' <> [descr] - Nothing -> pure events' - forM_ (L.nonEmpty events_) $ \events'' -> - sendGroupMemberMessages user gInfo conn events'' + -- signed items keep the author's original bytes/signature, unsigned are re-encoded; the welcome message + -- (regular groups only; never channels) is an authored element -- all batch together in order. + let fwdEls = map (uncurry encodeFwdElement) (concat fwdMsgsByItem) + welcomeEl <- welcomeElement + let (batches, dropped) = batchElements maxEncodedMsgLength (fwdEls <> maybe [] (: []) welcomeEl) + when (dropped > 0) $ toView $ CEvtChatErrors [ChatError $ CEInternalError ("sendHistory: dropped " <> show dropped <> " oversized history messages")] + forM_ batches $ \body -> + void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags False, VRValue Nothing body)] where + welcomeElement :: CM (Maybe ByteString) + welcomeElement = case descrEvent_ of + Just descr -> + withStore' (\db -> getMemberJoinRequest db user gInfo m) >>= \case + Just (_, Just _welcomeMsgId) -> pure Nothing + _ -> do + vr <- chatVersionRange + sharedMsgId <- SharedMsgId <$> drgRandomBytes 24 + case encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = descr} of + ECMEncoded body -> pure $ Just (encodeBatchElement Nothing body) + ECMLarge -> Nothing <$ toView (CEvtChatErrors [ChatError $ CEInternalError "sendHistory: welcome message too large"]) + Nothing -> pure Nothing descrEvent_ :: Maybe (ChatMsgEvent 'Json) descrEvent_ -- in channels sendHistory runs on the relay, which cannot author XMsgNew (GRRelay < GRObserver); @@ -1234,19 +1351,26 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c let GroupInfo {groupProfile = GroupProfile {description}} = gInfo fmap (\descr -> XMsgNew $ mcSimple (MCText descr)) description | otherwise = Nothing - itemForwardEvents :: CChatItem 'CTGroup -> CM [ChatMsgEvent 'Json] - itemForwardEvents cci = case cci of + itemForwardMsgs :: (CChatItem 'CTGroup, (Maybe SignedMsg, Maybe GroupMemberId)) -> CM [(GrpMsgForward, VerifiedMsg 'Json)] + itemForwardMsgs (cci, (signedMsg_, signedByGMId_)) = case cci of (CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent mc, file}) - | not (maybe False blockedByAdmin sender_) -> do + | not (maybe False blockedByAdmin (chatItemRcvFromMember ci)) -> do fInvDescr_ <- join <$> forM file getRcvFileInvDescr - processContentItem sender_ ci mc fInvDescr_ + -- channel items carry no from-member; a signed one falls back to the stored author (verified attribution) + member_ <- maybe (resolveAuthor signedByGMId_) (pure . Just) (chatItemRcvFromMember ci) + processContentItem member_ ci mc fInvDescr_ | otherwise -> pure [] - where sender_ = chatItemRcvFromMember ci - (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file}) -> do + (CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent mc, file, meta = CIMeta {showGroupAsSender}}) -> do fInvDescr_ <- join <$> forM file getSndFileInvDescr - processContentItem (Just membership) ci mc fInvDescr_ + let member_ = if showGroupAsSender && isNothing signedMsg_ then Nothing else Just membership + processContentItem member_ ci mc fInvDescr_ _ -> pure [] where + resolveAuthor :: Maybe GroupMemberId -> CM (Maybe GroupMember) + resolveAuthor Nothing = pure Nothing + resolveAuthor (Just gmId) = do + cxt <- chatStoreCxt + eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupMemberById db cxt user gmId) getRcvFileInvDescr :: CIFile 'MDRcv -> CM (Maybe (FileInvitation, RcvFileDescrText)) getRcvFileInvDescr ciFile@CIFile {fileId, fileProtocol, fileStatus} = do expired <- fileExpired @@ -1277,33 +1401,39 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c fInv = xftpFileInvitation fileName fileSize fInvDescr in Just (fInv, fileDescrText) | otherwise = Nothing - processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json] - processContentItem sender_ ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = + processContentItem :: Maybe GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [(GrpMsgForward, VerifiedMsg 'Json)] + processContentItem member_ ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ = if isNothing fInvDescr_ && not (msgContentHasText mc) then pure [] else do - let CIMeta {itemTs, itemSharedMsgId, itemTimed} = meta + let CIMeta {itemTs, itemSharedMsgId, itemTimed, showGroupAsSender} = meta quotedItemId_ = quoteItemId =<< quotedItem fInv_ = fst <$> fInvDescr_ (mc', _, mentions') = updatedMentionNames mc formattedText mentions mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions' - asGroup = isNothing sender_ - -- TODO [knocking] send history to other scopes too? - (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing asGroup mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False - -- for channel messages default chat version range to membership range - let senderVRange = maybe (memberChatVRange' membership) memberChatVRange' sender_ - xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} + -- for channel messages default chat version range to membership range + senderVRange = maybe (memberChatVRange' membership) memberChatVRange' member_ + -- member_ is Nothing only for as-group unsigned items -> FwdChannel; otherwise attribute to the member + -- (the author for signed as-group items), so the recipient can reconstruct the binding and verify + fwdSender = maybe FwdChannel (\am -> FwdMember (memberId' am) (memberShortenedName am)) member_ + fwd = GrpMsgForward {fwdSender, fwdBrokerTs = itemTs} + -- signed items forward the author's original bytes so the signature stays valid; unsigned re-encode current content + contentVM <- case signedMsg_ of + Just sm@SignedMsg {signedBody} + | Right chatMsg <- (J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json)) -> + pure $ VMSigned MSSVerified sm chatMsg + _ -> do + -- TODO [knocking] send history to other scopes too? + (chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing showGroupAsSender mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False + pure $ VMUnsigned ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent} fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of (Just fileDescrText, Just msgId) -> do partSize <- asks $ xftpDescrPartSize . config let parts = splitFileDescr partSize fileDescrText pure . L.toList $ L.map (XMsgFileDescr msgId) parts _ -> pure [] - let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents - fwdSender = maybe FwdChannel (\s -> FwdMember (memberId' s) (memberShortenedName s)) sender_ - fwd = GrpMsgForward {fwdSender, fwdBrokerTs = itemTs} - msgForwardEvents = map (XGrpMsgForward fwd) (xMsgNewChatMsg : fileDescrChatMsgs) - pure msgForwardEvents + let fileDescrVMs = map (VMUnsigned . ChatMessage senderVRange Nothing) fileDescrEvents + pure $ map ((,) fwd) (contentVM : fileDescrVMs) memberShortenedName :: GroupMember -> ContactName memberShortenedName GroupMember {memberProfile = LocalProfile {displayName}} @@ -1332,7 +1462,7 @@ setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM G setGroupLinkData nm user gInfo gLink = do cxt <- chatStoreCxt (conn, groupRelays) <- withFastStore $ \db -> - (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays linkType = if useRelays' gInfo then CCTChannel else CCTGroup sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) @@ -1342,7 +1472,7 @@ setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () setGroupLinkDataAsync user gInfo gLink = do cxt <- chatStoreCxt (conn, groupRelays) <- withStore $ \db -> - (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays setAgentConnShortLinkAsync user conn userLinkData (Just crClientData) @@ -1368,11 +1498,15 @@ updatePublicGroupData user gInfo pure (gInfo', gLink) setGroupLinkDataAsync user gInfo' gLink pure gInfo' + | useRelays' gInfo && isRelay (membership gInfo) = do + cxt <- chatStoreCxt + withStore $ \db -> updatePublicMemberCount db cxt user gInfo | otherwise = pure gInfo -updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool) -updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} - | profileChanged || countChanged = do +-- must not resolve names here: a background link-data refresh would leak channel membership to the resolver +updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> Maybe SimplexDomain -> CM (GroupInfo, Bool) +updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} resolvedDomain_ + | profileChanged || countChanged || verifyResolved = do cxt <- chatStoreCxt withStore $ \db -> do g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo @@ -1380,13 +1514,30 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G Just PublicGroupData {publicMemberCount} | countChanged -> setPublicMemberCount db cxt user g publicMemberCount _ -> pure g - pure (g', profileChanged) + g'' <- if verifyResolved then liftIO $ setGroupDomainVerified db user g' True else pure g' + pure (g'', profileChanged) | otherwise = pure (gInfo, False) where profileChanged = p /= groupProfile countChanged = case publicGroupData of Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount _ -> False + groupClaim GroupProfile {publicGroup} = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim) + newClaim = groupClaim groupProfile + verifyResolved = isJust resolvedDomain_ && resolvedDomain_ == newClaim + +updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact +updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim} + | profileChanged || verifyChanged = do + cxt <- chatStoreCxt + withFastStore $ \db -> do + ct' <- updateContactProfile db cxt user ct linkProfile + if verifyChanged then liftIO $ setContactDomainVerified db user ct' True else pure ct' + | otherwise = pure ct + where + profileChanged = fromLocalProfile profile /= linkProfile + claimChanged = (claimDomain <$> prevClaim) /= (claimDomain <$> newClaim) + verifyChanged = contactDomainVerified /= Just True || claimChanged -- TODO [relays] owner: set owners on updating link data (multi-owner) groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData) @@ -1438,10 +1589,9 @@ encodeShortLinkData d = decodeLinkUserData :: J.FromJSON a => ConnLinkData c -> IO (Maybe a) decodeLinkUserData cData | B.null s = pure Nothing - | B.head s == 'X' = case Z1.decompress $ B.drop 1 s of - Z1.Error e -> Nothing <$ logError ("Error decompressing link data: " <> tshow e) - Z1.Skip -> pure Nothing - Z1.Decompress s' -> decode s' + | B.head s == 'X' = case limitDecompress' maxDecompressedMsgLength $ B.drop 1 s of + Left e -> Nothing <$ logError ("Error decompressing link data: " <> tshow e) + Right s' -> decode s' | otherwise = decode s where decode s' = case J.eitherDecodeStrict s' of @@ -1592,6 +1742,30 @@ parseFileDescription :: FilePartyI p => Text -> CM (ValidFileDescription p) parseFileDescription = liftEither . first (ChatError . CEInvalidFileDescription) . (strDecode . encodeUtf8) +-- | Unique XFTP servers hosting the file's chunks, parsed from a stored file description. +fileDescrServers :: ValidFileDescription p -> [XFTPServer] +fileDescrServers (FD.ValidFileDescription FD.FileDescription {chunks}) = + S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks + +-- | XFTP servers the file's data chunks were uploaded to (sender's servers for sent items, +-- the same servers via the recipient description for received items). +-- Returns [] for non-XFTP/inline files or when no description is available; never fails the caller. +getChatItemFileServers :: User -> SMsgDirection d -> ChatItem c d -> CM [XFTPServer] +getChatItemFileServers user dir ci = case ci of + ChatItem {file = Just CIFile {fileId, fileProtocol = FPXFTP}} -> + itemFileServers fileId `catchAllErrors` \_ -> pure [] + _ -> pure [] + where + itemFileServers fileId = case dir of + SMDSnd -> do + sfd_ <- withStore' $ \db -> getSndFTPrivateSndDescr db user fileId + case sfd_ of + Just sfdText -> fileDescrServers <$> (parseFileDescription sfdText :: CM (ValidFileDescription 'FSender)) + Nothing -> pure [] + SMDRcv -> do + RcvFileDescr {fileDescrText} <- withStore $ \db -> getRcvFileDescrByRcvFileId db fileId + fileDescrServers <$> (parseFileDescription fileDescrText :: CM (ValidFileDescription 'FRecipient)) + sendDirectFileInline :: User -> Contact -> FileTransferMeta -> SharedMsgId -> CM () sendDirectFileInline user ct ft sharedMsgId = do msgDeliveryId <- sendFileInline_ ft sharedMsgId $ sendDirectContactMessage user ct @@ -1617,13 +1791,16 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg = chSize = fromIntegral chunkSize parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json) -parseChatMessage conn s = do +parseChatMessage conn s = snd <$> parseChatMessage' conn s +{-# INLINE parseChatMessage #-} + +parseChatMessage' :: Connection -> ByteString -> CM (Maybe SignedMsg, ChatMessage 'Json) +parseChatMessage' conn s = case parseChatMessages s of - [msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ _ m)) -> checkEncoding m) =<< msg + [msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ sm m)) -> (sm,) <$> checkEncoding m) =<< msg _ -> throwChatError $ CEException "parseChatMessage: single message is expected" where errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) -{-# INLINE parseChatMessage #-} getChatScopeInfo :: StoreCxt -> User -> GroupChatScope -> CM GroupChatScopeInfo getChatScopeInfo cxt user = \case @@ -1820,6 +1997,51 @@ closeFileHandle fileId files = do h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m) liftIO $ mapM_ hClose h_ `catchAll_` pure () +-- The roster file has no chat item, so chat-item file enumeration misses it; clean it up by group. +cleanupGroupRosterFile :: User -> GroupInfo -> CM () +cleanupGroupRosterFile User {userId} GroupInfo {groupId} = do + infos <- withStore' $ \db -> getGroupRosterFileInfo db userId groupId + forM_ infos $ \(fileId, filePath_) -> do + lift $ closeFileHandle fileId rcvFiles + forM_ filePath_ removeFsFile + withStore' $ \db -> do + deleteGroupRosterFile db userId groupId + deleteGroupRosterTransfers db groupId + +-- Supersede/cancel one source relay's in-flight roster transfer: remove its on-disk file + cached +-- handle first (the cascade only does rows), then the files + transfer rows. +cleanupRosterTransfer :: GroupInfo -> GroupMemberId -> CM () +cleanupRosterTransfer gInfo fromMemberId = + withStore' (\db -> getRosterTransferId db gInfo fromMemberId) >>= mapM_ cleanupRosterTransferById + +cleanupRosterTransferById :: Int64 -> CM () +cleanupRosterTransferById transferId = do + file_ <- withStore' $ \db -> getRosterTransferFile db transferId + forM_ file_ $ \(fileId, filePath_) -> do + lift $ closeFileHandle fileId rcvFiles + forM_ filePath_ removeFsFile + withStore' $ \db -> do + deleteRosterTransferFile db transferId + deleteRosterTransfer db transferId + +-- MUST evict the cached AppendMode handle before deleting chunks, else re-driven bytes append +-- after the stale prefix and corrupt the blob. +resetRosterPartialChunks :: RcvFileTransfer -> CM () +resetRosterPartialChunks ft@RcvFileTransfer {fileId, fileStatus} = do + lift $ closeFileHandle fileId rcvFiles + forM_ (rcvFilePath fileStatus) removeFsFile + withStore' $ \db -> deleteRcvFileChunks db ft + where + rcvFilePath = \case + RFSAccepted p -> Just p + RFSConnected p -> Just p + _ -> Nothing + +removeFsFile :: FilePath -> CM () +removeFsFile fp = do + p <- lift $ toFSFilePath fp + removeFile p `catchAllErrors` \_ -> pure () + deleteMembersConnections :: User -> [GroupMember] -> CM () deleteMembersConnections user members = deleteMembersConnections' user members False @@ -1850,7 +2072,22 @@ deleteOrUpdateMemberRecordIO db user@User {userId} gInfo m = do else checkGroupMemberHasItems db user m' >>= \case Just _ -> updateGroupMemberStatus db userId m' GSMemRemoved - Nothing -> deleteGroupMember db user m' + Nothing + | useRelays' gInfo -> updateGroupMemberRemovedAt db user m' + | otherwise -> deleteGroupMember db user m' + pure gInfo' + +-- Unlike deleteOrUpdateMemberRecord, skips checkGroupMemberHasItems. +fullyDeleteMemberRecord :: User -> GroupInfo -> GroupMember -> CM GroupInfo +fullyDeleteMemberRecord user gInfo m = + withStore' $ \db -> fullyDeleteMemberRecordIO db user gInfo m + +fullyDeleteMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo +fullyDeleteMemberRecordIO db user gInfo m = do + (gInfo', m') <- deleteSupportChatIfExists db user gInfo m + if useRelays' gInfo && not (isRelay m') + then updateGroupMemberRemovedAt db user m' + else deleteGroupMember db user m' pure gInfo' updateMemberRecordDeleted :: User -> GroupInfo -> GroupMember -> GroupMemberStatus -> CM GroupInfo @@ -1913,6 +2150,7 @@ presentUserBadge User {profile = LocalProfile {localBadge}} incognitoProfile p = Left e -> p <$ logError ("presentUserBadge: proof generation failed: " <> T.pack e) _ -> pure p + -- receiving side of contact/invitation link data: verify the badge proof from the link profile -- and set the crypto-free display badge for the UI (the raw proof stays in profile for APIPrepareContact) linkDataBadge :: ContactShortLinkData -> CM ContactShortLinkData @@ -1973,16 +2211,19 @@ createSndMessages idsEvents = do encodeMessage sharedMsgId = encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt} -groupMsgSigning :: GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning -groupMsgSigning gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {publicGroupId, memberPrivKey}} evt - | useRelays' gInfo && requiresSignature (toCMEventTag evt) = +groupMsgSigning :: Bool -> GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning +groupMsgSigning sign gInfo@GroupInfo {membership = GroupMember {memberId}, groupKeys = Just GroupKeys {publicGroupId, memberPrivKey}} evt + | useRelays' gInfo && shouldSign = Just $ MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey -groupMsgSigning _ _ = Nothing + where + tag = toCMEventTag evt + shouldSign = requiresSignature tag || (sign && signableContent tag) +groupMsgSigning _ _ _ = Nothing sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM () sendGroupMemberMessages user gInfo@GroupInfo {groupId} conn events = do when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) - let idsEvts = L.map (\evt -> (GroupId groupId, groupMsgSigning gInfo evt, evt)) events + let idsEvts = L.map (\evt -> (GroupId groupId, groupMsgSigning False gInfo evt, evt)) events mode = if useRelays' gInfo then BMBinary else BMJson (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts unless (null errs) $ toView $ CEvtChatErrors errs @@ -2037,6 +2278,26 @@ encodeConnInfoPQ pqSup v chatMsgEvent = do _ -> pure connInfo ECMLarge -> throwChatError $ CEException "large info" +-- conn-info wrapped as a signed element, so the receiver can verify the signature over the body +encodeSignedConnInfo :: MsgEncodingI e => MsgSigning -> ChatMsgEvent e -> CM ByteString +encodeSignedConnInfo signing chatMsgEvent = do + vr <- chatVersionRange + let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent} + case encodeChatMessage maxEncodedInfoLength info of + ECMEncoded body -> pure $ encodeBatchElement (Just $ signChatMsgBody signing body) body + ECMLarge -> throwChatError $ CEException "large signed info" + +-- signed XMember for a relay-group join: proves the joiner holds the member key it asserts, and carries +-- viaRelay = the target relay's memberId inside the signed body so a sibling relay can't accept a replay +encodeXMemberConnInfo :: GroupInfo -> MemberId -> Profile -> CM ByteString +encodeXMemberConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys} relayMemberId profileToSend = + case groupKeys of + Just GroupKeys {publicGroupId, memberPrivKey} -> + let xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey) (Just relayMemberId) + signing = MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey + in encodeSignedConnInfo signing xMemberEvt + Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" + deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption) deliverMessage conn cmEventTag msgBody msgId = do let msgFlags = MsgFlags {notification = hasNotification cmEventTag} @@ -2085,7 +2346,7 @@ deliverMessagesB msgReqs = do Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it prepareBatch (Right req) (Right ar) = Right (req, ar) prepareBatch (Left ce) _ = Left ce -- restore original ChatError - prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae (AgentConnId "") Nothing + prepareBatch _ (Left ae) = Left $ chatErrorAgent ae createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption)) createDelivery db ((Connection {connId}, _, (_, msgIds)), (agentMsgId, pqEnc')) = do Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds @@ -2098,24 +2359,111 @@ deliverMessagesB msgReqs = do where updatePQ = updateConnPQSndEnabled db connId pqSndEnabled' -sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage -sendGroupMessage user gInfo gcScope members chatMsgEvent = do - sendGroupMessages user gInfo gcScope False members (chatMsgEvent :| []) >>= \case +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> Bool -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage user gInfo gcScope members sign chatMsgEvent = do + sendGroupMessages user gInfo gcScope False members sign (chatMsgEvent :| []) >>= \case ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message" sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage sendGroupMessage' user gInfo members chatMsgEvent = - sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case + sendGroupMessages_ user gInfo members False (chatMsgEvent :| []) >>= \case ((Right msg) :| [], _) -> pure msg _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" -sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) -sendGroupMessages user gInfo scope asGroup members events = do +-- The roster change being broadcast, projected onto the current roster members in broadcastRoster. This lets the +-- roster blob be built (and sent) before the change is applied to the owner's own member records, so the owner +-- never demotes/removes a member locally before the change has been propagated to relays. +data RosterDelta + = RDRoleChanged GroupMemberRole [GroupMember] -- these members now hold this role + | RDRemoved [GroupMember] -- these members are removed from the group + +applyRosterDelta :: RosterDelta -> [GroupMember] -> [GroupMember] +applyRosterDelta delta current = case delta of + RDRoleChanged role changed -> map (\m -> (m :: GroupMember) {memberRole = role}) changed <> without changed + RDRemoved removed -> without removed + where + without ms = let ids = S.fromList (map groupMemberId' ms) in filter ((`S.notMember` ids) . groupMemberId') current + +-- TODO [relays] improvement: publish roster_version in link data so the owner can recover the latest version +-- TODO after restoring from a stale backup (relays accept only strictly-greater versions) +-- Reserve and persist the next roster version (committed before the events that carry it, so a recipient never +-- advances past a version the owner hasn't recorded), then broadcast the matching blob with the change projected +-- onto the served roster (so it excludes demoted/removed members). Returns the reserved version for the delta +-- that follows. The blob send is best-effort - a failed send heals on the next change or on resume. +broadcastRoster :: User -> GroupInfo -> RosterDelta -> CM VersionRoster +broadcastRoster user gInfo delta = do + let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo) + withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer + sendRosterBlob rosterVer `catchAllErrors` eToView + pure rosterVer + where + sendRosterBlob rosterVer = do + cxt <- chatStoreCxt + (relays, rosterMems) <- withStore' $ \db -> + (,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo + forM_ (L.nonEmpty relays) $ \relays' -> + sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster $ applyRosterDelta delta rosterMems) + +-- Send the current roster (no version bump) to a newly added relay so it can serve joiners. +sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM () +sendGroupRosterToRelay user gInfo relayMember = + forM_ (rosterVersion gInfo) $ \rosterVer -> do + cxt <- chatStoreCxt + rosterMems <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo + sendRoster user gInfo [relayMember] rosterVer (buildGroupRoster rosterMems) + +-- Row-less send (no files/snd_files rows, so no send-side cleanup); redelivery is the agent's. +sendRoster :: User -> GroupInfo -> [GroupMember] -> VersionRoster -> [RosterMember] -> CM () +sendRoster user gInfo members rosterVer roster = do + let blob = encodeRosterBlob roster + fileInv = InlineFileInvitation {fileSize = fromIntegral (B.length blob), fileDigest = FD.FileDigest $ LC.sha512Hash $ LB.fromStrict blob} + SndMessage {sharedMsgId} <- sendGroupMessage' user gInfo members (XGrpRoster GroupRoster {version = rosterVer, fileInv}) + sendInlineBlobChunks user gInfo members sharedMsgId blob + +-- Send a binary blob as BFileChunks under a shared_msg_id to the given members (chunked by fileChunkSize). +sendInlineBlobChunks :: User -> GroupInfo -> [GroupMember] -> SharedMsgId -> ByteString -> CM () +sendInlineBlobChunks user gInfo members sharedMsgId blob = do + chSize <- fromIntegral <$> asks (fileChunkSize . config) + go chSize 1 blob + where + go chSize chunkNo bytes = do + let (chunk, rest) = B.splitAt chSize bytes + void $ sendGroupMessage' user gInfo members (BFileChunk sharedMsgId (FileChunk chunkNo chunk)) + unless (B.null rest) $ go chSize (chunkNo + 1) rest + +-- Relay advertises its current web preview capability to channel owners. +-- Idempotent: sends only when the configured web domain differs from what was last sent, and only to +-- owners whose recorded chat version supports relayWebCapVersion (older apps can't parse XGrpRelayCap). +sendRelayCapIfNeeded :: User -> GroupInfo -> CM () +sendRelayCapIfNeeded user gInfo = do + ChatConfig {webPreviewConfig} <- asks config + let currentWebDomain = (\WebPreviewConfig {webDomain} -> webDomain) <$> webPreviewConfig + sentWebDomain <- withStore' (`getRelaySentWebDomain` gInfo) + when (currentWebDomain /= sentWebDomain) $ do + cxt <- chatStoreCxt + owners <- withStore' $ \db -> getGroupOwners db cxt user gInfo + let capableOwners = filter (\m -> memberCurrent m && m `supportsVersion` relayWebCapVersion) owners + unless (null capableOwners) $ do + void $ sendGroupMessage' user gInfo capableOwners (XGrpRelayCap RelayCapabilities {webDomain = currentWebDomain}) + withStore' $ \db -> updateRelaySentWebDomain db gInfo currentWebDomain + +sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> Bool -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages user gInfo scope asGroup members sign events = do + sendGroupProfileUpdate user gInfo scope asGroup members + sendGroupMessages_ user gInfo members sign events + +-- per-item signer variant of sendGroupMessages (used for per-item delete signing); preserves the profile-update prelude +sendGroupSignedMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> NonEmpty (Maybe MsgSigning, ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupSignedMessages user gInfo scope asGroup members signedEvents = do + sendGroupProfileUpdate user gInfo scope asGroup members + sendGroupSignedMessages_ gInfo members signedEvents + +sendGroupProfileUpdate :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> CM () +sendGroupProfileUpdate user gInfo scope asGroup members = -- TODO [knocking] send current profile to pending member after approval? when shouldSendProfileUpdate $ sendProfileUpdate `catchAllErrors` eToView - sendGroupMessages_ user gInfo members events where User {profile = p, userMemberProfileUpdatedAt} = user GroupInfo {userMemberProfileSentAt} = gInfo @@ -2130,9 +2478,8 @@ sendGroupMessages user gInfo scope asGroup members events = do _ -> False sendProfileUpdate = do let members' = filter (`supportsVersion` memberProfileUpdateVersion) members - allowSimplexLinks = groupUserAllowSimplexLinks gInfo -- shouldSendProfileUpdate excludes incognito membership, so the badge is presented - profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile p + profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate currentTs <- liftIO getCurrentTime withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs @@ -2143,9 +2490,12 @@ data GroupSndResult = GroupSndResult forwarded :: [GroupMember] } -sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) -sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do - let idsEvts = L.map (\evt -> (GroupId groupId, groupMsgSigning gInfo evt, evt)) events +sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> Bool -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupMessages_ _user gInfo recipientMembers sign events = + sendGroupSignedMessages_ gInfo recipientMembers $ L.map (\evt -> (groupMsgSigning sign gInfo evt, evt)) events + +sendGroupSignedMessages_ :: MsgEncodingI e => GroupInfo -> [GroupMember] -> NonEmpty (Maybe MsgSigning, ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult) +sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents = do sndMsgs_ <- lift $ createSndMessages idsEvts recipientMembers' <- liftIO $ shuffleMembers recipientMembers let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} @@ -2166,6 +2516,8 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do pending = zipWith3 (\mId pReq r -> (mId, fmap snd pReq, r)) pendingMemIds pendingReqs stored pure (sndMsgs_, GroupSndResult {sentTo, pending, forwarded}) where + events = L.map snd signedEvents + idsEvts = L.map (\(signing, evt) -> (GroupId groupId, signing, evt)) signedEvents shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers ms = do let (adminMs, otherMs) = partition isAdmin ms @@ -2330,10 +2682,14 @@ saveDirectRcvMSG conn@Connection {connId} agentMsgMeta chatMsg@ChatMessage {chat msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing pure (conn', msg) -saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage) +saveGroupRcvMsg :: forall e. MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage) saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta verifiedMsg = do let ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = verifiedChatMsg verifiedMsg - (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange + -- binary messages (file chunks) carry only the initial-version sentinel, not the sender's range; + -- applying it would downgrade the member's negotiated version and suppress version-gated delivery + (am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- case encoding @e of + SBinary -> pure (authorMember, conn) + SJson -> updateMemberChatVRange authorMember conn chatVRange let agentMsgId = fst $ recipient agentMsgMeta brokerTs = metaBrokerTs agentMsgMeta newMsg = NewRcvMessage {chatMsgEvent, verifiedMsg, brokerTs} @@ -2416,7 +2772,7 @@ saveSndChatItems user cd showGroupAsSender itemsData itemTimed live = do let hasLink_ = ciContentHasLink content (snd itemTexts) ciId <- createNewSndChatItem db user cd showGroupAsSender msg content quotedItem itemForwarded itemTimed live hasLink_ createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - let ci = mkChatItem_ cd showGroupAsSender ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False hasLink_ createdAt Nothing (MSSVerified <$ signedMsg_) createdAt + let ci = mkChatItem_ cd showGroupAsSender ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False hasLink_ createdAt Nothing (toMsgVerified (signMessagesRequired cd) (MSSVerified <$ signedMsg_)) createdAt Right <$> case cd of CDGroupSnd g _scope | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions _ -> pure ci @@ -2447,7 +2803,7 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMem hasLink_ = ciContentHasLink content ft_ (ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention hasLink_ brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt - let ci = mkChatItem_ cd showAsGroup ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention hasLink_ brokerTs forwardedByMember msgSigned createdAt + let ci = mkChatItem_ cd showAsGroup ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention hasLink_ brokerTs forwardedByMember (toMsgVerified (signMessagesRequired cd) msgSigned) createdAt ci' <- case toChatInfo cd of GroupChat g _ | not (null mentions') -> createGroupCIMentions db g ci mentions' _ -> pure ci @@ -2455,7 +2811,11 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMem where groupMentions db g membership = do mentions' <- getRcvCIMentions db user g ft_ mentions - let userReply = case cmToQuotedMsg chatMsgEvent of + -- messages of blocked members are hidden, they should not mention user + let senderBlocked = case cd of + CDGroupRcv _g _scope m -> memberBlocked m + _ -> False + userReply = not senderBlocked && case cmToQuotedMsg chatMsgEvent of Just QuotedMsg {msgRef = MsgRef {memberId = Just mId}} -> sameMemberId mId membership _ -> False userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions' @@ -2471,16 +2831,16 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMem _ -> Nothing -- TODO [mentions] optimize by avoiding unnecessary parsing -mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d -mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs = +mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgVerified -> UTCTime -> ChatItem c d +mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember msgVerified currentTs = let ts@(_, ft_) = ciContentTexts content hasLink_ = ciContentHasLink content ft_ - in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember Nothing currentTs + in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgVerified currentTs -mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> ChatItem c d -mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgSigned currentTs = +mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgVerified -> UTCTime -> ChatItem c d +mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgVerified currentTs = let itemStatus = ciCreateStatus content - meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender msgSigned currentTs currentTs + meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender msgVerified currentTs currentTs in ChatItem {chatDir = toCIDirection cd, meta, content, mentions = M.empty, formattedText, quotedItem, reactions = [], file} ciContentHasLink :: CIContent d -> Maybe MarkdownList -> Bool @@ -2493,18 +2853,24 @@ msgContentHasLink mc ft_ = case msgContentTag mc of MCLink_ -> True _ -> maybe False hasLinks ft_ -createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) -createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do +prepareAgentCreation :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> CM (CommandId, ConnId) +prepareAgentCreation user cmdFunction enableNtfs cMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode IKPQOff subMode + connId <- withAgent $ \a -> prepareConnectionToCreate a (aUserId user) enableNtfs cMode PQSupportOff pure (cmdId, connId) -joinAgentConnectionAsync :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM (CommandId, ConnId) -joinAgentConnectionAsync user conn_ enableNtfs cReqUri cInfo subMode = do +prepareAgentJoin :: User -> Maybe Connection -> Bool -> ConnectionRequestUri c -> CM (CommandId, ConnId) +prepareAgentJoin user conn_ enableNtfs cReqUri = do cmdId <- withStore' $ \db -> createCommand db user (dbConnId <$> conn_) CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) (aConnId <$> conn_) enableNtfs cReqUri cInfo PQSupportOff subMode + connId <- case conn_ of + Just conn -> pure $ aConnId conn + Nothing -> withAgent $ \a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff pure (cmdId, connId) +joinAgentConnectionAsync :: ConnectionModeI c => CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM () +joinAgentConnectionAsync cmdId updateConn connId enableNtfs cReqUri cInfo subMode = + withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode + allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn @@ -2512,13 +2878,17 @@ allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersi withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm withStore' $ \db -> updateConnectionStatus db conn ConnAccepted -agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> PQSupport -> VersionChat -> CM (CommandId, ConnId) -agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do +prepareAgentAccept :: User -> Bool -> InvitationId -> PQSupport -> CM (CommandId, ConnId) +prepareAgentAccept user enableNtfs invId pqSup = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact - dm <- encodeConnInfoPQ pqSup chatV msg - connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup pure (cmdId, connId) +agentAcceptContactAsync :: MsgEncodingI e => CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> VersionChat -> SubscriptionMode -> CM () +agentAcceptContactAsync cmdId connId enableNtfs invId msg pqSup chatV subMode = do + dm <- encodeConnInfoPQ pqSup chatV msg + withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) connId enableNtfs invId dm pqSup subMode + deleteAgentConnectionAsync :: ConnId -> CM () deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False {-# INLINE deleteAgentConnectionAsync #-} @@ -2637,7 +3007,7 @@ createFeatureEnabledItems_ :: User -> Contact -> CM [AChatItem] createFeatureEnabledItems_ user ct@Contact {mergedPreferences} = forM allChatFeatures $ \(ACF f) -> do let state = featureState $ getContactUserPreference f mergedPreferences - createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing + createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing Nothing createFeatureItems :: MsgDirectionI d => @@ -2667,15 +3037,15 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do unless (null errs) $ toView' $ CEvtChatErrors errs toView' $ CEvtNewChatItems user acis where - contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) + contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures (chatDir ct', False, contents) where - featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId) + featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus) featureCIContent_ f - | state /= state' = Just (fContent ciFeature state', Nothing) - | prefState /= prefState' = Just (fContent ciOffer prefState', Nothing) + | state /= state' = Just (fContent ciFeature state', Nothing, Nothing) + | prefState /= prefState' = Just (fContent ciOffer prefState', Nothing, Nothing) | otherwise = Nothing where fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d @@ -2688,9 +3058,12 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do cup = getContactUserPreference f cups cup' = getContactUserPreference f cups' +groupFeatures :: GroupInfo -> [AGroupFeature] +groupFeatures g = if useRelays' g then channelGroupFeatures else regularGroupFeatures + createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> GroupInfo -> CM () -createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = - forM_ allGroupFeatures $ \(AGF f) -> do +createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} g'@GroupInfo {fullGroupPreferences = gps'} = + forM_ (groupFeatures g') $ \(AGF f) -> do let state = groupFeatureState $ getGroupPreference f gps pref' = getGroupPreference f gps' state'@(_, param', role') = groupFeatureState pref' @@ -2704,20 +3077,20 @@ createGroupFeatureItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d - createGroupFeatureItems user cd ciContent g = createGroupFeatureItems_ user cd False ciContent g >>= toView . CEvtNewChatItems user createGroupFeatureItems_ :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> ShowGroupAsSender -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM [AChatItem] -createGroupFeatureItems_ user cd showGroupAsSender ciContent GroupInfo {fullGroupPreferences} = - forM allGroupFeatures $ \(AGF f) -> do +createGroupFeatureItems_ user cd showGroupAsSender ciContent g@GroupInfo {fullGroupPreferences} = + forM (groupFeatures g) $ \(AGF f) -> do let p = getGroupPreference f fullGroupPreferences (_, param, role) = groupFeatureState p - createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing + createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing Nothing createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () createInternalChatItem user cd content itemTs_ = do - ci <- createChatItem user cd False content Nothing itemTs_ + ci <- createChatItem user cd False content Nothing Nothing itemTs_ toView $ CEvtNewChatItems user [ci] -createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe UTCTime -> CM AChatItem -createChatItem user cd showGroupAsSender content sharedMsgId itemTs_ = - lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId)])]) >>= \case +createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe MsgSigStatus -> Maybe UTCTime -> CM AChatItem +createChatItem user cd showGroupAsSender content sharedMsgId msgSigned itemTs_ = + lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId, msgSigned)])]) >>= \case [Right ci] -> pure ci [Left e] -> throwError e rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) @@ -2729,7 +3102,7 @@ createChatItems :: (ChatTypeI c, MsgDirectionI d) => User -> Maybe UTCTime -> - [(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)])] -> + [(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)])] -> CM' [Either ChatError AChatItem] createChatItems user itemTs_ dirsCIContents = do createdAt <- liftIO getCurrentTime @@ -2738,24 +3111,25 @@ createChatItems user itemTs_ dirsCIContents = do void . withStoreBatch' $ \db -> map (updateChat db cxt createdAt) dirsCIContents withStoreBatch' $ \db -> concatMap (createACIs db itemTs createdAt) dirsCIContents where - updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> IO () + updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> IO () updateChat db cxt createdAt (cd, _, contents) - | any (ciRequiresAttention . fst) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats + | any (\(content, _, _) -> ciRequiresAttention content) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats | otherwise = pure () where memberChatStats :: Maybe (Int, MemberAttention, Int) memberChatStats = case cd of CDGroupRcv _g (Just scope) m -> do - let unread = length $ filter (ciRequiresAttention . fst) contents + let unread = length $ filter (\(content, _, _) -> ciRequiresAttention content) contents in Just (unread, memberAttentionChange unread itemTs_ (Just m) scope, 0) _ -> Nothing - createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> [IO AChatItem] + createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> [IO AChatItem] createACIs db itemTs createdAt (cd, showGroupAsSender, contents) = map createACI contents where - createACI (content, sharedMsgId) = do + createACI (content, sharedMsgId, msgSigned) = do let hasLink_ = ciContentHasLink content Nothing - ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ itemTs createdAt - let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt + msgVerified = toMsgVerified False msgSigned + ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ msgVerified itemTs createdAt + let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing msgVerified createdAt pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci -- rcvMem_ Nothing means message from channel - treated same as message from moderator, @@ -2788,7 +3162,7 @@ createLocalChatItems user cd itemsData createdAt = do createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) createItem db (content, ciFile, itemForwarded, ts@(_, ft_)) = do let hasLink_ = ciContentHasLink content ft_ - ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False hasLink_ createdAt Nothing Nothing createdAt + ciId <- createNewChatItem_ db user cd False Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False hasLink_ createdAt Nothing Nothing Nothing Nothing createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure $ mkChatItem_ cd False ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False hasLink_ createdAt Nothing Nothing createdAt @@ -2864,24 +3238,13 @@ simplexTeamContactProfile = { displayName = "Ask SimpleX Team", fullName = "", shortDescr = Just "Send questions about SimpleX Chat app and your suggestions", + description = Nothing, image = Just simplexChatImage, contactLink = Just $ CLFull adminContactReq, peerType = Nothing, preferences = Nothing, - badge = Nothing - } - -simplexStatusContactProfile :: Profile -simplexStatusContactProfile = - Profile - { displayName = "SimpleX Status", - fullName = "", - shortDescr = Just "Automatic server status and app release updates", - image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), - contactLink = Just (either error CLFull $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), - peerType = Just CPTBot, - preferences = Nothing, - badge = Nothing + badge = Nothing, + contactDomain = Nothing } timeItToView :: String -> CM' a -> CM' a diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index a058857076..a65bed75a0 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -24,29 +24,35 @@ import Control.Monad.IO.Unlift import Control.Monad.Reader import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (lefts, partitionEithers, rights) import Data.Foldable (foldr', foldrM) import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (find) +import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L +import qualified Data.IntSet as IS import Data.Map.Strict (Map) import qualified Data.Map.Strict as M +import qualified Data.Set as S import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime, getCurrentTime) +import Data.Time.Format (defaultTimeLocale, formatTime) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery +import Simplex.Chat.Files (getChatTempDirectory) import Simplex.Chat.Library.Internal +import Simplex.Chat.Web (channelContentChanged, channelProfileUpdated, channelRemoved) import Simplex.Chat.Messages -import Simplex.Chat.Messages.Batch (batchDeliveryTasks1, encodeBinaryBatch, encodeFwdElement) +import Simplex.Chat.Messages.Batch (batchDeliveryTasks1, batchProfiles, batchProfilesWithBody, encodeBinaryBatch, encodeFwdElement, maxBatchElementSize) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.ProfileGenerator (generateRandomProfile) @@ -74,7 +80,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Client (getAgentWorker, temporaryOrHostError, waitForUserNetwork, waitForWork, waitWhileSuspended, withWorkItems, withWork_) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), Worker (..)) +import Simplex.Messaging.Agent.Env.SQLite (Worker (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay) @@ -84,9 +90,11 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR +import qualified Simplex.Messaging.Crypto.Lazy as LC import Simplex.Messaging.Encoding (smpEncode) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (ErrorType (..), MsgFlags (..)) +import Simplex.Messaging.Parsers (parseAll) +import Simplex.Messaging.Protocol (ErrorType (..), MsgFlags (..), ServiceSub (..), ServiceSubError (..), ServiceSubResult (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM @@ -103,6 +111,13 @@ import UnliftIO.STM smallGroupsRcptsMemLimit :: Int smallGroupsRcptsMemLimit = 20 +-- Verifies member signatures over CBGroup <> (publicGroupId, memberId) <> signedBody under the given key. +-- signatures is NonEmpty so the verification can't be vacuously true. +verifyGroupSig :: C.PublicKeyEd25519 -> B64UrlByteString -> MemberId -> NonEmpty MsgSignature -> ByteString -> Bool +verifyGroupSig key publicGroupId memberId signatures signedBody = + let prefix = smpEncode CBGroup <> smpEncode (publicGroupId, memberId) + in all (\case (MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 key) sig (prefix <> signedBody)) signatures + processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessage _ _ (DEL_RCVQS delQs) = toView $ CEvtAgentRcvQueuesDeleted $ L.map rcvQ delQs @@ -111,7 +126,7 @@ processAgentMessage _ _ (DEL_RCVQS delQs) = processAgentMessage _ _ (DEL_CONNS connIds) = toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds processAgentMessage _ "" (ERR e) = - eToView $ ChatErrorAgent e (AgentConnId "") Nothing + eToView $ chatErrorAgent e processAgentMessage corrId connId msg = do lockEntity <- critical connId (withStore (`getChatLockEntity` AgentConnId connId)) withEntityLock "processAgentMessage" lockEntity $ do @@ -142,12 +157,23 @@ processAgentMessageNoConn = \case UP srv conns -> serverEvent srv SSActive conns SUSPENDED -> toView CEvtChatSuspended DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId + SERVICE_UP srv (ServiceSubResult e_ ss) -> serviceEvent srv $ ServiceSubUp (errText <$> e_) (smpQueueCount ss) + where + errText = \case + SSErrorServiceId {} -> "unexpected service ID" + SSErrorQueueCount {expectedQueueCount = n} -> "expected " <> tshow n <> " connections" + SSErrorQueueIdsHash {} -> "different IDs hash" + SERVICE_DOWN srv ss -> serviceEvent srv $ ServiceSubDown $ smpQueueCount ss + SERVICE_ALL srv -> serviceEvent srv ServiceSubAll + SERVICE_END srv ss -> serviceEvent srv $ ServiceSubEnd $ smpQueueCount ss ERRS cErrs -> errsEvent $ L.toList cErrs where hostEvent :: ChatEvent -> CM () hostEvent = whenM (asks $ hostEvents . config) . toView serverEvent :: SMPServer -> SubscriptionStatus -> [ConnId] -> CM () serverEvent srv nsStatus conns = toView $ CEvtSubscriptionStatus srv nsStatus $ map AgentConnId conns + serviceEvent :: SMPServer -> ServiceSubEvent -> CM () + serviceEvent srv = toView . CEvtServiceSubStatus srv errsEvent :: [(ConnId, AgentErrorType)] -> CM () errsEvent = toView . CEvtChatErrors . map (\(cId, e) -> ChatErrorAgent e (AgentConnId cId) Nothing) @@ -323,13 +349,24 @@ processAgentMsgRcvFile _corrId aFileId msg = do Just targetPath -> do fsTargetPath <- lift $ toFSFilePath targetPath renameFile xftpPath fsTargetPath - ci_ <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db fileId FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - lookupChatItemByFileId db cxt user fileId - agentXFTPDeleteRcvFile aFileId fileId - toView $ maybe (CEvtRcvStandaloneFileComplete user fsTargetPath ft) (CEvtRcvFileComplete user) ci_ + badDigest <- case ft of + RcvFileTransfer {fileInvitation = FileInvitation {fileDigest = Just d}, cryptoArgs} -> + (/= d) <$> cryptoFileDigest (CryptoFile fsTargetPath cryptoArgs) + _ -> pure False + if badDigest + then do + aci_ <- resetRcvCIFileStatus user fileId (CIFSRcvError $ FileErrOther "file digest") + forM_ aci_ cleanupACIFile + agentXFTPDeleteRcvFile aFileId fileId + forM_ aci_ $ \aci -> toView $ CEvtChatItemUpdated user aci + else do + ci_ <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + lookupChatItemByFileId db cxt user fileId + agentXFTPDeleteRcvFile aFileId fileId + toView $ maybe (CEvtRcvStandaloneFileComplete user fsTargetPath ft) (CEvtRcvFileComplete user) ci_ RFWARN e -> do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) @@ -355,8 +392,6 @@ processAgentMessageConn :: StoreCxt -> User -> ACorrId -> ConnId -> AEvent 'AECo processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. - -- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition - -- that will be resolved with app restart. entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db cxt user $ AgentConnId agentConnId) >>= updateConnStatus case agentMessage of END -> case entity of @@ -381,7 +416,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = agentMsgConnStatus :: Connection -> AEvent e -> Maybe ConnStatus agentMsgConnStatus Connection {connStatus = cs} = \case - JOINED True _ -> Just ConnSndReady + JOINED True -> Just ConnSndReady CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady CON _ -> Just ConnReady @@ -456,8 +491,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO [certs rcv] - JOINED _ _serviceId -> + JOINED _ -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () QCONT -> @@ -476,8 +510,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () Just ct@Contact {contactId} -> case agentMsg of - -- TODO [certs rcv] - INV (ACR _ cReq) _serviceId -> + INV (ACR _ cReq) -> -- [async agent commands] XGrpMemIntro continuation on receiving INV withCompletedCommand conn agentMsg $ \_ -> case cReq of @@ -564,7 +597,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = (gInfo, host) <- withStore $ \db -> do liftIO $ deleteContactCardKeepConn db connId ct createGroupInvitedViaLink db cxt user conn'' glInv - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) @@ -617,9 +650,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do groupInfo <- withStore $ \db -> getGroupInfo db cxt user groupId subMode <- chatReadVar subscriptionMode - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode + groupConnIds@(cmdId, grpConnId) <- prepareAgentCreation user CFCreateConnGrpInv True SCMInvitation gVar <- asks random withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode + withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) grpConnId True SCMInvitation CR.IKPQOff subMode -- TODO REMOVE LEGACY ^^^ SENT msgId proxy -> do void $ continueSending connEntity conn @@ -666,8 +700,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO [certs rcv] - JOINED sqSecured _serviceId -> + JOINED sqSecured -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do @@ -708,8 +741,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, customUserProfileId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of - -- TODO [certs rcv] - INV (ACR _ cReq) _serviceId -> + INV (ACR _ cReq) -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of @@ -816,8 +848,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemInfo memId _memProfile | sameMemberId memId m -> do let GroupMember {memberId = membershipMemId} = membership - allowSimplexLinks = groupUserAllowSimplexLinks gInfo - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability allowAgentConnectionAsync user conn' confId $ XGrpMemInfo membershipMemId membershipProfile @@ -864,6 +895,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = else pure gInfo pure (m {memberStatus = GSMemConnected}, gInfo') toView $ CEvtUserJoinedGroup user gInfo' m' + when (isRelay membership) $ do + cc <- ask + atomically $ channelProfileUpdated cc groupId groupProfile (gInfo'', m'', scopeInfo) <- mkGroupChatScope gInfo' m' -- Create e2ee, feature and group description chat items only on first connected relay ifM @@ -888,8 +922,21 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = GCInviteeMember | isRelay m -> do withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected - gLink <- withStore $ \db -> getGroupLink db user gInfo - setGroupLinkDataAsync user gInfo gLink + if m `supportsVersion` groupRosterVersion + then do + -- send the relay a roster (materializing version 0 for old channels with NULL roster_version); + -- the relay stays RSInvited (unpublishable) until it acks, so no joiner can impersonate a privileged member + gInfo' <- case rosterVersion gInfo of + Just _ -> pure gInfo + Nothing -> do + withStore' $ \db -> setGroupRosterVersion db gInfo (VersionRoster 0) + pure gInfo {rosterVersion = Just (VersionRoster 0)} + sendGroupRosterToRelay user gInfo' m + else do + -- a relay below groupRosterVersion can't ack a roster; publish it on connect as before + -- the handshake (getPublishableGroupRelays and the LINK handler include/activate it by version) + gLink <- withStore $ \db -> getGroupLink db user gInfo + setGroupLinkDataAsync user gInfo gLink | otherwise -> do (gInfo', mStatus) <- if not (memberPending m) @@ -1011,7 +1058,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure newDeliveryTasks processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> VerifiedMsg e -> CM (Maybe NewMessageDeliveryTask) processEvent gInfo' m' verifiedMsg = do - (m'', conn', msg@RcvMessage {msgId, chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta verifiedMsg + cc <- ask + (m'', conn', msg@RcvMessage {msgId, sharedMsgId_, chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta verifiedMsg let ctx js = DeliveryTaskContext js False checkSendAsGroup :: Maybe Bool -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) checkSendAsGroup asGroup_ a @@ -1050,25 +1098,38 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole -> fmap ctx <$> xGrpMemRole gInfo' m'' memId memRole msg brokerTs + XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' Nothing m'' memId memRole memberKey rosterVer msg brokerTs XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId - XGrpMemDel memId withMessages -> case encoding @e of - SJson -> fmap ctx <$> xGrpMemDel gInfo' m'' memId withMessages verifiedMsg msg brokerTs False + XGrpMemDel memId withMessages rosterVer -> case encoding @e of + SJson -> fmap ctx <$> xGrpMemDel gInfo' Nothing m'' memId withMessages rosterVer verifiedMsg msg brokerTs False SBinary -> pure Nothing XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs XGrpInfo p' -> fmap ctx <$> xGrpInfo gInfo' m'' p' msg brokerTs XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg + XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs + XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr + XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer -- TODO [knocking] why don't we forward these messages? XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe - BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta + BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' m'' sharedMsgId chunk msgMeta _ -> Nothing <$ messageError ("unsupported message: " <> tshow event) - forM deliveryTaskContext_ $ \taskContext -> + forM deliveryTaskContext_ $ \taskContext -> do + let contentChanged :: CM () + contentChanged = atomically $ channelContentChanged cc groupId + case event of + XMsgNew {} -> contentChanged + XMsgUpdate {} -> contentChanged + XMsgDel {} -> contentChanged + XMsgReact {} -> contentChanged + XGrpInfo p' -> atomically $ channelProfileUpdated cc groupId p' + XGrpDel {} -> atomically $ channelRemoved cc groupId + _ -> pure () pure $ NewMessageDeliveryTask {messageId = msgId, taskContext} checkSendRcpt :: [AParsedMsg] -> CM Bool checkSendRcpt aMsgs = do @@ -1119,7 +1180,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId updateGroupItemsStatus gInfo m conn msgId GSSSent (Just $ isJust proxy) - when continued $ sendPendingGroupMessages user gInfo m conn + when continued $ do + when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog + sendPendingGroupMessages user gInfo m conn SWITCH qd phase cStats -> do toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m @@ -1157,8 +1220,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () - -- TODO [certs rcv] - JOINED sqSecured _serviceId -> + JOINED sqSecured -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (sqSecured && connChatVersion >= batchSend2Version) $ do @@ -1172,9 +1234,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = CFGetRelayDataJoin -> do -- Update relay member with key, memberId and profile from link relayLinkData_ <- liftIO $ decodeLinkUserData cData - case (relayLinkData_, linkEntityId) of - (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> + relayMemberId <- case (relayLinkData_, linkEntityId) of + (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do withStore $ \db -> updateRelayMemberData db cxt user m (MemberId entityId) (MemberKey relayKey) p + pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" case cReq of CRContactUri crData@ConnReqUriData {crClientData} -> do @@ -1187,15 +1250,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} -- Update connection with data derived from cReq, now available after getConnShortLinkAsync withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup - let GroupMember {memberId = membershipMemId} = membership - incognitoProfile = incognitoMembershipProfile gInfo - profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) - memberPubKey <- case groupKeys gInfo of - Just GroupKeys {memberPrivKey} -> pure $ C.publicKey memberPrivKey - Nothing -> throwChatError $ CEInternalError "no group keys for channel membership" - dm <- encodeConnInfo $ XMember profileToSend membershipMemId (MemberKey memberPubKey) + let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo + profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo incognitoProfile + dm <- encodeXMemberConnInfo gInfo relayMemberId profileToSend subMode <- chatReadVar subscriptionMode - void $ joinAgentConnectionAsync user (Just conn) True cReq dm subMode + (cmdId, connId') <- prepareAgentJoin user (Just conn) True cReq + joinAgentConnectionAsync cmdId True connId' True cReq dm subMode CFGetRelayDataAccept -> do let GroupMember {memberId = MemberId expectedMemberId} = m if linkEntityId == Just expectedMemberId @@ -1203,7 +1263,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = relayProfile <- liftIO (decodeLinkUserData cData) >>= \case Just RelayShortLinkData {relayProfile = p} -> pure p Nothing -> throwChatError $ CEException "relay link: no relay link data" - (confId, m', relay) <- withStore $ \db -> do + (confId, m', relay) <- withStore $ \db -> do confId <- getRelayConfId db m liftIO $ updateGroupMemberStatus db userId m GSMemAccepted (m', relay) <- setRelayLinkAccepted db cxt user m (MemberKey relayKey) relayProfile @@ -1216,7 +1276,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = _ -> throwChatError $ CECommandError "unexpected cmdFunction" QCONT -> do continued <- continueSending connEntity conn - when continued $ sendPendingGroupMessages user gInfo m conn + when continued $ do + when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog + sendPendingGroupMessages user gInfo m conn MWARN msgId err -> do withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err) processConnMWARN connEntity conn err @@ -1289,13 +1351,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = r n'' = Just (ci, CIRcvDecryptionError mde n'') mdeUpdatedCI _ _ = Nothing - receiveFileChunk :: RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM () - receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case - FileChunkCancel -> - unless (rcvFileCompleteOrCancelled ft) $ do - cancelRcvFileTransfer user ft - ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId - toView $ CEvtRcvFileSndCancelled user ci ft + receiveFileChunk :: Maybe GroupInfo -> RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM () + receiveFileChunk gInfo_ ft@RcvFileTransfer {fileId, fileType, chunkSize} conn_ MsgMeta {recipient = (msgId, _), integrity} = \case + FileChunkCancel -> case fileType of + -- cancel only this source's transfer; other relays' in-flight transfers are independent + FTRoster -> do + t_ <- withStore' $ \db -> getRosterTransfer db fileId + forM_ t_ $ \RcvRosterTransfer {rosterTransferId} -> cleanupRosterTransferById rosterTransferId + FTNormal -> + unless (rcvFileCompleteOrCancelled ft) $ do + cancelRcvFileTransfer user ft + ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId + toView $ CEvtRcvFileSndCancelled user ci ft FileChunk {chunkNo, chunkBytes = chunk} -> do case integrity of MsgOk -> pure () @@ -1306,30 +1373,33 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = RcvChunkOk -> if B.length chunk /= fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" - else withAckMessage' "file msg" agentConnId meta $ appendFileChunk ft chunkNo chunk False + else appendFileChunk ft chunkNo chunk False RcvChunkFinal -> if B.length chunk > fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" else do appendFileChunk ft chunkNo chunk True - ci <- withStore $ \db -> do - liftIO $ do - updateRcvFileStatus db fileId FSComplete - updateCIFileStatus db user fileId CIFSRcvComplete - deleteRcvFileChunks db ft - getChatItemByFileId db cxt user fileId - toView $ CEvtRcvFileComplete user ci - mapM_ (deleteAgentConnectionAsync . aConnId) conn_ - RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () + case fileType of + FTRoster -> forM_ gInfo_ $ \gInfo -> rosterCompletion gInfo ft + FTNormal -> do + ci <- withStore $ \db -> do + liftIO $ do + updateRcvFileStatus db fileId FSComplete + updateCIFileStatus db user fileId CIFSRcvComplete + deleteRcvFileChunks db ft + getChatItemByFileId db cxt user fileId + toView $ CEvtRcvFileComplete user ci + mapM_ (deleteAgentConnectionAsync . aConnId) conn_ + RcvChunkDuplicate -> pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of REQ invId pqSupport _ connInfo -> do - ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + (signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo case chatMsgEvent of XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport - XMember p joiningMemberId joiningMemberKey -> memberJoinRequestViaRelay invId chatVRange p joiningMemberId joiningMemberKey + XMember p joiningMemberId joiningMemberKey viaRelay -> memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey viaRelay XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv XGrpRelayTest challenge _ -> xGrpRelayTest invId chatVRange challenge @@ -1345,9 +1415,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = gInfo <- getGroupInfo db cxt user groupId gLink <- getGroupLink db user gInfo relays <- liftIO $ getGroupRelays db gInfo - (relays', changed, newlyActive) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays + (relays', changed, newlyActiveLinks) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays liftIO $ setGroupInProgressDone db gInfo - pure (gInfo, gLink, relays', changed, newlyActive) + pure (gInfo, gLink, relays', changed, newlyActiveLinks) toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged let GroupSummary {publicMemberCount} = groupSummary gInfo -- Owner is counted in publicMemberCount; > 1 means at least one subscriber. @@ -1363,16 +1433,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = allRelayMembers events = XGrpRelayNew <$> newlyActive unless (null recipients) $ - void $ sendGroupMessages user gInfo Nothing False recipients events + void $ sendGroupMessages user gInfo Nothing False recipients False events where updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact]) -> IO ([GroupRelay], Bool, [ShortLinkContact]) - updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActive) = + updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActiveLinks) = case relayLink of Just rLink - | rLink `elem` relayLinks && relayStatus == RSAccepted -> do + -- version is gated upstream at publish (getPublishableGroupRelays): an RSAccepted relay + -- whose link is in the published data is necessarily pre-roster, so activate it too + | rLink `elem` relayLinks && (relayStatus == RSAcknowledgedRoster || relayStatus == RSAccepted) -> do relay' <- updateRelayStatus db relay RSActive - pure (relay' : acc, True, rLink : newlyActive) - | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActive) + pure (relay' : acc, True, rLink : newlyActiveLinks) + | rLink `elem` relayLinks -> pure (relay : acc, changed, newlyActiveLinks) | relayStatus == RSActive -> do -- Relay link absent from link data — deactivate. -- RSAccepted relays are not deactivated: their own link data update @@ -1381,8 +1453,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO the SMP server, but this owner won't receive a LINK callback for it -- TODO (LINK only fires in response to own setConnShortLink calls). relay' <- updateRelayStatus db relay RSInactive - pure (relay' : acc, True, newlyActive) - _ -> pure (relay : acc, changed, newlyActive) + pure (relay' : acc, True, newlyActiveLinks) + _ -> pure (relay : acc, changed, newlyActiveLinks) _ -> throwChatError $ CECommandError "LINK event expected for a group link only" _ -> throwChatError $ CECommandError "unexpected cmdFunction" MERR _ err -> do @@ -1423,12 +1495,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- they will be updated after connection is accepted. upsertDirectRequestItem cd (requestMsg_, prevSharedMsgId_) Nothing -> do - void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) let e2eContent = CIRcvDirectE2EEInfo $ e2eInfoEncrypted $ Just $ CR.pqSupportToEnc $ reqPQSup - void $ createChatItem user cd False e2eContent Nothing Nothing + void $ createChatItem user cd False e2eContent Nothing Nothing Nothing void $ createFeatureEnabledItems_ user ct forM_ (autoReply addressSettings) $ \mc -> forM_ welcomeSharedMsgId $ \sharedMsgId -> - createChatItem user (CDDirectSnd ct) False (CISndMsgContent mc) (Just sharedMsgId) Nothing + createChatItem user (CDDirectSnd ct) False (CISndMsgContent mc) (Just sharedMsgId) Nothing Nothing mapM (createRequestItem cd) requestMsg_ case autoAccept of Nothing -> do @@ -1453,13 +1525,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- they will be updated after connection is accepted. upsertBusinessRequestItem cd (requestMsg_, prevSharedMsgId_) Nothing -> do - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) -- TODO [short links] possibly, we can just keep them created where they are created on the business side due to auto-accept -- let e2eContent = CIRcvGroupE2EEInfo $ E2EInfo $ Just False -- no PQ encryption in groups - -- void $ createChatItem user cd False e2eContent Nothing Nothing + -- void $ createChatItem user cd False e2eContent Nothing Nothing Nothing -- void $ createFeatureEnabledItems_ user ct forM_ (autoReply addressSettings) $ \arMC -> forM_ welcomeSharedMsgId $ \sharedMsgId -> - createChatItem user (CDGroupSnd gInfo Nothing) False (CISndMsgContent arMC) (Just sharedMsgId) Nothing + createChatItem user (CDGroupSnd gInfo Nothing) False (CISndMsgContent arMC) (Just sharedMsgId) Nothing Nothing mapM (createRequestItem cd) requestMsg_ toView $ CEvtAcceptingBusinessRequest user gInfo where @@ -1523,7 +1595,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = upsertBusinessRequestItem (CDChannelRcv _ _) = const $ pure Nothing createRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> (SharedMsgId, MsgContent) -> CM AChatItem createRequestItem cd (sharedMsgId, mc) = do - aci <- createChatItem user cd False (CIRcvMsgContent mc) (Just sharedMsgId) Nothing + aci <- createChatItem user cd False (CIRcvMsgContent mc) (Just sharedMsgId) Nothing Nothing toView $ CEvtNewChatItems user [aci] pure aci upsertRequestItem :: ChatTypeI c => ChatDirection c 'MDRcv -> ((SharedMsgId, MsgContent) -> CM (Maybe AChatItem)) -> (SharedMsgId -> CM ()) -> (Maybe (SharedMsgId, MsgContent), Maybe SharedMsgId) -> CM (Maybe AChatItem) @@ -1551,7 +1623,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError "processContactConnMessage: chat version range incompatible for accepting group join request" | otherwise -> do let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' @@ -1580,29 +1652,47 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = let sig = C.signatureBytes $ C.sign' privKey challenge msg = XGrpRelayTest challenge (Just sig) subMode <- chatReadVar subscriptionMode - chatVR <- chatVersionRange - let chatV = chatVR `peerConnChatVersion` chatVRange - (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV + let chatV = vr cxt `peerConnChatVersion` chatVRange + (cmdId, acId) <- prepareAgentAccept user True invId PQSupportOff withStore $ \db -> do Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode liftIO $ setCommandConnId db user cmdId testCId + agentAcceptContactAsync cmdId acId True invId msg PQSupportOff chatV subMode | otherwise = messageError "relay test sent to non-relay link" where User {userChatRelay} = user -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays - -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) - memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Profile -> MemberId -> MemberKey -> CM () - memberJoinRequestViaRelay invId chatVRange p joiningMemberId joiningMemberKey = do + memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> Profile -> MemberId -> MemberKey -> Maybe MemberId -> CM () + memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey@(MemberKey joiningKey) viaRelay = do (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId case gLinkInfo_ of Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted gLinkMemRole Nothing (Just joiningMemberKey) + existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId) + case existing_ of + Just rosterMem + -- a privileged memberId's key is owner-authoritative (the roster); the joiner must prove + -- possession of that exact key, otherwise this is an attempt to impersonate it + | isRosterRole (memberRole' rosterMem) -> + if verifyKey gInfo rosterMem + then acceptJoin gInfo (Just rosterMem) (memberRole' rosterMem) + else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)" + _ -> acceptJoin gInfo Nothing gLinkMemRole + Nothing -> + messageError "memberJoinRequestViaRelay: no group link info for relay link" + where + -- replay defense: the viaRelay == own memberId check (viaRelay is in the signed body); without it a sibling relay could replay a privileged member's signed join + verifyKey gInfo rosterMem = case (signedMsg_, groupKeys gInfo) of + (Just SignedMsg {chatBinding = CBGroup, signatures, signedBody}, Just GroupKeys {publicGroupId}) -> + memberPubKey rosterMem == Just joiningKey + && verifyGroupSig joiningKey publicGroupId joiningMemberId signatures signedBody + && viaRelay == Just (memberId' (membership gInfo)) + _ -> False + acceptJoin gInfo existingMem_ acceptRole = do + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted acceptRole Nothing (Just joiningMemberKey) existingMem_ (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' - Nothing -> - messageError "memberJoinRequestViaRelay: no group link info for relay link" muteEventInChannel :: GroupInfo -> GroupMember -> Bool muteEventInChannel gInfo@GroupInfo {membership} m = @@ -1694,7 +1784,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unless shouldDelConns $ withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing -- If showCritical is True, then these errors don't result in ACK and show user visible alert -- This prevents losing the message that failed to be processed. - Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) (AgentConnId "") Nothing + Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ chatErrorAgent $ CRITICAL True message Left e -> do withLog (eInfo <> " error: " <> tshow e) $ ackMsg msgMeta Nothing throwError e @@ -1880,7 +1970,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = processFDMessage fileId aci fileDescr = do ft <- withStore $ \db -> getRcvFileTransfer db user fileId unless (rcvFileCompleteOrCancelled ft) $ do - (rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do + (rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs, fileInvitation = FileInvitation {fileSize}}) <- withStore $ \db -> do rfd <- appendRcvFD db userId fileId fileDescr -- reading second time in the same transaction as appending description -- to prevent race condition with accept @@ -1888,15 +1978,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure (rfd, ft') when fileDescrComplete $ toView $ CEvtRcvFileDescrReady user aci ft' rfd case (fileStatus, xftpRcvFile) of - (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs + (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd fileSize userApprovedRelays cryptoArgs _ -> pure () processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) - processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv' -> do + processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv -> do ChatConfig {fileChunkSize} <- asks config - let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' - inline <- receiveInlineMode fInv (Just mc) fileChunkSize - ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize + fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv + inline <- receiveInlineMode fInv' (Just mc) fileChunkSize + ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv' inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP (filePath, fileStatus, ft') <- case inline of Just IFMSent -> do @@ -1913,6 +2003,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = mkValidFileInvitation :: FileInvitation -> FileInvitation mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName} + validateFileInvitation :: FileInvitation -> CM FileInvitation + validateFileInvitation fInv@FileInvitation {fileName, fileSize} + | fileSize > 0 = pure $ mkValidFileInvitation fInv + | otherwise = throwChatError $ CEFileSize fileName + messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM () messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do updateRcvChatItem `catchCINotFound` \_ -> do @@ -2061,21 +2156,25 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createContentItem gInfo Nothing Nothing -- no delivery task - message already forwarded by relay pure Nothing - Just m@GroupMember {memberId} -> do - (gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m content msgScope_ - if blockedByAdmin m' - then createBlockedByAdmin gInfo' (Just m') scopeInfo $> Nothing - else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of - Just f -> rejected gInfo' (Just m') scopeInfo f $> Nothing - Nothing -> - withStore' (\db -> getCIModeration db cxt user gInfo' memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration gInfo' m' scopeInfo ciModeration - withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ - pure Nothing - Nothing -> do - createContentItem gInfo' (Just m') scopeInfo - pure $ Just $ infoToDeliveryContext gInfo' scopeInfo sentAsGroup + Just m@GroupMember {memberId} + -- only an owner may post as the channel; a non-owner's signed asGroup post (e.g. relay-injected) must not render as the channel + | sentAsGroup && memberRole' m < GROwner -> + messageError "x.msg.new: member is not allowed to send as group" $> Nothing + | otherwise -> do + (gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m content msgScope_ + if blockedByAdmin m' + then createBlockedByAdmin gInfo' (Just m') scopeInfo $> Nothing + else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of + Just f -> rejected gInfo' (Just m') scopeInfo f $> Nothing + Nothing -> + withStore' (\db -> getCIModeration db cxt user gInfo' memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration gInfo' m' scopeInfo ciModeration + withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ + pure Nothing + Nothing -> do + createContentItem gInfo' (Just m') scopeInfo + pure $ Just $ infoToDeliveryContext gInfo' scopeInfo sentAsGroup where rejected gInfo' m' scopeInfo f = newChatItem gInfo' m' scopeInfo (ciContentNoParse $ CIRcvGroupFeatureRejected f) Nothing Nothing False timed_ gInfo' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo' itemTTL @@ -2122,14 +2221,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = toView $ CEvtChatItemsDeleted user deletions False False -- m' is Maybe GroupMember createNonLive gInfo' m' scopeInfo file_ = do - saveRcvCI gInfo' m' scopeInfo (CIRcvMsgContent content, ts) (snd <$> file_) (timed_ gInfo') False mentions + let mentions' = if maybe False memberBlocked m' then M.empty else mentions + saveRcvCI gInfo' m' scopeInfo (CIRcvMsgContent content, ts) (snd <$> file_) (timed_ gInfo') False mentions' createContentItem gInfo' m' scopeInfo = do file_ <- processFileInv gInfo' m' newChatItem gInfo' m' scopeInfo (CIRcvMsgContent content, ts) (snd <$> file_) (timed_ gInfo') live' unless (maybe False memberBlocked m') $ autoAcceptFile file_ processFileInv gInfo' m' = let fileMember_ = if sentAsGroup then Nothing else m' - in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ + in processFileInvitation fInv_ content $ \db -> createRcvGroupFileTransfer db userId gInfo' fileMember_ FTNormal sharedMsgId_ newChatItem gInfo' m' scopeInfo ciContent ciFile_ timed live = do let mentions' = if maybe False memberBlocked m' then M.empty else mentions (ci, cInfo) <- saveRcvCI gInfo' m' scopeInfo ciContent ciFile_ timed live mentions' @@ -2139,7 +2239,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = groupMsgToView cInfo ci' {reactions} groupMessageUpdate :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> MsgContent -> Map MemberName MsgMention -> Maybe MsgScope -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> Maybe Bool -> CM (Maybe DeliveryTaskContext) - groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId} brokerTs ttl_ live_ asGroup_ + groupMessageUpdate gInfo@GroupInfo {groupId} m_ sharedMsgId mc mentions msgScope_ msg@RcvMessage {msgId, msgSigned, signedMsg_, signedByGMId_} brokerTs ttl_ live_ asGroup_ | Just m <- m_, prohibitedSimplexLinks gInfo m mc ft_ = messageWarning ("x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks) $> Nothing | otherwise = do @@ -2191,15 +2291,24 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Nothing -> getGroupChatItemBySharedMsgId db user gInfo Nothing sharedMsgId (cci,) <$> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci) case cci of - CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} - | isSender m' -> updateCI False ci scopeInfo oldMC itemLive (Just $ memberId' m') + CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive, msgVerified = itemVerified}, content = CIRcvMsgContent oldMC} + | isSender m' -> requireVerifiedEdit (CDGroupRcv gInfo scopeInfo m') itemVerified $ updateCI False ci scopeInfo oldMC itemLive (Just $ memberId' m') | otherwise -> messageError "x.msg.update: group member attempted to update a message of another member" $> Nothing - CChatItem SMDRcv ci@ChatItem {chatDir = CIChannelRcv, meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} - | maybe True (\m -> memberRole' m == GROwner) m_ -> updateCI True ci scopeInfo oldMC itemLive Nothing + CChatItem SMDRcv ci@ChatItem {chatDir = CIChannelRcv, meta = CIMeta {itemLive, msgVerified = itemVerified}, content = CIRcvMsgContent oldMC} + | maybe True (\m -> memberRole' m == GROwner) m_ -> requireVerifiedEdit (CDChannelRcv gInfo scopeInfo) itemVerified $ updateCI True ci scopeInfo oldMC itemLive Nothing | otherwise -> messageError "x.msg.update: member attempted to update channel message" $> Nothing _ -> messageError "x.msg.update: invalid message update" $> Nothing where isSender m' = maybe False (\m -> sameMemberId (memberId' m) m') m_ + -- a verified item requires a verified edit (fail-closed): unsigned is a forgery (bad-signature item); signed-but-no-key is unverifiable (drop with a log) + requireVerifiedEdit :: ChatDirection 'CTGroup 'MDRcv -> Maybe MsgVerified -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) + requireVerifiedEdit cd itemVerified action + | itemVerified == Just (MVSigned MSSVerified) = + case msgSigned of + Just MSSVerified -> action + Just MSSSignedNoKey -> logWarn "x.msg.update: unverified update of a signed item (no key to verify), dropped" $> Nothing + Nothing -> createInternalChatItem user cd (CIRcvGroupEvent RGEMsgBadSignature) (Just brokerTs) $> Nothing + | otherwise = action updateCI :: ShowGroupAsSender -> ChatItem 'CTGroup 'MDRcv -> Maybe GroupChatScopeInfo -> MsgContent -> Maybe Bool -> Maybe MemberId -> CM (Maybe DeliveryTaskContext) updateCI showGroupAsSender ci scopeInfo oldMC itemLive memberId = do let changed = mc /= oldMC @@ -2212,6 +2321,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = let edited = itemLive /= Just True ciMentions <- getRcvCIMentions db user gInfo ft_ mentions ci' <- updateGroupChatItem db user groupId ci {reactions} content edited live $ Just msgId + updateChatItemSignedMsg db (chatItemId' ci) signedMsg_ signedByGMId_ updateGroupCIMentions db gInfo ci' ciMentions toView $ CEvtChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo scopeInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId $ toChatScope <$> scopeInfo) ci ci' @@ -2223,7 +2333,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = groupMessageDelete :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> Maybe MemberId -> Maybe MsgScope -> Bool -> RcvMessage -> UTCTime -> CM (Maybe DeliveryTaskContext) groupMessageDelete gInfo@GroupInfo {membership} m_ sharedMsgId sndMemberId_ scope_ onlyHistory rcvMsg brokerTs = findItem >>= \case - Right cci@(CChatItem _ ci@ChatItem {chatDir}) -> case (chatDir, m_) of + Right cci@(CChatItem _ ci@ChatItem {chatDir}) -> requireVerifiedDelete cci $ case (chatDir, m_) of (CIGroupRcv mem, Just m@GroupMember {memberId}) -> let msgMemberId = fromMaybe memberId sndMemberId_ isAuthor = sameMemberId memberId mem @@ -2256,6 +2366,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | senderRole < GRModerator -> do messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e pure Nothing + -- a forged unsigned moderation would pre-censor a not-yet-received post via CIModeration; require verified (relay moderation always signs) + | useRelays' gInfo && msgSigned /= Just MSSVerified -> + messageError ("x.msg.del: unverified moderation of message not yet received, " <> tshow e) $> Nothing | otherwise -> case scope_ of Just (MSMember scopeMemberId) -> withStore $ \db -> do @@ -2269,7 +2382,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError ("x.msg.del: channel message not found, " <> tshow e) $> Nothing where isOwner = maybe True (\m -> memberRole' m == GROwner) m_ - RcvMessage {msgId} = rcvMsg + RcvMessage {msgId, msgSigned} = rcvMsg findItem = do let tryMemberLookup mId = withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user gInfo mId sharedMsgId) @@ -2299,6 +2412,23 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | senderRole < GRModerator || senderRole < memberRole = messageError "x.msg.del: message of another member with insufficient member permissions" $> Nothing | otherwise = a + -- a verified item requires a verified delete (fail-closed): unsigned is a forgery (bad-signature item); signed-but-no-key is unverifiable (drop with a log) + requireVerifiedDelete :: CChatItem 'CTGroup -> CM (Maybe DeliveryTaskContext) -> CM (Maybe DeliveryTaskContext) + requireVerifiedDelete cci@(CChatItem _ ChatItem {chatDir, meta = CIMeta {msgVerified = itemVerified}}) action + | itemVerified == Just (MVSigned MSSVerified) = + case msgSigned of + Just MSSVerified -> action + Just MSSSignedNoKey -> logWarn "x.msg.del: unverified delete of a signed item (no key to verify), dropped" $> Nothing + Nothing -> do + scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci) + let cd :: ChatDirection 'CTGroup 'MDRcv + cd = case chatDir of + CIGroupRcv mem -> CDGroupRcv gInfo scopeInfo mem + CIChannelRcv -> CDChannelRcv gInfo scopeInfo + CIGroupSnd -> CDGroupRcv gInfo scopeInfo membership + createInternalChatItem user cd (CIRcvGroupEvent RGEMsgBadSignature) (Just brokerTs) + pure Nothing + | otherwise = action delete :: CChatItem 'CTGroup -> Bool -> Maybe GroupMember -> CM (Maybe DeliveryTaskContext) delete cci asGroup byGroupMember = do scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci) @@ -2318,11 +2448,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO remove once XFile is discontinued processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () - processFileInvitation' ct fInv' msg@RcvMessage {sharedMsgId_} msgMeta = do + processFileInvitation' ct fInv msg@RcvMessage {sharedMsgId_} msgMeta = do ChatConfig {fileChunkSize} <- asks config - let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' - inline <- receiveInlineMode fInv Nothing fileChunkSize - RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize + fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv + inline <- receiveInlineMode fInv' Nothing fileChunkSize + RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv' inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" @@ -2333,10 +2463,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO remove once XFile is discontinued processGroupFileInvitation' :: GroupInfo -> GroupMember -> FileInvitation -> RcvMessage -> UTCTime -> CM () - processGroupFileInvitation' gInfo m fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} brokerTs = do + processGroupFileInvitation' gInfo m fInv msg@RcvMessage {sharedMsgId_} brokerTs = do ChatConfig {fileChunkSize} <- asks config - inline <- receiveInlineMode fInv Nothing fileChunkSize - RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) fInv inline fileChunkSize + fInv'@FileInvitation {fileName, fileSize} <- validateFileInvitation fInv + inline <- receiveInlineMode fInv' Nothing fileChunkSize + RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId gInfo (Just m) FTNormal sharedMsgId_ fInv' inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} content = ciContentNoParse $ CIRcvMsgContent $ MCFile "" @@ -2432,10 +2563,17 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = ft <- withStore $ \db -> getDirectFileIdBySharedMsgId db user ct sharedMsgId >>= getRcvFileTransfer db user receiveInlineChunk ft chunk meta - bFileChunkGroup :: GroupInfo -> SharedMsgId -> FileChunk -> MsgMeta -> CM () - bFileChunkGroup GroupInfo {groupId} sharedMsgId chunk meta = do - ft <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId >>= getRcvFileTransfer db user - receiveInlineChunk ft chunk meta + -- A group BFileChunk is a normal inline file chunk or a roster blob chunk, both located by + -- (group_id, shared_msg_id). A chunk matching no in-flight transfer (an orphaned re-served roster + -- chunk, or a missing normal file) is ignored; the outer withAckMessage acks it. + bFileChunkGroup :: GroupInfo -> GroupMember -> SharedMsgId -> FileChunk -> MsgMeta -> CM () + bFileChunkGroup gInfo@GroupInfo {groupId} fromMember sharedMsgId chunk meta = do + fileId_ <- withStore' $ \db -> getGroupRcvFileId db userId groupId (groupMemberId' fromMember) sharedMsgId + forM_ fileId_ $ \fileId -> do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId + case fileType ft of + FTRoster -> receiveRosterChunk gInfo ft meta chunk + FTNormal -> receiveInlineChunk ft chunk meta receiveInlineChunk :: RcvFileTransfer -> FileChunk -> MsgMeta -> CM () receiveInlineChunk RcvFileTransfer {fileId, fileStatus = RFSNew} FileChunk {chunkNo} _ @@ -2445,7 +2583,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = case chunk of FileChunk {chunkNo} -> when (chunkNo == 1) $ startReceivingFile user fileId _ -> pure () - receiveFileChunk ft Nothing meta chunk + receiveFileChunk Nothing ft Nothing meta chunk + + -- A roster re-serve re-sends the blob from chunk 1; discard any partial first, else chunk 1 over a + -- partial is out-of-order (RcvChunkError) and appending after the stale prefix corrupts the blob. + receiveRosterChunk :: GroupInfo -> RcvFileTransfer -> MsgMeta -> FileChunk -> CM () + receiveRosterChunk gInfo ft meta chunk = do + case chunk of + FileChunk {chunkNo} | chunkNo == 1 -> do + last_ <- withStore' $ \db -> getRcvFileLastChunkNo db ft + when (isJust last_) $ resetRosterPartialChunks ft + _ -> pure () + receiveFileChunk (Just gInfo) ft Nothing meta chunk xFileCancelGroup :: GroupInfo -> Maybe GroupMember -> SharedMsgId -> CM (Maybe DeliveryTaskContext) xFileCancelGroup g@GroupInfo {groupId} m_ sharedMsgId = do @@ -2503,18 +2652,19 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId - void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) + void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart) let GroupMember {groupMemberId, memberId = membershipMemId} = membership if sameGroupLinkId groupLinkId groupLinkId' then do subMode <- chatReadVar subscriptionMode dm <- encodeConnInfo $ XGrpAcpt membershipMemId - connIds <- joinAgentConnectionAsync user Nothing True connRequest dm subMode + connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest withStore' $ \db -> do setViaGroupLinkUri db groupId connId createMemberConnectionAsync db user hostId connIds connChatVersion peerChatVRange subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted + joinAgentConnectionAsync cmdId False acId True connRequest dm subMode toView $ CEvtUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) else do let content = CIRcvGroupInvitation (CIGroupInvitation {groupId, groupMemberId, localDisplayName, groupProfile, status = CIGISPending}) memRole @@ -2684,7 +2834,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when contentChanged $ updateBusinessChatProfile gInfo case memberContactId of Nothing -> do - m' <- withStore $ \db -> updateMemberProfile db cxt user m p' + m' <- withStore $ \db -> updateMemberProfile db cxt user m p'' unless (muteEventInChannel gInfo m') $ do when contentChanged $ forM_ msgTs_ $ createProfileUpdatedItem m' toView $ CEvtGroupMemberUpdated user gInfo m m' @@ -2709,8 +2859,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | otherwise = pure m where - contentChanged = not (sameProfileContent (redactedMemberProfile allowSimplexLinks (fromLocalProfile p)) (redactedMemberProfile allowSimplexLinks p')) - allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo && groupFeatureMemberAllowed SGFDirectMessages m gInfo + p'' = redactedMemberProfile gInfo m p' + contentChanged = not (sameProfileContent (redactedMemberProfile gInfo m (fromLocalProfile p)) p'') updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of Just bc | isMainBusinessMember bc m -> do g' <- withStore $ \db -> updateGroupProfileFromMember db user g p' @@ -2967,38 +3117,62 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = _ -> pure (conn', Nothing) xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ _) msgScope_ msg brokerTs = do - unless (useRelays' gInfo && isRelay m) $ checkHostRole m memRole + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do + unless (useRelays' gInfo) $ checkHostRole m memRole if sameMemberId memId (membership gInfo) then pure Nothing - else do + else withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case - Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do - (updatedMember, gInfo') <- withStore $ \db -> do - updatedMember <- updateUnknownMemberAnnounced db cxt user m unknownMember memInfo initialStatus - gInfo' <- - if memberPending updatedMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo - pure (updatedMember, gInfo') - gInfo'' <- updatePublicGroupData user gInfo' - toView $ CEvtUnknownMemberAnnounced user gInfo'' m unknownMember updatedMember - memberAnnouncedToView updatedMember gInfo'' - pure $ deliveryJobScope updatedMember + Right unknownMember@GroupMember {memberStatus = GSMemUnknown} + -- roster-established privileged member: the relay may update the profile only, + -- never the role or key (those are owner-authoritative via the roster, and + -- XGrpMemNew is unsigned) + | useRelays' gInfo && isPrivilegedRole (memberRole' unknownMember) -> do + -- a member's key is immutable per memberId and identical across relays; mismatch + -- is unambiguous relay misbehavior (role can legitimately differ across relays + -- under multi-relay skew, so we deliberately don't warn on role) + let assertedKey = (\(MemberKey k) -> k) <$> assertedKey_ + -- TODO [relays] member: surface relay-key-mismatch as a dedicated event / chat item / relay state + when (assertedKey /= memberPubKey unknownMember) $ + messageWarning $ "x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" <> safeDecodeUtf8 (strEncode memId) + updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db cxt user m unknownMember memInfo initialStatus + -- roster members can't be pending, so no members-require-attention update + gInfo' <- updatePublicGroupData user gInfo + toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember + memberAnnouncedToView updatedMember gInfo' + pure $ deliveryJobScope updatedMember + -- asserted privileged but NOT roster-established: relay conjuring a privileged member + | useRelays' gInfo && isPrivilegedRole memRole -> + messageError "x.grp.mem.new: privileged role not established by roster" $> Nothing + | otherwise -> do + (updatedMember, gInfo') <- withStore $ \db -> do + updatedMember <- updateUnknownMemberAnnounced db cxt user m unknownMember memInfo initialStatus + gInfo' <- + if memberPending updatedMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (updatedMember, gInfo') + gInfo'' <- updatePublicGroupData user gInfo' + toView $ CEvtUnknownMemberAnnounced user gInfo'' m unknownMember updatedMember + memberAnnouncedToView updatedMember gInfo'' + pure $ deliveryJobScope updatedMember Right _ | useRelays' gInfo -> logInfo "x.grp.mem.new: member already created via another relay" $> Nothing | otherwise -> messageError "x.grp.mem.new error: member already exists" $> Nothing - Left _ -> do - (newMember, gInfo') <- withStore $ \db -> do - newMember <- createNewGroupMember db cxt user gInfo m memInfo GCPostMember initialStatus - gInfo' <- - if memberPending newMember - then liftIO $ increaseGroupMembersRequireAttention db user gInfo - else pure gInfo - pure (newMember, gInfo') - gInfo'' <- updatePublicGroupData user gInfo' - memberAnnouncedToView newMember gInfo'' - pure $ deliveryJobScope newMember + Left _ + -- a privileged member absent from the roster is a relay conjuring one + | useRelays' gInfo && isPrivilegedRole memRole -> messageError "x.grp.mem.new: privileged member not established by roster" $> Nothing + | otherwise -> do + (newMember, gInfo') <- withStore $ \db -> do + newMember <- createNewGroupMember db cxt user gInfo m memInfo GCPostMember initialStatus + gInfo' <- + if memberPending newMember + then liftIO $ increaseGroupMembersRequireAttention db user gInfo + else pure gInfo + pure (newMember, gInfo') + gInfo'' <- updatePublicGroupData user gInfo' + memberAnnouncedToView newMember gInfo'' + pure $ deliveryJobScope newMember where initialStatus = case msgScope_ of Just (MSMember _) -> GSMemPendingReview @@ -3037,10 +3211,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = messageError "x.grp.mem.intro ignored: member already exists" Left _ | useRelays' gInfo -> do - -- owner key must only come from link data, not from relay intro + -- role + key are owner-authoritative (roster); an intro establishes neither - a privileged + -- claim is created at the channel default with no key until the owner-signed roster confirms it + defaultRole <- unknownMemberRole gInfo let memInfo' = case memInfo of MemberInfo mId mRole v p _ - | mRole == GROwner -> MemberInfo mId mRole v p Nothing + | mRole >= GRMember -> MemberInfo mId defaultRole v p Nothing _ -> memInfo void $ withStore $ \db -> createIntroReMember db cxt user gInfo memInfo' memRestrictions | otherwise -> do @@ -3050,16 +3226,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just (ChatVersionRange mcvr) | maxVersion mcvr >= groupDirectInvVersion -> do subMode <- chatReadVar subscriptionMode - -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createConn subMode + groupConnIds@(cmdId, connId) <- prepareAgentCreation user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation let chatV = maybe (minVersion (vr cxt)) (\peerVR -> vr cxt `peerConnChatVersion` fromChatVRange peerVR) memChatVRange void $ withStore $ \db -> do reMember <- createIntroReMember db cxt user gInfo memInfo memRestrictions createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode + withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId (chatHasNtfs chatSettings) SCMInvitation CR.IKPQOff subMode | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" _ -> messageError "x.grp.mem.intro can be only sent by host member" - where - createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation subMode sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM () sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do @@ -3099,39 +3273,308 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure toMember subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito - let allowSimplexLinks = groupUserAllowSimplexLinks gInfo - membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile allowSimplexLinks $ fromLocalProfile $ memberProfile membership + membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership dm <- encodeConnInfo $ XGrpMemInfo membershipMemId membershipProfile -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user Nothing (chatHasNtfs chatSettings) groupConnReq dm subMode - directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user Nothing True dcr dm subMode + let enableNtfsGrp = chatHasNtfs chatSettings + groupConnIds@(gCmdId, gAcId) <- prepareAgentJoin user Nothing enableNtfsGrp groupConnReq + directConnIds <- mapM (prepareAgentJoin user Nothing True) directConnReq let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo mcvr = maybe chatInitialVRange fromChatVRange memChatVRange chatV = vr cxt `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode + joinAgentConnectionAsync gCmdId False gAcId enableNtfsGrp groupConnReq dm subMode + forM_ ((,) <$> directConnIds <*> directConnReq) $ \((dCmdId, dAcId), dcr) -> + joinAgentConnectionAsync dCmdId False dAcId True dcr dm subMode - xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) - xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg@RcvMessage {msgSigned} brokerTs + -- rollback defense (channels): apply an owner-signed role/removal only at a version >= the persisted + -- roster_version (not the batch-constant gInfo, which a relay can stale by reordering events in one + -- batch), then advance it in the same transaction; a strictly lower version is a replay and is ignored. + -- Only an owner sender may advance it: a non-owner signed event is rejected by the action that follows, + -- but must not bump roster_version first, or every later owner roster at a lower version is dropped. + applyAtRosterVersion :: GroupInfo -> Maybe GroupMember -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope) + applyAtRosterVersion gInfo fwdRelay_ sender rosterVer_ action + | not (useRelays' gInfo) = action + | otherwise = case rosterVer_ of + Nothing -> action + Just _ | memberRole' sender /= GROwner -> action + Just v -> do + (accept, prevComplete) <- withStore' $ \db -> do + gate <- getGroupRosterVersion db gInfo + prevComplete <- getCompleteRosterVersion db gInfo + let fresh = maybe True (v >=) gate + when fresh $ do + setGroupRosterVersion db gInfo v + -- advance the frontier when this delta is the next version. One version can carry several deltas + -- (a multi-member role change), delivered in order, so seeing any one of them advances the frontier + -- past that whole version. + when (v == nextCompleteVersion prevComplete) $ + setCompleteRosterVersion db gInfo v + pure (fresh, prevComplete) + if accept + then (requestRosterOnGap v prevComplete `catchAllErrors` eToView) >> action + else messageWarning "x.grp.mem: roster version not newer than current, ignoring" $> Nothing + where + -- the next contiguous version after the complete frontier. With no frontier the baseline is the first + -- roster version (VersionRoster 0, see broadcastRoster): a subscriber seeing v0 without a prior roster is + -- current, not gapped, as it cannot have missed an earlier version - so v0 neither requests nor stays behind. + nextCompleteVersion = \case + Just (VersionRoster c) -> VersionRoster (c + 1) + Nothing -> VersionRoster 0 + -- a subscriber whose complete frontier (before this delta) lags more than one below it has missed versions: + -- ask the relay that forwarded it (it holds >= v = the new gate) to re-serve the full roster, carrying the + -- previous frontier so only a fuller snapshot is served. A stuck frontier re-asks on every following delta + -- until a roster fills it. Best-effort; relays and the direct path (fwdRelay_ = Nothing) don't ask, nor a + -- relay that predates roster support. + requestRosterOnGap v prevComplete + | isUserGrpFwdRelay gInfo = pure () + | otherwise = case fwdRelay_ of + Just relay + | gap, relay `supportsVersion` groupRosterVersion -> + void $ sendGroupMessage' user gInfo [relay] (XGrpRosterRequest prevComplete) + _ -> pure () + where + gap = v > nextCompleteVersion prevComplete + + xGrpMemRole :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpMemRole gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs + | memRole == GRRelay = messageError "x.grp.mem.role: relay role can't be assigned" $> Nothing | membershipMemId == memId = - let gInfo' = gInfo {membership = membership {memberRole = memRole}} - in changeMemberRole gInfo' membership $ RGEUserRole memRole - | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case - Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole - Left _ -> messageError "x.grp.mem.role with unknown member ID" $> Nothing + applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ + let gInfo' = gInfo {membership = membership {memberRole = memRole}} + in changeMemberRole gInfo' membership False (\db -> updateGroupMemberRole db user membership memRole) (RGEUserRole memRole) True + | otherwise = applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ do + defaultRole <- unknownMemberRole gInfo + -- an owner-signed event with a key TOFU-creates an unknown member only for a roster role; else a plain lookup + let allowCreate = useRelays' gInfo && senderRole == GROwner && isRosterRole memRole && isJust memberKey_ + withStore' (\db -> runExceptT $ getCreateUnknownGMByMemberId db cxt user gInfo memId (nameFromMemberId memId) defaultRole allowCreate) >>= \case + Right (Just (member, created)) + -- just created (keyless, and allowCreate ensured the event carries its key): pin key + role + | created, Just (MemberKey pubKey) <- memberKey_ -> + let gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + in changeMemberRole gInfo member created (\db -> void $ applyMemberKeyRole db member pubKey memRole) gEvent (not $ useRelays' gInfo) + -- known member: apply the role (its key is established via roster/intro; the event's key is ignored) + | otherwise -> + let gEvent = RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole + in changeMemberRole gInfo member created (\db -> updateGroupMemberRole db user member memRole) gEvent (not $ useRelays' gInfo) + -- in relay groups the roster may deliver role update for previously-unknown privileged members + _ | useRelays' gInfo -> pure Nothing + | otherwise -> messageError "x.grp.mem.role with unknown member ID" $> Nothing where GroupMember {memberId = membershipMemId} = membership - changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} gEvent - | senderRole < maximum ([GRAdmin, fromRole, memRole] :: [GroupMemberRole]) = + -- applyMember writes the change (role, or role + pinned key for a freshly TOFU-created member); + -- the delivery scope (relay forwarding) is computed on the pre-change role + changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} created applyMember gEvent createItem + | fromRole == GRRelay = + messageError "x.grp.mem.role: relay role can't be changed" $> Nothing + | senderRole < roleRequiredToChange fromRole memRole = messageError "x.grp.mem.role with insufficient member permissions" $> Nothing + | useRelays' gInfo && (isRosterRole memRole || isRosterRole fromRole) && senderRole /= GROwner = + messageError "x.grp.mem.role: only the owner can change member, moderator and admin roles in relay groups" $> Nothing + -- a forwarded role event the roster already applied is a no-op; suppress it. + -- a just-created member is keyless here, so fall through to pin its owner-attested key. + | useRelays' gInfo && not created && fromRole == memRole = pure $ memberEventDeliveryScope member | otherwise = do - withStore' $ \db -> updateGroupMemberRole db user member memRole - (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m - (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) - groupMsgToView cInfo ci + withStore' applyMember + (gInfo'', m') <- + if createItem + then do + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) + groupMsgToView cInfo ci + pure (gInfo'', m') + else pure (gInfo', m) toView CEvtMemberRole {user, groupInfo = gInfo'', byMember = m', member = member {memberRole = memRole}, fromRole, toRole = memRole, msgSigned} pure $ memberEventDeliveryScope member + -- The header only starts the transfer; the roster is applied and the version bumped only at + -- blob completion, so a withheld or corrupted blob leaves the last good roster intact. + -- fromMember is the relay that delivered THIS roster copy (the owner on a relay receiving directly, + -- a relay on a member receiving a forward); author is the owner who signed it. + xGrpRoster :: GroupInfo -> GroupMember -> GroupMember -> GroupRoster -> VerifiedMsg e -> Maybe SharedMsgId -> UTCTime -> CM (Maybe DeliveryJobScope) + xGrpRoster gInfo fromMember author GroupRoster {version = newVer, fileInv = InlineFileInvitation {fileSize, fileDigest}} verifiedMsg sharedMsgId_ brokerTs + -- only an owner may sign a roster; otherwise a relay could route it as a member whose key it controls + | memberRole' author /= GROwner = messageError "x.grp.roster: not signed by an owner" $> Nothing + | fileSize > maxGroupRosterBytes = messageError "x.grp.roster: roster blob size exceeds limit" $> Nothing + | otherwise = case verifiedMsg of + -- unreachable: XGrpRoster is in requiresSignature, so withVerifiedMsg rejected unsigned + VMUnsigned _ -> pure Nothing + VMSigned _ sm _ -> case sharedMsgId_ of + Nothing -> Nothing <$ messageWarning "x.grp.roster: missing shared message id" + Just sharedMsgId -> do + -- per-source pending version (THIS relay's own in-flight transfer), not a single group slot + pendingVer_ <- withStore' $ \db -> getRosterTransferVersion db gInfo (groupMemberId' fromMember) + -- accept a version not below BOTH applied and this source's pending (>=, Nothing below 0): a preceding + -- signed event may have already advanced rosterVersion to this blob's version; a lower one is a downgrade. + if newVer `notBelowRoster` rosterVersion gInfo && newVer `notBelowRoster` pendingVer_ + then startRosterTransfer sm sharedMsgId + else pure Nothing + where + startRosterTransfer sm sharedMsgId = do + -- supersede THIS source's own in-flight transfer (older version or a restart); other relays' transfers are independent + cleanupRosterTransfer gInfo (groupMemberId' fromMember) + let relayHdr = if isUserGrpFwdRelay gInfo then Just sm else Nothing + chSize <- asks $ fileChunkSize . config + let rosterFInv = FileInvitation {fileName = "roster", fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Just IFMSent, fileDescr = Nothing} + -- transfer record + its scratch file in one transaction (file owned by the transfer, keyed per source) + rft@RcvFileTransfer {fileId} <- withStore $ \db -> do + transferId <- liftIO $ createRosterTransfer db gInfo (groupMemberId' fromMember) newVer fileDigest (groupMemberId' author) brokerTs relayHdr + createRosterRcvFile db userId gInfo fromMember transferId sharedMsgId rosterFInv (Just IFMSent) chSize + -- accept the chat-item-free file before chunk 1 (FIFO before it) so chunk 1 isn't rejected on RFSNew + -- transient scratch file (consumed into roster_blob, then deleted): temp folder, not the user's files folder / Downloads + tmpDir <- lift getChatTempDirectory + rosterTs <- liftIO getCurrentTime + let GroupInfo {groupId = gId} = gInfo + rosterFile = "roster_" <> show gId <> "_" <> show (groupMemberId' fromMember) <> "_" <> formatTime defaultTimeLocale "%Y%m%d_%H%M%S" rosterTs + filePath <- getRcvFilePath fileId (Just tmpDir) rosterFile False + withStore' $ \db -> startRcvInlineFT db user rft filePath (Just IFMSent) + pure Nothing + + -- Roster version comparison treating Nothing (un-materialized) as below 0. Non-strict (>=) so a relay + -- accepts the owner's blob at the version a preceding signed event already advanced rosterVersion to. + notBelowRoster :: VersionRoster -> Maybe VersionRoster -> Bool + notBelowRoster v = maybe True (v >=) + + -- Blob arrived: verify the owner-attested digest over the plaintext and guard against + -- downgrade before applying; on a relay, ack the owner and re-serve to members. + rosterCompletion :: GroupInfo -> RcvFileTransfer -> CM () + rosterCompletion gInfo RcvFileTransfer {fileId, fileStatus} = + withStore' (\db -> getRosterTransfer db fileId) >>= \case + -- defensive: the file always has its transfer (created together, deleted together) + Nothing -> lift (closeFileHandle fileId rcvFiles) >> forM_ (rosterFilePath fileStatus) removeFsFile + Just RcvRosterTransfer {rosterTransferId = transferId, rosterTransferVersion = pendingVer, rosterTransferDigest = pendingDigest, rosterTransferOwnerGMId = ownerGMId, rosterTransferBrokerTs = rosterBrokerTs, rosterTransferHeader = header_} -> do + owner_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberById db cxt user ownerGMId) + blob <- readAssembledRoster + let isRelay' = isUserGrpFwdRelay gInfo + ackErr err = do + cleanupRosterTransferById transferId + when isRelay' $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err) + if FD.FileDigest (LC.sha512Hash (LB.fromStrict blob)) /= pendingDigest + then ackErr "relay could not verify the roster blob" + else case parseAll rosterBlobP blob of + Left _ -> ackErr "relay could not parse the roster blob" + Right entries -> case owner_ of + Nothing -> cleanupRosterTransferById transferId + Just author -> do + defaultRole <- unknownMemberRole gInfo + -- gate against the persisted roster_version inside the apply transaction: a roster from another + -- relay (or a preceding signed event) may already have advanced it past this one; a stale + -- completion (e.g. relay1 sent v5 then v6, relay2's v5 completes after v6) is rejected. + results_ <- withStore $ \db -> do + cur <- liftIO $ getGroupRosterVersion db gInfo + if maybe False (pendingVer <) cur + then pure Nothing + else do + res <- processRosterEntries db gInfo defaultRole (validateGroupRoster entries) + liftIO $ setGroupLiveRoster db gInfo pendingVer ownerGMId rosterBrokerTs header_ blob + pure (Just res) + cleanupRosterTransferById transferId + forM_ results_ $ \results -> do + emitRosterResults gInfo author rosterBrokerTs results + -- ack while setting up (own status accepted/acknowledged); a serving (active) relay must not ack broadcasts. + when (isRelay' && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do + sendRosterAck gInfo author pendingVer Nothing + withStore' $ \db -> void $ updateRelayOwnStatusFromTo db gInfo RSAccepted RSAcknowledgedRoster + where + rosterFilePath = \case + RFSAccepted p -> Just p + RFSConnected p -> Just p + RFSComplete p -> Just p + _ -> Nothing + readAssembledRoster = case rosterFilePath fileStatus of + Just fp -> readAt fp + Nothing -> throwChatError $ CEInternalError "roster file not in progress" + readAt fp = lift (toFSFilePath fp) >>= liftIO . B.readFile + + -- TOFU-apply an owner-signed (key, role) to a resolved member: pin the key if absent; for a keyed + -- member keep the trusted key (Left = reject a different one), else update the role. Right + -- (Just (member-at-new-role, fromRole)) when the role changed, Right Nothing when already current. + applyMemberKeyRole :: DB.Connection -> GroupMember -> C.PublicKeyEd25519 -> GroupMemberRole -> IO (Either MemberId (Maybe (GroupMember, GroupMemberRole))) + applyMemberKeyRole db m pubKey role = case memberPubKey m of + Just k + | k /= pubKey -> pure (Left (memberId' m)) + | memberRole' m == role -> pure (Right Nothing) + | otherwise -> updateGroupMemberRole db user m role $> Right (Just ((m :: GroupMember) {memberRole = role}, memberRole' m)) + Nothing -> setGroupMemberKeyRole db m pubKey role $> Right (Just ((m :: GroupMember) {memberRole = role}, memberRole' m)) + + -- TOFU apply: pin each member's key on first use, then update roles. + processRosterEntries :: DB.Connection -> GroupInfo -> GroupMemberRole -> [RosterMember] -> ExceptT StoreError IO ([MemberId], [(GroupMember, GroupMemberRole, Bool)]) + processRosterEntries db gInfo defaultRole entries = do + let rosterIds = map (\RosterMember {memberId} -> memberId) entries + (cs, as) <- foldrM applyRosterEntry ([], []) entries + currentPriv <- liftIO $ getGroupRosterMembers db cxt user gInfo + reverted <- liftIO $ fmap catMaybes $ forM currentPriv $ \m -> + if memberId' m `notElem` rosterIds + then updateGroupMemberRole db user m defaultRole $> Just ((m :: GroupMember) {memberRole = defaultRole}, memberRole' m, False) + else pure Nothing + pure (cs, as <> reverted) + where + -- entry-level failure (StoreError or IO exception) is muted; the entry is dropped + applyRosterEntry RosterMember {memberId, key = MemberKey pubKey, role} (cs, as) = + ( getCreateUnknownGMByMemberId db cxt user gInfo memberId (nameFromMemberId memberId) defaultRole True >>= \case + Nothing -> pure (cs, as) + Just (m, created) -> liftIO (applyMemberKeyRole db m pubKey role) >>= \case + Left mid -> pure (mid : cs, as) + Right Nothing -> pure (cs, as) + Right (Just (rm, fromR)) -> pure (cs, (rm, fromR, created) : as) + ) + `catchAllErrors` \_ -> pure (cs, as) + + emitRosterResults :: GroupInfo -> GroupMember -> UTCTime -> ([MemberId], [(GroupMember, GroupMemberRole, Bool)]) -> CM () + emitRosterResults gInfo@GroupInfo {membership} author rosterBrokerTs (conflicts, applied) = do + forM_ conflicts $ \mid' -> + messageWarning $ "x.grp.roster: member key conflict, keeping trusted key, memberId=" <> safeDecodeUtf8 (strEncode mid') + forM_ applied $ \(member, fromRole, created) -> + unless created $ emitRoleChange member fromRole + where + emitRoleChange member fromRole = do + let toRole = memberRole' member + (gInfo', author') <- + if sameMemberId (memberId' membership) member + then do + (gInfo', author', scopeInfo) <- mkGroupChatScope gInfo author + ci <- createChatItem user (CDGroupRcv gInfo' scopeInfo author') False (CIRcvGroupEvent $ RGEUserRole toRole) Nothing (Just MSSVerified) (Just rosterBrokerTs) + toView $ CEvtNewChatItems user [ci] + pure (gInfo', author') + else pure (gInfo, author) + toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified} + + sendRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM () + sendRosterAck gInfo owner ackVer err = void $ sendGroupMessage' user gInfo [owner] (XGrpRosterAck ackVer err) + + xGrpRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM () + xGrpRosterAck gInfo m ackVer err = do + relay_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupRelayByGMId db (groupMemberId' m)) + case relay_ of + Just relay@GroupRelay {relayStatus = RSAccepted} -> case err of + Nothing + | rosterVersion gInfo == Just ackVer -> do + (relay', gLink) <- withStore $ \db -> do + relay' <- liftIO $ updateRelayStatus db relay RSAcknowledgedRoster + gLink <- getGroupLink db user gInfo + pure (relay', gLink) + setGroupLinkDataAsync user gInfo gLink + toView $ CEvtGroupRelayUpdated user gInfo m relay' + | otherwise -> messageWarning "x.grp.roster.ack: stale version, awaiting ack for the current roster" + Just e -> do + relay' <- withStore' $ \db -> updateRelayStatusFromTo db relay RSAccepted RSRejected + toView $ CEvtGroupRelayUpdated user gInfo m relay' + messageError $ "x.grp.roster.ack: relay could not save roster, marked rejected: " <> e + _ -> pure () + + -- A relay re-serves the full roster to a subscriber that detected a version gap, but only when its STORED + -- blob is newer than BOTH the requester's version (Nothing = none) and the version it last served this member + -- - the latter bounds reflected amplification (a member can't re-trigger a full serve). Gating on the stored + -- blob (not roster_version, the gate) means the relay serves only a blob the requester will accept. + -- serveRoster records the served version (on all serve paths) and is a no-op without a roster. + xGrpRosterRequest :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM () + xGrpRosterRequest gInfo m reqVer_ = + when (isUserGrpFwdRelay gInfo) $ do + (stored_, served_) <- withStore' $ \db -> + (,) <$> getStoredRosterVersion db gInfo <*> getMemberRosterServedVersion db m + forM_ stored_ $ \stored -> + when (maybe True (stored >) reqVer_ && maybe True (stored >) served_) $ serveRoster user gInfo m + checkHostRole :: GroupMember -> GroupMemberRole -> CM () checkHostRole GroupMember {memberRole, localDisplayName} memRole = when (memberRole < GRAdmin || memberRole < memRole) $ throwChatError (CEGroupContactRole localDisplayName) @@ -3176,11 +3619,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do + xGrpMemDel :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope) + xGrpMemDel gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId - then checkRole membership $ do + then applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ checkRole membership $ do deleteGroupLinkIfExists user gInfo -- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False @@ -3188,11 +3631,11 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = updateGroupMemberStatus db userId membership GSMemRemoved when (maybe False (/= RSRejected) (relayOwnStatus gInfo)) $ updateRelayOwnStatus_ db gInfo RSInactive let membership' = membership {memberStatus = GSMemRemoved} - when withMessages $ deleteMessages gInfo membership' SMDSnd + when withMessages $ deleteMessages gInfo membership' deleteMemberItem msg gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} - else + else applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> do messageError "x.grp.mem.del with unknown member ID" @@ -3208,15 +3651,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = deleteMemberConnection' deletedMember True else deleteMemberConnection deletedMember let deliveryScope = memberEventDeliveryScope deletedMember + deletedMember' = deletedMember {memberStatus = GSMemRemoved} + when withMessages $ deleteMessages gInfo deletedMember' gInfo' <- case deliveryScope of -- Keep member record if it's support scope - it will be required for forwarding inside that scope. Just (DJSMemberSupport _) | shouldForward -> updateMemberRecordDeleted user gInfo deletedMember GSMemRemoved - -- Undeleted "member connected" chat item will prevent deletion of member record. - _ -> deleteOrUpdateMemberRecord user gInfo deletedMember + _ + | withMessages && groupFeatureMemberAllowed SGFFullDelete m gInfo -> + fullyDeleteMemberRecord user gInfo deletedMember + -- Undeleted "member connected" chat item will prevent deletion of member record. + | otherwise -> deleteOrUpdateMemberRecord user gInfo deletedMember gInfo'' <- updatePublicGroupData user gInfo' let wasDeleted = memberStatus == GSMemRemoved || memberStatus == GSMemLeft - deletedMember' = deletedMember {memberStatus = GSMemRemoved} - when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv -- Clear forwardedByMember if it references the deleted member, -- as the member record was already deleted above. let RcvMessage {forwardedByMember = fwdBy} = msg @@ -3233,9 +3679,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = (gi', m', scopeInfo) <- mkGroupChatScope gi m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg' brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci - deleteMessages :: MsgDirectionI d => GroupInfo -> GroupMember -> SMsgDirection d -> CM () - deleteMessages gInfo' delMem msgDir - | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem m msgDir + deleteMessages :: GroupInfo -> GroupMember -> CM () + deleteMessages gInfo' delMem + | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem | otherwise = markGroupMemberCIsDeleted user gInfo' delMem m forwardToMember :: GroupMember -> CM () forwardToMember member = @@ -3304,6 +3750,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unless (useRelays' g'') $ void $ forkIO $ void $ setGroupLinkData' NRMBackground user g'' Just _ -> updateGroupPrefs_ msgSigned g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p' + -- relay advertises its web capability now that the owner's version is known (bumped by saveGroupRcvMsg) + when (isRelay (membership g)) $ sendRelayCapIfNeeded user g pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> RcvMessage -> CM (Maybe DeliveryJobScope) @@ -3356,15 +3804,16 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = fromGroupId_ = Just groupId, fromGroupMemberId_ = Just (groupMemberId' m), fromGroupMemberConnId_ = Just mConnId, - groupDirectInvStartedConnection = isTrue $ autoAcceptMemberContacts user + groupDirectInvStartedConnection = autoAcceptMemberContacts user } joinExistingContact subMode mCt@Contact {contactId = mContactId} - | isTrue (autoAcceptMemberContacts user) = do - (cmdId, acId) <- joinConn subMode + | autoAcceptMemberContacts user = do + (cmdId, acId) <- prepareAgentJoin user Nothing True connReq mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode getContact db cxt user mContactId + joinMemberContactAsync cmdId acId subMode securityCodeChanged mCt' createItems mCt' m | otherwise = do @@ -3377,14 +3826,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectRcv mCt') (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing createItems mCt' m createNewContact subMode - | isTrue (autoAcceptMemberContacts user) = do - (cmdId, acId) <- joinConn subMode + | autoAcceptMemberContacts user = do + (cmdId, acId) <- prepareAgentJoin user Nothing True connReq -- [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 cxt user mContactId pure (mCt, m') + joinMemberContactAsync cmdId acId subMode createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createItems mCt m' | otherwise = do @@ -3397,12 +3847,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createInternalChatItem user (CDDirectRcv mCt) (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing createItems mCt m' - joinConn subMode = do + joinMemberContactAsync cmdId acId subMode = do -- [incognito] send membership incognito profile p <- presentUserBadge user (incognitoMembershipProfile g) $ userProfileDirect user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing True -- TODO PQ should negotitate contact connection with PQSupportOn? (use encodeConnInfoPQ) dm <- encodeConnInfo $ XInfo p - joinAgentConnectionAsync user Nothing True connReq dm subMode + joinAgentConnectionAsync cmdId False acId True connReq dm subMode createItems mCt' m' = do (g', m'', scopeInfo) <- mkGroupChatScope g m' createInternalChatItem user (CDGroupRcv g' scopeInfo m'') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing @@ -3424,18 +3874,30 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = unknownRole <- unknownMemberRole gInfo let allowCreate = toCMEventTag chatMsgEvent /= XGrpLeave_ withStore (\db -> getCreateUnknownGMByMemberId db cxt user gInfo memberId memberName unknownRole allowCreate) >>= \case - Just (author, unknown) -> do - when unknown $ toView $ CEvtUnknownMemberCreated user gInfo m author - void $ withVerifiedMsg gInfo scopeInfo author parsedMsg msgTs $ - (`processForwardedMsg` Just author) + Just (author, unknown) + | memberRemoved author -> + logInfo $ "x.grp.msg.forward: ignoring content from removed member, group " <> tshow (groupId' gInfo) <> ", member " <> safeDecodeUtf8 (strEncode memberId) <> ", event " <> tshow (toCMEventTag chatMsgEvent) + | not (useRelays' gInfo) && not (expectedForwarder author) -> + logInfo $ "x.grp.msg.forward: ignoring content from unexpected forwarder, group " <> tshow (groupId' gInfo) <> ", forwarder " <> tshow (groupMemberId' m) <> ", member " <> safeDecodeUtf8 (strEncode memberId) <> ", event " <> tshow (toCMEventTag chatMsgEvent) + | otherwise -> do + when unknown $ toView $ CEvtUnknownMemberCreated user gInfo m author + void $ withVerifiedMsg gInfo scopeInfo author parsedMsg msgTs $ + (`processForwardedMsg` Just author) Nothing -> pure () FwdChannel -> processForwardedMsg (VMUnsigned chatMsg) Nothing where + -- Forwards are only expected from the member that introduced us to the author: our host, or + -- the author's inviter. Unknown members have no such record, so any admin may forward theirs. + expectedForwarder :: GroupMember -> Bool + expectedForwarder author = + memberCategory m == GCHostMember + || invitedByGroupMemberId author == Just (groupMemberId' m) + || memberStatus author == GSMemUnknown -- ! see isForwardedGroupMsg: forwarded group events should include msgId to be deduplicated processForwardedMsg :: VerifiedMsg 'Json -> Maybe GroupMember -> CM () processForwardedMsg verifiedMsg author_ = do rcvMsg_ <- saveGroupFwdRcvMsg user gInfo m author_ verifiedMsg brokerTs - forM_ rcvMsg_ $ \rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} -> case event of + forM_ rcvMsg_ $ \rcvMsg@RcvMessage {sharedMsgId_, chatMsgEvent = ACME _ event} -> case event of XMsgNew mc -> void $ memberCanSend author_ scope $ newGroupContentMessage gInfo author_ mc rcvMsg msgTs True where @@ -3450,13 +3912,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = XInfo p -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p rcvMsg msgTs XGrpRelayNew rl -> withAuthor XGrpRelayNew_ $ \author -> void $ xGrpRelayNew gInfo author rl XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs - XGrpMemRole memId memRole -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo author memId memRole rcvMsg msgTs + XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo (Just m) author memId memRole memberKey rosterVer rcvMsg msgTs XGrpMemRestrict memId memRestrictions -> withAuthor XGrpMemRestrict_ $ \author -> void $ xGrpMemRestrict gInfo author memId memRestrictions rcvMsg msgTs - XGrpMemDel memId withMessages -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo author memId withMessages verifiedMsg rcvMsg msgTs True + XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo (Just m) author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs XGrpPrefs ps' -> withAuthor XGrpPrefs_ $ \author -> void $ xGrpPrefs gInfo author ps' rcvMsg + XGrpRoster gr -> withAuthor XGrpRoster_ $ \author -> void $ xGrpRoster gInfo m author gr verifiedMsg sharedMsgId_ msgTs _ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event) where withAuthor :: CMEventTag e -> (GroupMember -> CM ()) -> CM () @@ -3476,12 +3939,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup -> - let prefix = smpEncode chatBinding <> bindingData - bindingData = case groupKeys gInfo of - Just GroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId) - Nothing -> smpEncode (memberId, pubKey) -- forward compatibility for verifying signed messages in p2p groups - in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) + CBGroup + | Just GroupKeys {publicGroupId} <- groupKeys gInfo -> + signed MSSVerified <$ guard (verifyGroupSig pubKey publicGroupId memberId signatures signedBody) + | otherwise -> + let prefix = smpEncode chatBinding <> smpEncode (memberId, pubKey) -- forward compatibility for verifying signed messages in p2p groups + in signed MSSVerified <$ guard (all (\case (MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) where @@ -3633,17 +4096,13 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) "relay inactive" | otherwise -> withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do - let (body, taskIds, largeTaskIds) = batchDeliveryTasks1 (vr cxt) maxEncodedMsgLength nextTasks + let (body_, acceptedTasks, largeTasks) = batchDeliveryTasks1 (vr cxt) maxEncodedMsgLength nextTasks + senderGMIds = S.toList . S.fromList $ map (\MessageDeliveryTask {senderGMId} -> senderGMId) acceptedTasks withStore' $ \db -> do - createMsgDeliveryJob db gInfo jobScope (singleSenderGMId_ nextTasks) body - forM_ taskIds $ \taskId -> updateDeliveryTaskStatus db taskId DTSProcessed - forM_ largeTaskIds $ \taskId -> setDeliveryTaskErrStatus db taskId "large" - lift . void $ getDeliveryJobWorker True deliveryKey - where - singleSenderGMId_ :: NonEmpty MessageDeliveryTask -> Maybe GroupMemberId - singleSenderGMId_ (MessageDeliveryTask {senderGMId = senderGMId'} :| ts) - | all (\MessageDeliveryTask {senderGMId} -> senderGMId == senderGMId') ts = Just senderGMId' - | otherwise = Nothing + forM_ body_ $ \body -> createMsgDeliveryJob db gInfo jobScope senderGMIds body + forM_ acceptedTasks $ \t -> updateDeliveryTaskStatus db (deliveryTaskId t) DTSProcessed + forM_ largeTasks $ \t -> setDeliveryTaskErrStatus db (deliveryTaskId t) "large" + when (isJust body_) . lift . void $ getDeliveryJobWorker True deliveryKey -- DJRelayRemoved is allowed when RSInactive - it forwards XGrpMemDel about relay's own deletion DJRelayRemoved | workerScope /= DWSGroup -> @@ -3653,7 +4112,7 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do fwd = GrpMsgForward {fwdSender, fwdBrokerTs} body = encodeBinaryBatch [encodeFwdElement fwd verifiedMsg] withStore' $ \db -> do - createMsgDeliveryJob db gInfo jobScope (Just senderGMId) body + createMsgDeliveryJob db gInfo jobScope [senderGMId] body updateDeliveryTaskStatus db (deliveryTaskId task) DTSProcessed lift . void $ getDeliveryJobWorker True deliveryKey @@ -3672,6 +4131,27 @@ getDeliveryJobWorker hasWork deliveryKey = do getAgentWorker "delivery_job" hasWork a deliveryKey ws $ runDeliveryJobWorker a deliveryKey +-- TODO [relays] dissemination here is unsigned (relay-asserted profile). +-- Future: members sign an XMember on channel join, relay stores it per +-- member and forwards the signed XMember via this sidecar — enables +-- subscribers to verify member profiles out-of-band without trusting the relay. + +-- | Encode an XGrpMemNew for first-introduction dissemination as a direct +-- (non-forwarded) batch element. 'Left' when the encoded element wouldn't +-- fit a singleton batch (see 'maxBatchElementSize'). +encodeMemberNew :: VersionRangeChat -> GroupInfo -> GroupMember -> Either ChatError ByteString +encodeMemberNew vr gInfo member = case encodeChatMessage maxBatchElementSize chatMsg of + ECMEncoded bs -> Right bs + ECMLarge -> Left $ ChatError $ CEException $ "large profile element for member " <> show (groupMemberId' member) + where + chatMsg :: ChatMessage 'Json + chatMsg = + ChatMessage + { chatVRange = vr, + msgId = Nothing, + chatMsgEvent = XGrpMemNew (memberInfo gInfo member) Nothing + } + runDeliveryJobWorker :: AgentClient -> DeliveryWorkerKey -> Worker -> CM () runDeliveryJobWorker a deliveryKey Worker {doWork} = do delay <- asks $ deliveryWorkerDelay . config @@ -3713,7 +4193,10 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do deleteGroupConnections user gInfo True withStore' $ \db -> updateDeliveryJobStatus db jobId DJSComplete where - MessageDeliveryJob {jobId, jobScope, singleSenderGMId_, body, cursorGMId_ = startingCursor} = job + MessageDeliveryJob {jobId, jobScope, senderGMIds, body, cursorGMId_ = startingCursor} = job + singleSenderGMId_ = case senderGMIds of + [s] -> Just s + _ -> Nothing sendBodyToMembers :: CM () sendBodyToMembers -- channel @@ -3721,16 +4204,88 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do -- there's no member review in channels, so job spec includePending is ignored DJSGroup {} -> do bucketSize <- asks $ deliveryBucketSize . config - sendLoop bucketSize startingCursor + senders <- withStore' $ \db -> + fmap catMaybes . forM senderGMIds $ \sId -> + fmap (join . eitherToMaybe) . runExceptT $ do + sender <- getNonRemovedMemberById db cxt user sId + -- owners are already known to every member (group link + owner-intro in introduceInChannel), + -- so we never disseminate their profile (redundant, and races with joins re-announcing the owner) + if memberRole' sender == GROwner + then pure Nothing + else do + vec <- getMemberRelationsVector db sender + pure $ Just (sender, vec) + let missingSenders = length senderGMIds - length senders + when (missingSenders > 0) $ + logInfo $ "delivery job " <> tshow jobId <> ": " <> tshow missingSenders <> " senders missing; skipping their profile prepend" + -- Small profiles ride inline (extBody); the rest spill + -- into standalone batches that ship before the body. + (extBody, inBodySenders, overflowBatches, activeSenders) <- + if null senders + then pure (body, [], [], []) + else do + -- all members' profiles disseminate; privileged key/role come from the roster, not here + let (encoderErrs, validLabeled) = partitionEithers [(\bs -> (s, bs)) <$> encodeMemberNew (vr cxt) gInfo s | (s, _) <- senders] + (extBody', inBody, overflowLabeled, large1) = batchProfilesWithBody maxEncodedMsgLength body validLabeled + (overflowBatches', large2) = batchProfiles maxEncodedMsgLength overflowLabeled + packerErrs = [ChatError (CEInternalError $ "oversized profile element for member " <> show (groupMemberId' s)) | s <- large1 <> large2] + allErrs = encoderErrs <> packerErrs + unless (null allErrs) $ do + logInfo $ "delivery job " <> tshow jobId <> ": dropping " <> tshow (length allErrs) <> " oversized profile element(s)" + toView $ CEvtChatErrors allErrs + let active = inBody <> concatMap snd overflowBatches' + pure (extBody', inBody, overflowBatches', active) + -- Per-job constants — independent of the cursor page in sendLoop. + let senderVec = M.fromList [(groupMemberId' s, v) | (s, v) <- senders] + -- Body IDs: 0 = plain body, 1 = extBody, 2.. = overflow batches in order. + overflowWithIds = zip [2 :: Int ..] overflowBatches + sendLoop bucketSize startingCursor senderVec overflowWithIds inBodySenders extBody activeSenders where - sendLoop :: Int -> Maybe GroupMemberId -> CM () - sendLoop bucketSize cursorGMId_ = do + sendLoop :: Int -> Maybe GroupMemberId -> Map GroupMemberId ByteString -> [(Int, (ByteString, [GroupMember]))] -> [GroupMember] -> ByteString -> [GroupMember] -> CM () + sendLoop bucketSize cursorGMId_ senderVec overflowWithIds inBodySenders extBody activeSenders = do mems <- withStore' $ \db -> getGroupMembersByCursor db cxt user gInfo cursorGMId_ singleSenderGMId_ bucketSize unless (null mems) $ do - deliver body mems + let msgReqs = buildMsgReqs mems + unless (null msgReqs) $ void $ withAgent (`sendMessages` msgReqs) + -- Mark only (sender, recipient) pairs where the bit was MRNew — + -- skip recipients already MRIntroduced (steady-case savings). + let readyMems = [m | m <- mems, isJust (readyMemberConn m)] + markFor sender = do + vec <- M.lookup (groupMemberId' sender) senderVec + let ms = [(indexInGroup r, (IDSubjectIntroduced, MRIntroduced)) | r <- readyMems, getRelation (indexInGroup r) vec == MRNew] + if null ms then Nothing else Just (sender, ms) + senderMarks = mapMaybe markFor activeSenders + unless (null senderMarks) $ + withStore' $ \db -> + forM_ senderMarks $ \(sender, ms) -> + setMemberVectorNewRelations db sender ms let cursorGMId' = groupMemberId' $ last mems withStore' $ \db -> updateDeliveryJobCursor db jobId cursorGMId' - unless (length mems < bucketSize) $ sendLoop bucketSize (Just cursorGMId') + unless (length mems < bucketSize) $ + sendLoop bucketSize (Just cursorGMId') senderVec overflowWithIds inBodySenders extBody activeSenders + where + -- First recipient needing body i carries VRValue (Just i); rest use VRRef i. + -- First piece per connection: aConnId; rest: empty (agent convention). + buildMsgReqs :: [GroupMember] -> [MsgReq] + buildMsgReqs mems = reverse . snd $ foldl' addRecipient (IS.empty, []) mems + where + addRecipient acc r = case readyMemberConn r of + Just (_, conn) -> snd $ foldl' (addPiece conn) (0 :: Int, acc) (recipientBodyPieces r) + Nothing -> acc + addPiece conn (k, (issued, reqs)) (bid, msgBody) = + let vor + | IS.member bid issued = VRRef bid + | otherwise = VRValue (Just bid) msgBody + issued' = IS.insert bid issued + connId = if k == 0 then aConnId conn else B.empty + in (k + 1, (issued', (connId, PQEncOff, MsgFlags False, vor) : reqs)) + recipientBodyPieces r = + [(i, b) | (i, (b, ss)) <- overflowWithIds, any missing ss] + <> [if any missing inBodySenders then (1, extBody) else (0, body)] + where + missing s = case M.lookup (groupMemberId' s) senderVec of + Just vec -> getRelation (indexInGroup r) vec == MRNew + Nothing -> True DJSMemberSupport scopeGMId -> do -- for member support scope we just load all recipients in one go, without cursor modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo @@ -3749,8 +4304,8 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do -- fully connected group | otherwise = case singleSenderGMId_ of Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays" - Just singleSenderGMId -> do - sender <- withStore $ \db -> getGroupMemberById db cxt user singleSenderGMId + Just sId -> do + sender <- withStore $ \db -> getGroupMemberById db cxt user sId ms <- buildMemberList sender unless (null ms) $ deliver body ms where @@ -3923,7 +4478,7 @@ runRelayRequestWorker a Worker {doWork} = do sigKeys <- liftIO $ atomically $ C.generateKeyPair gVar let crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with relayMemId as linkEntityId (no server request) - (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) + (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) Nothing ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 5800ab5bdd..836fb004fe 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -55,7 +55,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, sumTypeJSON) -import Simplex.Messaging.Protocol (BlockingInfo, MsgBody) +import Simplex.Messaging.Protocol (BlockingInfo, MsgBody, XFTPServer) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection @@ -417,6 +417,13 @@ toChatInfo = \case CDLocalSnd l -> LocalChat l CDLocalRcv l -> LocalChat l +signMessagesRequired :: ChatDirection c d -> Bool +signMessagesRequired = \case + CDChannelRcv g _ -> groupFeatureAllowed SGFSignMessages g + CDGroupRcv g _ _ -> groupFeatureAllowed SGFSignMessages g + CDGroupSnd g _ -> groupFeatureAllowed SGFSignMessages g + _ -> False + contactChatDeleted :: ChatDirection c d -> Bool contactChatDeleted = \case CDDirectSnd Contact {chatDeleted} -> chatDeleted @@ -517,7 +524,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta editable :: Bool, forwardedByMember :: Maybe GroupMemberId, showGroupAsSender :: ShowGroupAsSender, - msgSigned :: Maybe MsgSigStatus, + msgVerified :: Maybe MsgVerified, createdAt :: UTCTime, updatedAt :: UTCTime } @@ -525,12 +532,12 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta type ShowGroupAsSender = Bool -mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> Maybe MsgSigStatus -> UTCTime -> UTCTime -> CIMeta c d -mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender msgSigned createdAt updatedAt = +mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> Bool -> Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> Bool -> Maybe MsgVerified -> UTCTime -> UTCTime -> CIMeta c d +mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive userMention hasLink_ currentTs itemTs forwardedByMember showGroupAsSender msgVerified createdAt updatedAt = let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs editable = deletable && isNothing itemForwarded hasLink = BoolDef hasLink_ - in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, hasLink, deletable, editable, forwardedByMember, showGroupAsSender, msgSigned, createdAt, updatedAt} + in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, userMention, hasLink, deletable, editable, forwardedByMember, showGroupAsSender, msgVerified, createdAt, updatedAt} deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool deletable' itemContent itemDeleted itemTs allowedInterval currentTs = @@ -561,7 +568,7 @@ dummyMeta itemId ts itemText = editable = False, forwardedByMember = Nothing, showGroupAsSender = False, - msgSigned = Nothing, + msgVerified = Nothing, createdAt = ts, updatedAt = ts } @@ -1172,6 +1179,8 @@ data RcvMessage = RcvMessage chatMsgEvent :: AChatMsgEvent, sharedMsgId_ :: Maybe SharedMsgId, msgSigned :: Maybe MsgSigStatus, + signedMsg_ :: Maybe SignedMsg, + signedByGMId_ :: Maybe GroupMemberId, forwardedByMember :: Maybe GroupMemberId } @@ -1336,7 +1345,8 @@ instance TextEncoding CIForwardedFromTag where data ChatItemInfo = ChatItemInfo { itemVersions :: [ChatItemVersion], memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus), - forwardedFromChatItem :: Maybe AChatItem + forwardedFromChatItem :: Maybe AChatItem, + fileXftpServers :: [XFTPServer] } deriving (Show) diff --git a/src/Simplex/Chat/Messages/Batch.hs b/src/Simplex/Chat/Messages/Batch.hs index a9e835a83e..9c0ed521c7 100644 --- a/src/Simplex/Chat/Messages/Batch.hs +++ b/src/Simplex/Chat/Messages/Batch.hs @@ -13,20 +13,29 @@ module Simplex.Chat.Messages.Batch encodeBinaryBatch, batchMessages, batchDeliveryTasks1, + batchElements, + batchProfilesWithBody, + batchProfiles, + maxBatchElementSize, ) where import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as B -import Data.Int (Int64) -import Data.List (foldl') +import Data.Char (ord) +import Data.Function (on) +import Data.List (foldl', sortBy) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L +import Data.Ord (Down (..)) +import Data.Word (Word8) import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) import Simplex.Chat.Delivery import Simplex.Chat.Messages import Simplex.Chat.Protocol -import Simplex.Chat.Types (VersionRangeChat) +import Data.Maybe (isJust) +import Simplex.Chat.Types (GroupMember (..), LocalProfile (..), VersionRangeChat) import Simplex.Messaging.Encoding (Large (..), smpEncode, smpEncodeList) data BatchMode = BMJson | BMBinary @@ -70,29 +79,47 @@ batchMessages mode maxLen = addBatch . foldr addToBatch ([], [], [], 0, 0) let encoded = encodeBatch mode bodies in Right (MsgBatch encoded msgs) : batches --- | Batches delivery tasks into (batch, [taskIds], [largeTaskIds]). +-- | Batches delivery tasks into (batch if any task was accepted, accepted, large). -- Always uses binary batch format for relay groups. -batchDeliveryTasks1 :: VersionRangeChat -> Int -> NonEmpty MessageDeliveryTask -> (ByteString, [Int64], [Int64]) +batchDeliveryTasks1 :: VersionRangeChat -> Int -> NonEmpty MessageDeliveryTask -> (Maybe ByteString, [MessageDeliveryTask], [MessageDeliveryTask]) batchDeliveryTasks1 _vr maxLen = toResult . foldl' addToBatch ([], [], [], 0, 0) . L.toList where - addToBatch :: ([ByteString], [Int64], [Int64], Int, Int) -> MessageDeliveryTask -> ([ByteString], [Int64], [Int64], Int, Int) - addToBatch (msgBodies, taskIds, largeTaskIds, len, n) task - -- too large: skip, record taskId in largeTaskIds - | msgLen > maxLen = (msgBodies, taskIds, taskId : largeTaskIds, len, n) + addToBatch :: ([ByteString], [MessageDeliveryTask], [MessageDeliveryTask], Int, Int) -> MessageDeliveryTask -> ([ByteString], [MessageDeliveryTask], [MessageDeliveryTask], Int, Int) + addToBatch (msgBodies, accepted, large, len, n) task + -- element can't fit even a singleton batch (4-byte binary-batch framing) + | msgLen + 4 > maxLen = (msgBodies, accepted, task : large, len, n) -- fits: include in batch -- batch overhead: '=' + count (2) + 2-byte length prefix per element - | len' + (n + 1) * 2 + 2 <= maxLen = (msgBody : msgBodies, taskId : taskIds, largeTaskIds, len', n + 1) + | len' + (n + 1) * 2 + 2 <= maxLen = (msgBody : msgBodies, task : accepted, large, len', n + 1) -- doesn't fit: stop adding further messages - | otherwise = (msgBodies, taskIds, largeTaskIds, len, n) + | otherwise = (msgBodies, accepted, large, len, n) where - MessageDeliveryTask {taskId, fwdSender, brokerTs = fwdBrokerTs, verifiedMsg} = task + MessageDeliveryTask {fwdSender, brokerTs = fwdBrokerTs, verifiedMsg} = task msgBody = encodeFwdElement GrpMsgForward {fwdSender, fwdBrokerTs} verifiedMsg msgLen = B.length msgBody len' = len + msgLen - toResult :: ([ByteString], [Int64], [Int64], Int, Int) -> (ByteString, [Int64], [Int64]) - toResult (msgBodies, taskIds, largeTaskIds, _, _) = + toResult :: ([ByteString], [MessageDeliveryTask], [MessageDeliveryTask], Int, Int) -> (Maybe ByteString, [MessageDeliveryTask], [MessageDeliveryTask]) + toResult (msgBodies, accepted, large, _, _) = let encoded = encodeBinaryBatch (reverse msgBodies) - in (encoded, reverse taskIds, reverse largeTaskIds) + body = if null accepted then Nothing else Just encoded + in (body, reverse accepted, reverse large) + +-- | Pack pre-encoded elements into binary batches within maxLen, preserving order. +-- Elements may mix forward ('encodeFwdElement') and authored ('encodeBatchElement') +-- forms; the receiver parses each by prefix. Also returns the count dropped as too large. +batchElements :: Int -> [ByteString] -> ([ByteString], Int) +batchElements maxLen = finish . foldl' addToBatch ([], [], 0, 0, 0) + where + addToBatch (batches, elems, len, n, dropped) el + | elLen + 4 > maxLen = (batches, elems, len, n, dropped + 1) + | len + elLen + (n + 1) * 2 + 2 <= maxLen = (batches, el : elems, len + elLen, n + 1, dropped) + | otherwise = (closeBatch elems : batches, [el], elLen, 1, dropped) + where + elLen = B.length el + closeBatch elems = encodeBinaryBatch (reverse elems) + finish (batches, elems, _, n, dropped) + | n == 0 = (reverse batches, dropped) + | otherwise = (reverse (closeBatch elems : batches), dropped) -- | Encode a batch element for relay groups: >[/]. encodeFwdElement :: GrpMsgForward -> VerifiedMsg 'Json -> ByteString @@ -118,3 +145,83 @@ batchLen _ _ 0 = 0 batchLen _ len 1 = len batchLen BMJson len n = len + n + 1 -- (n - 1) commas + 2 brackets batchLen BMBinary len n = len + n * 2 + 2 -- 2-byte length prefix per element + '=' + count + +-- | Largest element that fits a singleton 'encodeBinaryBatch' inside an +-- agent SMP message: '=' + count(1) + Word16 length prefix(2) = 4 bytes +-- of framing on top of the element. +maxBatchElementSize :: Int +maxBatchElementSize = maxEncodedMsgLength - 4 + +-- | Sort key for the profile packers. No-image profiles are processed +-- first so they pack densely; image-bearing profiles take any remaining +-- space or spill to overflow. +hasImage :: GroupMember -> Bool +hasImage GroupMember {memberProfile = LocalProfile {image}} = isJust image + +-- | Greedy-pack profile elements with 'body' (no-image members first) +-- while the result fits 'maxLen'. Returns (extBody, accepted, overflow, +-- large): the senders whose profile is now inline, the labeled elements +-- that did not fit, and the senders whose element doesn't fit even a +-- singleton batch (must be dropped — equivalent to 'batchMessages' +-- 'errLarge'). +-- +-- Precondition on 'body': must be either 'B.empty' or output of +-- 'encodeBinaryBatch' — the function reads byte 1 as the existing +-- element count and drops bytes 0-1 before reassembly. Passing +-- arbitrary bytes produces malformed output. +batchProfilesWithBody :: Int -> ByteString -> [(GroupMember, ByteString)] -> (ByteString, [GroupMember], [(GroupMember, ByteString)], [GroupMember]) +batchProfilesWithBody maxLen body labeled = + let (_, _, acceptedPairs, overflow, large) = + foldl' step initState (sortBy (compare `on` (hasImage . fst)) labeled) + in (buildBody acceptedPairs, map fst acceptedPairs, overflow, large) + where + initEmpty = B.null body + initLen = B.length body + initCount = if initEmpty then 0 else ord (B.index body 1) + -- (predicted total bytes, predicted count, accepted pairs, overflow, large) + initState = (initLen, initCount, [], [], []) + step (totalLen, count, acceptedPairs, overflow, large) (s, e) + | B.length e + 4 > maxLen = (totalLen, count, acceptedPairs, overflow, s : large) + | count >= 255 = full + | candidateLen <= maxLen = (candidateLen, count + 1, (s, e) : acceptedPairs, overflow, large) + | otherwise = full + where + full = (totalLen, count, acceptedPairs, (s, e) : overflow, large) + -- First element on an empty body costs '=' + count(1) + Word16(2) + element; + -- every subsequent element costs just Word16(2) + element. + candidateLen + | initEmpty && null acceptedPairs = 4 + B.length e + | otherwise = totalLen + 2 + B.length e + -- Assemble the final body once: existing tail (sans '=' + count) with + -- the accepted elements (each length-prefixed) inserted in front, and + -- a refreshed count byte. + buildBody [] = body + buildBody acceptedPairs = + let prefixedNew = B.concat [smpEncode (Large e) | (_, e) <- acceptedPairs] + newCount = initCount + length acceptedPairs + tail_ = if initEmpty then B.empty else B.drop 2 body + in B.concat [B.singleton '=', BS.singleton (fromIntegral newCount :: Word8), prefixedNew, tail_] + +-- | Pack labeled profile elements into one or more (batch, senders) +-- pairs, each bounded by 'maxLen', plus a list of senders whose element +-- doesn't fit even a singleton batch (must be dropped — equivalent to +-- 'batchMessages' 'errLarge'). No-image members first (matches +-- 'batchProfilesWithBody'). +batchProfiles :: Int -> [(GroupMember, ByteString)] -> ([(ByteString, [GroupMember])], [GroupMember]) +batchProfiles maxLen = + finish . foldr addToBatch ([], [], [], 0, 0, []) . sortBy (compare `on` (Down . hasImage . fst)) + where + addToBatch :: (GroupMember, ByteString) -> ([(ByteString, [GroupMember])], [ByteString], [GroupMember], Int, Int, [GroupMember]) -> ([(ByteString, [GroupMember])], [ByteString], [GroupMember], Int, Int, [GroupMember]) + addToBatch (s, e) acc@(batches, elems, members, len, n, large) + | B.length e + 4 > maxLen = (batches, elems, members, len, n, s : large) + -- batch overhead: '=' + count (2) + 2-byte length prefix per element + | n + 1 <= 255 && len + B.length e + (n + 1) * 2 + 2 <= maxLen = + (batches, e : elems, s : members, len + B.length e, n + 1, large) + -- doesn't fit current — flush and start new with this element alone + | otherwise = + (flush acc, [e], [s], B.length e, 1, large) + flush :: ([(ByteString, [GroupMember])], [ByteString], [GroupMember], Int, Int, [GroupMember]) -> [(ByteString, [GroupMember])] + flush (batches, _, _, _, 0, _) = batches + flush (batches, elems, members, _, _, _) = + (encodeBinaryBatch elems, members) : batches + finish acc@(_, _, _, _, _, large) = (flush acc, large) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index d932194934..4e3dc3ab34 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -50,6 +50,7 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) +import Simplex.Messaging.Agent.Protocol (AgentErrorType) import Simplex.Messaging.Agent.Store.Interface (closeDBStore, reopenDBStore) import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Crypto as C @@ -73,6 +74,7 @@ data DBMigrationResult | DBMErrorNotADatabase {dbFile :: String} | DBMErrorMigration {dbFile :: String, migrationError :: MigrationError} | DBMErrorSQL {dbFile :: String, migrationSQLError :: String} + | DBMAgentError {agentError :: AgentErrorType} deriving (Show) $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult) @@ -259,6 +261,9 @@ mobileChatOpts dbOptions = tbqSize = 4096, deviceName = Nothing, chatRelay = False, + webPreviewConfig = Nothing, + chatRelayServer = Nothing, + headless = False, highlyAvailable = False, yesToUpMigrations = False, migrationBackupPath = Just "", @@ -275,7 +280,9 @@ mobileChatOpts dbOptions = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Nothing + createBot = Nothing, + userDisplayName = Nothing, + userImageFile = Nothing } defaultMobileConfig :: ChatConfig @@ -303,12 +310,12 @@ chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do let migrationConfig = MigrationConfig confirmMigrations (Just "") chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey chatDBFunctions) migrationConfig agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey []) migrationConfig - liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore} + ExceptT $ initialize chatStore ChatDatabase {chatStore, agentStore} where opts = mobileChatOpts $ removeDbKey chatDbOpts initialize st db = do - user_ <- getActiveUser_ st - newChatController db user_ defaultMobileConfig opts backgroundMode + user_ <- liftIO $ getActiveUser_ st + first DBMAgentError <$> newChatController db user_ defaultMobileConfig opts backgroundMode migrate createStore dbOpts confirmMigrations = ExceptT $ (first (DBMErrorMigration errDbStr) <$> createStore dbOpts confirmMigrations) diff --git a/src/Simplex/Chat/Names.hs b/src/Simplex/Chat/Names.hs new file mode 100644 index 0000000000..081d7129a5 --- /dev/null +++ b/src/Simplex/Chat/Names.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Names + ( SimplexDomainClaim (..), + SimplexDomainProof (..), + mkDomainClaim, + claimDomain, + ) +where + +import qualified Data.Aeson.TH as JQ +import Simplex.Chat.Badges (ProofPresHeader) +import Simplex.Messaging.Agent.Protocol (OwnerId, SimplexDomain) +import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) +#if defined(dbPostgres) +import Database.PostgreSQL.Simple.FromField (FromField (..)) +import Database.PostgreSQL.Simple.ToField (ToField (..)) +#else +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +#endif + +-- A name claim proof: signed by the address owner's key over proof payload - see verifyDomainProof. +data SimplexDomainProof = SimplexDomainProof + { linkOwnerId :: Maybe (StrJSON "OwnerId" OwnerId), + presHeader :: ProofPresHeader, + signature :: C.Signature 'C.Ed25519 + } + deriving (Eq, Show) + +$(JQ.deriveJSON defaultJSON ''SimplexDomainProof) + +instance ToField SimplexDomainProof where toField = toField . encodeJSON + +instance FromField SimplexDomainProof where fromField = fromTextField_ decodeJSON + +data SimplexDomainClaim = SimplexDomainClaim + { domain :: StrJSON "SimplexDomain" SimplexDomain, + proof :: Maybe SimplexDomainProof + } + deriving (Eq, Show) + +mkDomainClaim :: SimplexDomain -> SimplexDomainClaim +mkDomainClaim = (`SimplexDomainClaim` Nothing) . StrJSON + +claimDomain :: SimplexDomainClaim -> SimplexDomain +claimDomain (SimplexDomainClaim n _) = unStrJSON n + +$(JQ.deriveJSON defaultJSON ''SimplexDomainClaim) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6816a5f692..a216bbb1fc 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -49,7 +49,7 @@ import Simplex.Chat.Operators.Conditions import Simplex.Chat.Protocol (RelayCapabilities (..), RelayProfile (..)) import Simplex.Chat.Types (ShortLinkContact, User) import Simplex.Chat.Types.Shared (RelayStatus) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol (sameShortLinkContact) import Simplex.Messaging.Agent.Store.DB (FromField (..), ToField (..), fromTextField_) import Simplex.Messaging.Agent.Store.Entity @@ -176,6 +176,22 @@ operatorRoles p op = case p of SPSMP -> smpRoles op SPXFTP -> xftpRoles op +data ServerRolesOverride = ServerRolesOverride + { storage :: Maybe Bool, + proxy :: Maybe Bool, + names :: Maybe Bool + } + deriving (Eq, Show) + +emptyServerRolesOverride :: ServerRolesOverride +emptyServerRolesOverride = ServerRolesOverride {storage = Nothing, proxy = Nothing, names = Nothing} + +-- each role: override if set, else the operator's role (if any), else default (receive on, proxy on, names off) +resolveServerRoles :: UserProtocol p => SProtocolType p -> Maybe ServerOperator -> ServerRolesOverride -> ServerRoles +resolveServerRoles p op ServerRolesOverride {storage, proxy, names} = + ServerRoles {storage = fromMaybe s storage, proxy = fromMaybe pr proxy, names = fromMaybe n names} + where ServerRoles {storage = s, proxy = pr, names = n} = maybe (ServerRoles True True False) (operatorRoles p) op + conditionsAccepted :: ServerOperator -> Bool conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of CAAccepted {} -> True @@ -245,6 +261,7 @@ data UserServer' s (p :: ProtocolType) = UserServer preset :: Bool, tested :: Maybe Bool, enabled :: Bool, + roles :: ServerRolesOverride, deleted :: Bool } deriving (Show) @@ -330,7 +347,7 @@ newUserServer = newUserServer_ False True newUserServer_ :: Bool -> Bool -> ProtoServerWithAuth p -> NewUserServer p newUserServer_ preset enabled server = - UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, deleted = False} + UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, roles = emptyServerRolesOverride, deleted = False} presetChatRelay :: Bool -> RelayProfile -> [Text] -> ShortLinkContact -> NewUserChatRelay presetChatRelay = newChatRelay_ True @@ -439,13 +456,13 @@ agentServerCfgs :: UserProtocol p => SProtocolType p -> [(Text, ServerOperator)] agentServerCfgs p opDomains = mapMaybe agentServer where agentServer :: UserServer' s p -> Maybe (ServerCfg p) - agentServer srv@UserServer {server, enabled} = + agentServer srv@UserServer {server, enabled, roles = srvRoles} = case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of Just (_, op@ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled}) - | opEnabled -> Just ServerCfg {server, enabled, operator = Just opId, roles = operatorRoles p op} + | opEnabled -> Just ServerCfg {server, enabled, operator = Just opId, roles = resolveServerRoles p (Just op) srvRoles} | otherwise -> Nothing Nothing -> - Just ServerCfg {server, enabled, operator = Nothing, roles = allRoles} + Just ServerCfg {server, enabled, operator = Nothing, roles = resolveServerRoles p Nothing srvRoles} matchingHost :: Text -> TransportHost -> Bool matchingHost d = \case @@ -511,7 +528,9 @@ data UserServersError | USEDuplicateChatRelayAddress {duplicateChatRelay :: Text, duplicateAddress :: ShortLinkContact} deriving (Show) -data UserServersWarning = USWNoChatRelays {user :: Maybe User} +data UserServersWarning + = USWNoChatRelays {user :: Maybe User} + | USWNoNamesServers {user :: Maybe User} deriving (Show) validateUserServers :: UserServersClass u' => [u'] -> [(User, [UserOperatorServers])] -> ([UserServersError], [UserServersWarning]) @@ -522,11 +541,10 @@ validateUserServers curr others = (currUserErrs <> concatMap otherUserErrs other noServersErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> Maybe User -> [u] -> [UserServersError] noServersErrs p user uss | noServers opEnabled = [USENoServers p' user] - | otherwise = [USEStorageMissing p' user | noServers (hasRole storage)] <> [USEProxyMissing p' user | noServers (hasRole proxy)] + | otherwise = [USEStorageMissing p' user | not (any (hasRole p (\ServerRoles {storage} -> storage)) uss)] <> [USEProxyMissing p' user | not (any (hasRole p (\ServerRoles {proxy} -> proxy)) uss)] where p' = AProtocolType p noServers cond = not $ any srvEnabled $ userServers p $ filter cond uss - hasRole roleSel = maybe True (\op@ServerOperator {enabled} -> enabled && roleSel (operatorRoles p op)) . operator' srvEnabled (AUS _ UserServer {deleted, enabled}) = enabled && not deleted serverErrs :: (UserServersClass u, ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [u] -> [UserServersError] serverErrs p uss = mapMaybe duplicateErr_ srvs @@ -540,6 +558,10 @@ validateUserServers curr others = (currUserErrs <> concatMap otherUserErrs other allHosts = concatMap (\(AUS _ srv) -> L.toList $ srvHost srv) srvs userServers :: (UserServersClass u, UserProtocol p) => SProtocolType p -> [u] -> [AUserServer p] userServers p = map aUserServer' . concatMap (servers' p) + -- a group covers a role if its operator is enabled and some enabled server resolves it on + hasRole :: (UserServersClass u, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> u -> Bool + hasRole p roleSel u = + opEnabled u && any (\(AUS _ UserServer {enabled, deleted, roles}) -> enabled && not deleted && roleSel (resolveServerRoles p (operator' u) roles)) (map aUserServer' (servers' p u)) chatRelayErrs :: UserServersClass u => [u] -> [UserServersError] chatRelayErrs uss = concatMap duplicateErrs_ cRelays where @@ -552,15 +574,15 @@ validateUserServers curr others = (currUserErrs <> concatMap otherUserErrs other addAddress (xs, dups) x | any (sameShortLinkContact x) xs = (xs, x : dups) | otherwise = (x : xs, dups) - currUserWarns = noChatRelaysWarns Nothing curr - otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss + currUserWarns = noChatRelaysWarns Nothing curr <> noNamesServersWarns Nothing curr + otherUserWarns (user, uss) = noChatRelaysWarns (Just user) uss <> noNamesServersWarns (Just user) uss noChatRelaysWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning] - noChatRelaysWarns user uss - | noChatRelays opEnabled = [USWNoChatRelays user] - | otherwise = [] + noChatRelaysWarns user uss = [USWNoChatRelays user | noChatRelays opEnabled] where noChatRelays cond = not $ any relayEnabled $ userChatRelays $ filter cond uss relayEnabled (AUCR _ UserChatRelay {deleted, enabled}) = enabled && not deleted + noNamesServersWarns :: UserServersClass u => Maybe User -> [u] -> [UserServersWarning] + noNamesServersWarns user uss = [USWNoNamesServers user | not (any (hasRole SPSMP (\ServerRoles {names} -> names)) uss)] userChatRelays :: UserServersClass u => [u] -> [AUserChatRelay] userChatRelays = map aUserChatRelay' . concatMap chatRelays' opEnabled :: UserServersClass u => u -> Bool @@ -585,6 +607,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''ServerOperatorConditions) +$(JQ.deriveJSON defaultJSON ''ServerRolesOverride) + instance ProtocolTypeI p => ToJSON (UserServer' s p) where toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index 53f31e005d..d55d23e2b9 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -39,8 +39,8 @@ operatorFlux = serverDomains = ["simplexonflux.com"], conditionsAcceptance = CARequired Nothing, enabled = True, - smpRoles = ServerRoles {storage = False, proxy = True}, - xftpRoles = ServerRoles {storage = False, proxy = True} + smpRoles = ServerRoles {storage = False, proxy = True, names = True}, + xftpRoles = allRoles } -- Please note: if any servers are removed from the lists below, they MUST be added here. diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index dde223f6b9..1846cb3582 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -22,13 +22,13 @@ where import Control.Logger.Simple (LogLevel (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), WebPreviewConfig (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) import Simplex.Messaging.Client (HostMode (..), SMPWebPortServers (..), SocksMode (..), textToHostMode) import Simplex.Messaging.Encoding.String @@ -50,7 +50,9 @@ data ChatOpts = ChatOpts autoAcceptFileSize :: Integer, muteNotifications :: Bool, markRead :: Bool, - createBot :: Maybe CreateBotOpts + createBot :: Maybe CreateBotOpts, + userDisplayName :: Maybe Text, + userImageFile :: Maybe FilePath } data CoreChatOpts = CoreChatOpts @@ -66,6 +68,9 @@ data CoreChatOpts = CoreChatOpts tbqSize :: Natural, deviceName :: Maybe Text, chatRelay :: Bool, + webPreviewConfig :: Maybe WebPreviewConfig, + chatRelayServer :: Maybe SMPServerWithAuth, + headless :: Bool, highlyAvailable :: Bool, yesToUpMigrations :: Bool, migrationBackupPath :: Maybe FilePath, @@ -74,7 +79,8 @@ data CoreChatOpts = CoreChatOpts data CreateBotOpts = CreateBotOpts { botDisplayName :: Text, - allowFiles :: Bool + allowFiles :: Bool, + clientService :: Bool } data ChatCmdLog = CCLAll | CCLMessages | CCLNone @@ -239,6 +245,59 @@ coreChatOptsP appDir defaultDbName = do ( long "relay" <> help "Run as a chat relay client" ) + webPreviewConfig <- do + webDomain_ <- + optional $ + strOption + ( long "relay-web-domain" + <> metavar "DOMAIN" + <> help "Domain for channel web previews (relay only)" + ) + webJsonDir_ <- + optional $ + strOption + ( long "relay-web-dir" + <> metavar "DIR" + <> help "Directory for channel web preview JSON files (relay only)" + ) + webCorsFile <- + optional $ + strOption + ( long "relay-web-cors-file" + <> metavar "FILE" + <> help "Path to generated Caddy CORS config file (relay only)" + ) + webUpdateInterval <- + option auto + ( long "relay-web-interval" + <> metavar "SECONDS" + <> help "Interval between web preview regeneration in seconds (relay only)" + <> value 300 + ) + webPreviewItemCount <- + option auto + ( long "relay-web-item-count" + <> metavar "COUNT" + <> help "Number of recent messages in channel web preview (relay only)" + <> value 50 + ) + pure $ case (webDomain_, webJsonDir_) of + (Just webDomain, Just webJsonDir) -> Just WebPreviewConfig {webDomain, webJsonDir, webCorsFile, webUpdateInterval, webPreviewItemCount} + (Nothing, Nothing) -> Nothing + _ -> errorWithoutStackTrace "--relay-web-domain and --relay-web-dir must both be provided" + chatRelayServer <- + optional $ + option + strParse + ( long "relay-address-server" + <> metavar "SERVER" + <> help "SMP server to use for chat relay address link (requires --relay)" + ) + headless <- + switch + ( long "headless" + <> help "Run chat relay without interactive prompts, e.g. as a service (requires --relay; on first run also --user-display-name to create the profile)" + ) highlyAvailable <- switch ( long "ha" @@ -282,6 +341,13 @@ coreChatOptsP appDir defaultDbName = do tbqSize, deviceName, chatRelay, + webPreviewConfig, + chatRelayServer = case chatRelayServer of + Just _ | not chatRelay -> errorWithoutStackTrace "--relay-address-server option requires --relay option" + _ -> chatRelayServer, + headless = case headless of + True | not chatRelay -> errorWithoutStackTrace "--headless option requires --relay option" + _ -> headless, highlyAvailable, yesToUpMigrations, migrationBackupPath, @@ -390,6 +456,25 @@ chatOptsP appDir defaultDbName = do ( long "create-bot-allow-files" <> help "Flag for created bot to allow files (only allowed together with --create-bot option)" ) + createBotClientService <- + switch + ( long "create-bot-client-service" + <> help "Flag for created bot to use client service certificate" + ) + userDisplayName <- + optional $ + strOption + ( long "user-display-name" + <> metavar "NAME" + <> help "Use existing active user with this display name, or create one on the first start (incompatible with --create-bot-display-name)" + ) + userImageFile <- + optional $ + strOption + ( long "user-image-file" + <> metavar "FILE" + <> help "Set user profile image from .png/.jpg/.jpeg file when the profile is created (requires --user-display-name); ignored if the user already exists (use \"/set profile image file \" to change it)" + ) pure ChatOpts { coreOptions, @@ -405,10 +490,17 @@ chatOptsP appDir defaultDbName = do muteNotifications, markRead, createBot = case createBotDisplayName of - Just botDisplayName -> Just CreateBotOpts {botDisplayName, allowFiles = createBotAllowFiles} + Just botDisplayName + | isJust userDisplayName -> error "--user-display-name and --create-bot-display-name are mutually exclusive" + | otherwise -> Just CreateBotOpts {botDisplayName, allowFiles = createBotAllowFiles, clientService = createBotClientService} Nothing | createBotAllowFiles -> error "--create-bot-allow-files option requires --create-bot-name option" - | otherwise -> Nothing + | createBotClientService -> error "--create-bot-client-service option requires --create-bot-name option" + | otherwise -> Nothing, + userDisplayName, + userImageFile = case userImageFile of + Just _ | isNothing userDisplayName -> error "--user-image-file option requires --user-display-name option" + _ -> userImageFile } parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p] diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index 7d272481f6..b460a73d3d 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -10,7 +10,7 @@ generateRandomProfile :: IO Profile generateRandomProfile = do adjective <- pick adjectives noun <- pickNoun adjective 2 - pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing} + pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} where pick :: [a] -> IO a pick xs = (xs !!) <$> randomRIO (0, length xs - 1) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 86202fe598..19ba3d022d 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -48,13 +48,14 @@ import Data.Time.Clock (UTCTime) import Data.Time.Clock.System (systemToUTCTime, utcToSystemTime) import Data.Type.Equality import Data.Typeable (Typeable) -import Data.Word (Word32) +import Data.Word (Word16, Word32) import Simplex.Chat.Badges (LocalBadge) import Simplex.Chat.Call import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared +import qualified Simplex.FileTransfer.Description as FD import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize) @@ -83,12 +84,14 @@ import Simplex.Messaging.Version hiding (version) -- 15 - support specifying message scopes for group messages (2025-03-12) -- 16 - support short link data (2025-06-10) -- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10) +-- 18 - relay web capabilities (2026-05-31) +-- 19 - group roster (2026-06-18) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 17 +currentChatVersion = VersionChat 19 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -155,6 +158,15 @@ shortLinkDataVersion = VersionChat 16 memberSupportVoiceVersion :: VersionChat memberSupportVoiceVersion = VersionChat 17 +-- relay sends web preview capabilities to owner +relayWebCapVersion :: VersionChat +relayWebCapVersion = VersionChat 18 + +-- owner-signed roster (promoted members/moderators/admins) and the relay roster-ack handshake; +-- a relay below this version is published without the handshake (it can't ack a roster) +groupRosterVersion :: VersionChat +groupRosterVersion = VersionChat 19 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -368,6 +380,36 @@ data GrpMsgForward = GrpMsgForward } deriving (Eq, Show) +-- | Owner-signed roster header for the privileged (moderator/admin/member) set; owners +-- are not included, their keys come from the link. The member list itself is not +-- here: it is sent as a binary blob over the inline file transfer, and this header +-- carries only its inline-file invitation (size + owner-attested digest). +data GroupRoster = GroupRoster + { version :: VersionRoster, + fileInv :: InlineFileInvitation + } + deriving (Eq, Show) + +-- | Lean always-inline file invitation for the roster blob, carried in the signed +-- header. The digest authenticates the unsigned blob; integrity is entirely the digest. +data InlineFileInvitation = InlineFileInvitation + { fileSize :: Integer, + fileDigest :: FD.FileDigest + } + deriving (Eq, Show) + +data RosterMember = RosterMember + { memberId :: MemberId, + key :: MemberKey, -- trust-on-first-use pinned per memberId + role :: GroupMemberRole, + privileges :: Word16 -- reserved: serialized as 0, parsed and ignored in v1 + } + deriving (Eq, Show) + +-- RosterMember is binary-only: it rides in the roster blob, never in a JSON message. +instance Encoding RosterMember where + smpEncode RosterMember {memberId, key, role, privileges} = smpEncode (memberId, key, role, privileges) + smpP = RosterMember <$> smpP <*> smpP <*> smpP <*> smpP instance Encoding FwdSender where smpEncode = \case @@ -434,6 +476,11 @@ data MsgSigning = MsgSigning encodeChatBinding :: ChatBinding -> ByteString -> ByteString encodeChatBinding cb bindingData = smpEncode cb <> bindingData +signChatMsgBody :: MsgSigning -> ByteString -> SignedMsg +signChatMsgBody MsgSigning {bindingTag, bindingData, keyRef, privKey} msgBody = + let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) + in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig L.:| [], signedBody = msgBody} + data ChatMsgEvent (e :: MsgEncoding) where XMsgNew :: MsgContainer -> ChatMsgEvent 'Json XMsgFileDescr :: {msgId :: SharedMsgId, fileDescr :: FileDescr} -> ChatMsgEvent 'Json @@ -447,7 +494,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json XInfo :: Profile -> ChatMsgEvent 'Json XContact :: {profile :: Profile, contactReqId :: Maybe XContactId, welcomeMsgId :: Maybe SharedMsgId, requestMsg :: Maybe (SharedMsgId, MsgContent)} -> ChatMsgEvent 'Json - XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey} -> ChatMsgEvent 'Json + XMember :: {profile :: Profile, newMemberId :: MemberId, newMemberKey :: MemberKey, viaRelay :: Maybe MemberId} -> ChatMsgEvent 'Json XDirectDel :: ChatMsgEvent 'Json XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json XGrpAcpt :: MemberId -> ChatMsgEvent 'Json @@ -466,16 +513,19 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpMemInv :: MemberId -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemFwd :: MemberInfo -> IntroInvitation -> ChatMsgEvent 'Json XGrpMemInfo :: MemberId -> Profile -> ChatMsgEvent 'Json - XGrpMemRole :: MemberId -> GroupMemberRole -> ChatMsgEvent 'Json + XGrpMemRole :: MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> ChatMsgEvent 'Json XGrpMemRestrict :: MemberId -> MemberRestrictions -> ChatMsgEvent 'Json XGrpMemCon :: MemberId -> ChatMsgEvent 'Json XGrpMemConAll :: MemberId -> ChatMsgEvent 'Json -- TODO not implemented - XGrpMemDel :: MemberId -> Bool -> ChatMsgEvent 'Json + XGrpMemDel :: MemberId -> Bool -> Maybe VersionRoster -> ChatMsgEvent 'Json XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json XGrpPrefs :: GroupPreferences -> ChatMsgEvent 'Json XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> Maybe MsgScope -> ChatMsgEvent 'Json + XGrpRoster :: GroupRoster -> ChatMsgEvent 'Json + XGrpRosterAck :: VersionRoster -> Maybe Text -> ChatMsgEvent 'Json + XGrpRosterRequest :: Maybe VersionRoster -> ChatMsgEvent 'Json XGrpMsgForward :: GrpMsgForward -> ChatMessage 'Json -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json @@ -519,6 +569,7 @@ isForwardedGroupMsg ev = case ev of XGrpDel -> True XGrpInfo _ -> True XGrpPrefs _ -> True + XGrpRoster _ -> True _ -> False data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} @@ -764,6 +815,13 @@ isVoice = \case MCVoice {} -> True _ -> False +isMedia :: MsgContent -> Bool +isMedia = \case + MCImage {} -> True + MCVideo {} -> True + MCFile {} -> True + _ -> False + isReport :: MsgContent -> Bool isReport = \case MCReport {} -> True @@ -787,6 +845,8 @@ data MsgMention = MsgMention {memberId :: MemberId} newtype MsgMentions = MsgMentions (Map MemberName MsgMention) deriving (Eq, Show) +$(JQ.deriveJSON defaultJSON ''InlineFileInvitation) + $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MCL") ''MsgChatLink) $(JQ.deriveJSON defaultJSON ''LinkOwnerSig) @@ -887,6 +947,28 @@ maxCompressedMsgLength = 13380 maxDecompressedMsgLength :: Int maxDecompressedMsgLength = 65536 +-- Defensive entry-count bound for the roster blob parser (rosterBlobP) and the +-- promotion cap over the promoted (member/moderator/admin) set. +maxGroupRosterSize :: Int +maxGroupRosterSize = 256 + +-- Receive-side byte bound: reject an owner-signed header whose claimed fileSize exceeds what +-- maxGroupRosterSize entries can occupy (128 B/entry is a generous worst case), before a file is created. +-- 128 B/entry ~ memberId + X.509 Ed25519 key (44 B) + role + privileges + 1-byte length prefixes (~2x the ~65 B typical). +maxGroupRosterBytes :: Integer +maxGroupRosterBytes = fromIntegral maxGroupRosterSize * 128 + +-- The byte sequence the owner-signed digest is computed over and verified against +-- before parsing. Word16 count (smpEncodeList's 1-byte count is too small for the future cap). +encodeRosterBlob :: [RosterMember] -> ByteString +encodeRosterBlob ms = smpEncode (fromIntegral (length ms) :: Word16) <> B.concat (map smpEncode ms) + +rosterBlobP :: A.Parser [RosterMember] +rosterBlobP = do + n <- fromIntegral <$> smpP @Word16 + when (n > maxGroupRosterSize) $ fail "roster: too many entries" + A.count n smpP + -- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead) -- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008 maxEncodedInfoLength :: Int @@ -1023,6 +1105,9 @@ data CMEventTag (e :: MsgEncoding) where XGrpInfo_ :: CMEventTag 'Json XGrpPrefs_ :: CMEventTag 'Json XGrpDirectInv_ :: CMEventTag 'Json + XGrpRoster_ :: CMEventTag 'Json + XGrpRosterAck_ :: CMEventTag 'Json + XGrpRosterRequest_ :: CMEventTag 'Json XGrpMsgForward_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json XInfoProbeCheck_ :: CMEventTag 'Json @@ -1083,6 +1168,9 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpInfo_ -> "x.grp.info" XGrpPrefs_ -> "x.grp.prefs" XGrpDirectInv_ -> "x.grp.direct.inv" + XGrpRoster_ -> "x.grp.roster" + XGrpRosterAck_ -> "x.grp.roster.ack" + XGrpRosterRequest_ -> "x.grp.roster.request" XGrpMsgForward_ -> "x.grp.msg.forward" XInfoProbe_ -> "x.info.probe" XInfoProbeCheck_ -> "x.info.probe.check" @@ -1144,6 +1232,9 @@ instance StrEncoding ACMEventTag where "x.grp.info" -> XGrpInfo_ "x.grp.prefs" -> XGrpPrefs_ "x.grp.direct.inv" -> XGrpDirectInv_ + "x.grp.roster" -> XGrpRoster_ + "x.grp.roster.ack" -> XGrpRosterAck_ + "x.grp.roster.request" -> XGrpRosterRequest_ "x.grp.msg.forward" -> XGrpMsgForward_ "x.info.probe" -> XInfoProbe_ "x.info.probe.check" -> XInfoProbeCheck_ @@ -1191,7 +1282,7 @@ toCMEventTag msg = case msg of XGrpMemInv _ _ -> XGrpMemInv_ XGrpMemFwd _ _ -> XGrpMemFwd_ XGrpMemInfo _ _ -> XGrpMemInfo_ - XGrpMemRole _ _ -> XGrpMemRole_ + XGrpMemRole {} -> XGrpMemRole_ XGrpMemRestrict _ _ -> XGrpMemRestrict_ XGrpMemCon _ -> XGrpMemCon_ XGrpMemConAll _ -> XGrpMemConAll_ @@ -1201,6 +1292,9 @@ toCMEventTag msg = case msg of XGrpInfo _ -> XGrpInfo_ XGrpPrefs _ -> XGrpPrefs_ XGrpDirectInv {} -> XGrpDirectInv_ + XGrpRoster _ -> XGrpRoster_ + XGrpRosterAck {} -> XGrpRosterAck_ + XGrpRosterRequest _ -> XGrpRosterRequest_ XGrpMsgForward {} -> XGrpMsgForward_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ @@ -1259,13 +1353,20 @@ requiresSignature = \case XGrpMemRestrict_ -> True XGrpLeave_ -> True XGrpRelayNew_ -> True + XGrpRoster_ -> True XInfo_ -> True _ -> False --- TODO [relays] relay: vectors tracking which members received which other member profiles/keys. --- TODO - don't forward XGrpLeave/XInfo to members who haven't seen sender's profile/key. --- TODO - unverifiedAllowed is a temporary workaround postponing targeted event forwarding. +-- | Content events a member may sign (XMsgNew opt-in; XMsgUpdate/XMsgDel when the target was signed). +signableContent :: CMEventTag e -> Bool +signableContent = \case + XMsgNew_ -> True + XMsgUpdate_ -> True + XMsgDel_ -> True + _ -> False +-- TODO [relays] can be tightened — sender keys are now disseminated via +-- TODO prepended XGrpMemNew before forwarded XInfo/XGrpLeave reach the recipient. -- Allow signed but unverified XGrpLeave/XInfo between subscribers when sender's key is unknown. -- Owner keys are always known, so subscribers are required to verify from owners. -- Likewise, subscriber keys are always known to owners, so owners are required to verify from subscribers. @@ -1329,7 +1430,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do reqContent <- opt "content" let requestMsg = (,) <$> reqMsgId <*> reqContent pure XContact {profile, contactReqId, welcomeMsgId, requestMsg} - XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" + XMember_ -> XMember <$> p "profile" <*> p "newMemberId" <*> p "newMemberKey" <*> opt "viaRelay" XDirectDel_ -> pure XDirectDel XGrpInv_ -> XGrpInv <$> p "groupInvitation" XGrpAcpt_ -> XGrpAcpt <$> p "memberId" @@ -1351,16 +1452,19 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do XGrpMemInv_ -> XGrpMemInv <$> p "memberId" <*> p "memberIntro" XGrpMemFwd_ -> XGrpMemFwd <$> p "memberInfo" <*> p "memberIntro" XGrpMemInfo_ -> XGrpMemInfo <$> p "memberId" <*> p "profile" - XGrpMemRole_ -> XGrpMemRole <$> p "memberId" <*> p "role" + XGrpMemRole_ -> XGrpMemRole <$> p "memberId" <*> p "role" <*> opt "memberKey" <*> opt "rosterVersion" XGrpMemRestrict_ -> XGrpMemRestrict <$> p "memberId" <*> p "memberRestrictions" XGrpMemCon_ -> XGrpMemCon <$> p "memberId" XGrpMemConAll_ -> XGrpMemConAll <$> p "memberId" - XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") + XGrpMemDel_ -> XGrpMemDel <$> p "memberId" <*> Right (fromRight False $ p "messages") <*> opt "rosterVersion" XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" XGrpPrefs_ -> XGrpPrefs <$> p "groupPreferences" XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" <*> opt "scope" + XGrpRoster_ -> XGrpRoster <$> (GroupRoster <$> p "version" <*> p "fileInv") + XGrpRosterAck_ -> XGrpRosterAck <$> p "version" <*> opt "error" + XGrpRosterRequest_ -> XGrpRosterRequest <$> opt "version" XGrpMsgForward_ -> do fwdSender <- opt "memberId" >>= \case Just memberId -> FwdMember memberId . fromMaybe "" <$> opt "memberName" @@ -1402,7 +1506,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId] XInfo profile -> o ["profile" .= profile] XContact {profile, contactReqId, welcomeMsgId, requestMsg} -> o $ ("contactReqId" .=? contactReqId) $ ("welcomeMsgId" .=? welcomeMsgId) $ ("msgId" .=? (fst <$> requestMsg)) $ ("content" .=? (snd <$> requestMsg)) $ ["profile" .= profile] - XMember {profile, newMemberId, newMemberKey} -> o ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey] + XMember {profile, newMemberId, newMemberKey, viaRelay} -> o $ ("viaRelay" .=? viaRelay) ["profile" .= profile, "newMemberId" .= newMemberId, "newMemberKey" .= newMemberKey] XDirectDel -> JM.empty XGrpInv groupInv -> o ["groupInvitation" .= groupInv] XGrpAcpt memId -> o ["memberId" .= memId] @@ -1423,16 +1527,19 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en XGrpMemInv memId memIntro -> o ["memberId" .= memId, "memberIntro" .= memIntro] XGrpMemFwd memInfo memIntro -> o ["memberInfo" .= memInfo, "memberIntro" .= memIntro] XGrpMemInfo memId profile -> o ["memberId" .= memId, "profile" .= profile] - XGrpMemRole memId role -> o ["memberId" .= memId, "role" .= role] + XGrpMemRole memId role memberKey rosterVersion -> o $ ("memberKey" .=? memberKey) $ ("rosterVersion" .=? rosterVersion) ["memberId" .= memId, "role" .= role] XGrpMemRestrict memId memRestrictions -> o ["memberId" .= memId, "memberRestrictions" .= memRestrictions] XGrpMemCon memId -> o ["memberId" .= memId] XGrpMemConAll memId -> o ["memberId" .= memId] - XGrpMemDel memId messages -> o $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] + XGrpMemDel memId messages rosterVersion -> o $ ("rosterVersion" .=? rosterVersion) $ ("messages" .=? if messages then Just True else Nothing) ["memberId" .= memId] XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] XGrpPrefs p -> o ["groupPreferences" .= p] XGrpDirectInv connReq content scope -> o $ ("content" .=? content) $ ("scope" .=? scope) ["connReq" .= connReq] + XGrpRoster GroupRoster {version, fileInv} -> o ["version" .= version, "fileInv" .= fileInv] + XGrpRosterAck version err -> o $ ("error" .=? err) ["version" .= version] + XGrpRosterRequest version -> o $ ("version" .=? version) [] XGrpMsgForward GrpMsgForward {fwdSender, fwdBrokerTs} msg -> o $ encodeFwdSender fwdSender ["msg" .= msg, "msgTs" .= fwdBrokerTs] where encodeFwdSender = \case diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index cfe8e944a5..89100ff890 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -539,7 +539,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo Left e -> eToView' $ ChatErrorRemoteCtrl $ RCEProtocolError e takeRCStep :: RCStepTMVar a -> CM a -takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, agentConnId = AgentConnId "", connectionEntity_ = Nothing}) . atomically . takeTMVar +takeRCStep = liftError' (chatErrorAgent . RCP) . atomically . takeTMVar type GetChunk = Int -> IO ByteString diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index e5ebf8e2bd..80c928567e 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -112,19 +112,20 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do db [sql| SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, 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.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, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified 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.contact_status = ? AND c.deleted = 0 |] (userId, contactId, CSActive) toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} + toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn @@ -143,28 +144,28 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 9c5fe0cd91..75652349b5 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -73,7 +73,7 @@ createOrUpdateContactRequest isSimplexTeam invId cReqChatVRange@(VersionRange minV maxV) - profile@Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} + profile@Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} xContactId_ welcomeMsgId_ requestMsg_ @@ -112,11 +112,11 @@ createOrUpdateContactRequest [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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, @@ -148,11 +148,11 @@ createOrUpdateContactRequest SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? @@ -168,8 +168,8 @@ createOrUpdateContactRequest liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId) :. ("" :: LocalAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId) :. ("" :: LocalAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) profileId <- liftIO $ insertedRowId db liftIO $ DB.execute @@ -238,6 +238,7 @@ createOrUpdateContactRequest SET display_name = ?, full_name = ?, short_descr = ?, + description = ?, image = ?, contact_link = ?, updated_at = ?, @@ -257,7 +258,7 @@ createOrUpdateContactRequest AND contact_request_id = ? ) |] - ((displayName, fullName, shortDescr, image, contactLink, currentTs) :. badgeToRow badge badgeVerified :. (userId, contactRequestId)) + ((displayName, fullName, shortDescr, description, image, contactLink, currentTs) :. badgeToRow badge badgeVerified :. (userId, contactRequestId)) updateRequest currentTs = if displayName == oldDisplayName then diff --git a/src/Simplex/Chat/Store/Delivery.hs b/src/Simplex/Chat/Store/Delivery.hs index cd18e62148..add8c64e7f 100644 --- a/src/Simplex/Chat/Store/Delivery.hs +++ b/src/Simplex/Chat/Store/Delivery.hs @@ -33,7 +33,9 @@ import qualified Data.Aeson as J import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import qualified Data.List.NonEmpty as L +import Data.Maybe (isNothing) import Data.Text (Text) +import qualified Data.Text as T import Data.Time.Clock (UTCTime, getCurrentTime) import Simplex.Chat.Delivery import Simplex.Chat.Protocol hiding (Binary) @@ -45,6 +47,7 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Encoding (smpDecode) import Simplex.Messaging.Util (eitherToMaybe, firstRow') +import Text.Read (readMaybe) #if defined(dbPostgres) import Database.PostgreSQL.Simple (In (..), Only (..), (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) @@ -153,7 +156,7 @@ getMsgDeliveryTask_ db taskId = toTask ((Only taskId') :. jobScopeRow :. (senderGMId, senderMemberId, senderMemberName, brokerTs, Binary msgBody, chatBinding_, sigs_, BI showGroupAsSender)) = case (toJobScope_ jobScopeRow, J.eitherDecodeStrict' msgBody) of (Just jobScope, Right chatMsg) -> - let fwdSender = if showGroupAsSender then FwdChannel else FwdMember senderMemberId senderMemberName + let fwdSender = if showGroupAsSender && isNothing chatBinding_ then FwdChannel else FwdMember senderMemberId senderMemberName -- Re-parsed from msg_body: validates stored content against current code. -- Signed: original bytes preserved (re-encoding would invalidate signature). -- Unsigned: re-encoded from parsed ChatMessage on forward (sanitizes content). @@ -245,8 +248,8 @@ deleteDoneDeliveryTasks db createdAtCutoff = do |] (createdAtCutoff, DTSProcessed, DTSError) -createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> Maybe GroupMemberId -> ByteString -> IO () -createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body = do +createMsgDeliveryJob :: DB.Connection -> GroupInfo -> DeliveryJobScope -> [GroupMemberId] -> ByteString -> IO () +createMsgDeliveryJob db gInfo jobScope senderGMIds body = do currentTs <- getCurrentTime DB.execute db @@ -254,12 +257,17 @@ createMsgDeliveryJob db gInfo jobScope singleSenderGMId_ body = do INSERT INTO delivery_jobs ( group_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, - single_sender_group_member_id, body, job_status, created_at, updated_at + sender_group_member_ids, body, job_status, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?) |] - ((Only groupId) :. jobScopeRow_ jobScope :. (singleSenderGMId_, Binary body, DJSPending, currentTs, currentTs)) + ((Only groupId) :. jobScopeRow_ jobScope :. (senderColumn, Binary body, DJSPending, currentTs, currentTs)) where GroupInfo {groupId} = gInfo + -- NULL ↔ []; non-empty list ↔ comma-separated decimal Int64s. + senderColumn :: Maybe Text + senderColumn + | null senderGMIds = Nothing + | otherwise = Just $ T.intercalate "," $ map (T.pack . show) senderGMIds getPendingDeliveryJobScopes :: DB.Connection -> IO [DeliveryWorkerKey] getPendingDeliveryJobScopes db = @@ -272,7 +280,7 @@ getPendingDeliveryJobScopes db = |] (Only DJSPending) -type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe GroupMemberId, Binary ByteString, Maybe GroupMemberId) +type MessageDeliveryJobRow = (Only Int64) :. DeliveryJobScopeRow :. (Maybe Text, Binary ByteString, Maybe GroupMemberId) getNextDeliveryJob :: DB.Connection -> DeliveryWorkerKey -> IO (Either StoreError (Maybe MessageDeliveryJob)) getNextDeliveryJob db deliveryKey = do @@ -302,17 +310,26 @@ getNextDeliveryJob db deliveryKey = do SELECT delivery_job_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, - single_sender_group_member_id, body, cursor_group_member_id + sender_group_member_ids, body, cursor_group_member_id FROM delivery_jobs WHERE delivery_job_id = ? |] (Only jobId) where toDeliveryJob :: MessageDeliveryJobRow -> Either StoreError MessageDeliveryJob - toDeliveryJob ((Only jobId') :. jobScopeRow :. (singleSenderGMId_, Binary body, cursorGMId_)) = - case toJobScope_ jobScopeRow of - Just jobScope -> Right $ MessageDeliveryJob {jobId = jobId', jobScope, singleSenderGMId_, body, cursorGMId_} - Nothing -> Left $ SEInvalidDeliveryJob jobId' + toDeliveryJob ((Only jobId') :. jobScopeRow :. (senderGMIdsText_, Binary body, cursorGMId_)) = do + jobScope <- maybe (Left $ SEInvalidDeliveryJob jobId') Right $ toJobScope_ jobScopeRow + -- NULL or empty string means []; otherwise the value must parse + -- as a comma-separated decimal Int64 list. An unparseable + -- segment surfaces as job error rather than silent degradation. + senderGMIds <- case senderGMIdsText_ of + Nothing -> Right [] + Just t -> maybe (Left $ SEInvalidDeliveryJob jobId') Right $ parseSenderGMIds t + Right $ MessageDeliveryJob {jobId = jobId', jobScope, senderGMIds, body, cursorGMId_} + parseSenderGMIds :: Text -> Maybe [GroupMemberId] + parseSenderGMIds t + | T.null t = Just [] + | otherwise = traverse (readMaybe . T.unpack) (T.splitOn "," t) markJobFailed :: Int64 -> IO () markJobFailed jobId = DB.execute db "UPDATE delivery_jobs SET failed = 1 where delivery_job_id = ?" (Only jobId) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 5068c5c61c..723b12d448 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -46,10 +46,12 @@ module Simplex.Chat.Store.Direct deleteContactWithoutGroups, getDeletedContacts, getContactByName, + getContactToConnect, getContact, getContactViaShortLinkToConnect, getContactIdByName, updateContactProfile, + setContactDomainVerified, updateContactUserPreferences, updateContactAlias, updateContactConnectionAlias, @@ -98,6 +100,7 @@ where import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class +import Data.Bifunctor (first) import Data.Either (rights) import Data.Functor (($>)) import Data.Int (Int64) @@ -108,16 +111,18 @@ import Data.Type.Equality import Simplex.Chat.Badges (badgeToRow) import Simplex.Chat.Messages import Simplex.Chat.Store.Shared +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), SConnectionMode (..), SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport, pattern PQSupportOff) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util ((<$$>)) #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) @@ -316,11 +321,12 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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, @@ -397,13 +403,13 @@ createIncognitoProfile db User {userId} p = do createdAt <- getCurrentTime createIncognitoProfile_ db userId createdAt p -createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> ExceptT StoreError IO Contact -createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId = do +createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe Bool -> ExceptT StoreError IO Contact +createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId verified_ = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) ctUserPreferences = newContactUserPrefs user p - contactId <- createContact_ db cxt user p ctUserPreferences prepared "" currentTs - getContact db cxt user contactId + ct <- getContact db cxt user =<< createContact_ db cxt user p ctUserPreferences prepared "" currentTs + liftIO $ maybe (pure ct) (setContactDomainVerified db user ct) verified_ updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact updatePreparedContactUser @@ -559,22 +565,41 @@ updateContactProfile :: DB.Connection -> StoreCxt -> User -> Contact -> Profile updateContactProfile db cxt user@User {userId} c p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) lp p' - let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let nameVerified = if claimChanged then Nothing else prevVerification + profile = toLocalProfile profileId p'' localAlias currentTs badgeVerified nameVerified updateContactProfile' currentTs badgeVerified profile where - Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias}, userPreferences} = c - Profile {displayName = newName, preferences} = p' + Contact {contactId, localDisplayName, profile = lp@LocalProfile {profileId, displayName, localAlias, contactDomain = prevClaim, contactDomainVerified = prevVerification}, userPreferences} = c + Profile {displayName = newName, contactDomain, preferences} = p' mergedPreferences = contactUserPreferences user userPreferences preferences $ contactConnIncognito c + claimChanged = (domain <$> prevClaim) /= (domain <$> contactDomain) + p'' = (p' :: Profile) {contactDomain = (\d -> d {proof = if claimChanged then Nothing else proof =<< prevClaim}) <$> contactDomain} + clearVerificationIfClaimChanged = + when claimChanged $ + DB.execute db "UPDATE contact_profiles SET contact_domain_verified = NULL WHERE user_id = ? AND contact_profile_id = ?" (userId, profileId) updateContactProfile' currentTs badgeVerified profile | displayName == newName = do - liftIO $ updateContactProfile_' db userId profileId p' badgeVerified currentTs + liftIO $ updateContactProfile_' db userId profileId p'' badgeVerified currentTs + liftIO clearVerificationIfClaimChanged pure c {profile, mergedPreferences} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do - updateContactProfile_' db userId profileId p' badgeVerified currentTs + updateContactProfile_' db userId profileId p'' badgeVerified currentTs updateContactLDN_ db user contactId localDisplayName ldn currentTs + clearVerificationIfClaimChanged pure $ Right c {localDisplayName = ldn, profile, mergedPreferences} +setContactDomainVerified :: DB.Connection -> User -> Contact -> Bool -> IO Contact +setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} verified = do + DB.execute + db + [sql| + UPDATE contact_profiles SET contact_domain_verified = ? + WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?) + |] + (BI verified, userId, contactId) + pure (ct {profile = p {contactDomainVerified = Just verified}} :: Contact) + updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do updatedAt <- getCurrentTime @@ -706,16 +731,17 @@ updateContactProfile_ db userId profileId profile badgeVerified = do updateContactProfile_' db userId profileId profile badgeVerified currentTs updateContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} badgeVerified updatedAt = +updateContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, preferences, peerType, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs) updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -724,16 +750,17 @@ updateMemberContactProfileReset_ db userId profileId profile badgeVerified = do updateMemberContactProfileReset_' db userId profileId profile badgeVerified currentTs updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt = +updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) -- update only member profile fields (when member has associated contact - we keep contactLink and prefs) updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> IO () @@ -742,16 +769,17 @@ updateMemberContactProfile_ db userId profileId profile badgeVerified = do updateMemberContactProfile_' db userId profileId profile badgeVerified currentTs updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> Maybe Bool -> UTCTime -> IO () -updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, badge} badgeVerified updatedAt = +updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactDomain, badge} badgeVerified updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, updatedAt) :. badgeToRow badge badgeVerified :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, updatedAt) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain :. (userId, profileId)) updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO () updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do @@ -770,6 +798,22 @@ getContactByName db cxt user localDisplayName = do cId <- getContactIdByName db user localDisplayName getContact db cxt user cId +getContactToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, Contact)) +getContactToConnect db cxt user@User {userId} = \case + CTLink sl -> first (`CCLink` Just sl) <$$> getContactViaShortLinkToConnect db cxt user sl + CTName ni -> + liftIO (maybeFirstRow id $ DB.query db byNameQuery (userId, nameDomain ni)) >>= \case + Just (ctId :: Int64, Just (ACR cMode cReq), Just (sLnk :: ShortLinkContact)) | Just Refl <- testEquality cMode SCMContact -> + Just . (CCLink cReq (Just sLnk),) <$> getContact db cxt user ctId + _ -> pure Nothing + where + byNameQuery = + [sql| + SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0 + |] + getUserContacts :: DB.Connection -> StoreCxt -> User -> IO [Contact] getUserContacts db cxt user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) @@ -805,11 +849,12 @@ contactRequestQuery = SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) |] @@ -925,11 +970,12 @@ getContact_ db cxt user@User {userId} contactId deleted = do [sql| SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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/Files.hs b/src/Simplex/Chat/Store/Files.hs index 5289a3b304..5d862ea258 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -20,6 +20,7 @@ module Simplex.Chat.Store.Files createSndFileTransferXFTP, createSndFTDescrXFTP, setSndFTPrivateSndDescr, + getSndFTPrivateSndDescr, updateSndFTDescrXFTP, createExtraSndFTDescrs, updateSndFTDeliveryXFTP, @@ -31,12 +32,19 @@ module Simplex.Chat.Store.Files getSharedMsgIdByFileId, getFileIdBySharedMsgId, getGroupFileIdBySharedMsgId, + getGroupRcvFileId, + getGroupRosterFileInfo, + deleteGroupRosterFile, + getRosterTransferFile, + deleteRosterTransferFile, + getRcvFileLastChunkNo, getDirectFileIdBySharedMsgId, getChatRefByFileId, lookupChatRefByFileId, updateSndFileStatus, createRcvFileTransfer, createRcvGroupFileTransfer, + createRosterRcvFile, createRcvStandaloneFileTransfer, appendRcvFD, getRcvFileDescrByRcvFileId, @@ -79,6 +87,7 @@ import Data.Functor ((<&>)) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Text (Text) +import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Data.Type.Equality @@ -88,6 +97,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared +import Simplex.FileTransfer.Description (FileDigest) import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) @@ -201,6 +211,15 @@ setSndFTPrivateSndDescr db User {userId} fileId sfdText = do "UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (sfdText, currentTs, userId, fileId) +getSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> IO (Maybe Text) +getSndFTPrivateSndDescr db User {userId} fileId = + fmap (maybe Nothing fromOnly) $ + maybeFirstRow id $ + DB.query + db + "SELECT private_snd_file_descr FROM files WHERE user_id = ? AND file_id = ?" + (userId, fileId) + updateSndFTDescrXFTP :: DB.Connection -> User -> SndFileTransfer -> Text -> IO () updateSndFTDescrXFTP db user@User {userId} sft@SndFileTransfer {fileId, fileDescrId} rfdText = do currentTs <- getCurrentTime @@ -320,6 +339,64 @@ getGroupFileIdBySharedMsgId db userId groupId sharedMsgId = |] (userId, groupId, sharedMsgId) +-- Resolve the in-flight received group inline file for a chunk: read its file_type by shared_msg_id +-- (LIMIT 1 is safe -- all files sharing a shared_msg_id share a type), then look up by type: a roster +-- file is scoped to its source relay (every relay re-serves the owner's same shared_msg_id, so the source +-- disambiguates), a normal file is by shared_msg_id. Nothing => no in-flight transfer (orphaned chunk). +getGroupRcvFileId :: DB.Connection -> UserId -> Int64 -> GroupMemberId -> SharedMsgId -> IO (Maybe Int64) +getGroupRcvFileId db userId groupId fromMemberId sharedMsgId = do + fileType_ <- getFileType + case fileType_ of + Just FTRoster -> + maybeFirstRow fromOnly $ + DB.query db (rcvFileIdQ <> " AND r.group_member_id = ?") (userId, groupId, sharedMsgId, FTRoster, fromMemberId) + Just FTNormal -> + maybeFirstRow fromOnly $ + DB.query db rcvFileIdQ (userId, groupId, sharedMsgId, FTNormal) + Nothing -> pure Nothing + where + getFileType = + maybeFirstRow fromOnly $ + DB.query db "SELECT file_type FROM files WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? LIMIT 1" (userId, groupId, sharedMsgId) + rcvFileIdQ = + [sql| + SELECT f.file_id FROM files f + JOIN rcv_files r ON r.file_id = f.file_id + WHERE f.user_id = ? AND f.group_id = ? AND f.shared_msg_id = ? AND f.file_type = ? + |] + +-- The roster scratch file for a transfer (for fs/handle cleanup before deleting the transfer). +-- A transfer owns exactly one file (created together in one transaction), so this is single-valued. +getRosterTransferFile :: DB.Connection -> Int64 -> IO (Maybe (Int64, Maybe FilePath)) +getRosterTransferFile db transferId = + maybeFirstRow id $ DB.query db "SELECT file_id, file_path FROM files WHERE roster_transfer_id = ?" (Only transferId) + +-- Deletes a transfer's file row; rcv_files and rcv_file_chunks cascade on the FK. +deleteRosterTransferFile :: DB.Connection -> Int64 -> IO () +deleteRosterTransferFile db transferId = + DB.execute db "DELETE FROM files WHERE roster_transfer_id = ?" (Only transferId) + +-- For roster-file cleanup keyed on the group (not a chat item): every matching file_id and its on-disk +-- path, so the caller evicts the handle and removes the file for each — delete-all like deleteGroupRosterFile. +getGroupRosterFileInfo :: DB.Connection -> UserId -> Int64 -> IO [(Int64, Maybe FilePath)] +getGroupRosterFileInfo db userId groupId = + DB.query + db + "SELECT file_id, file_path FROM files WHERE user_id = ? AND group_id = ? AND file_type = ?" + (userId, groupId, FTRoster) + +-- Deletes the roster files row; rcv_files and rcv_file_chunks cascade on the FK. +deleteGroupRosterFile :: DB.Connection -> UserId -> Int64 -> IO () +deleteGroupRosterFile db userId groupId = + DB.execute db "DELETE FROM files WHERE user_id = ? AND group_id = ? AND file_type = ?" (userId, groupId, FTRoster) + +-- The highest stored chunk number, or Nothing if no partial chunks exist (used to decide +-- whether an arriving chunk 1 is a re-driven transfer that must reset). +getRcvFileLastChunkNo :: DB.Connection -> RcvFileTransfer -> IO (Maybe Integer) +getRcvFileLastChunkNo db RcvFileTransfer {fileId} = + maybeFirstRow fromOnly $ + DB.query db "SELECT chunk_number FROM rcv_file_chunks WHERE file_id = ? ORDER BY chunk_number DESC LIMIT 1" (Only fileId) + getDirectFileIdBySharedMsgId :: DB.Connection -> User -> Contact -> SharedMsgId -> ExceptT StoreError IO Int64 getDirectFileIdBySharedMsgId db User {userId} Contact {contactId} sharedMsgId = ExceptT . firstRow fromOnly (SEFileIdNotFoundBySharedMsgId sharedMsgId) $ @@ -378,10 +455,10 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType = FTNormal, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing} -createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer -createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do +createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupInfo -> Maybe GroupMember -> FileType -> Maybe SharedMsgId -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer +createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gName} m_ fileType sharedMsgId_ f@FileInvitation {fileName, fileSize, fileDigest, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ @@ -393,15 +470,34 @@ createRcvGroupFileTransfer db userId GroupInfo {groupId, localDisplayName = gNam fileId <- liftIO $ do DB.execute db - "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" - (userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, currentTs, currentTs) + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, created_at, updated_at, file_digest) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, fileProtocol, fileType, sharedMsgId_, currentTs, currentTs) :. Only fileDigest) insertedRowId db liftIO $ DB.execute db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, fileType, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = grpMemberId_, cryptoArgs = Nothing} + +-- Roster scratch file owned by a per-source transfer: group_member_id is the delivering relay (so chunk +-- streams from different relays are distinct files), roster_transfer_id links to the metadata record. +createRosterRcvFile :: DB.Connection -> UserId -> GroupInfo -> GroupMember -> Int64 -> SharedMsgId -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer +createRosterRcvFile db userId GroupInfo {groupId} src@GroupMember {localDisplayName = senderName} transferId sharedMsgId f@FileInvitation {fileName, fileSize, fileConnReq, fileInline} rcvFileInline chunkSize = do + currentTs <- liftIO getCurrentTime + let grpMemberId_ = groupMemberId' src + fileId <- liftIO $ do + DB.execute + db + "INSERT INTO files (user_id, group_id, file_name, file_size, chunk_size, file_inline, ci_file_status, protocol, file_type, shared_msg_id, roster_transfer_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((userId, groupId, fileName, fileSize, chunkSize, fileInline, CIFSRcvInvitation, FPSMP, FTRoster) :. (sharedMsgId, transferId, currentTs, currentTs)) + insertedRowId db + liftIO $ + DB.execute + db + "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, grpMemberId_, currentTs, currentTs) + pure RcvFileTransfer {fileId, xftpRcvFile = Nothing, fileInvitation = f, fileStatus = RFSNew, fileType = FTRoster, rcvFileInline, senderDisplayName = senderName, chunkSize, cancelled = False, grpMemberId = Just grpMemberId_, cryptoArgs = Nothing} createRcvStandaloneFileTransfer :: DB.Connection -> UserId -> CryptoFile -> Int64 -> Word32 -> ExceptT StoreError IO Int64 createRcvStandaloneFileTransfer db userId (CryptoFile filePath cfArgs_) fileSize chunkSize = do @@ -422,7 +518,7 @@ createRcvStandaloneFileTransfer db userId (CryptoFile filePath cfArgs_) fileSize createRcvFD_ :: DB.Connection -> UserId -> UTCTime -> FileDescr -> ExceptT StoreError IO RcvFileDescr createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do - when (fileDescrPartNo /= 0) $ throwError SERcvFileInvalidDescrPart + when (fileDescrPartNo /= 0 || not (rcvFileDescrWithinLimits fileDescrPartNo fileDescrText)) $ throwError SERcvFileInvalidDescrPart fileDescrId <- liftIO $ do DB.execute db @@ -450,8 +546,8 @@ appendRcvFD db userId fileId fd@FileDescr {fileDescrText, fileDescrPartNo, fileD fileDescrPartNo = rfdPNo, fileDescrComplete = rfdComplete } -> do - when (fileDescrPartNo /= rfdPNo + 1 || rfdComplete) $ throwError SERcvFileInvalidDescrPart let fileDescrText' = rfdText <> fileDescrText + when (fileDescrPartNo /= rfdPNo + 1 || rfdComplete || not (rcvFileDescrWithinLimits fileDescrPartNo fileDescrText')) $ throwError SERcvFileInvalidDescrPart liftIO $ DB.execute db @@ -463,6 +559,23 @@ appendRcvFD db userId fileId fd@FileDescr {fileDescrText, fileDescrPartNo, fileD (fileDescrText', fileDescrPartNo, BI fileDescrComplete, fileDescrId) pure RcvFileDescr {fileDescrId, fileDescrText = fileDescrText', fileDescrPartNo, fileDescrComplete} +-- Upper bounds sized above the largest legitimate received description; derived from simplexmq's +-- chunk tiers and redundancy, so a change there must revisit them. +-- ~1280 chunks max = maxFileSizeHard (5gb) / largest chunk tier (4mb). +-- ~150 chars per chunk in the description YAML = replicaId 24 + Ed25519 key 64 + SHA-256 digest 44 + chunkNo/colons. +-- Total ~0.18 MB at 1 replica/chunk (~0.42 MB at 3x), under the 1mb text and 1024 part caps. +maxRcvFileDescrParts :: Int +maxRcvFileDescrParts = 1024 + +maxRcvFileDescrTextLength :: Int +maxRcvFileDescrTextLength = 1024 * 1024 + +rcvFileDescrWithinLimits :: Int -> Text -> Bool +rcvFileDescrWithinLimits partNo descrText = + partNo >= 0 + && partNo <= maxRcvFileDescrParts + && T.length descrText <= maxRcvFileDescrTextLength + getRcvFileDescrByRcvFileId :: DB.Connection -> FileTransferId -> ExceptT StoreError IO RcvFileDescr getRcvFileDescrByRcvFileId db fileId = do liftIO (getRcvFileDescrByRcvFileId_ db fileId) >>= \case @@ -530,7 +643,7 @@ getRcvFileTransfer_ db userId fileId = do SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, - r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN contacts cs ON cs.contact_id = f.contact_id @@ -544,9 +657,9 @@ getRcvFileTransfer_ db userId fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. Only (Maybe ContactName) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe BoolInt) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, BoolInt, BoolInt) :. (Maybe ContactName, FileType, Maybe FileDigest) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. Only groupName_) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, BI agentRcvFileDeleted, BI userApprovedRelays) :. (groupName_, fileType, fileDigest_)) = case contactName_ <|> memberName_ <|> groupName_ <|> standaloneName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> @@ -561,10 +674,10 @@ getRcvFileTransfer_ db userId fileId = do (Just _, Just _) -> Just "" -- filePath marks files that are accepted from contact or, in this case, set by createRcvDirectFileTransfer _ -> Nothing ft senderDisplayName fileStatus = - let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} + let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = fileDigest_, fileConnReq, fileInline, fileDescr = Nothing} cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_ - in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} + in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, fileType, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} filePath = case filePath_ of Nothing -> throwError $ SERcvFileInvalid fileId Just fp -> pure fp @@ -660,7 +773,15 @@ createRcvFileChunk db RcvFileTransfer {fileId, fileInvitation = FileInvitation { currentTs <- getCurrentTime DB.execute db - "INSERT OR REPLACE INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) VALUES (?,?,?,?,?)" + [sql| + INSERT INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) + VALUES (?,?,?,?,?) + ON CONFLICT (file_id, chunk_number) DO UPDATE SET + chunk_agent_msg_id = excluded.chunk_agent_msg_id, + chunk_stored = 0, + created_at = excluded.created_at, + updated_at = excluded.updated_at + |] (fileId, chunkNo, msgId, currentTs, currentTs) pure status where diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index c6b5684945..d607541bda 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -42,21 +42,24 @@ module Simplex.Chat.Store.Groups setGroupInvitationChatItemId, getGroup, getGroupInfoByUserContactLinkConnReq, - getGroupInfoViaUserShortLink, + getGroupInfoViaUserTarget, getGroupViaShortLinkToConnect, getGroupInfoByGroupLinkHash, updateGroupProfile, + setGroupDomainVerified, updateGroupPreferences, updateGroupProfileFromMember, getGroupIdByName, getGroupMemberIdByName, getActiveMembersByName, getGroupInfoByName, + getGroupToConnect, getGroupMember, getHostMember, getMentionedGroupMember, getMentionedMemberByMemberId, getGroupMemberById, + getNonRemovedMemberById, getGroupMemberByIndex, getGroupMemberByMemberId, getCreateUnknownGMByMemberId, @@ -66,8 +69,13 @@ module Simplex.Chat.Store.Groups getGroupMembersByIndexes, getSupportScopeMembersByIndexes, getGroupModerators, + getGroupRosterMembers, + getGroupAdminsMods, + getGroupOnlyMembers, + getGroupOwners, getGroupRelayMembers, getGroupMembersForExpiration, + getRemovedMembersToCleanup, deleteGroupChatItems, deleteGroupMembers, cleanupHostGroupLinkConn, @@ -81,7 +89,25 @@ module Simplex.Chat.Store.Groups getGroupRelayById, getGroupRelayByGMId, getGroupRelays, - getConnectedGroupRelays, + getPublishableGroupRelays, + setGroupRosterVersion, + getGroupRosterVersion, + getStoredRosterVersion, + setMemberRosterServedVersion, + getMemberRosterServedVersion, + setCompleteRosterVersion, + getCompleteRosterVersion, + getStoredGroupRoster, + RcvRosterTransfer (..), + createRosterTransfer, + getRosterTransferVersion, + getRosterTransferId, + getRosterTransfer, + setGroupLiveRoster, + deleteRosterTransfer, + deleteGroupRosterTransfers, + setGroupMemberKeyRole, + setGroupMemberVerified, createRelayForOwner, getCreateRelayForMember, createRelayConnection, @@ -96,9 +122,12 @@ module Simplex.Chat.Store.Groups createRelayRequestGroup, updateRelayOwnStatusFromTo, updateRelayOwnStatus_, + getRelaySentWebDomain, + updateRelaySentWebDomain, isRelayGroupRejected, allowRelayGroup, getRelayServedGroups, + getRelayPublishableGroups, getRelayInactiveGroups, createNewContactMemberAsync, createJoiningMember, @@ -117,6 +146,7 @@ module Simplex.Chat.Store.Groups updateRelayGroupKeys, updateGroupMemberStatus, updateGroupMemberStatusById, + updateGroupMemberRemovedAt, updateGroupMemberAccepted, deleteGroupMemberSupportChat, updateGroupMembersRequireAttention, @@ -166,6 +196,7 @@ module Simplex.Chat.Store.Groups createLinkOwnerMember, updatePreparedChannelMember, updateUnknownMemberAnnounced, + updateRosterMemberAnnounced, updateUserMemberProfileSentAt, setGroupCustomData, setGroupUIThemes, @@ -182,7 +213,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) -import Data.Bifunctor (second) +import Data.Bifunctor (first, second) import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Char (toLower) @@ -197,6 +228,7 @@ import qualified Data.Text as T import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Badges (BadgeRow, badgeToRow, verifyBadge_) +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Chat.Messages import Simplex.Chat.Operators import Simplex.Chat.Protocol hiding (Binary) @@ -207,8 +239,10 @@ import Simplex.Chat.Types.MemberRelations (IntroductionDirection (..), MemberRel import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId) +import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), SimplexDomain, SimplexNameInfo (..), SimplexNameType (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) +import qualified Simplex.FileTransfer.Description as FD +import Simplex.Messaging.Encoding (smpDecode, smpEncode) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import Simplex.Messaging.Agent.Store.Entity (DBEntityId) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -226,11 +260,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, 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 ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) +type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, 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 Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember -toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, 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, peerType, Just localAlias, contactPreferences) :. badgeRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) = - Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) +toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, 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, description, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = + Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) toMaybeGroupMember _ _ _ = Nothing createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink @@ -352,6 +386,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) Nothing -> (Nothing, Nothing, Nothing) fullGroupPreferences = mergeGroupPreferences groupPreferences + rosterVersion0 = if useRelays then Just (VersionRoster 0) else Nothing currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do @@ -369,9 +404,9 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) @@ -382,11 +417,11 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs) - :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_) + :. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0) ) insertedRowId db let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys @@ -413,10 +448,12 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays chatItemTTL = Nothing, uiThemes = Nothing, groupSummary = GroupSummary {currentMembers = 1, publicMemberCount = publicMemberCount_}, + rosterVersion = rosterVersion0, customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys + groupKeys, + groupDomainVerified = Nothing } -- | creates a new group record for the group the current user was invited to, or returns an existing one @@ -490,10 +527,12 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti chatItemTTL = Nothing, uiThemes = Nothing, groupSummary = GroupSummary {currentMembers = 2, publicMemberCount = Nothing}, + rosterVersion = Nothing, customData = Nothing, membersRequireAttention = 0, viaGroupLinkUri = Nothing, - groupKeys = Nothing + groupKeys = Nothing, + groupDomainVerified = Nothing }, groupMemberId ) @@ -560,7 +599,8 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe updatedAt = createdAt, supportChat = Nothing, memberPubKey, - relayLink = Nothing + relayLink = Nothing, + memberVerifiedCode = Nothing } where memberChatVRange@(VersionRange minV maxV) = vr @@ -609,8 +649,8 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) -createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) -createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ = do +createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> Maybe SimplexDomain -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) +createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ verifiedDomain = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) (groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing useRelays Nothing publicMemberCount_ currentTs @@ -629,7 +669,12 @@ createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile b forM_ hostMember_ $ \hostMember -> when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember g <- getGroupInfo db cxt user groupId - pure (g, hostMember_) + -- a business has no domain in its profile, so set it out-of-band; a channel already has it (createGroup_), just verify + g' <- liftIO $ case verifiedDomain of + Just d | business -> setPreparedGroupDomain db user g d + Just _ -> setGroupDomainVerified db user g True + Nothing -> pure g + pure (g', hostMember_) where insertHost_ currentTs groupId groupLDN = do randHostId <- liftIO $ encodedRandomBytes gVar 12 @@ -652,7 +697,7 @@ createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile b insertedRowId db setGroupBusinessChatInfo :: GroupId -> GroupMember -> GroupMember -> IO () setGroupBusinessChatInfo groupId membership hostMember = do - let businessChatInfo = Just BusinessChatInfo {chatType = BCBusiness, businessId = memberId' hostMember, customerId = memberId' membership} + let businessChatInfo = Just BusinessChatInfo {chatType = BCBusiness, businessId = memberId' hostMember, customerId = memberId' membership, businessDomain = Nothing} updateBusinessChatInfo db groupId businessChatInfo updateBusinessChatInfo :: DB.Connection -> GroupId -> Maybe BusinessChatInfo -> IO () @@ -871,9 +916,9 @@ createGroup_ db userId groupProfile prepared business useRelays relayOwnStatus p INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((displayName, fullName, shortDescr, description, image, groupType_, groupLink_, publicGroupId_) :. publicGroupAccessRow publicGroup :. (userId, groupPreferences, memberAdmission, currentTs, currentTs)) @@ -1040,6 +1085,26 @@ getGroupInfoByName db cxt user gName = do gId <- getGroupIdByName db user gName getGroupInfo db cxt user gId +getGroupToConnect :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> ExceptT StoreError IO (Maybe (CreatedLinkContact, GroupInfo)) +getGroupToConnect db cxt user@User {userId} = \case + CTLink sl -> first (`CCLink` Just sl) <$$> getGroupViaShortLinkToConnect db cxt user sl + CTName ni -> + -- @name is a business (presents as a contact); #name is a channel. The same domain can have both, + -- so the group type must match the requested name type. + let businessCond = case nameType ni of + NTContact -> " AND g.business_chat IS NOT NULL" + NTPublicGroup -> " AND g.business_chat IS NULL" + in liftIO (maybeFirstRow id $ DB.query db (byNameQuery <> businessCond) (userId, nameDomain ni)) >>= \case + Just (gId :: Int64, Just cReq, Just (sLnk :: ShortLinkContact)) -> Just . (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user gId + _ -> pure Nothing + where + byNameQuery = + [sql| + SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1 + |] + getGroupMember :: DB.Connection -> StoreCxt -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember getGroupMember db cxt user@User {userId} groupId groupMemberId = do currentTs <- liftIO getCurrentTime @@ -1100,6 +1165,15 @@ getGroupMemberById db cxt user@User {userId} groupMemberId = do (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?") (groupMemberId, userId) +getNonRemovedMemberById :: DB.Connection -> StoreCxt -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember +getNonRemovedMemberById db cxt user@User {userId} groupMemberId = do + ts <- liftIO getCurrentTime + ExceptT . firstRow (toContactMember ts cxt user) (SEGroupMemberNotFound groupMemberId) $ + DB.query + db + (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ? AND m.member_status NOT IN (?,?,?,?)") + (groupMemberId, userId, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) + getGroupMemberByIndex :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupMember getGroupMemberByIndex db cxt user GroupInfo {groupId} indexInGroup = do currentTs <- liftIO getCurrentTime @@ -1199,6 +1273,47 @@ getGroupModerators db cxt user@User {userId, userContactId} GroupInfo {groupId} (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") (userId, groupId, userContactId, GRModerator, GRAdmin, GROwner) +-- The full roster set - members, moderators and admins - excluding owners (link-anchored) and +-- left/removed members. For the privileged subset only use getGroupAdminsMods; for plain members +-- only use getGroupOnlyMembers. +getGroupRosterMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupRosterMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") + (userId, groupId, userContactId, GRMember, GRModerator, GRAdmin) + +-- Moderators and admins only (excluding owners and plain members) - the set introduced to a +-- joiner; plain members are learned from the roster blob, not via introductions. +getGroupAdminsMods :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupAdminsMods db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?)") + (userId, groupId, userContactId, GRModerator, GRAdmin) + +getGroupOnlyMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupOnlyMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?") + (userId, groupId, userContactId, GRMember) + +getGroupOwners :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupOwners db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + currentTs <- getCurrentTime + filter memberCurrent . map (toContactMember currentTs cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?") + (userId, groupId, userContactId, GROwner) + getGroupRelayMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] getGroupRelayMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do currentTs <- getCurrentTime @@ -1226,6 +1341,15 @@ getGroupMembersForExpiration db cxt user@User {userId, userContactId} GroupInfo ) (groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) +getRemovedMembersToCleanup :: DB.Connection -> StoreCxt -> User -> UTCTime -> IO [GroupMember] +getRemovedMembersToCleanup db cxt user@User {userId} cutoffTs = do + ts <- getCurrentTime + map (toContactMember ts cxt user) + <$> DB.query + db + (groupMemberQuery <> " WHERE m.user_id = ? AND m.removed_at < ?") + (userId, cutoffTs) + getGroupInvitation :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation getGroupInvitation db cxt user groupId = getConnRec_ user >>= \case @@ -1279,7 +1403,8 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, updatedAt = createdAt, supportChat = Nothing, memberPubKey = Nothing, - relayLink = Nothing + relayLink = Nothing, + memberVerifiedCode = Nothing } where insertMember_ = do @@ -1339,21 +1464,30 @@ getGroupRelays db GroupInfo {groupId} = (groupRelayQuery <> " WHERE gr.group_id = ?") (Only groupId) -getConnectedGroupRelays :: DB.Connection -> GroupInfo -> IO [GroupRelay] -getConnectedGroupRelays db GroupInfo {groupId} = - map toGroupRelay - <$> DB.query - db - ( groupRelayQuery - <> " " - <> [sql| - JOIN group_members m ON m.group_member_id = gr.group_member_id - WHERE gr.group_id = ? - AND m.member_status = ? - AND gr.relay_status IN (?,?) - |] - ) - (groupId, GSMemConnected, RSAccepted, RSActive) +-- Relays whose link is published to subscribers: acked relays (RSAcknowledgedRoster/RSActive) plus +-- pre-roster relays at RSAccepted (below groupRosterVersion, they can't ack a roster), gated by the +-- relay's negotiated version read from its member connection. +getPublishableGroupRelays :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupRelay] +getPublishableGroupRelays db cxt user gInfo@GroupInfo {groupId} = do + relays <- + map toGroupRelay + <$> DB.query + db + ( groupRelayQuery + <> " " + <> [sql| + JOIN group_members m ON m.group_member_id = gr.group_member_id + WHERE gr.group_id = ? + AND m.member_status = ? + AND gr.relay_status IN (?,?,?) + |] + ) + (groupId, GSMemConnected, RSAccepted, RSAcknowledgedRoster, RSActive) + members <- getGroupRelayMembers db cxt user gInfo + pure [gr | gr@GroupRelay {groupMemberId} <- relays, m <- members, groupMemberId' m == groupMemberId, publishable gr m] + where + publishable GroupRelay {relayStatus} m = + relayStatus /= RSAccepted || not (m `supportsVersion` groupRosterVersion) groupRelayQuery :: Query groupRelayQuery = @@ -1371,6 +1505,197 @@ toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, f relayCap = RelayCapabilities {webDomain} in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink, relayCap} +setGroupRosterVersion :: DB.Connection -> GroupInfo -> VersionRoster -> IO () +setGroupRosterVersion db GroupInfo {groupId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE groups SET roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId) + +-- Persisted roster version (the gate baseline; the in-memory gInfo copy is batch-constant and stale on reorder). +getGroupRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getGroupRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The version of the roster blob actually stored (written with the blob in setGroupLiveRoster), as opposed to +-- roster_version (the acceptance gate). The owner sends the blob before the delta, so these normally match; a +-- failed blob send leaves the gate (advanced by the delta) ahead of the stored blob until a later roster completes. +getStoredRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getStoredRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT stored_roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The newest roster version a relay re-served to this member on its catch-up request: bounds reflected +-- amplification, so a member can't re-trigger a full serve at a version it was already served. +setMemberRosterServedVersion :: DB.Connection -> GroupMember -> VersionRoster -> IO () +setMemberRosterServedVersion db GroupMember {groupMemberId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE group_member_id = ?" (v, currentTs, groupMemberId) + +getMemberRosterServedVersion :: DB.Connection -> GroupMember -> IO (Maybe VersionRoster) +getMemberRosterServedVersion db GroupMember {groupMemberId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT roster_served_version FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + +-- The highest version up to which the subscriber holds a complete, contiguous picture: advances by 1 on a +-- contiguous delta and to the roster's version on a roster apply, but stays put on a gapped delta (so a stuck +-- value re-triggers the catch-up request on every following delta until a roster fills the gap). This is the +-- subscriber's "what I have" for both gap detection and the request - as opposed to roster_version (highest seen, +-- the revert gate) and stored_roster_version (the blob a relay holds). +setCompleteRosterVersion :: DB.Connection -> GroupInfo -> VersionRoster -> IO () +setCompleteRosterVersion db GroupInfo {groupId} v = do + currentTs <- getCurrentTime + DB.execute db "UPDATE groups SET applied_complete_roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId) + +getCompleteRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster) +getCompleteRosterVersion db GroupInfo {groupId} = + fmap join . maybeFirstRow fromOnly $ + DB.query db "SELECT applied_complete_roster_version FROM groups WHERE group_id = ?" (Only groupId) + +-- The live roster header a relay re-serves to joiners, with the completed blob and its stored version +-- (all written together at completion, so the blob and version are present whenever the header is). +-- Returns the stored version, not roster_version (the gate), so callers serve/record exactly what they hold. +getStoredGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString, Maybe VersionRoster)) +getStoredGroupRoster db GroupInfo {groupId} = + (>>= toRoster) + <$> maybeFirstRow + id + ( DB.query + db + "SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob, stored_roster_version FROM groups WHERE group_id = ?" + (Only groupId) + ) + where + toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_, storedVer_) = + (\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_, storedVer_)) <$> eitherToMaybe (smpDecode sigsBs) + toRoster _ = Nothing + +-- A per-source in-flight roster transfer, keyed (group_id, from_member_id): replaces the single +-- roster_pending_* slot, so two relays serving one member can't share a chunk stream. The signed-header +-- columns are relay-only (NULL on members), promoted to the live roster_msg_* on groups at completion. +createRosterTransfer :: DB.Connection -> GroupInfo -> GroupMemberId -> VersionRoster -> FD.FileDigest -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> IO Int64 +createRosterTransfer db GroupInfo {groupId} fromMemberId v digest ownerGMId brokerTs sm_ = do + -- one in-flight transfer per (group, source): drop any prior row from this source so the INSERT can't hit + -- the UNIQUE constraint even if the caller's fs/handle cleanup was skipped (the scratch file would then leak + -- until group delete, but the transfer never gets stuck). Normally cleanupRosterTransfer ran first. + DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + DB.execute + db + [sql| + INSERT INTO rcv_roster_transfers + (group_id, from_member_id, roster_version, roster_digest, sending_owner_gm_id, broker_ts, + roster_msg_chat_binding, roster_msg_signatures, roster_msg_body) + VALUES (?,?,?,?,?,?,?,?,?) + |] + ( (groupId, fromMemberId, v, Binary (FD.unFileDigest digest), ownerGMId, brokerTs) + :. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_) + ) + insertedRowId db + +getRosterTransferVersion :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe VersionRoster) +getRosterTransferVersion db GroupInfo {groupId} fromMemberId = + maybeFirstRow fromOnly $ + DB.query db "SELECT roster_version FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + +getRosterTransferId :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe Int64) +getRosterTransferId db GroupInfo {groupId} fromMemberId = + maybeFirstRow fromOnly $ + DB.query db "SELECT roster_transfer_id FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId) + +-- An in-flight received roster transfer (a rcv_roster_transfers row joined to its scratch file), read at +-- completion. The header is the relay's re-serve SignedMsg -- present only on a serving relay (NULL on a +-- member, whose live roster_msg_* stay NULL so it never re-serves). +data RcvRosterTransfer = RcvRosterTransfer + { rosterTransferId :: Int64, + rosterTransferVersion :: VersionRoster, + rosterTransferDigest :: FD.FileDigest, + rosterTransferOwnerGMId :: GroupMemberId, + rosterTransferBrokerTs :: UTCTime, + rosterTransferHeader :: Maybe SignedMsg + } + deriving (Show) + +-- The in-flight transfer for a received roster file (joined via files.roster_transfer_id), with its +-- relay-only signed header. Read at completion to apply, promote into the live roster, and ack. +getRosterTransfer :: DB.Connection -> Int64 -> IO (Maybe RcvRosterTransfer) +getRosterTransfer db fileId = + (>>= toTransfer) + <$> maybeFirstRow + id + ( DB.query + db + [sql| + SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts, + t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body + FROM rcv_roster_transfers t + JOIN files f ON f.roster_transfer_id = t.roster_transfer_id + WHERE f.file_id = ? + |] + (Only fileId) + ) + where + toTransfer (tId, v, Binary d, ownerGMId, brokerTs, cb_, sigs_, body_) = + Just + RcvRosterTransfer + { rosterTransferId = tId, + rosterTransferVersion = v, + rosterTransferDigest = FD.FileDigest d, + rosterTransferOwnerGMId = ownerGMId, + rosterTransferBrokerTs = brokerTs, + rosterTransferHeader = sm_ + } + where + sm_ = case (cb_, sigs_, body_) of + (Just cb, Just (Binary sigsBs), Just (Binary body)) -> + (\sigs -> SignedMsg cb sigs body) <$> eitherToMaybe (smpDecode sigsBs) + _ -> Nothing + +-- Write the single live roster on groups from a completed transfer's values (header NULL on a member, +-- so its live roster_msg_* stay NULL and it never re-serves; only relays re-serve). +-- Sets all three versions to the completed blob's version: the gate (roster_version - refuse anything older), +-- the stored version (stored_roster_version - the blob actually held and re-served), and the complete frontier +-- (applied_complete_roster_version - a snapshot makes the picture complete up to its version). Deltas advance the +-- gate always and the complete frontier only when contiguous, so complete <= stored <= roster_version normally. +setGroupLiveRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> ByteString -> IO () +setGroupLiveRoster db GroupInfo {groupId} v ownerGMId brokerTs sm_ blob = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE groups SET + roster_version = ?, stored_roster_version = ?, applied_complete_roster_version = ?, roster_blob = ?, + roster_sending_owner_gm_id = ?, roster_broker_ts = ?, + roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, + updated_at = ? + WHERE group_id = ? + |] + ( (v, v, v, Binary blob, ownerGMId, brokerTs) + :. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_, currentTs, groupId) + ) + +-- Delete one in-flight transfer row (its files/rcv_files/rcv_file_chunks are removed separately, with +-- the on-disk file). Caller removes the fs file + cached handle first. +deleteRosterTransfer :: DB.Connection -> Int64 -> IO () +deleteRosterTransfer db transferId = + DB.execute db "DELETE FROM rcv_roster_transfers WHERE roster_transfer_id = ?" (Only transferId) + +-- All in-flight transfers for a group (group delete). +deleteGroupRosterTransfers :: DB.Connection -> Int64 -> IO () +deleteGroupRosterTransfers db groupId = + DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ?" (Only groupId) + +setGroupMemberKeyRole :: DB.Connection -> GroupMember -> C.PublicKeyEd25519 -> GroupMemberRole -> IO () +setGroupMemberKeyRole db GroupMember {groupMemberId} pubKey role = do + currentTs <- getCurrentTime + DB.execute db "UPDATE group_members SET member_pub_key = ?, member_role = ?, updated_at = ? WHERE group_member_id = ?" (pubKey, role, currentTs, groupMemberId) + +setGroupMemberVerified :: DB.Connection -> User -> GroupMemberId -> Maybe Text -> IO () +setGroupMemberVerified db User {userId} groupMemberId code = do + updatedAt <- getCurrentTime + DB.execute + db + "UPDATE group_members SET member_security_code = ?, member_security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?" + (code, code $> updatedAt, updatedAt, userId, groupMemberId) + createRelayForOwner :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember createRelayForOwner db cxt gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do currentTs <- liftIO getCurrentTime @@ -1383,11 +1708,11 @@ createRelayForOwner db cxt gVar user@User {userId, userContactId} GroupInfo {gro db [sql| INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id, user_id, local_display_name, contact_profile_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, indexInGroup, MemberId memId, GRRelay, GCInviteeMember, GSMemInvited, fromInvitedBy userContactId IBUser, groupMemberId' membership) + ( (groupId, indexInGroup, MemberId memId, GRRelay, GCInviteeMember, GSMemInvited, Binary B.empty, fromInvitedBy userContactId IBUser, groupMemberId' membership) :. (userId, localDisplayName, memProfileId, currentTs, currentTs) ) liftIO $ insertedRowId db @@ -1421,12 +1746,12 @@ getCreateRelayForMember db cxt gVar user@User {userId, userContactId} GroupInfo db [sql| INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, user_id, local_display_name, contact_profile_id, created_at, updated_at, relay_link ) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, indexInGroup, memberId, GRRelay, GCHostMember, GSMemAccepted, fromInvitedBy userContactId IBUnknown) + ( (groupId, indexInGroup, memberId, GRRelay, GCHostMember, GSMemAccepted, Binary B.empty, fromInvitedBy userContactId IBUnknown) :. (userId, localDisplayName, profileId, currentTs, currentTs, relayLink) ) insertedRowId db @@ -1606,12 +1931,12 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb db [sql| INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, memberStatus) + ( (groupId, indexInGroup, memberId, memberRole, GCHostMember, memberStatus, Binary B.empty) :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) :. (minV, maxV) ) @@ -1629,6 +1954,14 @@ updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do let inactiveAt_ = if relayStatus == RSInactive then Just currentTs else Nothing DB.execute db "UPDATE groups SET relay_own_status = ?, relay_inactive_at = ?, updated_at = ? WHERE group_id = ?" (relayStatus, inactiveAt_, currentTs, groupId) +getRelaySentWebDomain :: DB.Connection -> GroupInfo -> IO (Maybe Text) +getRelaySentWebDomain db GroupInfo {groupId} = + join <$> maybeFirstRow fromOnly (DB.query db "SELECT relay_sent_web_domain FROM groups WHERE group_id = ?" (Only groupId)) + +updateRelaySentWebDomain :: DB.Connection -> GroupInfo -> Maybe Text -> IO () +updateRelaySentWebDomain db GroupInfo {groupId} webDomain_ = + DB.execute db "UPDATE groups SET relay_sent_web_domain = ? WHERE group_id = ?" (webDomain_, groupId) + -- Flip every RSRejected row sharing the targeted group's relay_request_group_link -- to RSInactive in one statement; returns the refreshed GroupInfo for the targeted groupId. allowRelayGroup :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO GroupInfo @@ -1671,9 +2004,27 @@ getRelayServedGroups db cxt User {userId, userContactId} = do <$> DB.query db ( groupInfoQuery - <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?)" + <> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?, ?)" ) - (userId, userContactId, RSAccepted, RSActive) + (userId, userContactId, RSAccepted, RSAcknowledgedRoster, RSActive) + +getRelayPublishableGroups :: DB.Connection -> User -> IO [(Int64, B64UrlByteString, Maybe PublicGroupAccess)] +getRelayPublishableGroups db User {userId, userContactId} = + map toRow <$> + DB.query + db + [sql| + SELECT g.group_id, gp.public_group_id, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? + WHERE g.user_id = ? AND g.relay_own_status IN (?, ?) + AND gp.public_group_id IS NOT NULL + |] + (userContactId, userId, RSAccepted, RSActive) + where + toRow ((gId, pgId) :. accessRow) = (gId, pgId, toPublicGroupAccess accessRow) getRelayInactiveGroups :: DB.Connection -> StoreCxt -> User -> NominalDiffTime -> IO [GroupInfo] getRelayInactiveGroups db cxt User {userId, userContactId} ttl = do @@ -1722,7 +2073,7 @@ createJoiningMember User {userId, userContactId} GroupInfo {groupId, membership} cReqChatVRange - Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} + Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} cReqXContactId_ cReqMemberId_ welcomeMsgId_ @@ -1735,8 +2086,8 @@ createJoiningMember liftIO $ DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) profileId <- liftIO $ insertedRowId db case cReqMemberId_ of Just memberId -> do @@ -1990,6 +2341,18 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do |] (memStatus, currentTs, userId, groupMemberId) +updateGroupMemberRemovedAt :: DB.Connection -> User -> GroupMember -> IO () +updateGroupMemberRemovedAt db User {userId} GroupMember {groupMemberId} = do + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET member_status = ?, removed_at = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + (GSMemRemoved, currentTs, currentTs, userId, groupMemberId) + updateGroupMemberAccepted :: DB.Connection -> User -> GroupMember -> GroupMemberStatus -> GroupMemberRole -> IO GroupMember updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} status role = do currentTs <- getCurrentTime @@ -2091,13 +2454,13 @@ createNewGroupMember db cxt user gInfo invitingMember memInfo@MemberInfo {profil createNewMember_ db user gInfo newMember badgeVerified currentTs createNewMemberProfile_ :: DB.Connection -> StoreCxt -> User -> Profile -> UTCTime -> ExceptT StoreError IO (Text, ProfileId, Maybe Bool) -createNewMemberProfile_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, badge, preferences} createdAt = +createNewMemberProfile_ db cxt User {userId} Profile {displayName, fullName, shortDescr, description, image, contactLink, badge, preferences} createdAt = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, userId, preferences, createdAt, createdAt) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, userId, preferences, createdAt, createdAt) :. badgeToRow badge badgeVerified) profileId <- insertedRowId db pure $ Right (ldn, profileId, badgeVerified) @@ -2155,7 +2518,7 @@ createNewMember_ invitedBy, invitedByGroupMemberId = memInvitedByGroupMemberId, localDisplayName, - memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified, + memberProfile = toLocalProfile memberContactProfileId memberProfile "" createdAt badgeVerified Nothing, memberContactId, memberContactProfileId, activeConn, @@ -2164,7 +2527,8 @@ createNewMember_ updatedAt = createdAt, supportChat = Nothing, memberPubKey, - relayLink = Nothing + relayLink = Nothing, + memberVerifiedCode = Nothing } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -2199,7 +2563,7 @@ updateGroupMemberRole db User {userId} GroupMember {groupMemberId} memRole = setMemberVectorNewRelations :: DB.Connection -> GroupMember -> [(Int64, (IntroductionDirection, MemberRelation))] -> IO () setMemberVectorNewRelations db GroupMember {groupMemberId} relations = do - v_ <- maybeFirstRow fromOnly $ + v_ <- fmap join . maybeFirstRow fromOnly $ DB.query db ( "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" @@ -2263,7 +2627,7 @@ setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {ind getMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString getMemberRelationsVector db GroupMember {groupMemberId} = - ExceptT . firstRow fromOnly (SEGroupMemberNotFound groupMemberId) $ + ExceptT . firstRow (fromMaybe B.empty . fromOnly) (SEGroupMemberNotFound groupMemberId) $ DB.query db "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" @@ -2342,38 +2706,52 @@ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo -updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} +updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, publicGroup = oldPublicGroup}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} | displayName == newName = liftIO $ do currentTs <- getCurrentTime updateGroupProfile_ currentTs - pure (g :: GroupInfo) {groupProfile = p', fullGroupPreferences} + clearVerificationIfClaimChanged + pure $ (g' :: GroupInfo) {groupProfile = p', fullGroupPreferences} | otherwise = ExceptT . withLocalDisplayName db userId newName $ \ldn -> do currentTs <- getCurrentTime updateGroupProfile_ currentTs updateGroup_ ldn currentTs - pure $ Right (g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences} + clearVerificationIfClaimChanged + pure $ Right $ (g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences} where fullGroupPreferences = mergeGroupPreferences groupPreferences + groupClaim pg = domain <$> (pg >>= publicGroupAccess >>= groupDomainClaim) + claimChanged = groupClaim oldPublicGroup /= groupClaim publicGroup + g' = if claimChanged then (g :: GroupInfo) {groupDomainVerified = Nothing} else g + clearVerificationIfClaimChanged = + when claimChanged $ + DB.execute db "UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ?" (userId, groupId) (groupType_, groupLink_) = case publicGroup of Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink) Nothing -> (Nothing, Nothing) + -- group_domain is owned by publicGroup; when the incoming profile has no publicGroup (a business, whose domain + -- is set out-of-band) the CASE leaves the stored group_domain unchanged instead of clearing it. updateGroupProfile_ currentTs = - DB.execute - db - [sql| - UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, - group_type = ?, group_link = ?, - group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, - preferences = ?, member_admission = ?, updated_at = ? - WHERE group_profile_id IN ( - SELECT group_profile_id - FROM groups - WHERE user_id = ? AND group_id = ? - ) - |] - ((newName, fullName, shortDescr, description, image, groupType_, groupLink_) :. publicGroupAccessRow publicGroup :. (groupPreferences, memberAdmission, currentTs, userId, groupId)) + let (groupWebPage_, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_) = publicGroupAccessRow publicGroup + in DB.execute + db + [sql| + UPDATE group_profiles + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + group_type = ?, group_link = ?, + group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, + preferences = ?, member_admission = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + |] + ( (newName, fullName, shortDescr, description, image, groupType_, groupLink_) + :. (groupWebPage_, isJust publicGroup, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_) + :. (groupPreferences, memberAdmission, currentTs, userId, groupId) + ) updateGroup_ ldn currentTs = do DB.execute db @@ -2381,6 +2759,27 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, (ldn, currentTs, userId, groupId) safeDeleteLDN db user localDisplayName +setGroupDomainVerified :: DB.Connection -> User -> GroupInfo -> Bool -> IO GroupInfo +setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified = do + DB.execute + db + "UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?" + (BI verified, userId, groupId) + pure g {groupDomainVerified = Just verified} + +-- A business group has no publicGroup claim, so the domain it was connected by (from its address) is written +-- directly to group_domain and marked verified, so it is found by the local name search (getGroupToConnect). +setPreparedGroupDomain :: DB.Connection -> User -> GroupInfo -> SimplexDomain -> IO GroupInfo +setPreparedGroupDomain db user@User {userId} g@GroupInfo {groupId} domain = do + DB.execute + db + [sql| + UPDATE group_profiles SET group_domain = ? + WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?) + |] + (domain, userId, groupId) + setGroupDomainVerified db user g True + updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do currentTs <- getCurrentTime @@ -2399,10 +2798,10 @@ updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} pure (g :: GroupInfo) {groupProfile = p {groupPreferences = Just ps}, fullGroupPreferences = mergeGroupPreferences $ Just ps} updateGroupProfileFromMember :: DB.Connection -> User -> GroupInfo -> Profile -> ExceptT StoreError IO GroupInfo -updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, shortDescr = sd, image = img} = do +updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName = n, fullName = fn, shortDescr = sd, description = descr, image = img} = do p <- getGroupProfile -- to avoid any race conditions with UI let g' = g {groupProfile = p} :: GroupInfo - p' = p {displayName = n, fullName = fn, shortDescr = sd, image = img} :: GroupProfile + p' = p {displayName = n, fullName = fn, shortDescr = sd, description = descr, image = img} :: GroupProfile updateGroupProfile db user g' p' where getGroupProfile = @@ -2413,7 +2812,7 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName [sql| SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id @@ -2439,31 +2838,52 @@ getGroupInfoByUserContactLinkConnReq db cxt user@User {userId} (cReqSchema1, cRe (userId, cReqSchema1, cReqSchema2) maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db cxt user) groupId_ -getGroupInfoViaUserShortLink :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) -getGroupInfoViaUserShortLink db cxt user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do - (cReq, groupId) <- ExceptT getConnReqGroup - (cReq,) <$> getGroupInfo db cxt user groupId +getGroupInfoViaUserTarget :: DB.Connection -> StoreCxt -> User -> ContactNameOrLink -> IO (Maybe (CreatedLinkContact, GroupInfo)) +getGroupInfoViaUserTarget db cxt user@User {userId} target = fmap eitherToMaybe $ runExceptT $ do + (cReq, sLnk, groupId) <- ExceptT getConnReqGroup + (CCLink cReq (Just sLnk),) <$> getGroupInfo db cxt user groupId where getConnReqGroup = - firstRow' toConnReqGroupId (SEInternalError "group link not found") $ - DB.query - db - [sql| - SELECT conn_req_contact, group_id - FROM user_contact_links - WHERE user_id = ? AND short_link_contact = ? - |] - (userId, shortLink) + firstRow' toConnReqGroupId (SEInternalError "group link not found") $ case target of + CTLink shortLink -> + DB.query + db + [sql| + SELECT conn_req_contact, short_link_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + |] + (userId, shortLink) + CTName ni -> + DB.query + db + [sql| + SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id + FROM user_contact_links ucl + JOIN groups g ON g.group_id = ucl.group_id + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE ucl.user_id = ? AND gp.group_domain = ? + |] + (userId, nameDomain ni) toConnReqGroupId = \case -- cReq is "not null", group_id is nullable - (cReq, Just groupId) -> Right (cReq, groupId) + (cReq, Just (sLnk :: ShortLinkContact), Just groupId) -> Right (cReq, sLnk, groupId) _ -> Left $ SEInternalError "no conn req or group ID" getGroupViaShortLinkToConnect :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> ExceptT StoreError IO (Maybe (ConnReqContact, GroupInfo)) -getGroupViaShortLinkToConnect db cxt user@User {userId} shortLink = - liftIO (maybeFirstRow id $ DB.query db "SELECT group_id, conn_full_link_to_connect FROM groups WHERE user_id = ? AND conn_short_link_to_connect = ?" (userId, shortLink)) >>= \case +getGroupViaShortLinkToConnect db cxt user@User {userId, userContactId} shortLink = + liftIO (maybeFirstRow id $ DB.query db q (userContactId, userId, shortLink, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)) >>= \case Just (gId :: Int64, Just cReq) -> Just . (cReq,) <$> getGroupInfo db cxt user gId _ -> pure Nothing + where + q = + [sql| + SELECT g.group_id, g.conn_full_link_to_connect + FROM groups g + JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? + WHERE g.user_id = ? AND g.conn_short_link_to_connect = ? + AND mu.member_status NOT IN (?,?,?,?) + |] getGroupInfoByGroupLinkHash :: DB.Connection -> StoreCxt -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) getGroupInfoByGroupLinkHash db cxt user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do @@ -3009,7 +3429,7 @@ updateMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember -> Profi updateMemberProfile db cxt user@User {userId} m p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p' - let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing updateMemberProfile' currentTs badgeVerified memberProfile where GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m @@ -3032,7 +3452,7 @@ updateContactMemberProfile :: DB.Connection -> StoreCxt -> User -> GroupMember - updateContactMemberProfile db cxt user@User {userId} m ct@Contact {contactId} p' = do currentTs <- liftIO getCurrentTime badgeVerified <- liftIO $ profileBadgeVerified (badgeKeys cxt) (memberProfile m) p' - let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified + let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing updateContactMemberProfile' currentTs badgeVerified profile where GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m @@ -3110,11 +3530,11 @@ createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupI where VersionRange minV maxV = vr cxt --- member_pub_key is not updated here — introduced members are owners --- whose keys are loaded from link data (trusted out-of-band). --- Updating from an in-band message would allow a compromised relay to substitute keys. +-- Intro refreshes only profile / status / peer version. Role and key stay owner-authoritative +-- (the owner-signed roster for members/moderators/admins, link data for owners), so taking either from +-- an in-band relayed intro would let a compromised relay substitute them. updatePreparedChannelMember :: DB.Connection -> StoreCxt -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember -updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do +updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} = do _ <- updateMemberProfile db cxt user member profile currentTs <- liftIO getCurrentTime liftIO $ @@ -3122,14 +3542,13 @@ updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupM db [sql| UPDATE group_members - SET member_role = ?, - member_status = ?, + SET member_status = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ? |] - (memberRole, GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) + (GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) getGroupMemberById db cxt user groupMemberId where VersionRange minV maxV = maybe memberChatVRange fromChatVRange v @@ -3161,6 +3580,30 @@ updateUnknownMemberAnnounced db cxt user@User {userId} invitingMember unknownMem VersionRange minV maxV = maybe memberChatVRange fromChatVRange v memberPubKey_ = (\(MemberKey k) -> k) <$> memberKey +-- Like updateUnknownMemberAnnounced but preserves member_role and member_pub_key +-- (roster-established for moderators/admins; the dissemination carries only the profile). +updateRosterMemberAnnounced :: DB.Connection -> StoreCxt -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember +updateRosterMemberAnnounced db cxt user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} status = do + _ <- updateMemberProfile db cxt user unknownMember profile + currentTs <- liftIO getCurrentTime + liftIO $ + DB.execute + db + [sql| + UPDATE group_members + SET member_category = ?, + member_status = ?, + invited_by_group_member_id = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + |] + ((GCPostMember, status, groupMemberId' invitingMember) :. (minV, maxV, currentTs, userId, groupMemberId)) + getGroupMemberById db cxt user groupMemberId + where + VersionRange minV maxV = maybe memberChatVRange fromChatVRange v + updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO () updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs = DB.execute diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index edbe7a6acb..7b71e61512 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -61,11 +61,12 @@ module Simplex.Chat.Store.Messages markDirectChatItemDeleted, updateGroupChatItemStatus, updateGroupChatItem, + updateChatItemSignedMsg, createGroupCIMentions, updateGroupCIMentions, deleteGroupChatItem, updateGroupChatItemModerated, - updateMemberCIsModerated, + deleteMemberCIs, updateGroupCIBlockedByAdmin, markGroupChatItemDeleted, markMemberCIsDeleted, @@ -137,6 +138,7 @@ module Simplex.Chat.Store.Messages getGroupSndStatuses, getGroupSndStatusCounts, getGroupHistoryItems, + getGroupWebPreviewItems, ) where @@ -211,9 +213,19 @@ getGroupFileInfo db User {userId} GroupInfo {groupId} = <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId) getGroupMemberFileInfo :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO [CIFileInfo] -getGroupMemberFileInfo db User {userId} GroupInfo {groupId} GroupMember {groupMemberId} = - map toFileInfo - <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ?") (userId, groupId, groupMemberId) +getGroupMemberFileInfo db User {userId} GroupInfo {groupId, membership} member + | groupMemberId' member == groupMemberId' membership = + map toFileInfo + <$> DB.query + db + (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id IS NULL AND i.item_sent = 1") + (userId, groupId) + | otherwise = + map toFileInfo + <$> DB.query + db + (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id = ?") + (userId, groupId, groupMemberId' member) deleteGroupChatItemsMessages :: DB.Connection -> User -> GroupInfo -> IO () deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do @@ -227,10 +239,7 @@ createNewSndMessage db gVar connOrGroupId chatMsgEvent msgSigning_ encodeMessage case encodeMessage (SharedMsgId sharedMsgId) of ECMLarge -> pure $ Left SELargeMsg ECMEncoded msgBody -> do - let signedMsg_ = signBody <$> msgSigning_ - signBody MsgSigning {bindingTag, bindingData, keyRef, privKey} = - let sig = C.ASignature C.SEd25519 $ C.sign' privKey (encodeChatBinding bindingTag bindingData <> msgBody) - in SignedMsg {chatBinding = bindingTag, signatures = MsgSignature keyRef sig :| [], signedBody = msgBody} + let signedMsg_ = (`signChatMsgBody` msgBody) <$> msgSigning_ createdAt <- getCurrentTime DB.execute db @@ -330,7 +339,7 @@ createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, verifiedMsg, b ((MDRcv, toCMEventTag chatMsgEvent, DB.Binary msgBody, (\SignedMsg {chatBinding} -> chatBinding) <$> signedMsg_, DB.Binary . smpEncode . signatures <$> signedMsg_, brokerTs, currentTs, currentTs, connId_, groupId_) :. (sharedMsgId_, authorMember, forwardedByMember)) msgId <- insertedRowId db - pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgSigned, forwardedByMember} + pure RcvMessage {msgId, chatMsgEvent = ACME (encoding @e) chatMsgEvent, sharedMsgId_, msgSigned, signedMsg_, signedByGMId_ = signedMsg_ *> authorMember, forwardedByMember} (msgSigned, signedMsg_, msgBody) = verifiedMsgParts verifiedMsg updateSndMsgDeliveryStatus :: DB.Connection -> Int64 -> AgentMsgId -> MsgDeliveryStatus 'MDSnd -> IO () @@ -537,7 +546,7 @@ setSupportChatMemberAttention db cxt user g m memberAttention = do createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> ShowGroupAsSender -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> IO ChatItemId createNewSndChatItem db user chatDirection showGroupAsSender SndMessage {msgId, sharedMsgId, signedMsg_} ciContent quotedItem itemForwarded timed live hasLink createdAt = - createNewChatItem_ db user chatDirection showGroupAsSender createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False hasLink createdAt Nothing (MSSVerified <$ signedMsg_) createdAt + createNewChatItem_ db user chatDirection showGroupAsSender createdByMsgId (Just sharedMsgId) ciContent quoteRow itemForwarded timed live False hasLink createdAt Nothing (toMsgVerified (signMessagesRequired chatDirection) (MSSVerified <$ signedMsg_)) signedMsg_ Nothing createdAt where createdByMsgId = if msgId == 0 then Nothing else Just msgId quoteRow :: NewQuoteRow @@ -552,9 +561,9 @@ createNewSndChatItem db user chatDirection showGroupAsSender SndMessage {msgId, CIQGroupRcv Nothing -> (Just False, Nothing) createNewRcvChatItem :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> CIContent 'MDRcv -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> UTCTime -> IO (ChatItemId, Maybe (CIQuote c), Maybe CIForwardedFrom) -createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgSigned, forwardedByMember} sharedMsgId_ ciContent timed live userMention hasLink itemTs createdAt = do +createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgSigned, signedMsg_, signedByGMId_, forwardedByMember} sharedMsgId_ ciContent timed live userMention hasLink itemTs createdAt = do let showAsGroup = case chatDirection of CDChannelRcv {} -> True; _ -> False - ciId <- createNewChatItem_ db user chatDirection showAsGroup (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember msgSigned createdAt + ciId <- createNewChatItem_ db user chatDirection showAsGroup (Just msgId) sharedMsgId_ ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember (toMsgVerified (signMessagesRequired chatDirection) msgSigned) signedMsg_ signedByGMId_ createdAt quotedItem <- mapM (getChatItemQuote_ db user chatDirection) quotedMsg pure (ciId, quotedItem, itemForwarded) where @@ -573,15 +582,15 @@ createNewRcvChatItem db user chatDirection RcvMessage {msgId, chatMsgEvent, msgS CDChannelRcv GroupInfo {membership = GroupMember {memberId = userMemberId}} _ -> (Just $ Just userMemberId == memberId, memberId) -createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> UTCTime -> UTCTime -> IO ChatItemId -createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink itemTs = - createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing Nothing +createNewChatItemNoMsg :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Bool -> Maybe MsgVerified -> UTCTime -> UTCTime -> IO ChatItemId +createNewChatItemNoMsg db user chatDirection showGroupAsSender ciContent sharedMsgId_ hasLink msgVerified itemTs = + createNewChatItem_ db user chatDirection showGroupAsSender Nothing sharedMsgId_ ciContent quoteRow Nothing Nothing False False hasLink itemTs Nothing msgVerified Nothing Nothing where quoteRow :: NewQuoteRow quoteRow = (Nothing, Nothing, Nothing, Nothing, Nothing) -createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> IO ChatItemId -createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember msgSigned createdAt = do +createNewChatItem_ :: forall c d. MsgDirectionI d => DB.Connection -> User -> ChatDirection c d -> ShowGroupAsSender -> Maybe MessageId -> Maybe SharedMsgId -> CIContent d -> NewQuoteRow -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> UTCTime -> Maybe GroupMemberId -> Maybe MsgVerified -> Maybe SignedMsg -> Maybe GroupMemberId -> UTCTime -> IO ChatItemId +createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ sharedMsgId ciContent quoteRow itemForwarded timed live userMention hasLink itemTs forwardedByMember msgVerified signedMsg_ signedByGMId_ createdAt = do DB.execute db [sql| @@ -590,20 +599,26 @@ createNewChatItem_ db User {userId} chatDirection showGroupAsSender msgId_ share user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, - forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, item_viewed, show_group_as_sender, msg_signed, timed_ttl, timed_delete_at, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, item_viewed, show_group_as_sender, msg_signed, item_msg_body, item_chat_binding, item_signatures, item_signed_by_group_member_id, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ((userId, msgId_) :. idsRow :. groupScopeRow :. itemRow :. quoteRow' :. forwardedFromRow) ciId <- insertedRowId db forM_ msgId_ $ \msgId -> insertChatItemMessage_ db ciId msgId createdAt pure ciId where - itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgSigStatus) :. (Maybe Int, Maybe UTCTime) - itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, mcTag_, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> justTrue live, BI userMention, BI hasLink, BI itemViewed, BI showGroupAsSender, msgSigned) :. ciTimedRow timed + itemRow :: (SMsgDirection d, UTCTime, CIContent d, Text, Text, CIStatus d, Maybe MsgContentTag, Maybe SharedMsgId, Maybe GroupMemberId, BoolInt) :. (UTCTime, UTCTime, Maybe BoolInt, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgVerified) :. (Maybe (DB.Binary ByteString), Maybe ChatBinding, Maybe (DB.Binary ByteString), Maybe GroupMemberId) :. (Maybe Int, Maybe UTCTime) + itemRow = (msgDirection @d, itemTs, ciContent, toCIContentTag ciContent, ciContentToText ciContent, ciCreateStatus ciContent, mcTag_, sharedMsgId, forwardedByMember, BI includeInHistory) :. (createdAt, createdAt, BI <$> justTrue live, BI userMention, BI hasLink, BI itemViewed, BI showGroupAsSender, msgVerified) :. signedRow :. ciTimedRow timed + -- keep the signature and its author on history items so sendHistory can re-forward them signed and attributed + signedRow :: (Maybe (DB.Binary ByteString), Maybe ChatBinding, Maybe (DB.Binary ByteString), Maybe GroupMemberId) + signedRow = case signedMsg_ of + Just SignedMsg {chatBinding, signatures, signedBody} | includeInHistory -> + (Just (DB.Binary signedBody), Just chatBinding, Just (DB.Binary (smpEncode signatures)), signedByGMId_) + _ -> (Nothing, Nothing, Nothing, Nothing) quoteRow' = let (a, b, c, d, e) = quoteRow in (a, b, c, BI <$> d, e) idsRow :: (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId, Maybe NoteFolderId) idsRow = case chatDirection of @@ -706,10 +721,10 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -1127,11 +1142,11 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -2078,6 +2093,7 @@ updateGroupChatItemsRead db User {userId} GroupInfo {groupId} = do [sql| UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ? WHERE user_id = ? AND group_id = ? + AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? |] (CISRcvRead, currentTs, userId, groupId, CISRcvNew) @@ -2134,6 +2150,7 @@ getGroupUnreadTimedItems db User {userId} groupId scope = SELECT chat_item_id, timed_ttl FROM chat_items WHERE user_id = ? AND group_id = ? + AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL |] (userId, groupId, CISRcvNew) @@ -2258,7 +2275,7 @@ updateLocalChatItemsRead db User {userId} noteFolderId = do type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) -type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt, Maybe MsgSigStatus) +type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe BoolInt, BoolInt, BoolInt, Maybe MsgVerified) type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) @@ -2745,6 +2762,19 @@ updateGroupChatItem db user groupId ci newContent edited live msgId_ = do liftIO $ updateGroupChatItem_ db user groupId ci' msgId_ pure ci' +-- overwrite the stored signed bytes and author when a signed edit is applied, so sendHistory forwards the latest signed content +updateChatItemSignedMsg :: DB.Connection -> ChatItemId -> Maybe SignedMsg -> Maybe GroupMemberId -> IO () +updateChatItemSignedMsg db itemId signedMsg_ signedByGMId_ = + DB.execute + db + "UPDATE chat_items SET item_msg_body = ?, item_chat_binding = ?, item_signatures = ?, item_signed_by_group_member_id = ? WHERE chat_item_id = ? AND include_in_history = 1" + (body_, cb_, sigs_, author_, itemId) + where + (body_, cb_, sigs_, author_) = case signedMsg_ of + Just SignedMsg {chatBinding, signatures, signedBody} -> + (Just (DB.Binary signedBody), Just chatBinding, Just (DB.Binary (smpEncode signatures)), signedByGMId_) + Nothing -> (Nothing, Nothing, Nothing, Nothing) + -- this function assumes that the group item with correct chat direction already exists, -- it should be checked before calling it updateGroupChatItem_ :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> Maybe MessageId -> IO () @@ -2819,39 +2849,60 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} -updateMemberCIsModerated :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO () -updateMemberCIsModerated db User {userId} GroupInfo {groupId, membership} member byGroupMember md deletedTs = do - itemIds <- updateCIs =<< getCurrentTime +deleteMemberCIs :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO () +deleteMemberCIs db User {userId} GroupInfo {groupId, membership} member = do + items <- selectItems + let itemMemberId = memberId' member #if defined(dbPostgres) - let inItemIds = Only $ In (map fromOnly itemIds) - DB.execute db "DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ?)" inItemIds - DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" inItemIds + let itemIds = map fst items + sharedMsgIds = mapMaybe snd items + unless (null itemIds) $ do + DB.execute + db + [sql| + DELETE FROM messages WHERE message_id IN ( + SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ? + ) + |] + (Only (In itemIds)) + DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" (Only (In itemIds)) + unless (null sharedMsgIds) $ + DB.execute + db + "DELETE FROM chat_item_reactions WHERE group_id = ? AND shared_msg_id IN ? AND item_member_id IS NOT DISTINCT FROM ?" + (groupId, In sharedMsgIds, itemMemberId) + unless (null itemIds) $ + DB.execute + db + "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND chat_item_id IN ?" + (userId, groupId, In itemIds) #else - DB.executeMany db deleteChatItemMessagesQuery itemIds - DB.executeMany db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" itemIds + forM_ items $ \(itemId, itemSharedMsgId_) -> do + deleteChatItemMessages_ db itemId + deleteChatItemVersions_ db itemId + forM_ itemSharedMsgId_ $ \sharedMsgId -> + DB.execute + db + "DELETE FROM chat_item_reactions WHERE group_id = ? AND shared_msg_id = ? AND item_member_id IS NOT DISTINCT FROM ?" + (groupId, sharedMsgId, itemMemberId) + DB.execute + db + "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" + (userId, groupId, itemId) #endif where - memId = groupMemberId' member - updateQuery = - [sql| - UPDATE chat_items - SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? - |] - updateCIs :: UTCTime -> IO [Only Int64] - updateCIs currentTs - | memId == groupMemberId' membership = + selectItems :: IO [(ChatItemId, Maybe SharedMsgId)] + selectItems + | groupMemberId' member == groupMemberId' membership = DB.query db - (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id") - (columns :. (userId, groupId)) + "SELECT chat_item_id, shared_msg_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id IS NULL AND item_sent = 1" + (userId, groupId) | otherwise = DB.query db - (updateQuery <> " AND group_member_id = ? RETURNING chat_item_id") - (columns :. (userId, groupId, memId)) - where - columns = (deletedTs, groupMemberId' byGroupMember, msgDirToModeratedContent_ md, ciModeratedText, currentTs) + "SELECT chat_item_id, shared_msg_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ?" + (userId, groupId, groupMemberId' member) updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do @@ -3040,26 +3091,26 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, - rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, - rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, - dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, - dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -3663,18 +3714,21 @@ getGroupSndStatusCounts db itemId = |] (Only itemId) -getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] +getGroupHistoryItems :: DB.Connection -> User -> GroupInfo -> GroupMember -> Int -> IO [Either StoreError (CChatItem 'CTGroup, (Maybe SignedMsg, Maybe GroupMemberId))] getGroupHistoryItems db user@User {userId} g@GroupInfo {groupId} m count = do - ciIds <- getLastItemIds_ - reverse <$> mapM (runExceptT . getGroupCIWithReactions db user g) ciIds + items <- getLastItems_ + reverse <$> mapM loadItem items where - getLastItemIds_ :: IO [ChatItemId] - getLastItemIds_ = - map fromOnly + -- forward-info (stored signature + author, both NULL for unsigned/regular items) rides along with the item id, + -- so signed items can be re-served as FwdMember (attributed, verifiable) rather than FwdChannel + loadItem (ciId, fwdInfo) = runExceptT $ (,fwdInfo) <$> getGroupCIWithReactions db user g ciId + getLastItems_ :: IO [(ChatItemId, (Maybe SignedMsg, Maybe GroupMemberId))] + getLastItems_ = + map toItem <$> DB.query db [sql| - SELECT i.chat_item_id + SELECT i.chat_item_id, i.item_chat_binding, i.item_signatures, i.item_msg_body, i.item_signed_by_group_member_id FROM chat_items i LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? WHERE s.group_snd_item_status_id IS NULL @@ -3685,3 +3739,24 @@ getGroupHistoryItems db user@User {userId} g@GroupInfo {groupId} m count = do LIMIT ? |] (groupMemberId' m, userId, groupId, count) + toItem :: (ChatItemId, Maybe ChatBinding, Maybe ByteString, Maybe ByteString, Maybe GroupMemberId) -> (ChatItemId, (Maybe SignedMsg, Maybe GroupMemberId)) + toItem (ciId, cb_, sigs_, body_, signedByGMId_) = + (ciId, (SignedMsg <$> cb_ <*> (sigs_ >>= eitherToMaybe . smpDecode) <*> body_, signedByGMId_)) + +getGroupWebPreviewItems :: DB.Connection -> User -> GroupInfo -> Int -> IO [Either StoreError (CChatItem 'CTGroup)] +getGroupWebPreviewItems db user@User {userId} g@GroupInfo {groupId} count = do + ciIds <- + map fromOnly + <$> DB.query + db + [sql| + SELECT i.chat_item_id + FROM chat_items i + WHERE i.user_id = ? AND i.group_id = ? + AND i.include_in_history = 1 + AND i.item_deleted = 0 + ORDER BY i.item_ts DESC, i.chat_item_id DESC + LIMIT ? + |] + (userId, groupId, count) + reverse <$> mapM (runExceptT . getGroupCIWithReactions db user g) ciIds diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 4c9a1b1c91..3131cbd245 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -33,6 +33,18 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260507_relay_inactive_at import Simplex.Chat.Store.Postgres.Migrations.M20260514_relay_request_group_link_index import Simplex.Chat.Store.Postgres.Migrations.M20260515_public_group_access import Simplex.Chat.Store.Postgres.Migrations.M20260516_supporter_badges +import Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders +import Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services +import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at +import Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain +import Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster +import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name +import Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup +import Simplex.Chat.Store.Postgres.Migrations.M20260707_file_digest +import Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code +import Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description +import Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history +import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -65,7 +77,19 @@ schemaMigrations = ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at), ("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index), ("20260515_public_group_access", m20260515_public_group_access, Just down_m20260515_public_group_access), - ("20260516_supporter_badges", m20260516_supporter_badges, Just down_m20260516_supporter_badges) + ("20260516_supporter_badges", m20260516_supporter_badges, Just down_m20260516_supporter_badges), + ("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders), + ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), + ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), + ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), + ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup), + ("20260707_file_digest", m20260707_file_digest, Just down_m20260707_file_digest), + ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), + ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), + ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), + ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260529_delivery_job_senders.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260529_delivery_job_senders.hs new file mode 100644 index 0000000000..660e33561f --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260529_delivery_job_senders.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +-- delivery_jobs.sender_group_member_ids: comma-separated decimal GroupMemberIds. +-- NULL means [] (sender-less jobs, e.g. DJRelayRemoved). One column carries +-- single- and multi-sender jobs uniformly; the per-job introduction bits live +-- in group_members.member_relations_vector (MRIntroduced). +module Simplex.Chat.Store.Postgres.Migrations.M20260529_delivery_job_senders where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260529_delivery_job_senders :: Text +m20260529_delivery_job_senders = + [r| +DROP INDEX idx_delivery_jobs_single_sender_group_member_id; + +ALTER TABLE delivery_jobs ADD COLUMN sender_group_member_ids TEXT; + +UPDATE delivery_jobs +SET sender_group_member_ids = single_sender_group_member_id::text +WHERE single_sender_group_member_id IS NOT NULL; + +ALTER TABLE delivery_jobs DROP COLUMN single_sender_group_member_id; +|] + +down_m20260529_delivery_job_senders :: Text +down_m20260529_delivery_job_senders = + [r| +-- Pre-up the FK was ON DELETE CASCADE, so orphan delivery_jobs cannot +-- exist. After up the FK was dropped and orphans may accumulate. Drop +-- them here, matching pre-up semantics, before re-adding the FK column. +DELETE FROM delivery_jobs +WHERE sender_group_member_ids IS NOT NULL + AND length(sender_group_member_ids) > 0 + AND position(',' in sender_group_member_ids) = 0 + AND NOT EXISTS ( + SELECT 1 FROM group_members + WHERE group_member_id = sender_group_member_ids::bigint + ); + +ALTER TABLE delivery_jobs ADD COLUMN single_sender_group_member_id BIGINT REFERENCES group_members(group_member_id) ON DELETE CASCADE; + +UPDATE delivery_jobs +SET single_sender_group_member_id = + CASE + WHEN sender_group_member_ids IS NULL THEN NULL + WHEN position(',' in sender_group_member_ids) > 0 THEN NULL + WHEN length(sender_group_member_ids) = 0 THEN NULL + ELSE sender_group_member_ids::bigint + END; + +ALTER TABLE delivery_jobs DROP COLUMN sender_group_member_ids; + +CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON delivery_jobs(single_sender_group_member_id); +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260530_client_services.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260530_client_services.hs new file mode 100644 index 0000000000..2a37f8f4e3 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260530_client_services.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260530_client_services where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260530_client_services :: Text +m20260530_client_services = + [r| +ALTER TABLE users ADD COLUMN client_service SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20260530_client_services :: Text +down_m20260530_client_services = + [r| +ALTER TABLE users DROP COLUMN client_service; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260531_member_removed_at.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260531_member_removed_at.hs new file mode 100644 index 0000000000..9dde712f0b --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260531_member_removed_at.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260531_member_removed_at :: Text +m20260531_member_removed_at = + [r| +ALTER TABLE group_members ADD COLUMN removed_at TIMESTAMPTZ; +|] + +down_m20260531_member_removed_at :: Text +down_m20260531_member_removed_at = + [r| +ALTER TABLE group_members DROP COLUMN removed_at; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260601_relay_sent_web_domain.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260601_relay_sent_web_domain.hs new file mode 100644 index 0000000000..1b8efbcead --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260601_relay_sent_web_domain.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260601_relay_sent_web_domain where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260601_relay_sent_web_domain :: Text +m20260601_relay_sent_web_domain = + [r| +ALTER TABLE groups ADD COLUMN relay_sent_web_domain TEXT; +|] + +down_m20260601_relay_sent_web_domain :: Text +down_m20260601_relay_sent_web_domain = + [r| +ALTER TABLE groups DROP COLUMN relay_sent_web_domain; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs new file mode 100644 index 0000000000..892b2c70da --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260602_group_roster.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260602_group_roster where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260602_group_roster :: Text +m20260602_group_roster = + [r| +ALTER TABLE groups ADD COLUMN roster_version BIGINT; +ALTER TABLE groups ADD COLUMN roster_msg_body BYTEA; +ALTER TABLE groups ADD COLUMN roster_msg_chat_binding TEXT; +ALTER TABLE groups ADD COLUMN roster_msg_signatures BYTEA; +ALTER TABLE groups ADD COLUMN roster_sending_owner_gm_id BIGINT; +ALTER TABLE groups ADD COLUMN roster_broker_ts TIMESTAMPTZ; +ALTER TABLE groups ADD COLUMN roster_blob BYTEA; + +CREATE TABLE rcv_roster_transfers( + roster_transfer_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + group_id BIGINT NOT NULL REFERENCES groups ON DELETE CASCADE, + from_member_id BIGINT NOT NULL REFERENCES group_members ON DELETE CASCADE, + roster_version BIGINT NOT NULL, + roster_digest BYTEA NOT NULL, + sending_owner_gm_id BIGINT NOT NULL, + broker_ts TIMESTAMPTZ NOT NULL, + roster_msg_body BYTEA, + roster_msg_chat_binding TEXT, + roster_msg_signatures BYTEA, + created_at TEXT NOT NULL DEFAULT (now()), + updated_at TEXT NOT NULL DEFAULT (now()) +); +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON rcv_roster_transfers(group_id, from_member_id); +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON rcv_roster_transfers(from_member_id); + +ALTER TABLE files ADD COLUMN shared_msg_id BYTEA; +ALTER TABLE files ADD COLUMN file_type TEXT NOT NULL DEFAULT 'normal'; +ALTER TABLE files ADD COLUMN roster_transfer_id BIGINT; +CREATE INDEX idx_files_group_id_shared_msg_id ON files(group_id, shared_msg_id); +CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); +|] + +down_m20260602_group_roster :: Text +down_m20260602_group_roster = + [r| +DROP INDEX idx_files_roster_transfer_id; +DROP INDEX idx_files_group_id_shared_msg_id; +ALTER TABLE files DROP COLUMN roster_transfer_id; +ALTER TABLE files DROP COLUMN file_type; +ALTER TABLE files DROP COLUMN shared_msg_id; + +DROP INDEX idx_rcv_roster_transfers_from_member_id; +DROP INDEX idx_rcv_roster_transfers_group_id_from_member_id; +DROP TABLE rcv_roster_transfers; + +ALTER TABLE groups DROP COLUMN roster_blob; +ALTER TABLE groups DROP COLUMN roster_broker_ts; +ALTER TABLE groups DROP COLUMN roster_sending_owner_gm_id; +ALTER TABLE groups DROP COLUMN roster_msg_signatures; +ALTER TABLE groups DROP COLUMN roster_msg_chat_binding; +ALTER TABLE groups DROP COLUMN roster_msg_body; +ALTER TABLE groups DROP COLUMN roster_version; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs new file mode 100644 index 0000000000..14a0ae2a05 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260603_simplex_name.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260603_simplex_name :: Text +m20260603_simplex_name = + [r| +ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified SMALLINT; + +ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT; +ALTER TABLE groups ADD COLUMN group_domain_verified SMALLINT; + +ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BYTEA; + +ALTER TABLE server_operators ADD COLUMN smp_role_names SMALLINT NOT NULL DEFAULT 0; +UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux'; +|] + +down_m20260603_simplex_name :: Text +down_m20260603_simplex_name = + [r| +ALTER TABLE contact_profiles DROP COLUMN contact_domain; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified; + +ALTER TABLE group_profiles DROP COLUMN group_domain_proof; +ALTER TABLE groups DROP COLUMN group_domain_verified; + +ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key; + +ALTER TABLE server_operators DROP COLUMN smp_role_names; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs new file mode 100644 index 0000000000..f5b94e7d0b --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260629_roster_catchup.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260629_roster_catchup where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +-- Roster catch-up bookkeeping. Three monotonic per-group roster versions with distinct roles - normally equal, +-- diverging across gaps and failed transfers (applied_complete <= stored <= roster_version): +-- roster_version (added in M20260602) - the GATE: highest version seen. Advanced by every accepted owner delta +-- and by a roster apply. Revert protection: a completing roster older than this is rejected, since its +-- snapshot would undo a newer applied delta. +-- stored_roster_version - the blob HELD: the version of the roster blob actually stored (written with the blob). +-- What a relay can re-serve; a failed blob receive leaves it behind the gate (which the delta still advances) +-- until a later roster completes. Relay-side; on a member it is set but the blob is unused. +-- applied_complete_roster_version - the COMPLETE frontier: highest version up to which the picture is contiguous. +-- Advances by 1 on a contiguous delta and to the roster's version on apply, but stays put on a gapped delta. +-- The subscriber's "what I have" for gap detection and the catch-up request: a value below the gate means +-- missed versions, so each following delta re-asks the forwarding relay until a roster fills the frontier. +-- Also adds group_members.roster_served_version - the newest version a relay re-served a given member, bounding +-- reflected amplification (a member can't re-trigger a full serve at a version it was already served). +-- Backfill an existing roster's stored and complete versions from roster_version: pre-upgrade the picture is +-- contiguous up to roster_version (no gap detection existed), so a fresh NULL frontier would read every group's +-- next delta as a gap and make every subscriber request a re-serve at once. +m20260629_roster_catchup :: Text +m20260629_roster_catchup = + [r| +ALTER TABLE group_members ADD COLUMN roster_served_version BIGINT; +ALTER TABLE groups ADD COLUMN stored_roster_version BIGINT; +ALTER TABLE groups ADD COLUMN applied_complete_roster_version BIGINT; +UPDATE groups SET stored_roster_version = roster_version, applied_complete_roster_version = roster_version WHERE roster_version IS NOT NULL; +|] + +down_m20260629_roster_catchup :: Text +down_m20260629_roster_catchup = + [r| +ALTER TABLE group_members DROP COLUMN roster_served_version; +ALTER TABLE groups DROP COLUMN stored_roster_version; +ALTER TABLE groups DROP COLUMN applied_complete_roster_version; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260707_file_digest.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260707_file_digest.hs new file mode 100644 index 0000000000..56e4f00db7 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260707_file_digest.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260707_file_digest where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260707_file_digest :: Text +m20260707_file_digest = + [r| +ALTER TABLE files ADD COLUMN file_digest BYTEA; +|] + +down_m20260707_file_digest :: Text +down_m20260707_file_digest = + [r| +ALTER TABLE files DROP COLUMN file_digest; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260714_member_security_code.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260714_member_security_code.hs new file mode 100644 index 0000000000..e5ab89ca58 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260714_member_security_code.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260714_member_security_code :: Text +m20260714_member_security_code = + [r| +ALTER TABLE group_members ADD COLUMN member_security_code TEXT; +ALTER TABLE group_members ADD COLUMN member_security_code_verified_at TIMESTAMPTZ; +|] + +down_m20260714_member_security_code :: Text +down_m20260714_member_security_code = + [r| +ALTER TABLE group_members DROP COLUMN member_security_code; +ALTER TABLE group_members DROP COLUMN member_security_code_verified_at; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs new file mode 100644 index 0000000000..d27636e826 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260715_profile_description.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20260715_profile_description :: Text +m20260715_profile_description = + T.pack + [r| +ALTER TABLE contact_profiles ADD COLUMN description TEXT; +|] + +down_m20260715_profile_description :: Text +down_m20260715_profile_description = + T.pack + [r| +ALTER TABLE contact_profiles DROP COLUMN description; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260716_signed_history.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260716_signed_history.hs new file mode 100644 index 0000000000..e7578d9429 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260716_signed_history.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260716_signed_history :: Text +m20260716_signed_history = + [r| +ALTER TABLE chat_items ADD COLUMN item_msg_body BYTEA; +ALTER TABLE chat_items ADD COLUMN item_chat_binding TEXT; +ALTER TABLE chat_items ADD COLUMN item_signatures BYTEA; +ALTER TABLE chat_items ADD COLUMN item_signed_by_group_member_id BIGINT REFERENCES group_members ON DELETE SET NULL; + +CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items(item_signed_by_group_member_id); +|] + +down_m20260716_signed_history :: Text +down_m20260716_signed_history = + [r| +DROP INDEX idx_chat_items_item_signed_by_group_member_id; + +ALTER TABLE chat_items DROP COLUMN item_msg_body; +ALTER TABLE chat_items DROP COLUMN item_chat_binding; +ALTER TABLE chat_items DROP COLUMN item_signatures; +ALTER TABLE chat_items DROP COLUMN item_signed_by_group_member_id; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260720_server_roles.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260720_server_roles.hs new file mode 100644 index 0000000000..dcfb532e03 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260720_server_roles.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260720_server_roles :: Text +m20260720_server_roles = + [r| +ALTER TABLE protocol_servers ADD COLUMN role_storage SMALLINT; +ALTER TABLE protocol_servers ADD COLUMN role_proxy SMALLINT; +ALTER TABLE protocol_servers ADD COLUMN role_names SMALLINT; +|] + +down_m20260720_server_roles :: Text +down_m20260720_server_roles = + [r| +ALTER TABLE protocol_servers DROP COLUMN role_storage; +ALTER TABLE protocol_servers DROP COLUMN role_proxy; +ALTER TABLE protocol_servers DROP COLUMN role_names; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 68c43efa19..e901c8858e 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -345,7 +345,11 @@ CREATE TABLE test_chat_schema.chat_items ( show_group_as_sender smallint DEFAULT 0 NOT NULL, has_link smallint DEFAULT 0 NOT NULL, msg_signed text, - item_viewed smallint DEFAULT 0 NOT NULL + item_viewed smallint DEFAULT 0 NOT NULL, + item_msg_body bytea, + item_chat_binding text, + item_signatures bytea, + item_signed_by_group_member_id bigint ); @@ -540,7 +544,11 @@ CREATE TABLE test_chat_schema.contact_profiles ( badge_extra text, badge_master_key bytea, badge_signature bytea, - badge_key_idx bigint + badge_key_idx bigint, + contact_domain text, + contact_domain_proof text, + contact_domain_verified smallint, + description text ); @@ -644,14 +652,14 @@ CREATE TABLE test_chat_schema.delivery_jobs ( job_scope_spec_tag text, job_scope_include_pending smallint, job_scope_support_gm_id bigint, - single_sender_group_member_id bigint, body bytea, cursor_group_member_id bigint, job_status text NOT NULL, job_err_reason text, failed smallint DEFAULT 0, created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL + updated_at timestamp with time zone DEFAULT now() NOT NULL, + sender_group_member_ids text ); @@ -752,7 +760,11 @@ CREATE TABLE test_chat_schema.files ( file_crypto_key bytea, file_crypto_nonce bytea, note_folder_id bigint, - redirect_file_id bigint + redirect_file_id bigint, + shared_msg_id bytea, + file_type text DEFAULT 'normal'::text NOT NULL, + roster_transfer_id bigint, + file_digest bytea ); @@ -827,7 +839,11 @@ CREATE TABLE test_chat_schema.group_members ( index_in_group bigint DEFAULT 0 NOT NULL, member_relations_vector bytea, relay_link bytea, - member_pub_key bytea + member_pub_key bytea, + removed_at timestamp with time zone, + roster_served_version bigint, + member_security_code text, + member_security_code_verified_at timestamp with time zone ); @@ -862,7 +878,8 @@ CREATE TABLE test_chat_schema.group_profiles ( group_web_page text, group_domain text, domain_web_page bigint, - allow_embedding bigint + allow_embedding bigint, + group_domain_proof text ); @@ -976,8 +993,19 @@ CREATE TABLE test_chat_schema.groups ( public_member_count bigint, relay_request_retries bigint DEFAULT 0 NOT NULL, relay_request_delay bigint DEFAULT 0 NOT NULL, - relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL, - relay_inactive_at timestamp with time zone + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, + relay_inactive_at timestamp with time zone, + relay_sent_web_domain text, + roster_version bigint, + roster_msg_body bytea, + roster_msg_chat_binding text, + roster_msg_signatures bytea, + roster_sending_owner_gm_id bigint, + roster_broker_ts timestamp with time zone, + roster_blob bytea, + group_domain_verified smallint, + stored_roster_version bigint, + applied_complete_roster_version bigint ); @@ -1159,7 +1187,10 @@ CREATE TABLE test_chat_schema.protocol_servers ( user_id bigint NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL, - protocol text DEFAULT 'smp'::text NOT NULL + protocol text DEFAULT 'smp'::text NOT NULL, + role_storage smallint, + role_proxy smallint, + role_names smallint ); @@ -1204,6 +1235,34 @@ CREATE TABLE test_chat_schema.rcv_files ( +CREATE TABLE test_chat_schema.rcv_roster_transfers ( + roster_transfer_id bigint NOT NULL, + group_id bigint NOT NULL, + from_member_id bigint NOT NULL, + roster_version bigint NOT NULL, + roster_digest bytea NOT NULL, + sending_owner_gm_id bigint NOT NULL, + broker_ts timestamp with time zone NOT NULL, + roster_msg_body bytea, + roster_msg_chat_binding text, + roster_msg_signatures bytea, + created_at text DEFAULT now() NOT NULL, + updated_at text DEFAULT now() NOT NULL +); + + + +ALTER TABLE test_chat_schema.rcv_roster_transfers ALTER COLUMN roster_transfer_id ADD GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME test_chat_schema.rcv_roster_transfers_roster_transfer_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + CREATE TABLE test_chat_schema.received_probes ( received_probe_id bigint NOT NULL, contact_id bigint, @@ -1337,7 +1396,8 @@ CREATE TABLE test_chat_schema.server_operators ( xftp_role_storage smallint DEFAULT 1 NOT NULL, xftp_role_proxy smallint DEFAULT 1 NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, - updated_at timestamp with time zone DEFAULT now() NOT NULL + updated_at timestamp with time zone DEFAULT now() NOT NULL, + smp_role_names smallint DEFAULT 0 NOT NULL ); @@ -1414,7 +1474,8 @@ CREATE TABLE test_chat_schema.user_contact_links ( business_address smallint DEFAULT 0, short_link_contact bytea, short_link_data_set smallint DEFAULT 0 NOT NULL, - short_link_large_data_set smallint DEFAULT 0 NOT NULL + short_link_large_data_set smallint DEFAULT 0 NOT NULL, + link_priv_sig_key bytea ); @@ -1447,7 +1508,8 @@ CREATE TABLE test_chat_schema.users ( ui_themes text, active_order bigint DEFAULT 0 NOT NULL, auto_accept_member_contacts smallint DEFAULT 0 NOT NULL, - is_user_chat_relay smallint DEFAULT 0 NOT NULL + is_user_chat_relay smallint DEFAULT 0 NOT NULL, + client_service smallint DEFAULT 0 NOT NULL ); @@ -1736,6 +1798,11 @@ ALTER TABLE ONLY test_chat_schema.rcv_files +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_pkey PRIMARY KEY (roster_transfer_id); + + + ALTER TABLE ONLY test_chat_schema.received_probes ADD CONSTRAINT received_probes_pkey PRIMARY KEY (received_probe_id); @@ -2021,6 +2088,10 @@ CREATE INDEX idx_chat_items_item_deleted_by_group_member_id ON test_chat_schema. +CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON test_chat_schema.chat_items USING btree (item_signed_by_group_member_id); + + + CREATE INDEX idx_chat_items_item_status ON test_chat_schema.chat_items USING btree (item_status); @@ -2217,10 +2288,6 @@ CREATE INDEX idx_delivery_jobs_next ON test_chat_schema.delivery_jobs USING btre -CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON test_chat_schema.delivery_jobs USING btree (single_sender_group_member_id); - - - CREATE INDEX idx_delivery_tasks_created_at ON test_chat_schema.delivery_tasks USING btree (created_at); @@ -2273,10 +2340,18 @@ CREATE INDEX idx_files_group_id ON test_chat_schema.files USING btree (group_id) +CREATE INDEX idx_files_group_id_shared_msg_id ON test_chat_schema.files USING btree (group_id, shared_msg_id); + + + CREATE INDEX idx_files_redirect_file_id ON test_chat_schema.files USING btree (redirect_file_id); +CREATE INDEX idx_files_roster_transfer_id ON test_chat_schema.files USING btree (roster_transfer_id); + + + CREATE INDEX idx_files_user_id ON test_chat_schema.files USING btree (user_id); @@ -2449,6 +2524,14 @@ CREATE INDEX idx_rcv_files_group_member_id ON test_chat_schema.rcv_files USING b +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON test_chat_schema.rcv_roster_transfers USING btree (from_member_id); + + + +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON test_chat_schema.rcv_roster_transfers USING btree (group_id, from_member_id); + + + CREATE INDEX idx_received_probes_contact_id ON test_chat_schema.received_probes USING btree (contact_id); @@ -2694,6 +2777,11 @@ ALTER TABLE ONLY test_chat_schema.chat_items +ALTER TABLE ONLY test_chat_schema.chat_items + ADD CONSTRAINT chat_items_item_signed_by_group_member_id_fkey FOREIGN KEY (item_signed_by_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE SET NULL; + + + ALTER TABLE ONLY test_chat_schema.chat_items ADD CONSTRAINT chat_items_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; @@ -2849,11 +2937,6 @@ ALTER TABLE ONLY test_chat_schema.delivery_jobs -ALTER TABLE ONLY test_chat_schema.delivery_jobs - ADD CONSTRAINT delivery_jobs_single_sender_group_member_id_fkey FOREIGN KEY (single_sender_group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; - - - ALTER TABLE ONLY test_chat_schema.delivery_tasks ADD CONSTRAINT delivery_tasks_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; @@ -3139,6 +3222,16 @@ ALTER TABLE ONLY test_chat_schema.rcv_files +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_from_member_id_fkey FOREIGN KEY (from_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + +ALTER TABLE ONLY test_chat_schema.rcv_roster_transfers + ADD CONSTRAINT rcv_roster_transfers_group_id_fkey FOREIGN KEY (group_id) REFERENCES test_chat_schema.groups(group_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.received_probes ADD CONSTRAINT received_probes_contact_id_fkey FOREIGN KEY (contact_id) REFERENCES test_chat_schema.contacts(contact_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index bfd198d885..ce6a8c4c9f 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -20,7 +20,6 @@ module Simplex.Chat.Store.Profiles UserMsgReceiptSettings (..), UserContactLink (..), GroupLinkInfo (..), - createUserRecord, createUserRecordAt, getUsersInfo, getUsers, @@ -38,6 +37,7 @@ module Simplex.Chat.Store.Profiles getUserFileInfo, deleteUserRecord, updateUserPrivacy, + updateClientService, updateAllContactReceipts, updateUserContactReceipts, updateUserGroupReceipts, @@ -50,10 +50,11 @@ module Simplex.Chat.Store.Profiles getUserAddressConnection, deleteUserAddress, getUserAddress, + setUserSimplexDomain, getUserContactLinkById, getGroupLinkInfo, getUserContactLinkByConnReq, - getUserContactLinkViaShortLink, + getUserContactLinkViaTarget, setUserContactLinkShortLink, getContactWithoutConnViaAddress, getContactWithoutConnViaShortAddress, @@ -105,12 +106,13 @@ import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared +import Simplex.Chat.Names (claimDomain, mkDomainClaim) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) -import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), SimplexDomain, SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -130,11 +132,8 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) #endif -createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> Bool -> ExceptT StoreError IO User -createUserRecord db auId p userChatRelay activeUser = createUserRecordAt db auId p userChatRelay activeUser =<< liftIO getCurrentTime - -createUserRecordAt :: DB.Connection -> AgentUserId -> Profile -> Bool -> Bool -> UTCTime -> ExceptT StoreError IO User -createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDescr, image, peerType, preferences = userPreferences} userChatRelay activeUser currentTs = +createUserRecordAt :: DB.Connection -> AgentUserId -> Bool -> Bool -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User +createUserRecordAt db (AgentUserId auId) userChatRelay clientService Profile {displayName, fullName, shortDescr, description, image, peerType, preferences = userPreferences} activeUser currentTs = checkConstraint SEDuplicateName . liftIO $ do when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" let showNtfs = True @@ -144,9 +143,9 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe order <- getNextActiveOrder db DB.execute db - "INSERT INTO users (agent_user_id, local_display_name, active_user, is_user_chat_relay, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, created_at, updated_at) VALUES (?,?,?,?,?,0,?,?,?,?,?,?)" + "INSERT INTO users (agent_user_id, local_display_name, active_user, is_user_chat_relay, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, client_service, created_at, updated_at) VALUES (?,?,?,?,?,0,?,?,?,?,?,?,?)" ( (auId, displayName, BI activeUser, BI userChatRelay, order) - :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, currentTs, currentTs) + :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI clientService, currentTs, currentTs) ) userId <- insertedRowId db DB.execute @@ -155,8 +154,8 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe (displayName, displayName, userId, currentTs, currentTs) DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (displayName, fullName, shortDescr, image, peerType, userId, userPreferences, currentTs, currentTs) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, chat_peer_type, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + (displayName, fullName, shortDescr, description, image, peerType, userId, userPreferences, currentTs, currentTs) profileId <- insertedRowId db DB.execute db @@ -164,7 +163,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 currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, Nothing, BI userChatRelay) :. localBadgeToRow Nothing + pure $ toUser currentTs $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, description, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI userChatRelay, BI clientService, Nothing) :. localBadgeToRow Nothing :. (Nothing, Nothing, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] @@ -297,6 +296,17 @@ updateUserPrivacy db User {userId, showNtfs, viewPwdHash} = where hashSalt = L.unzip . fmap (\UserPwdHash {hash, salt} -> (hash, salt)) +updateClientService :: DB.Connection -> UserId -> Bool -> IO () +updateClientService db userId enable = + DB.execute + db + [sql| + UPDATE users + SET client_service = ? + WHERE user_id = ? + |] + (BI enable, userId) + updateAllContactReceipts :: DB.Connection -> Bool -> IO () updateAllContactReceipts db onOff = DB.execute @@ -324,7 +334,7 @@ updateUserProfile db user p' currentTs <- getCurrentTime updateUserProfileFields_' db userId profileId p' currentTs userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs - pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} + pure user {profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} | otherwise = checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime @@ -336,29 +346,29 @@ updateUserProfile db user p' (newName, newName, userId, currentTs, currentTs) updateUserProfileFields_' db userId profileId p' currentTs updateContactLDN_ db user userContactId localDisplayName newName currentTs - pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False)) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} + pure user {localDisplayName = newName, profile = (toLocalProfile profileId p' localAlias currentTs (Just False) Nothing) {localBadge}, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'} where updateUserMemberProfileUpdatedAt_ currentTs | userMemberProfileChanged = do DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) pure $ Just currentTs | otherwise = pure userMemberProfileUpdatedAt - userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || img' /= image - User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user - Profile {displayName = newName, fullName = fn', shortDescr = d', image = img', preferences} = p' + userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || desc' /= description || img' /= image + User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, localBadge, localAlias}, userMemberProfileUpdatedAt} = user + Profile {displayName = newName, fullName = fn', shortDescr = d', description = desc', image = img', preferences} = p' fullPreferences = fullPreferences' preferences -- own profile field update; leaves the badge columns alone (the credential is owned by setUserBadge/addUserBadge) updateUserProfileFields_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO () -updateUserProfileFields_' db userId profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType} updatedAt = +updateUserProfileFields_' db userId profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType} updatedAt = DB.execute db [sql| UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? |] - ((displayName, fullName, shortDescr, image, contactLink, preferences, peerType, updatedAt) :. (userId, profileId)) + ((displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, updatedAt) :. (userId, profileId)) -- store the user's own badge credential; touches only the badge columns. -- bumps user_member_profile_updated_at so groups receive the updated profile (with the badge) on the next message. @@ -376,6 +386,15 @@ setUserBadge db user@User {userId, profile = p@LocalProfile {profileId}} localBa DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (ts, userId) pure (user :: User) {profile = p {localBadge}, userMemberProfileUpdatedAt = Just ts} +setUserSimplexDomain :: DB.Connection -> User -> Maybe SimplexDomain -> IO User +setUserSimplexDomain db user@User {userId, profile = p@LocalProfile {profileId}} domain_ = do + ts <- getCurrentTime + DB.execute + db + "UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ?" + (domain_, ts, userId, profileId) + pure (user :: User) {profile = p {contactDomain = mkDomainClaim <$> domain_}} + setUserProfileContactLink :: DB.Connection -> User -> Maybe UserContactLink -> IO User setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profileId}} ucl_ = do ts <- getCurrentTime @@ -398,24 +417,24 @@ getUserContactProfiles db User {userId} = <$> DB.query db [sql| - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? |] (Only userId) where - toContactProfile :: (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) -> Profile - toContactProfile (displayName, fullName, shortDescr, image, contactLink, peerType, preferences) = Profile {displayName, fullName, shortDescr, image, contactLink, peerType, preferences, badge = Nothing} + toContactProfile :: (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe SimplexDomain, Maybe Preferences) -> Profile + toContactProfile (displayName, fullName, shortDescr, description, image, contactLink, peerType, domain_, preferences) = Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain = mkDomainClaim <$> domain_, peerType, preferences, badge = Nothing} -createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode = +createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> C.PrivateKeyEd25519 -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode linkPrivSigKey = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime let slDataSet = BI (isJust shortLink) DB.execute db - "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (userId, cReq, shortLink, slDataSet, slDataSet, currentTs, currentTs) + "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, link_priv_sig_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (userId, cReq, shortLink, slDataSet, slDataSet, linkPrivSigKey, currentTs, currentTs) userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff @@ -538,10 +557,16 @@ getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = maybeFirstRow toUserContactLink $ DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2) -getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink) -getUserContactLinkViaShortLink db User {userId} shortLink = - maybeFirstRow toUserContactLink $ - DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) +getUserContactLinkViaTarget :: DB.Connection -> User -> ContactNameOrLink -> IO (Maybe UserContactLink) +getUserContactLinkViaTarget db User {userId, profile = LocalProfile {contactDomain}} = \case + CTLink shortLink -> + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink) + CTName ni + | (claimDomain <$> contactDomain) == Just (nameDomain ni) -> + maybeFirstRow toUserContactLink $ + DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND group_id IS NULL AND short_link_contact IS NOT NULL") (Only userId) + | otherwise -> pure Nothing userContactLinkQuery :: Query userContactLinkQuery = @@ -617,41 +642,45 @@ getProtocolServers db p User {userId} = <$> DB.query db [sql| - SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names FROM protocol_servers WHERE user_id = ? AND protocol = ? |] (userId, decodeLatin1 $ strEncode p) where - toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) -> UserServer p - toUserServer (serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) = + toUserServer :: ((DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, BoolInt, Maybe BoolInt, BoolInt) :. (Maybe BoolInt, Maybe BoolInt, Maybe BoolInt)) -> UserServer p + toUserServer ((serverId, host, port, keyHash, auth_, BI preset, tested, BI enabled) :. (rStorage, rProxy, rNames)) = let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, deleted = False} + roles = ServerRolesOverride (unBI <$> rStorage) (unBI <$> rProxy) (unBI <$> rNames) + in UserServer {serverId, server, preset, tested = unBI <$> tested, enabled, roles, deleted = False} insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) -insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do +insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled, roles} = do DB.execute db [sql| INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - (serverColumns p server :. (BI preset, BI <$> tested, BI enabled, userId, ts, ts)) + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled) :. roleColumns roles :. (userId, ts, ts)) sId <- insertedRowId db pure (srv :: NewUserServer p) {serverId = DBEntityId sId} updateProtocolServer :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> UTCTime -> UserServer p -> IO () -updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled} = +updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled, roles} = DB.execute db [sql| UPDATE protocol_servers SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, - preset = ?, tested = ?, enabled = ?, updated_at = ? + preset = ?, tested = ?, enabled = ?, + role_storage = ?, role_proxy = ?, role_names = ?, updated_at = ? WHERE smp_server_id = ? |] - (serverColumns p server :. (BI preset, BI <$> tested, BI enabled, ts, serverId)) + (serverColumns p server :. (BI preset, BI <$> tested, BI enabled) :. roleColumns roles :. (ts, serverId)) serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = @@ -659,6 +688,9 @@ serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) auth = safeDecodeUtf8 . unBasicAuth <$> auth_ in (protocol, host, port, keyHash, auth) +roleColumns :: ServerRolesOverride -> (Maybe BoolInt, Maybe BoolInt, Maybe BoolInt) +roleColumns ServerRolesOverride {storage, proxy, names} = (BI <$> storage, BI <$> proxy, BI <$> names) + getChatRelays :: DB.Connection -> User -> IO [UserChatRelay] getChatRelays db User {userId} = map toChatRelay @@ -744,10 +776,13 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles, db [sql| UPDATE server_operators - SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? WHERE server_operator_id = ? |] - (BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId) + (BI enabled, BI smpStorage, BI smpProxy, BI smpNames, BI xftpStorage, BI xftpProxy, currentTs, operatorId) + where + ServerRoles {storage = smpStorage, proxy = smpProxy, names = smpNames} = smpRoles + ServerRoles {storage = xftpStorage, proxy = xftpProxy} = xftpRoles getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)] getUpdateServerOperators db presetOps newUser = do @@ -782,22 +817,28 @@ getUpdateServerOperators db presetOps newUser = do db [sql| UPDATE server_operators - SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ? + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ? WHERE server_operator_id = ? |] - (tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId) + (tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI smpStorage, BI smpProxy, BI smpNames, BI xftpStorage, BI xftpProxy, operatorId) + where + ServerRoles {storage = smpStorage, proxy = smpProxy, names = smpNames} = smpRoles + ServerRoles {storage = xftpStorage, proxy = xftpProxy} = xftpRoles insertOperator :: NewServerOperator -> IO ServerOperator insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do DB.execute db [sql| INSERT INTO server_operators - (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) - VALUES (?,?,?,?,?,?,?,?,?) + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles)) + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI smpStorage, BI smpProxy, BI smpNames, BI xftpStorage, BI xftpProxy) opId <- insertedRowId db pure op {operatorId = DBEntityId opId} + where + ServerRoles {storage = smpStorage, proxy = smpProxy, names = smpNames} = smpRoles + ServerRoles {storage = xftpStorage, proxy = xftpProxy} = xftpRoles autoAcceptConditions op UsageConditions {conditionsCommit} now = acceptConditions_ db op conditionsCommit now True $> op {conditionsAcceptance = CAAccepted (Just now) True} @@ -806,14 +847,14 @@ serverOperatorQuery :: Query serverOperatorQuery = [sql| SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators |] getServerOperators_ :: DB.Connection -> IO [ServerOperator] getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery -toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') = ServerOperator { operatorId, @@ -823,11 +864,12 @@ toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI en serverDomains = T.splitOn "," domains, conditionsAcceptance = CARequired Nothing, enabled, - smpRoles = serverRoles smpRoles', - xftpRoles = serverRoles xftpRoles' + smpRoles = serverRolesSMP smpRoles', + xftpRoles = serverRolesXFTP xftpRoles' } where - serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy} + serverRolesSMP (BI storage, BI proxy, BI names) = ServerRoles {storage, proxy, names} + serverRolesXFTP (BI storage, BI proxy) = ServerRoles {storage, proxy, names = False} getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 5bf628b062..7fc18617e7 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -156,6 +156,18 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260507_relay_inactive_at import Simplex.Chat.Store.SQLite.Migrations.M20260514_relay_request_group_link_index import Simplex.Chat.Store.SQLite.Migrations.M20260515_public_group_access import Simplex.Chat.Store.SQLite.Migrations.M20260516_supporter_badges +import Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders +import Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services +import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at +import Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain +import Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster +import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name +import Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup +import Simplex.Chat.Store.SQLite.Migrations.M20260707_file_digest +import Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code +import Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description +import Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history +import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -311,7 +323,19 @@ schemaMigrations = ("20260507_relay_inactive_at", m20260507_relay_inactive_at, Just down_m20260507_relay_inactive_at), ("20260514_relay_request_group_link_index", m20260514_relay_request_group_link_index, Just down_m20260514_relay_request_group_link_index), ("20260515_public_group_access", m20260515_public_group_access, Just down_m20260515_public_group_access), - ("20260516_supporter_badges", m20260516_supporter_badges, Just down_m20260516_supporter_badges) + ("20260516_supporter_badges", m20260516_supporter_badges, Just down_m20260516_supporter_badges), + ("20260529_delivery_job_senders", m20260529_delivery_job_senders, Just down_m20260529_delivery_job_senders), + ("20260530_client_services", m20260530_client_services, Just down_m20260530_client_services), + ("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at), + ("20260601_relay_sent_web_domain", m20260601_relay_sent_web_domain, Just down_m20260601_relay_sent_web_domain), + ("20260602_group_roster", m20260602_group_roster, Just down_m20260602_group_roster), + ("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name), + ("20260629_roster_catchup", m20260629_roster_catchup, Just down_m20260629_roster_catchup), + ("20260707_file_digest", m20260707_file_digest, Just down_m20260707_file_digest), + ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), + ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), + ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), + ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260529_delivery_job_senders.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260529_delivery_job_senders.hs new file mode 100644 index 0000000000..9346b16128 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260529_delivery_job_senders.hs @@ -0,0 +1,55 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- delivery_jobs.sender_group_member_ids: comma-separated decimal GroupMemberIds. +-- NULL means [] (sender-less jobs, e.g. DJRelayRemoved). One column carries +-- single- and multi-sender jobs uniformly; the per-job introduction bits live +-- in group_members.member_relations_vector (MRIntroduced). +module Simplex.Chat.Store.SQLite.Migrations.M20260529_delivery_job_senders where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260529_delivery_job_senders :: Query +m20260529_delivery_job_senders = + [sql| +DROP INDEX idx_delivery_jobs_single_sender_group_member_id; + +ALTER TABLE delivery_jobs ADD COLUMN sender_group_member_ids TEXT; + +UPDATE delivery_jobs +SET sender_group_member_ids = CAST(single_sender_group_member_id AS TEXT) +WHERE single_sender_group_member_id IS NOT NULL; + +ALTER TABLE delivery_jobs DROP COLUMN single_sender_group_member_id; +|] + +down_m20260529_delivery_job_senders :: Query +down_m20260529_delivery_job_senders = + [sql| +-- Pre-up the FK was ON DELETE CASCADE, so orphan delivery_jobs cannot +-- exist. After up the FK was dropped and orphans may accumulate. Drop +-- them here, matching pre-up semantics, before re-adding the FK column. +DELETE FROM delivery_jobs +WHERE sender_group_member_ids IS NOT NULL + AND length(sender_group_member_ids) > 0 + AND instr(sender_group_member_ids, ',') = 0 + AND NOT EXISTS ( + SELECT 1 FROM group_members + WHERE group_member_id = CAST(sender_group_member_ids AS INTEGER) + ); + +ALTER TABLE delivery_jobs ADD COLUMN single_sender_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE; + +UPDATE delivery_jobs +SET single_sender_group_member_id = + CASE + WHEN sender_group_member_ids IS NULL THEN NULL + WHEN instr(sender_group_member_ids, ',') > 0 THEN NULL + WHEN length(sender_group_member_ids) = 0 THEN NULL + ELSE CAST(sender_group_member_ids AS INTEGER) + END; + +ALTER TABLE delivery_jobs DROP COLUMN sender_group_member_ids; + +CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON delivery_jobs(single_sender_group_member_id); +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260530_client_services.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260530_client_services.hs new file mode 100644 index 0000000000..d65f8f1c67 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260530_client_services.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260530_client_services where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260530_client_services :: Query +m20260530_client_services = + [sql| +ALTER TABLE users ADD COLUMN client_service INTEGER NOT NULL DEFAULT 0; +|] + +down_m20260530_client_services :: Query +down_m20260530_client_services = + [sql| +ALTER TABLE users DROP COLUMN client_service; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260531_member_removed_at.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260531_member_removed_at.hs new file mode 100644 index 0000000000..c63e6a37f9 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260531_member_removed_at.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260531_member_removed_at :: Query +m20260531_member_removed_at = + [sql| +ALTER TABLE group_members ADD COLUMN removed_at TEXT; +|] + +down_m20260531_member_removed_at :: Query +down_m20260531_member_removed_at = + [sql| +ALTER TABLE group_members DROP COLUMN removed_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260601_relay_sent_web_domain.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260601_relay_sent_web_domain.hs new file mode 100644 index 0000000000..922a563356 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260601_relay_sent_web_domain.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260601_relay_sent_web_domain where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260601_relay_sent_web_domain :: Query +m20260601_relay_sent_web_domain = + [sql| +ALTER TABLE groups ADD COLUMN relay_sent_web_domain TEXT; +|] + +down_m20260601_relay_sent_web_domain :: Query +down_m20260601_relay_sent_web_domain = + [sql| +ALTER TABLE groups DROP COLUMN relay_sent_web_domain; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs new file mode 100644 index 0000000000..d68fea3a56 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260602_group_roster.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260602_group_roster where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260602_group_roster :: Query +m20260602_group_roster = + [sql| +ALTER TABLE groups ADD COLUMN roster_version INTEGER; +ALTER TABLE groups ADD COLUMN roster_msg_body BLOB; +ALTER TABLE groups ADD COLUMN roster_msg_chat_binding TEXT; +ALTER TABLE groups ADD COLUMN roster_msg_signatures BLOB; +ALTER TABLE groups ADD COLUMN roster_sending_owner_gm_id INTEGER; +ALTER TABLE groups ADD COLUMN roster_broker_ts TEXT; +ALTER TABLE groups ADD COLUMN roster_blob BLOB; + +CREATE TABLE rcv_roster_transfers( + roster_transfer_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + from_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + roster_version INTEGER NOT NULL, + roster_digest BLOB NOT NULL, + sending_owner_gm_id INTEGER NOT NULL, + broker_ts TEXT NOT NULL, + roster_msg_body BLOB, + roster_msg_chat_binding TEXT, + roster_msg_signatures BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON rcv_roster_transfers(group_id, from_member_id); +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON rcv_roster_transfers(from_member_id); + +ALTER TABLE files ADD COLUMN shared_msg_id BLOB; +ALTER TABLE files ADD COLUMN file_type TEXT NOT NULL DEFAULT 'normal'; +ALTER TABLE files ADD COLUMN roster_transfer_id INTEGER; +CREATE INDEX idx_files_group_id_shared_msg_id ON files(group_id, shared_msg_id); +CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); +|] + +down_m20260602_group_roster :: Query +down_m20260602_group_roster = + [sql| +DROP INDEX idx_files_roster_transfer_id; +DROP INDEX idx_files_group_id_shared_msg_id; +ALTER TABLE files DROP COLUMN roster_transfer_id; +ALTER TABLE files DROP COLUMN file_type; +ALTER TABLE files DROP COLUMN shared_msg_id; + +DROP INDEX idx_rcv_roster_transfers_from_member_id; +DROP INDEX idx_rcv_roster_transfers_group_id_from_member_id; +DROP TABLE rcv_roster_transfers; + +ALTER TABLE groups DROP COLUMN roster_blob; +ALTER TABLE groups DROP COLUMN roster_broker_ts; +ALTER TABLE groups DROP COLUMN roster_sending_owner_gm_id; +ALTER TABLE groups DROP COLUMN roster_msg_signatures; +ALTER TABLE groups DROP COLUMN roster_msg_chat_binding; +ALTER TABLE groups DROP COLUMN roster_msg_body; +ALTER TABLE groups DROP COLUMN roster_version; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs new file mode 100644 index 0000000000..112f06a0a1 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260603_simplex_name.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260603_simplex_name :: Query +m20260603_simplex_name = + [sql| +ALTER TABLE contact_profiles ADD COLUMN contact_domain TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_proof TEXT; +ALTER TABLE contact_profiles ADD COLUMN contact_domain_verified INTEGER; + +ALTER TABLE group_profiles ADD COLUMN group_domain_proof TEXT; +ALTER TABLE groups ADD COLUMN group_domain_verified INTEGER; + +ALTER TABLE user_contact_links ADD COLUMN link_priv_sig_key BLOB; + +ALTER TABLE server_operators ADD COLUMN smp_role_names INTEGER NOT NULL DEFAULT 0; +UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex' OR server_operator_tag = 'flux'; +|] + +down_m20260603_simplex_name :: Query +down_m20260603_simplex_name = + [sql| +ALTER TABLE contact_profiles DROP COLUMN contact_domain; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_proof; +ALTER TABLE contact_profiles DROP COLUMN contact_domain_verified; + +ALTER TABLE group_profiles DROP COLUMN group_domain_proof; +ALTER TABLE groups DROP COLUMN group_domain_verified; + +ALTER TABLE user_contact_links DROP COLUMN link_priv_sig_key; + +ALTER TABLE server_operators DROP COLUMN smp_role_names; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs new file mode 100644 index 0000000000..1dacc665ea --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260629_roster_catchup.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260629_roster_catchup where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +-- Roster catch-up bookkeeping. Three monotonic per-group roster versions with distinct roles - normally equal, +-- diverging across gaps and failed transfers (applied_complete <= stored <= roster_version): +-- roster_version (added in M20260602) - the GATE: highest version seen. Advanced by every accepted owner delta +-- and by a roster apply. Revert protection: a completing roster older than this is rejected, since its +-- snapshot would undo a newer applied delta. +-- stored_roster_version - the blob HELD: the version of the roster blob actually stored (written with the blob). +-- What a relay can re-serve; a failed blob receive leaves it behind the gate (which the delta still advances) +-- until a later roster completes. Relay-side; on a member it is set but the blob is unused. +-- applied_complete_roster_version - the COMPLETE frontier: highest version up to which the picture is contiguous. +-- Advances by 1 on a contiguous delta and to the roster's version on apply, but stays put on a gapped delta. +-- The subscriber's "what I have" for gap detection and the catch-up request: a value below the gate means +-- missed versions, so each following delta re-asks the forwarding relay until a roster fills the frontier. +-- Also adds group_members.roster_served_version - the newest version a relay re-served a given member, bounding +-- reflected amplification (a member can't re-trigger a full serve at a version it was already served). +-- Backfill an existing roster's stored and complete versions from roster_version: pre-upgrade the picture is +-- contiguous up to roster_version (no gap detection existed), so a fresh NULL frontier would read every group's +-- next delta as a gap and make every subscriber request a re-serve at once. +m20260629_roster_catchup :: Query +m20260629_roster_catchup = + [sql| +ALTER TABLE group_members ADD COLUMN roster_served_version INTEGER; +ALTER TABLE groups ADD COLUMN stored_roster_version INTEGER; +ALTER TABLE groups ADD COLUMN applied_complete_roster_version INTEGER; +UPDATE groups SET stored_roster_version = roster_version, applied_complete_roster_version = roster_version WHERE roster_version IS NOT NULL; +|] + +down_m20260629_roster_catchup :: Query +down_m20260629_roster_catchup = + [sql| +ALTER TABLE group_members DROP COLUMN roster_served_version; +ALTER TABLE groups DROP COLUMN stored_roster_version; +ALTER TABLE groups DROP COLUMN applied_complete_roster_version; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260707_file_digest.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260707_file_digest.hs new file mode 100644 index 0000000000..086932aa46 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260707_file_digest.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260707_file_digest where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260707_file_digest :: Query +m20260707_file_digest = + [sql| +ALTER TABLE files ADD COLUMN file_digest BLOB; +|] + +down_m20260707_file_digest :: Query +down_m20260707_file_digest = + [sql| +ALTER TABLE files DROP COLUMN file_digest; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260714_member_security_code.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260714_member_security_code.hs new file mode 100644 index 0000000000..cfcd710171 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260714_member_security_code.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260714_member_security_code :: Query +m20260714_member_security_code = + [sql| +ALTER TABLE group_members ADD COLUMN member_security_code TEXT; +ALTER TABLE group_members ADD COLUMN member_security_code_verified_at TEXT; +|] + +down_m20260714_member_security_code :: Query +down_m20260714_member_security_code = + [sql| +ALTER TABLE group_members DROP COLUMN member_security_code; +ALTER TABLE group_members DROP COLUMN member_security_code_verified_at; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs new file mode 100644 index 0000000000..5966493ca4 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260715_profile_description.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260715_profile_description :: Query +m20260715_profile_description = + [sql| +ALTER TABLE contact_profiles ADD COLUMN description TEXT; +|] + +down_m20260715_profile_description :: Query +down_m20260715_profile_description = + [sql| +ALTER TABLE contact_profiles DROP COLUMN description; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260716_signed_history.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260716_signed_history.hs new file mode 100644 index 0000000000..3b4e81f10f --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260716_signed_history.hs @@ -0,0 +1,28 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260716_signed_history :: Query +m20260716_signed_history = + [sql| +ALTER TABLE chat_items ADD COLUMN item_msg_body BLOB; +ALTER TABLE chat_items ADD COLUMN item_chat_binding TEXT; +ALTER TABLE chat_items ADD COLUMN item_signatures BLOB; +ALTER TABLE chat_items ADD COLUMN item_signed_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL; + +CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items(item_signed_by_group_member_id); +|] + +down_m20260716_signed_history :: Query +down_m20260716_signed_history = + [sql| +DROP INDEX idx_chat_items_item_signed_by_group_member_id; + +ALTER TABLE chat_items DROP COLUMN item_msg_body; +ALTER TABLE chat_items DROP COLUMN item_chat_binding; +ALTER TABLE chat_items DROP COLUMN item_signatures; +ALTER TABLE chat_items DROP COLUMN item_signed_by_group_member_id; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260720_server_roles.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260720_server_roles.hs new file mode 100644 index 0000000000..ca6982593a --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260720_server_roles.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260720_server_roles :: Query +m20260720_server_roles = + [sql| +ALTER TABLE protocol_servers ADD COLUMN role_storage INTEGER; +ALTER TABLE protocol_servers ADD COLUMN role_proxy INTEGER; +ALTER TABLE protocol_servers ADD COLUMN role_names INTEGER; +|] + +down_m20260720_server_roles :: Query +down_m20260720_server_roles = + [sql| +ALTER TABLE protocol_servers DROP COLUMN role_storage; +ALTER TABLE protocol_servers DROP COLUMN role_proxy; +ALTER TABLE protocol_servers DROP COLUMN role_names; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index de1e0a093d..a986773cb2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -293,6 +293,15 @@ Query: Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +Query: + INSERT INTO client_services + (user_id, host, port, server_key_hash, service_cert_hash, service_cert, service_priv_key) + VALUES (?,?,?,?,?,?,?) + ON CONFLICT (user_id, host, port, server_key_hash) DO NOTHING + RETURNING 1 + +Plan: + Query: INSERT INTO conn_confirmations (confirmation_id, conn_id, sender_key, e2e_snd_pub_key, ratchet_state, sender_conn_info, smp_reply_queues, smp_client_version, accepted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0); @@ -457,6 +466,27 @@ Plan: SCAN ntf_tokens_to_delete USE TEMP B-TREE FOR DISTINCT +Query: + SELECT c.service_cert_hash, c.service_cert, c.service_priv_key, c.service_id + FROM client_services c + JOIN servers s ON c.host = s.host AND c.port = s.port + WHERE c.user_id = ? AND c.host = ? AND c.port = ? + AND COALESCE(c.server_key_hash, s.key_hash) = ? + +Plan: +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING INDEX idx_server_certs_user_id_host_port (user_id=? AND host=? AND port=?) + +Query: + SELECT c.service_id, c.service_queue_count, c.service_queue_ids_hash + FROM client_services c + JOIN servers s ON s.host = c.host AND s.port = c.port + WHERE c.user_id = ? AND c.host = ? AND c.port = ? AND COALESCE(c.server_key_hash, s.key_hash) = ? AND service_id IS NOT NULL + +Plan: +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING INDEX idx_server_certs_user_id_host_port (user_id=? AND host=? AND port=?) + Query: SELECT confirmation_id, ratchet_state, own_conn_info, sender_key, e2e_snd_pub_key, sender_conn_info, smp_reply_queues, smp_client_version FROM conn_confirmations @@ -602,11 +632,11 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_rcv_id (conn_ Query: INSERT INTO rcv_queues - ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, + ( host, port, rcv_id, rcv_service_assoc, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, queue_mode, status, to_subscribe, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash, link_id, link_key, link_priv_sig_key, link_enc_fixed_data, ntf_public_key, ntf_private_key, ntf_id, rcv_ntf_dh_secret - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); Plan: @@ -657,6 +687,21 @@ Query: Plan: SEARCH snd_file_chunk_replica_recipients USING INDEX idx_snd_file_chunk_replica_recipients_snd_file_chunk_replica_id (snd_file_chunk_replica_id=?) +Query: + UPDATE client_services + SET service_id = ? + FROM servers s + WHERE client_services.user_id = ? + AND client_services.host = ? + AND client_services.port = ? + AND s.host = client_services.host + AND s.port = client_services.port + AND COALESCE(client_services.server_key_hash, s.key_hash) = ? + +Plan: +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=? AND host=? AND port=?) + Query: UPDATE conn_confirmations SET accepted = 1, @@ -746,6 +791,16 @@ Query: Plan: SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) +Query: + UPDATE rcv_queues + SET rcv_service_assoc = 0 + FROM connections c + WHERE c.conn_id = rcv_queues.conn_id AND c.user_id = ? + +Plan: +SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) +SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?) + Query: UPDATE rcv_queues SET status = ? @@ -816,7 +871,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -831,7 +886,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -846,7 +901,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -861,7 +916,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -876,7 +931,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -888,6 +943,18 @@ SEARCH q USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) SEARCH c USING PRIMARY KEY (conn_id=?) +Query: + SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id + FROM rcv_queues q + JOIN servers s ON q.host = s.host AND q.port = s.port + JOIN connections c ON q.conn_id = c.conn_id + WHERE c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ? AND q.rcv_service_assoc = 0 ORDER BY q.rcv_id LIMIT ? +Plan: +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH q USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING PRIMARY KEY (conn_id=?) + Query: SELECT c.user_id, q.conn_id, q.host, q.port, COALESCE(q.server_key_hash, s.key_hash), q.rcv_id, q.rcv_private_key, q.status, c.enable_ntfs, q.client_notice_id, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id @@ -912,6 +979,10 @@ SEARCH q USING INDEX idx_rcv_queues_to_subscribe (to_subscribe=? AND host=? AND SEARCH c USING PRIMARY KEY (conn_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) +Query: DELETE FROM client_services WHERE user_id = ? +Plan: +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=?) + Query: DELETE FROM commands WHERE command_id = ? Plan: SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) @@ -1002,6 +1073,7 @@ SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=? AND snd_queue Query: DELETE FROM users WHERE user_id = 2 Plan: SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=?) SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_user_id (user_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_user_id (user_id=?) SEARCH rcv_files USING COVERING INDEX idx_rcv_files_user_id (user_id=?) @@ -1010,6 +1082,7 @@ SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) Query: DELETE FROM users WHERE user_id = ? Plan: SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=?) SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_user_id (user_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_user_id (user_id=?) SEARCH rcv_files USING COVERING INDEX idx_rcv_files_user_id (user_id=?) @@ -1041,6 +1114,7 @@ Plan: Query: INSERT INTO servers (host, port, key_hash) VALUES (?,?,?) ON CONFLICT (host, port) DO NOTHING RETURNING 1 Plan: +SEARCH client_services USING COVERING INDEX idx_server_certs_host_port (host=? AND port=?) SEARCH inv_short_links USING COVERING INDEX idx_inv_short_links_link_id (host=? AND port=?) SEARCH commands USING COVERING INDEX idx_commands_server_commands (host=? AND port=?) SEARCH ntf_subscriptions USING COVERING INDEX idx_ntf_subscriptions_smp_host_smp_port (smp_host=? AND smp_port=?) @@ -1257,6 +1331,10 @@ Query: UPDATE rcv_queues SET rcv_primary = ?, replace_rcv_queue_id = ? WHERE con Plan: SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=? AND rcv_queue_id=?) +Query: UPDATE rcv_queues SET rcv_service_assoc = 1 WHERE host = ? AND port = ? AND rcv_id = ? +Plan: +SEARCH rcv_queues USING PRIMARY KEY (host=? AND port=? AND rcv_id=?) + Query: UPDATE rcv_queues SET to_subscribe = 0 WHERE to_subscribe = 1 Plan: SEARCH rcv_queues USING COVERING INDEX idx_rcv_queues_to_subscribe (to_subscribe=?) 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 e7188f3cc2..14e2299514 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -30,8 +30,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -41,6 +41,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -83,8 +84,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -94,6 +95,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -121,11 +123,11 @@ Plan: Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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, @@ -144,28 +146,28 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupInfo {membership} mu.group_member_id, mu.group_id, mu.index_in_group, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, -- GroupInfo {membership = GroupMember {memberProfile}} - pu.display_name, pu.full_name, pu.short_descr, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at, -- from GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -288,8 +290,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -299,6 +301,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -323,8 +326,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -334,6 +337,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -358,8 +362,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -369,6 +373,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -397,11 +402,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? @@ -443,6 +448,14 @@ Query: Plan: SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_server_operator_id (server_operator_id=?) +Query: + SELECT conn_req_contact, short_link_contact, group_id + FROM user_contact_links + WHERE user_id = ? AND short_link_contact = ? + +Plan: +SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) + Query: SELECT timed_ttl FROM chat_items @@ -451,6 +464,18 @@ Query: Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT ucl.conn_req_contact, ucl.short_link_contact, ucl.group_id + FROM user_contact_links ucl + JOIN groups g ON g.group_id = ucl.group_id + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE ucl.user_id = ? AND gp.group_domain = ? + +Plan: +SEARCH ucl USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE chat_items SET user_id = ?, updated_at = ? @@ -464,6 +489,7 @@ Query: SET display_name = ?, full_name = ?, short_descr = ?, + description = ?, image = ?, contact_link = ?, updated_at = ?, @@ -498,6 +524,23 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_profiles + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, + group_type = ?, group_link = ?, + group_web_page = ?, group_domain = CASE WHEN ? THEN ? ELSE group_domain END, domain_web_page = ?, allow_embedding = ?, group_domain_proof = ?, + preferences = ?, member_admission = ?, updated_at = ? + WHERE group_profile_id IN ( + SELECT group_profile_id + FROM groups + WHERE user_id = ? AND group_id = ? + ) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_profiles SET user_id = ?, updated_at = ? @@ -536,49 +579,14 @@ SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) Query: INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) -SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) -SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) -SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) -SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) -SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) -SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) -SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) -SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) -SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) -SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) -SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) -SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) -SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) -SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) -SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) -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: - INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, - user_id, local_display_name, contact_profile_id, created_at, updated_at, relay_link - ) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - -Plan: -SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -588,6 +596,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -611,8 +620,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -622,6 +631,43 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +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: + INSERT INTO group_members + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, + user_id, local_display_name, contact_profile_id, created_at, updated_at, relay_link + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -646,8 +692,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -657,6 +703,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -683,11 +730,12 @@ Plan: Query: SELECT - c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, + c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, 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.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, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified 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.contact_status = ? AND c.deleted = 0 @@ -700,7 +748,7 @@ Query: SELECT delivery_job_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, - single_sender_group_member_id, body, cursor_group_member_id + sender_group_member_ids, body, cursor_group_member_id FROM delivery_jobs WHERE delivery_job_id = ? @@ -907,14 +955,6 @@ Plan: SEARCH chat_items USING INDEX idx_chat_items_groups_item_viewed (user_id=?) USE TEMP B-TREE FOR ORDER BY -Query: - SELECT conn_req_contact, group_id - FROM user_contact_links - WHERE user_id = ? AND short_link_contact = ? - -Plan: -SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?) - Query: SELECT conn_req_contact, group_id FROM user_contact_links @@ -994,7 +1034,7 @@ SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_next (group_id=? A Query: SELECT gp.display_name, gp.full_name, gp.short_descr, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, gp.preferences, gp.member_admission FROM group_profiles gp JOIN groups g ON gp.group_profile_id = g.group_profile_id @@ -1017,30 +1057,15 @@ Plan: SCAN groups USE TEMP B-TREE FOR ORDER BY -Query: - SELECT i.chat_item_id - FROM chat_items i - LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? - WHERE s.group_snd_item_status_id IS NULL - AND i.user_id = ? AND i.group_id = ? - AND i.include_in_history = 1 - AND i.item_deleted = 0 - ORDER BY i.item_ts DESC, i.chat_item_id DESC - LIMIT ? - -Plan: -SEARCH i USING COVERING INDEX idx_chat_items_groups_history (user_id=? AND group_id=? AND include_in_history=? AND item_deleted=?) -SEARCH s USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) LEFT-JOIN - Query: SELECT i.chat_item_id, -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN contacts c ON m.contact_id = c.contact_id @@ -1066,6 +1091,21 @@ Plan: SEARCH f USING INTEGER PRIMARY KEY (rowid=?) SEARCH i USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT i.chat_item_id, i.item_chat_binding, i.item_signatures, i.item_msg_body, i.item_signed_by_group_member_id + FROM chat_items i + LEFT JOIN group_snd_item_statuses s ON s.chat_item_id = i.chat_item_id AND s.group_member_id = ? + WHERE s.group_snd_item_status_id IS NULL + AND i.user_id = ? AND i.group_id = ? + AND i.include_in_history = 1 + AND i.item_deleted = 0 + ORDER BY i.item_ts DESC, i.chat_item_id DESC + LIMIT ? + +Plan: +SEARCH i USING INDEX idx_chat_items_groups_history (user_id=? AND group_id=? AND include_in_history=? AND item_deleted=?) +SEARCH s USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id_group_member_id (chat_item_id=? AND group_member_id=?) LEFT-JOIN + Query: SELECT ldn_suffix FROM display_names WHERE user_id = ? AND ldn_base = ? @@ -1091,6 +1131,17 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) +Query: + SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts, + t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body + FROM rcv_roster_transfers t + JOIN files f ON f.roster_transfer_id = t.roster_transfer_id + WHERE f.file_id = ? + +Plan: +SEARCH f USING INTEGER PRIMARY KEY (rowid=?) +SEARCH t USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE chat_items SET user_id = ?, updated_at = ? @@ -1186,13 +1237,13 @@ SEARCH group_relays USING COVERING INDEX idx_group_relays_chat_relay_id (chat_re Query: INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, + ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id, user_id, local_display_name, contact_profile_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1202,6 +1253,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -1227,8 +1279,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1238,6 +1290,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -1258,9 +1311,9 @@ Query: INSERT INTO group_profiles (display_name, full_name, short_descr, description, image, group_type, group_link, public_group_id, - group_web_page, group_domain, domain_web_page, allow_embedding, + group_web_page, group_domain, domain_web_page, allow_embedding, group_domain_proof, user_id, preferences, member_admission, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1277,8 +1330,8 @@ Query: INSERT INTO groups (use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs, created_at, updated_at, chat_ts, user_member_profile_sent_at, - root_priv_key, root_pub_key, member_priv_key, public_member_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -1292,8 +1345,8 @@ Plan: Query: INSERT INTO server_operators - (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy) - VALUES (?,?,?,?,?,?,?,?,?) + (server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy) + VALUES (?,?,?,?,?,?,?,?,?,?) Plan: @@ -1335,26 +1388,26 @@ Query: -- GroupMember m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, - p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, -- quoted ChatItem ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent, -- quoted GroupMember rm.group_member_id, rm.group_id, rm.index_in_group, rm.member_id, rm.peer_chat_min_version, rm.peer_chat_max_version, rm.member_role, rm.member_category, rm.member_status, rm.show_messages, rm.member_restriction, rm.invited_by, rm.invited_by_group_member_id, rm.local_display_name, rm.contact_id, rm.contact_profile_id, rp.contact_profile_id, - rp.display_name, rp.full_name, rp.short_descr, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, - rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, + rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences, + rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified, rm.created_at, rm.updated_at, - rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, + rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at, -- deleted by GroupMember dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category, dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id, - dbp.display_name, dbp.full_name, dbp.short_descr, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, - dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, + dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences, + dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified, dbm.created_at, dbm.updated_at, - dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link + dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -1401,11 +1454,12 @@ SEARCH ri USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AN Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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, @@ -1532,10 +1586,11 @@ Query: SELECT chat_item_id, timed_ttl FROM chat_items WHERE user_id = ? AND group_id = ? + AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?) Query: SELECT chat_item_moderation_id, moderator_member_id, created_by_msg_id, moderated_at @@ -1642,6 +1697,18 @@ Plan: SEARCH i USING INDEX idx_chat_items_group_id (group_id=?) SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT i.chat_item_id + FROM chat_items i + WHERE i.user_id = ? AND i.group_id = ? + AND i.include_in_history = 1 + AND i.item_deleted = 0 + ORDER BY i.item_ts DESC, i.chat_item_id DESC + LIMIT ? + +Plan: +SEARCH i USING COVERING INDEX idx_chat_items_groups_history (user_id=? AND group_id=? AND include_in_history=? AND item_deleted=?) + Query: SELECT i.chat_item_id, i.contact_id, i.group_id, i.group_scope_tag, i.group_scope_group_member_id, i.note_folder_id FROM chat_items i @@ -1668,7 +1735,7 @@ Query: SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, - r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, g.local_display_name, f.file_type, f.file_digest FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN contacts cs ON cs.contact_id = f.contact_id @@ -1783,23 +1850,6 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) -Query: - UPDATE group_profiles - SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, - group_type = ?, group_link = ?, - group_web_page = ?, group_domain = ?, domain_web_page = ?, allow_embedding = ?, - preferences = ?, member_admission = ?, updated_at = ? - WHERE group_profile_id IN ( - SELECT group_profile_id - FROM groups - WHERE user_id = ? AND group_id = ? - ) - -Plan: -SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) -LIST SUBQUERY 1 -SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE groups SET relay_request_inv_id = ?, @@ -1864,8 +1914,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1875,6 +1925,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -1899,8 +1950,8 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1910,6 +1961,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -1947,6 +1999,17 @@ Query: Plan: +Query: + INSERT INTO rcv_file_chunks (file_id, chunk_number, chunk_agent_msg_id, created_at, updated_at) + VALUES (?,?,?,?,?) + ON CONFLICT (file_id, chunk_number) DO UPDATE SET + chunk_agent_msg_id = excluded.chunk_agent_msg_id, + chunk_stored = 0, + created_at = excluded.created_at, + updated_at = excluded.updated_at + +Plan: + Query: INSERT INTO remote_hosts (host_device_name, store_path, bind_addr, bind_iface, bind_port, ca_key, ca_cert, id_key, host_fingerprint, host_dh_pub) @@ -1958,11 +2021,12 @@ Plan: Query: SELECT -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, 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.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, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, + cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, -- 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.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, @@ -2044,11 +2108,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -2074,11 +2138,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -2104,11 +2168,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id @@ -3638,8 +3702,8 @@ Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? @@ -3659,6 +3723,15 @@ Plan: SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT ct.contact_id, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + WHERE ct.user_id = ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1 AND ct.deleted = 0 + +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete FROM xftp_file_descriptions d @@ -3682,7 +3755,7 @@ SEARCH f USING PRIMARY KEY (file_id=?) SEARCH d USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT display_name, full_name, short_descr, image, contact_link, chat_peer_type, preferences + SELECT display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, contact_domain, preferences FROM contact_profiles WHERE user_id = ? @@ -3709,6 +3782,15 @@ Plan: SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=?) SEARCH f USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +Query: + SELECT f.file_id FROM files f + JOIN rcv_files r ON r.file_id = f.file_id + WHERE f.user_id = ? AND f.group_id = ? AND f.shared_msg_id = ? AND f.file_type = ? + AND r.group_member_id = ? +Plan: +SEARCH f USING INDEX idx_files_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) +SEARCH r USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=? AND rowid=?) + Query: SELECT file_id, contact_id, group_id, note_folder_id FROM files @@ -3725,6 +3807,49 @@ Query: Plan: SEARCH files USING INTEGER PRIMARY KEY (rowid=?) +Query: + SELECT g.group_id, g.conn_full_link_to_connect + FROM groups g + JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? + WHERE g.user_id = ? AND g.conn_short_link_to_connect = ? + AND mu.member_status NOT IN (?,?,?,?) + +Plan: +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1 + AND g.business_chat IS NOT NULL +Plan: +SEARCH g USING INDEX sqlite_autoindex_groups_2 (user_id=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT g.group_id, g.conn_full_link_to_connect, g.conn_short_link_to_connect FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + WHERE g.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified = 1 + AND g.business_chat IS NULL +Plan: +SEARCH g USING INDEX sqlite_autoindex_groups_2 (user_id=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + +Query: + SELECT g.group_id, gp.public_group_id, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof + FROM groups g + JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id + JOIN group_members mu ON mu.group_id = g.group_id AND mu.contact_id = ? + WHERE g.user_id = ? AND g.relay_own_status IN (?, ?) + AND gp.public_group_id IS NOT NULL + +Plan: +SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) +SEARCH g USING INTEGER PRIMARY KEY (rowid=?) +SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT group_member_id FROM group_members @@ -3931,7 +4056,17 @@ SEARCH cs USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: - SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled + SELECT s.member_relations_vector, r.index_in_group + FROM group_members s, group_members r + WHERE s.local_display_name = ? AND r.local_display_name = ? + +Plan: +SCAN s +SEARCH r USING AUTOMATIC PARTIAL COVERING INDEX (local_display_name=?) + +Query: + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names FROM protocol_servers WHERE user_id = ? AND protocol = ? @@ -3970,22 +4105,6 @@ Query: Plan: SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) -Query: - UPDATE chat_items - SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? - AND group_member_id = ? RETURNING chat_item_id -Plan: -SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) - -Query: - UPDATE chat_items - SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? - AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id -Plan: -SEARCH chat_items USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) - Query: UPDATE chat_items SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? @@ -4053,6 +4172,19 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_category = ?, + member_status = ?, + invited_by_group_member_id = ?, + peer_chat_min_version = ?, + peer_chat_max_version = ?, + updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_id = ?, member_pub_key = ?, updated_at = ? @@ -4071,8 +4203,7 @@ SCAN group_members Query: UPDATE group_members - SET member_role = ?, - member_status = ?, + SET member_status = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? @@ -4655,12 +4786,12 @@ Query: user_id, created_by_msg_id, contact_id, group_id, group_member_id, note_folder_id, group_scope_tag, group_scope_group_member_id, -- meta item_sent, item_ts, item_content, item_content_tag, item_text, item_status, msg_content_tag, shared_msg_id, - forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, item_viewed, show_group_as_sender, msg_signed, timed_ttl, timed_delete_at, + forwarded_by_group_member_id, include_in_history, created_at, updated_at, item_live, user_mention, has_link, item_viewed, show_group_as_sender, msg_signed, item_msg_body, item_chat_binding, item_signatures, item_signed_by_group_member_id, timed_ttl, timed_delete_at, -- quote quoted_shared_msg_id, quoted_sent_at, quoted_content, quoted_sent, quoted_member_id, -- forwarded from fwd_from_tag, fwd_from_chat_name, fwd_from_msg_dir, fwd_from_contact_id, fwd_from_group_id, fwd_from_chat_item_id - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4721,8 +4852,8 @@ Query: Plan: Query: - INSERT INTO contact_profiles (display_name, full_name, short_descr, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) @@ -4731,7 +4862,7 @@ Query: INSERT INTO delivery_jobs ( group_id, worker_scope, job_scope_spec_tag, job_scope_include_pending, job_scope_support_gm_id, - single_sender_group_member_id, body, job_status, created_at, updated_at + sender_group_member_ids, body, job_status, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?) Plan: @@ -4772,8 +4903,17 @@ Plan: Query: INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, + role_storage, role_proxy, role_names, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + INSERT INTO rcv_roster_transfers + (group_id, from_member_id, roster_version, roster_digest, sending_owner_gm_id, broker_ts, + roster_msg_chat_binding, roster_msg_signatures, roster_msg_body) + VALUES (?,?,?,?,?,?,?,?,?) Plan: @@ -4903,10 +5043,11 @@ SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ? WHERE user_id = ? AND group_id = ? + AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL AND item_status = ? Plan: -SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?) +SEARCH chat_items USING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?) Query: UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ? @@ -5031,7 +5172,7 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5039,8 +5180,9 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = ?, preferences = ?, chat_peer_type = ?, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5048,8 +5190,9 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5057,8 +5200,9 @@ SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE contact_profiles - SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?, - badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ? + SET display_name = ?, full_name = ?, short_descr = ?, description = ?, image = ?, updated_at = ?, + badge_proof = ?, badge_pres_header = ?, badge_expiry = ?, badge_type = ?, badge_verified = ?, badge_extra = ?, badge_master_key = ?, badge_signature = ?, badge_key_idx = ?, + contact_domain = ?, contact_domain_proof = ? WHERE user_id = ? AND contact_profile_id = ? Plan: @@ -5072,6 +5216,15 @@ Query: Plan: SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE contact_profiles SET contact_domain_verified = ? + WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?) + +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contacts SET contact_group_member_id = NULL, contact_grp_inv_sent = 0, updated_at = ? @@ -5120,6 +5273,14 @@ Query: Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_members + SET member_status = ?, removed_at = ?, updated_at = ? + WHERE user_id = ? AND group_member_id = ? + +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_status = ?, updated_at = ? @@ -5179,6 +5340,15 @@ SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) LIST SUBQUERY 1 SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE group_profiles SET group_domain = ? + WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?) + +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +LIST SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_relays SET base_web_url = ?, updated_at = ? @@ -5245,6 +5415,17 @@ Query: Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups SET + roster_version = ?, stored_roster_version = ?, applied_complete_roster_version = ?, roster_blob = ?, + roster_sending_owner_gm_id = ?, roster_broker_ts = ?, + roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?, + updated_at = ? + WHERE group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE msg_deliveries SET delivery_status = ?, updated_at = ? @@ -5256,12 +5437,21 @@ SEARCH msg_deliveries USING INDEX idx_msg_deliveries_agent_msg_id (connection_id Query: UPDATE protocol_servers SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, - preset = ?, tested = ?, enabled = ?, updated_at = ? + preset = ?, tested = ?, enabled = ?, + role_storage = ?, role_proxy = ?, role_names = ?, updated_at = ? WHERE smp_server_id = ? Plan: SEARCH protocol_servers USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE rcv_file_chunks + SET chunk_stored = 1, updated_at = ? + WHERE file_id = ? AND chunk_number = ? + +Plan: +SEARCH rcv_file_chunks USING PRIMARY KEY (file_id=? AND chunk_number=?) + Query: UPDATE rcv_files SET to_receive = 1, user_approved_relays = ?, updated_at = ? @@ -5288,7 +5478,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE server_operators - SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? + SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ? WHERE server_operator_id = ? Plan: @@ -5323,6 +5513,14 @@ Query: Plan: SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE users + SET client_service = ? + WHERE user_id = ? + +Plan: +SEARCH users USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE users SET view_pwd_hash = ?, view_pwd_salt = ?, show_ntfs = ? @@ -5363,21 +5561,21 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5401,21 +5599,21 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5432,21 +5630,21 @@ Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id @@ -5463,11 +5661,12 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.business_group_id = ? @@ -5479,11 +5678,12 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contact_requests cr JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.contact_request_id = ? @@ -5494,10 +5694,10 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5522,10 +5722,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5543,10 +5743,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5563,10 +5763,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5583,10 +5783,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5603,10 +5803,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5623,10 +5823,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5643,10 +5843,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5663,10 +5863,30 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + 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.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.group_member_id = ? AND m.user_id = ? AND m.member_status NOT IN (?,?,?,?) +Plan: +SEARCH m USING INTEGER PRIMARY KEY (rowid=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5683,10 +5903,50 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + 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.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ? +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + 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.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?) +Plan: +SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5703,10 +5963,10 @@ SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JO Query: SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -5720,6 +5980,26 @@ SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +Query: + SELECT + m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, + m.created_at, m.updated_at, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + 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.user_contact_link_id, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM group_members m + JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) + LEFT JOIN connections c ON c.group_member_id = m.group_member_id + WHERE m.user_id = ? AND m.removed_at < ? +Plan: +SEARCH m USING INDEX idx_group_members_user_id (user_id=?) +SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN + Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i @@ -5765,6 +6045,15 @@ Plan: SEARCH i USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) +Query: + SELECT f.file_id, f.ci_file_status, f.file_path + FROM chat_items i + JOIN files f ON f.chat_item_id = i.chat_item_id + WHERE i.user_id = ? AND i.group_id = ? AND i.group_member_id IS NULL AND i.item_sent = 1 +Plan: +SEARCH i USING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) +SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) + Query: SELECT f.file_id, f.ci_file_status, f.file_path FROM chat_items i @@ -5781,11 +6070,11 @@ Query: FROM group_relays gr JOIN chat_relays cr ON cr.chat_relay_id = gr.chat_relay_id - JOIN group_members m ON m.group_member_id = gr.group_member_id - WHERE gr.group_id = ? - AND m.member_status = ? - AND gr.relay_status IN (?,?) - + JOIN group_members m ON m.group_member_id = gr.group_member_id + WHERE gr.group_id = ? + AND m.member_status = ? + AND gr.relay_status IN (?,?,?) + Plan: SEARCH gr USING INDEX idx_group_relays_group_id (group_id=?) SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) @@ -5879,7 +6168,7 @@ SEARCH remote_hosts USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators Plan: @@ -5887,16 +6176,16 @@ SCAN server_operators Query: SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy + server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy FROM server_operators WHERE server_operator_id = ? Plan: 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5907,9 +6196,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5921,9 +6210,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5935,9 +6224,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5950,9 +6239,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5964,9 +6253,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5978,9 +6267,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -5992,9 +6281,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -6006,9 +6295,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -6019,9 +6308,9 @@ SEARCH uct USING INTEGER PRIMARY KEY (rowid=?) 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 @@ -6264,6 +6553,18 @@ SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) +Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND chat_item_id = ? +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) +SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_chat_item_id (chat_item_id=?) +SEARCH chat_item_versions USING COVERING INDEX idx_chat_item_versions_chat_item_id (chat_item_id=?) +SEARCH calls USING COVERING INDEX idx_calls_chat_item_id (chat_item_id=?) +SEARCH chat_item_messages USING COVERING INDEX sqlite_autoindex_chat_item_messages_2 (chat_item_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_chat_item_id (fwd_from_chat_item_id=?) +SEARCH files USING COVERING INDEX idx_files_chat_item_id (chat_item_id=?) +SEARCH groups USING COVERING INDEX idx_groups_chat_item_id (chat_item_id=?) + Query: DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_group_shared_msg_id (user_id=? AND group_id=? AND group_member_id=?) @@ -6388,6 +6689,14 @@ SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND loca SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +Query: DELETE FROM files WHERE roster_transfer_id = ? +Plan: +SEARCH files USING COVERING INDEX idx_files_roster_transfer_id (roster_transfer_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + Query: DELETE FROM files WHERE user_id = ? AND contact_id = ? Plan: SEARCH files USING INDEX idx_files_contact_id (contact_id=?) @@ -6396,11 +6705,19 @@ SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) -Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? +Query: DELETE FROM files WHERE user_id = ? AND group_id = ? AND file_type = ? Plan: -SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH files USING INDEX idx_files_group_id (group_id=?) +SEARCH extra_xftp_file_descriptions USING COVERING INDEX idx_extra_xftp_file_descriptions_file_id (file_id=?) +SEARCH rcv_files USING INTEGER PRIMARY KEY (rowid=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_file_id (file_id=?) +SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=?) + +Query: DELETE FROM group_members WHERE member_id = ? +Plan: +SCAN group_members +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -6410,6 +6727,38 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_group_member_id (group_member_id=?) +SEARCH pending_group_messages USING COVERING INDEX idx_pending_group_messages_group_member_id (group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_forwarded_by_group_member_id (forwarded_by_group_member_id=?) +SEARCH messages USING COVERING INDEX idx_messages_author_group_member_id (author_group_member_id=?) +SEARCH connections USING COVERING INDEX idx_connections_group_member_id (group_member_id=?) +SEARCH rcv_files USING COVERING INDEX idx_rcv_files_group_member_id (group_member_id=?) +SEARCH snd_files USING COVERING INDEX idx_snd_files_group_member_id (group_member_id=?) +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_id = ? +Plan: +SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) +SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) +SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) +SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) +SEARCH received_probes USING COVERING INDEX idx_received_probes_group_member_id (group_member_id=?) +SEARCH sent_probe_hashes USING COVERING INDEX idx_sent_probe_hashes_group_member_id (group_member_id=?) +SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_member_id=?) +SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) +SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) +SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -6429,8 +6778,8 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) -SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_single_sender_group_member_id (single_sender_group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_sender_group_member_id (sender_group_member_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -6440,6 +6789,7 @@ SEARCH sent_probes USING COVERING INDEX idx_sent_probes_group_member_id (group_m SEARCH group_snd_item_statuses USING COVERING INDEX idx_group_snd_item_statuses_group_member_id (group_member_id=?) SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_moderator_member_id (moderator_member_id=?) SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_member_id (group_member_id=?) +SEARCH chat_items USING COVERING INDEX idx_chat_items_item_signed_by_group_member_id (item_signed_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_scope_group_member_id (group_scope_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_forwarded_by_group_member_id (forwarded_by_group_member_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_item_deleted_by_group_member_id (item_deleted_by_group_member_id=?) @@ -6459,6 +6809,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM groups WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_id (group_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_group_id (group_id=?) SEARCH delivery_tasks USING COVERING INDEX idx_delivery_tasks_group_id (group_id=?) @@ -6531,6 +6882,18 @@ Query: DELETE FROM rcv_file_chunks WHERE file_id = ? Plan: SEARCH rcv_file_chunks USING COVERING INDEX idx_rcv_file_chunks_file_id (file_id=?) +Query: DELETE FROM rcv_roster_transfers WHERE group_id = ? +Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=?) + +Query: DELETE FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ? +Plan: +SEARCH rcv_roster_transfers USING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=? AND from_member_id=?) + +Query: DELETE FROM rcv_roster_transfers WHERE roster_transfer_id = ? +Plan: +SEARCH rcv_roster_transfers USING INTEGER PRIMARY KEY (rowid=?) + Query: DELETE FROM received_probes WHERE created_at <= ? Plan: SEARCH received_probes USING COVERING INDEX idx_received_probes_created_at (created_at ? Plan: SEARCH chat_items USING INTEGER PRIMARY KEY (rowid>?) +Query: SELECT count(1) FROM files WHERE file_digest IS NOT NULL +Plan: +SCAN files + Query: SELECT count(1) FROM group_members Plan: SCAN group_members USING COVERING INDEX idx_group_members_invited_by_group_member_id @@ -6900,6 +7298,18 @@ Query: SELECT file_id FROM files WHERE user_id = ? AND redirect_file_id = ? Plan: SEARCH files USING INDEX idx_files_redirect_file_id (redirect_file_id=?) +Query: SELECT file_id, file_path FROM files WHERE roster_transfer_id = ? +Plan: +SEARCH files USING INDEX idx_files_roster_transfer_id (roster_transfer_id=?) + +Query: SELECT file_id, file_path FROM files WHERE user_id = ? AND group_id = ? AND file_type = ? +Plan: +SEARCH files USING INDEX idx_files_group_id (group_id=?) + +Query: SELECT file_type FROM files WHERE user_id = ? AND group_id = ? AND shared_msg_id = ? LIMIT 1 +Plan: +SEARCH files USING INDEX idx_files_group_id_shared_msg_id (group_id=? AND shared_msg_id=?) + Query: SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ? Plan: SEARCH g USING INTEGER PRIMARY KEY (rowid=?) @@ -6928,10 +7338,6 @@ Query: SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contac Plan: SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) -Query: SELECT group_id, conn_full_link_to_connect FROM groups WHERE user_id = ? AND conn_short_link_to_connect = ? -Plan: -SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=?) - Query: SELECT group_id, relay_own_status FROM groups WHERE relay_own_status IS NOT NULL ORDER BY group_id Plan: SCAN groups @@ -6968,6 +7374,22 @@ Query: SELECT max(active_order) FROM users Plan: SEARCH users +Query: SELECT member_id FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + +Query: SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1 +Plan: +SCAN group_members + +Query: SELECT member_pub_key FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + +Query: SELECT member_pub_key FROM group_members WHERE member_role = 'moderator' +Plan: +SCAN group_members + Query: SELECT member_relations_vector FROM group_members WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -6976,6 +7398,18 @@ Query: SELECT member_relations_vector FROM group_members WHERE group_member_id = Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT member_role FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + +Query: SELECT member_role, member_pub_key FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + +Query: SELECT member_role, member_pub_key FROM group_members WHERE member_id = ? +Plan: +SCAN group_members + Query: SELECT member_status FROM group_members WHERE local_display_name = ? Plan: SCAN group_members @@ -6984,6 +7418,10 @@ Query: SELECT member_status FROM group_members WHERE member_role = 'relay' Plan: SCAN group_members +Query: SELECT member_status, removed_at FROM group_members WHERE local_display_name = ? +Plan: +SCAN group_members + Query: SELECT member_xcontact_id, member_welcome_shared_msg_id FROM group_members WHERE user_id = ? AND group_id = ? AND group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -7004,6 +7442,10 @@ Query: SELECT relay_own_status FROM groups WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT relay_sent_web_domain FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT relay_status FROM group_relays Plan: SCAN group_relays @@ -7012,14 +7454,50 @@ Query: SELECT relay_status FROM group_relays WHERE group_relay_id = ? Plan: SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT roster_blob FROM groups WHERE roster_blob IS NOT NULL +Plan: +SCAN groups + +Query: SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob, stored_roster_version FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT roster_served_version FROM group_members WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT roster_transfer_id FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ? +Plan: +SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=? AND from_member_id=?) + +Query: SELECT roster_version FROM groups +Plan: +SCAN groups + +Query: SELECT roster_version FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: SELECT roster_version FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ? +Plan: +SEARCH rcv_roster_transfers USING INDEX idx_rcv_roster_transfers_group_id_from_member_id (group_id=? AND from_member_id=?) + Query: SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT shared_msg_id FROM chat_items WHERE shared_msg_id IS NOT NULL ORDER BY chat_item_id DESC LIMIT 1 +Plan: +SCAN chat_items + Query: SELECT should_sync FROM connections_sync WHERE connections_sync_id = 1 Plan: SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?) +Query: SELECT stored_roster_version FROM groups WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: SELECT summary_current_members_count FROM groups WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -7044,6 +7522,10 @@ Query: SELECT xgrplinkmem_received FROM group_members WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE chat_items SET item_msg_body = ?, item_chat_binding = ?, item_signatures = ?, item_signed_by_group_member_id = ? WHERE chat_item_id = ? AND include_in_history = 1 +Plan: +SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ? WHERE user_id = ? AND item_status = ? Plan: SEARCH chat_items USING INDEX idx_chat_items_user_id_item_status (user_id=? AND item_status=?) @@ -7104,6 +7586,14 @@ Query: UPDATE connections_sync SET should_sync = 1 WHERE connections_sync_id = 1 Plan: SEARCH connections_sync USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user_id = ? AND contact_profile_id = ? +Plan: +SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE contact_profiles SET image = ? WHERE display_name = ? +Plan: +SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?) + Query: UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ? Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) @@ -7236,6 +7726,10 @@ Query: UPDATE group_members SET member_profile_id = ?, updated_at = ? WHERE grou Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_members SET member_pub_key = ?, member_role = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -7248,6 +7742,18 @@ Query: UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_memb Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_members SET member_role = ?, member_pub_key = NULL WHERE local_display_name = ? +Plan: +SCAN group_members + +Query: UPDATE group_members SET member_security_code = ?, member_security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE group_members SET roster_served_version = ?, updated_at = ? WHERE group_member_id = ? +Plan: +SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) @@ -7268,6 +7774,14 @@ Query: UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_rela Plan: SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET applied_complete_roster_version = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -7296,6 +7810,14 @@ Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE use Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + +Query: UPDATE groups SET group_domain_verified = NULL WHERE user_id = ? AND group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) @@ -7328,6 +7850,14 @@ Query: UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? W Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET roster_blob = ? WHERE roster_blob IS NOT NULL +Plan: +SCAN groups + +Query: UPDATE groups SET roster_version = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET send_rcpts = NULL Plan: SCAN groups diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 2d7ea7ff70..e42b537225 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -28,7 +28,11 @@ CREATE TABLE contact_profiles( badge_extra TEXT, badge_master_key BLOB, badge_signature BLOB, - badge_key_idx INTEGER + badge_key_idx INTEGER, + contact_domain TEXT, + contact_domain_proof TEXT, + contact_domain_verified INTEGER, + description TEXT ) STRICT; CREATE TABLE users( user_id INTEGER PRIMARY KEY, @@ -48,7 +52,8 @@ CREATE TABLE users( ui_themes TEXT, active_order INTEGER NOT NULL DEFAULT 0, auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, - is_user_chat_relay INTEGER NOT NULL DEFAULT 0, -- 1 for active user + is_user_chat_relay INTEGER NOT NULL DEFAULT 0, + client_service 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 @@ -138,7 +143,8 @@ CREATE TABLE group_profiles( group_web_page TEXT, group_domain TEXT, domain_web_page INTEGER, - allow_embedding INTEGER + allow_embedding INTEGER, + group_domain_proof TEXT ) STRICT; CREATE TABLE groups( group_id INTEGER PRIMARY KEY, -- local group ID @@ -190,7 +196,18 @@ CREATE TABLE groups( relay_request_retries INTEGER NOT NULL DEFAULT 0, relay_request_delay INTEGER NOT NULL DEFAULT 0, relay_request_execute_at TEXT NOT NULL DEFAULT '1970-01-01 00:00:00', - relay_inactive_at TEXT, -- received + relay_inactive_at TEXT, + relay_sent_web_domain TEXT, + roster_version INTEGER, + roster_msg_body BLOB, + roster_msg_chat_binding TEXT, + roster_msg_signatures BLOB, + roster_sending_owner_gm_id INTEGER, + roster_broker_ts TEXT, + roster_blob BLOB, + group_domain_verified INTEGER, + stored_roster_version INTEGER, + applied_complete_roster_version INTEGER, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -234,6 +251,10 @@ CREATE TABLE group_members( member_relations_vector BLOB, relay_link BLOB, member_pub_key BLOB, + removed_at TEXT, + roster_served_version INTEGER, + member_security_code TEXT, + member_security_code_verified_at TEXT, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -275,7 +296,11 @@ CREATE TABLE files( file_crypto_key BLOB, file_crypto_nonce BLOB, note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE, - redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE + redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE, + shared_msg_id BLOB, + file_type TEXT NOT NULL DEFAULT 'normal', + roster_transfer_id INTEGER, + file_digest BLOB ) STRICT; CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -380,6 +405,7 @@ CREATE TABLE user_contact_links( short_link_contact BLOB, short_link_data_set INTEGER NOT NULL DEFAULT 0, short_link_large_data_set INTEGER NOT NULL DEFAULT 0, + link_priv_sig_key BLOB, UNIQUE(user_id, local_display_name) ) STRICT; CREATE TABLE contact_requests( @@ -482,7 +508,11 @@ CREATE TABLE chat_items( show_group_as_sender INTEGER NOT NULL DEFAULT 0, has_link INTEGER NOT NULL DEFAULT 0, msg_signed TEXT, - item_viewed INTEGER NOT NULL DEFAULT 0 + item_viewed INTEGER NOT NULL DEFAULT 0, + item_msg_body BLOB, + item_chat_binding TEXT, + item_signatures BLOB, + item_signed_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL ) STRICT; CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE chat_item_messages( @@ -535,6 +565,9 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', + role_storage INTEGER, + role_proxy INTEGER, + role_names INTEGER, UNIQUE(user_id, host, port) ) STRICT; CREATE TABLE xftp_file_descriptions( @@ -687,6 +720,8 @@ CREATE TABLE server_operators( xftp_role_proxy INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + smp_role_names INTEGER NOT NULL DEFAULT 0 ) STRICT; CREATE TABLE usage_conditions( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -747,7 +782,6 @@ CREATE TABLE delivery_jobs( job_scope_spec_tag TEXT, job_scope_include_pending INTEGER, job_scope_support_gm_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, - single_sender_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE, body BLOB, cursor_group_member_id INTEGER, job_status TEXT NOT NULL, @@ -755,6 +789,8 @@ CREATE TABLE delivery_jobs( failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + sender_group_member_ids TEXT ) STRICT; CREATE TABLE group_member_status_predicates( member_status TEXT NOT NULL PRIMARY KEY, @@ -794,6 +830,20 @@ CREATE TABLE group_relays( , base_web_url TEXT ) STRICT; +CREATE TABLE rcv_roster_transfers( + roster_transfer_id INTEGER PRIMARY KEY, + group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE, + from_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + roster_version INTEGER NOT NULL, + roster_digest BLOB NOT NULL, + sending_owner_gm_id INTEGER NOT NULL, + broker_ts TEXT NOT NULL, + roster_msg_body BLOB, + roster_msg_chat_binding TEXT, + roster_msg_signatures BLOB, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +) STRICT; CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -1233,9 +1283,6 @@ CREATE INDEX idx_delivery_jobs_group_id ON delivery_jobs(group_id); CREATE INDEX idx_delivery_jobs_job_scope_support_gm_id ON delivery_jobs( job_scope_support_gm_id ); -CREATE INDEX idx_delivery_jobs_single_sender_group_member_id ON delivery_jobs( - single_sender_group_member_id -); CREATE INDEX idx_delivery_jobs_next ON delivery_jobs( group_id, worker_scope, @@ -1316,6 +1363,21 @@ ON groups( relay_request_group_link ) WHERE relay_request_group_link IS NOT NULL; +CREATE UNIQUE INDEX idx_rcv_roster_transfers_group_id_from_member_id ON rcv_roster_transfers( + group_id, + from_member_id +); +CREATE INDEX idx_rcv_roster_transfers_from_member_id ON rcv_roster_transfers( + from_member_id +); +CREATE INDEX idx_files_group_id_shared_msg_id ON files( + group_id, + shared_msg_id +); +CREATE INDEX idx_files_roster_transfer_id ON files(roster_transfer_id); +CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items( + item_signed_by_group_member_id +); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index bd0d22f379..050cf7dc97 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -33,13 +33,14 @@ import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Type.Equality import Simplex.Chat.Badges (BadgeRow, badgeToRow, rowToBadge, verifyBadge_) +import Simplex.Chat.Names (SimplexDomainProof, SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Messages import Simplex.Chat.Remote.Types import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), UserId, connMode) +import Simplex.Messaging.Agent.Protocol (AConnShortLink (..), AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionRequestUri, CreatedConnLink (..), SimplexDomain, UserId, connMode) import Simplex.Messaging.Agent.Store (AnyStoreError (..)) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.Common (withSavepoint) @@ -48,6 +49,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Encoding.String (StrJSON (..)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (AnyError (..)) @@ -319,14 +321,14 @@ createConnection_ db userId connType entityId acId connStatus connChatVersion pe ent ct = if connType == ct then entityId else Nothing createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 -createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, image} = do +createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, description, image} = do DB.execute db [sql| - INSERT INTO contact_profiles (display_name, full_name, short_descr, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) |] - (displayName, fullName, shortDescr, image, userId, Just (BI True), createdAt, createdAt) + (displayName, fullName, shortDescr, description, image, userId, Just (BI True), createdAt, createdAt) insertedRowId db updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO () @@ -413,13 +415,13 @@ createContact db cxt user profile = do void $ createContact_ db cxt user profile emptyChatPrefs Nothing "" currentTs createContact_ :: DB.Connection -> StoreCxt -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> ExceptT StoreError IO ContactId -createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = +createContact_ db cxt User {userId} Profile {displayName, fullName, shortDescr, description, image, contactLink, contactDomain, peerType, badge, preferences} ctUserPreferences prepared localAlias currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do badgeVerified <- verifyBadge_ (badgeKeys cxt) badge DB.execute db - "INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" - ((displayName, fullName, shortDescr, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified) + "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, chat_peer_type, user_id, local_alias, preferences, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, contact_domain, contact_domain_proof) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + ((displayName, fullName, shortDescr, description, image, contactLink, peerType) :. (userId, localAlias, preferences, currentTs, currentTs) :. badgeToRow badge badgeVerified :. contactDomainToRow contactDomain) profileId <- insertedRowId db DB.execute db @@ -486,13 +488,15 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) -type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, 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) :. BadgeRow +type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, 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) :. BadgeRow :. ContactDomainRow type ContactRow = Only ContactId :. ContactRow' +type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt) + toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow) :. connRow) = - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} +toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn @@ -501,6 +505,15 @@ toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName groupDirectInv = toGroupDirectInvitation groupDirectInvRow in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} +rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim +rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_ + +rowToDomainVerified :: ContactDomainRow -> Maybe Bool +rowToDomainVerified (_, _, domainVerification_) = unBI <$> domainVerification_ + +contactDomainToRow :: Maybe SimplexDomainClaim -> (Maybe SimplexDomain, Maybe SimplexDomainProof) +contactDomainToRow d = (claimDomain <$> d, proof =<< d) + toPreparedContact :: PreparedContactRow -> Maybe PreparedContact toPreparedContact (connFullLink, connShortLink, welcomeSharedMsgId, requestSharedMsgId) = (\cl@(ACCL m _) -> PreparedContact {connLinkToConnect = cl, uiConnLinkType = connMode m, welcomeSharedMsgId, requestSharedMsgId}) @@ -524,37 +537,37 @@ getProfileById db userId profileId = do DB.query db [sql| - SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, - cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx + SELECT cp.contact_profile_id, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, cp.preferences, -- , ct.user_preferences + cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified FROM contact_profiles cp WHERE cp.user_id = ? AND cp.contact_profile_id = ? |] (userId, profileId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest -toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow) = do - let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} +toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do + let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} 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.chat_peer_type, ucp.preferences, - 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, u.is_user_chat_relay, - ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx + 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.description, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, + 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.is_user_chat_relay, u.client_service, u.ui_themes, + ucp.badge_proof, ucp.badge_pres_header, ucp.badge_expiry, ucp.badge_type, ucp.badge_verified, ucp.badge_extra, ucp.badge_master_key, ucp.badge_signature, ucp.badge_key_idx, ucp.contact_domain, ucp.contact_domain_proof, ucp.contact_domain_verified 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 :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides, BoolInt) :. BadgeRow -> User -toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes, BI userChatRelay) :. badgeRow) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts = BoolDef autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, uiThemes, userChatRelay = BoolDef userChatRelay} +toUser :: UTCTime -> (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, BoolInt, Maybe UIThemeEntityOverrides) :. BadgeRow :. ContactDomainRow -> User +toUser now ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, description, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI userChatRelay, BI clientService, uiThemes) :. badgeRow :. domainRow) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, userChatRelay = BoolDef userChatRelay, clientService = BoolDef clientService, uiThemes} where - profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} + profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ @@ -670,26 +683,26 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact) :. GroupKeysRow :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow -type PublicGroupAccessRow = (Maybe Text, Maybe Text, Maybe BoolInt, Maybe BoolInt) +type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof) -type GroupMemberRow = (GroupMemberId, GroupId, 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, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact) +type GroupMemberRow = (GroupMemberId, GroupId, 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, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime) -type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow +type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. userMemberRow) = +toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow) groupKeys = toGroupKeys publicGroupId_ groupKeysRow groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} - businessChat = toBusinessChatInfo businessRow + businessChat = toBusinessChatInfo (toPublicGroupAccess accessRow >>= groupDomainClaim) businessRow preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} - in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, customData, membersRequireAttention, viaGroupLinkUri, groupKeys} + in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, groupDomainVerified = unBI <$> groupDomainVerified} toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case @@ -704,14 +717,14 @@ toPublicGroupProfile _ _ _ _ = Nothing publicGroupAccessRow :: Maybe PublicGroupProfile -> PublicGroupAccessRow publicGroupAccessRow pgp = case pgp >>= publicGroupAccess of - Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} -> - (groupWebPage, groupDomain, Just (BI domainWebPage), Just (BI allowEmbedding)) - Nothing -> (Nothing, Nothing, Nothing, Nothing) + Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding} -> + (groupWebPage, claimDomain <$> groupDomainClaim, Just (BI domainWebPage), Just (BI allowEmbedding), proof =<< groupDomainClaim) + Nothing -> (Nothing, Nothing, Nothing, Nothing, Nothing) toPublicGroupAccess :: PublicGroupAccessRow -> Maybe PublicGroupAccess -toPublicGroupAccess (groupWebPage, groupDomain, domainWebPage_, allowEmbedding_) - | isJust groupWebPage || isJust groupDomain || domainWebPage || allowEmbedding = - Just PublicGroupAccess {groupWebPage, groupDomain, domainWebPage, allowEmbedding} +toPublicGroupAccess (groupWebPage, groupDomain_, domainWebPage_, allowEmbedding_, groupDomainProof_) + | isJust groupWebPage || isJust groupDomain_ || domainWebPage || allowEmbedding = + Just PublicGroupAccess {groupWebPage, groupDomainClaim = (`SimplexDomainClaim` groupDomainProof_) . StrJSON <$> groupDomain_, domainWebPage, allowEmbedding} | otherwise = Nothing where domainWebPage = maybe False unBI domainWebPage_ @@ -724,12 +737,13 @@ toGroupKeys (Just publicGroupId) (rootPrivKey_, rootPubKey_, Just memberPrivKey) toGroupKeys _ _ = Nothing toGroupMember :: UTCTime -> Int64 -> GroupMemberRow -> GroupMember -toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink)) = +toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) = let memberProfile = rowToLocalProfile now profileRow memberSettings = GroupMemberSettings {showMessages} blockedByAdmin = maybe False mrsBlocked memberRestriction_ invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing + memberVerifiedCode = SecurityCode <$> memberCode_ <*> memberCodeVerifiedAt_ memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer supportChat = case supportChatTs_ of Just chatTs -> @@ -749,10 +763,10 @@ groupMemberQuery = [sql| SELECT m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction, - m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, - p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, + m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, + p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, 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.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, @@ -767,12 +781,12 @@ toContactMember now cxt User {userContactId} (memberRow :. connRow) = (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile -rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences) :. badgeRow) = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} +rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, preferences) :. badgeRow :. domainRow) = + LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, localAlias, preferences} -toBusinessChatInfo :: BusinessChatInfoRow -> Maybe BusinessChatInfo -toBusinessChatInfo (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId} -toBusinessChatInfo _ = Nothing +toBusinessChatInfo :: Maybe SimplexDomainClaim -> BusinessChatInfoRow -> Maybe BusinessChatInfo +toBusinessChatInfo businessDomain (Just chatType, Just businessId, Just customerId) = Just BusinessChatInfo {chatType, businessId, customerId, businessDomain} +toBusinessChatInfo _ _ = Nothing groupInfoQuery :: Query groupInfoQuery = groupInfoQueryFields <> " " <> groupInfoQueryFrom @@ -783,21 +797,21 @@ groupInfoQueryFields = SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.short_descr, g.local_alias, gp.description, gp.image, gp.group_type, gp.group_link, gp.public_group_id, - gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, + gp.group_web_page, gp.group_domain, gp.domain_web_page, gp.allow_embedding, gp.group_domain_proof, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.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.use_relays, g.relay_own_status, - g.ui_themes, g.summary_current_members_count, g.public_member_count, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, + g.ui_themes, g.summary_current_members_count, g.public_member_count, g.roster_version, g.custom_data, g.chat_item_ttl, g.members_require_attention, g.via_group_link_uri, g.group_domain_verified, g.root_priv_key, g.root_pub_key, g.member_priv_key, -- GroupMember - membership mu.group_member_id, mu.group_id, mu.index_in_group, 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.chat_peer_type, pu.local_alias, pu.preferences, - pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, + pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences, + pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified, mu.created_at, mu.updated_at, - mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link + mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at |] groupInfoQueryFrom :: Query diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 38b0e91a8a..b5bb0a124b 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -18,6 +18,7 @@ import Simplex.Chat.View (ChatResponseEvent, smpProxyModeStr) import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..)) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) +import System.IO (BufferMode (..), hSetBuffering, stdout) import System.Terminal (withTerminal) simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () @@ -27,19 +28,29 @@ simplexChatCLI cfg server_ = do simplexChatCLI' cfg opts server_ simplexChatCLI' :: ChatConfig -> ChatOpts -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () -simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServerPort} server_ = do +simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServerPort, coreOptions = CoreChatOpts {headless}} server_ = do if null chatCmd then case chatServerPort of Just chatPort -> case server_ of Just server -> server chatPort cfg opts Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure - _ -> runCLI + _ + | headless -> do + hSetBuffering stdout LineBuffering + welcome cfg opts + simplexChatCore cfg opts runHeadless + | otherwise -> runCLI else simplexChatCore cfg opts runCommand where runCLI = do welcome cfg opts t <- withTerminal pure simplexChatTerminal cfg opts t + runHeadless user cc = forever $ do + (rh, r) <- atomically $ readTBQueue $ outputQ cc + case r of + Left _ -> printResponseEvent (rh, Just user) cfg r + Right _ -> pure () runCommand user cc = do when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do (_, r) <- atomically . readTBQueue $ outputQ cc diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 10f492c328..dce1a2a9c9 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -4,6 +4,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} @@ -19,6 +20,7 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilyDependencies #-} +{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} @@ -34,6 +36,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.Attoparsec.Combinator (lookAhead) import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString, pack, unpack) import qualified Data.ByteString.Char8 as B @@ -46,16 +49,18 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock (UTCTime) +import Data.Type.Equality (testEquality, (:~:) (Refl)) import Data.Typeable (Typeable) import Data.Word (Word16) import Simplex.Chat.Badges (BadgeInfo (..), BadgeProof (..), BadgeStatus (..), LocalBadge (..), localBadgeInfo, localBadgeStatus, mkBadgeStatus, verifyBadge) +import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Messaging.Crypto.BBS (BBSPublicKey) import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink, ConnectionMode (..), ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexDomain, SimplexNameInfo (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -137,17 +142,19 @@ data User = User showNtfs :: Bool, sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool, - autoAcceptMemberContacts :: BoolDef, + autoAcceptMemberContacts :: Bool, userMemberProfileUpdatedAt :: Maybe UTCTime, - uiThemes :: Maybe UIThemeEntityOverrides, - userChatRelay :: BoolDef + userChatRelay :: BoolDef, + clientService :: BoolDef, + uiThemes :: Maybe UIThemeEntityOverrides } deriving (Show) data NewUser = NewUser { profile :: Maybe Profile, pastTimestamp :: Bool, - userChatRelay :: Bool + userChatRelay :: BoolDef, + clientService :: BoolDef } deriving (Show) @@ -488,9 +495,11 @@ data GroupInfo = GroupInfo uiThemes :: Maybe UIThemeEntityOverrides, customData :: Maybe CustomData, groupSummary :: GroupSummary, + rosterVersion :: Maybe VersionRoster, membersRequireAttention :: Int, viaGroupLinkUri :: Maybe ConnReqContact, - groupKeys :: Maybe GroupKeys + groupKeys :: Maybe GroupKeys, + groupDomainVerified :: Maybe Bool } deriving (Eq, Show) @@ -638,12 +647,6 @@ groupFeatureUserAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupInfo - groupFeatureUserAllowed feature GroupInfo {membership = GroupMember {memberRole}, fullGroupPreferences} = groupFeatureMemberAllowed' feature memberRole fullGroupPreferences --- A connection link in a profile description enables a direct connection, so a description --- keeps its links only when both SimpleX links and direct messages are allowed. -groupUserAllowSimplexLinks :: GroupInfo -> Bool -groupUserAllowSimplexLinks g = - groupFeatureUserAllowed SGFSimplexLinks g && groupFeatureUserAllowed SGFDirectMessages g - mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) @@ -691,11 +694,13 @@ data Profile = Profile { displayName :: ContactName, fullName :: Text, shortDescr :: Maybe Text, -- short description limited to 160 characters + description :: Maybe Text, -- long description (businesses/bots); redacted per group policy in member profiles image :: Maybe ImageData, contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, peerType :: Maybe ChatPeerType, - badge :: Maybe BadgeProof + badge :: Maybe BadgeProof, + contactDomain :: Maybe SimplexDomainClaim -- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- - contact_profile_id -- - incognito @@ -728,21 +733,22 @@ instance TextEncoding ChatPeerType where profileFromName :: ContactName -> Profile profileFromName displayName = - Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing} + Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, preferences = Nothing, peerType = Nothing, badge = Nothing, contactDomain = Nothing} -- check if profiles match ignoring preferences profilesMatch :: LocalProfile -> LocalProfile -> Bool profilesMatch - LocalProfile {displayName = n1, fullName = fn1, image = i1} - LocalProfile {displayName = n2, fullName = fn2, image = i2} = - n1 == n2 && fn1 == fn2 && i1 == i2 + LocalProfile {displayName = n1, fullName = fn1, image = i1, shortDescr = d1, description = desc1} + LocalProfile {displayName = n2, fullName = fn2, image = i2, shortDescr = d2, description = desc2} = + n1 == n2 && fn1 == fn2 && i1 == i2 && d1 == d2 && desc1 == desc2 -- equal for profile-update detection: badge proofs are re-generated for every presentation, -- so compare badges by disclosed info (not proof bytes) - a re-presentation of the same badge is a no-op sameProfileContent :: Profile -> Profile -> Bool sameProfileContent p@Profile {badge = b} p'@Profile {badge = b'} = - p {badge = Nothing} == p' {badge = Nothing} && (proofInfo <$> b) == (proofInfo <$> b') + clearProofs p == clearProofs p' && (proofInfo <$> b) == (proofInfo <$> b') where + clearProofs pr@Profile {contactDomain} = pr {badge = Nothing, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} proofInfo :: BadgeProof -> BadgeInfo proofInfo (BadgeProof _ _ _ info) = info @@ -773,34 +779,37 @@ data LocalProfile = LocalProfile displayName :: ContactName, fullName :: Text, shortDescr :: Maybe Text, + description :: Maybe Text, image :: Maybe ImageData, contactLink :: Maybe ConnLinkContact, preferences :: Maybe Preferences, peerType :: Maybe ChatPeerType, localBadge :: Maybe LocalBadge, - localAlias :: LocalAlias + localAlias :: LocalAlias, + contactDomain :: Maybe SimplexDomainClaim, + contactDomainVerified :: Maybe Bool } deriving (Eq, Show) localProfileId :: LocalProfile -> ProfileId localProfileId LocalProfile {profileId} = profileId -toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> LocalProfile -toLocalProfile profileId Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge} localAlias now verified = - LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge, localAlias} +toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe Bool -> LocalProfile +toLocalProfile profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified = + LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified} where - localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now verified info)) <$> badge + localBadge = (\b@(BadgeProof _ _ _ info) -> PeerBadge b (mkBadgeStatus now badgeVerified info)) <$> badge fromLocalProfile :: LocalProfile -> Profile -fromLocalProfile LocalProfile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, localBadge} = - Profile {displayName, fullName, shortDescr, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge} +fromLocalProfile LocalProfile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, localBadge, contactDomain} = + -- the name proof is re-signed on each send + Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, badge = localBadge >>= wireBadge, contactDomain = (\d -> d {proof = Nothing} :: SimplexDomainClaim) <$> contactDomain} where - -- any stored peer proof rides the wire (receivers verify independently); the own credential is presented fresh, and a display-only badge never sends wireBadge :: LocalBadge -> Maybe BadgeProof wireBadge = \case - PeerBadge b _ -> Just b - OwnBadge _ _ -> Nothing - ShownBadge _ _ -> Nothing + PeerBadge b _ -> Just b -- stored peer proof sent as is + OwnBadge _ _ -> Nothing -- the own credential is not sent, proof is generated on send + ShownBadge _ _ -> Nothing -- a display-only badge is not sent profileBadgeVerified :: Map Int BBSPublicKey -> LocalProfile -> Profile -> IO (Maybe Bool) profileBadgeVerified keys LocalProfile {localBadge} Profile {badge = newBadge} = @@ -839,7 +848,7 @@ instance ToField GroupType where toField = toField . textEncode data PublicGroupAccess = PublicGroupAccess { groupWebPage :: Maybe Text, - groupDomain :: Maybe Text, + groupDomainClaim :: Maybe SimplexDomainClaim, domainWebPage :: Bool, allowEmbedding :: Bool } @@ -885,8 +894,13 @@ instance FromJSON ImageData where parseJSON = fmap ImageData . J.parseJSON instance ToJSON ImageData where - toJSON (ImageData t) = J.toJSON t - toEncoding (ImageData t) = J.toEncoding t + toJSON (ImageData t) = J.toJSON $ safeImageData t + toEncoding (ImageData t) = J.toEncoding $ safeImageData t + +safeImageData :: Text -> Text +safeImageData t + | "data:" `T.isPrefixOf` t = t + | otherwise = "" instance ToField ImageData where toField (ImageData t) = toField t @@ -1014,6 +1028,11 @@ newtype MemberKey = MemberKey C.PublicKeyEd25519 deriving (Eq, Show) deriving newtype (StrEncoding) +-- Binary encoding for the roster blob; delegates to the Ed25519 key. +instance Encoding MemberKey where + smpEncode (MemberKey k) = smpEncode k + smpP = MemberKey <$> smpP + instance FromJSON MemberKey where parseJSON = strParseJSON "MemberKey" @@ -1033,7 +1052,9 @@ data MemberInfo = MemberInfo data BusinessChatInfo = BusinessChatInfo { chatType :: BusinessChatType, businessId :: MemberId, - customerId :: MemberId + customerId :: MemberId, + -- TODO [names] sent in protocol in GroupInvitation + businessDomain :: Maybe SimplexDomainClaim } deriving (Eq, Show) @@ -1120,7 +1141,10 @@ data GroupMember = GroupMember updatedAt :: UTCTime, supportChat :: Maybe GroupSupportChat, memberPubKey :: Maybe C.PublicKeyEd25519, - relayLink :: Maybe ShortLinkContact + relayLink :: Maybe ShortLinkContact, + -- out-of-band verified security code for connectionless (channel) members; + -- regular members carry it in activeConn instead (see memberSecurityCode) + memberVerifiedCode :: Maybe SecurityCode } deriving (Eq, Show) @@ -1200,7 +1224,7 @@ incognitoMembershipProfile GroupInfo {membership = m@GroupMember {memberProfile} | otherwise = Nothing memberSecurityCode :: GroupMember -> Maybe SecurityCode -memberSecurityCode GroupMember {activeConn} = connectionCode =<< activeConn +memberSecurityCode GroupMember {activeConn, memberVerifiedCode} = memberVerifiedCode <|> (connectionCode =<< activeConn) memberBlocked :: GroupMember -> Bool memberBlocked m = blockedByAdmin m || not (showMessages $ memberSettings m) @@ -1535,11 +1559,38 @@ instance ToJSON InlineFileMode where toJSON = J.String . textEncode toEncoding = JE.text . textEncode +-- Discriminates ordinary chat files from the roster blob file, so the receive +-- completion / cancel paths branch on the type rather than on chat_item_id (note +-- folders and redirects also lack a chat item). +data FileType = FTNormal | FTRoster + deriving (Eq, Show) + +instance TextEncoding FileType where + textEncode = \case + FTNormal -> "normal" + FTRoster -> "roster" + textDecode = \case + "normal" -> Just FTNormal + "roster" -> Just FTRoster + _ -> Nothing + +instance FromField FileType where fromField = fromTextField_ textDecode + +instance ToField FileType where toField = toField . textEncode + +instance FromJSON FileType where + parseJSON = textParseJSON "FileType" + +instance ToJSON FileType where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + data RcvFileTransfer = RcvFileTransfer { fileId :: FileTransferId, xftpRcvFile :: Maybe XFTPRcvFile, fileInvitation :: FileInvitation, fileStatus :: RcvFileStatus, + fileType :: FileType, rcvFileInline :: Maybe InlineFileMode, senderDisplayName :: ContactName, chunkSize :: Integer, @@ -1742,6 +1793,49 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation type ConnReqContact = ConnectionRequestUri 'CMContact +data ConnectTarget (m :: ConnectionMode) where + CTFullContact :: ConnectionRequestUri 'CMContact -> ConnectTarget 'CMContact + CTShortContact :: ContactNameOrLink -> ConnectTarget 'CMContact + CTDomain :: SimplexDomain -> ConnectTarget 'CMContact + CTInv :: ConnectionLink 'CMInvitation -> ConnectTarget 'CMInvitation + +data ContactNameOrLink = CTName SimplexNameInfo | CTLink (ConnShortLink 'CMContact) + deriving (Eq, Show) + +deriving instance Eq (ConnectTarget m) + +deriving instance Show (ConnectTarget m) + +data AConnectTarget = forall m. ConnectionModeI m => ACTarget (SConnectionMode m) (ConnectTarget m) + deriving (ToJSON, FromJSON) via (StrJSON "AConnectTarget" AConnectTarget) + +instance Eq AConnectTarget where + ACTarget m t == ACTarget m' t' = case testEquality m m' of + Just Refl -> t == t' + _ -> False + +deriving instance Show AConnectTarget + +instance StrEncoding AConnectTarget where + strEncode (ACTarget _ t) = case t of + CTFullContact cr -> strEncode cr + CTShortContact (CTName n) -> strEncode n + CTShortContact (CTLink sl) -> strEncode sl + CTDomain d -> strEncode d + CTInv l -> strEncode l + strP = + (ACTarget SCMContact . CTShortContact . CTName <$> (lookAhead nameStart *> strP)) + <|> (aConnectTarget <$> strP) + <|> (ACTarget SCMContact . CTDomain <$> strP) + where + nameStart = "@" <|> "#" <|> "simplex:/name" + +aConnectTarget :: AConnectionLink -> AConnectTarget +aConnectTarget (ACL SCMInvitation cl) = ACTarget SCMInvitation (CTInv cl) +aConnectTarget (ACL SCMContact cl) = ACTarget SCMContact $ case cl of + CLFull cr -> CTFullContact cr + CLShort sl -> CTShortContact (CTLink sl) + type CreatedLinkInvitation = CreatedConnLink 'CMInvitation type CreatedLinkContact = CreatedConnLink 'CMContact @@ -1815,6 +1909,15 @@ sameVerificationCode c1 c2 = noSpaces c1 == noSpaces c2 where noSpaces = T.filter (/= ' ') +-- keys are ordered so both members derive the same code regardless of who computes it +channelMemberCode :: C.PublicKeyEd25519 -> C.PublicKeyEd25519 -> Text +channelMemberCode k1 k2 = + let (lo, hi) = if b1 <= b2 then (b1, b2) else (b2, b1) + in verificationCode $ C.sha256Hash (lo <> hi) + where + b1 = C.pubKeyBytes k1 + b2 = C.pubKeyBytes k2 + aConnId :: Connection -> ConnId aConnId Connection {agentConnId = AgentConnId cId} = cId @@ -2084,6 +2187,12 @@ data StoreCxt = StoreCxt {vr :: VersionRangeChat, badgeKeys :: Map Int BBSPublic pattern VersionChat :: Word16 -> VersionChat pattern VersionChat v = Version v +-- A monotonic per-change counter, not a negotiated protocol version: Int64 rather than the Word16 of +-- Version, so a long-lived high-churn channel cannot wrap and be permanently rejected by relays (v >= cur). +newtype VersionRoster = VersionRoster Int64 + deriving (Eq, Ord, Show) + deriving newtype (FromJSON, ToJSON, FromField, ToField) + -- this newtype exists to have a concise JSON encoding of version ranges in chat protocol messages in the form of "1-2" or just "1" newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRangeChat} deriving (Eq, Show) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index be189379c9..c26f187343 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -179,6 +179,7 @@ data GroupFeature | GFSupport | GFSessions | GFComments + | GFSignMessages deriving (Show) data SGroupFeature (f :: GroupFeature) where @@ -194,6 +195,7 @@ data SGroupFeature (f :: GroupFeature) where SGFSupport :: SGroupFeature 'GFSupport SGFSessions :: SGroupFeature 'GFSessions SGFComments :: SGroupFeature 'GFComments + SGFSignMessages :: SGroupFeature 'GFSignMessages deriving instance Show (SGroupFeature f) @@ -223,6 +225,7 @@ groupFeatureNameText = \case GFSupport -> "Chat with admins" GFSessions -> "Chat sessions" GFComments -> "Comments" + GFSignMessages -> "Sign messages" groupFeatureNameText' :: SGroupFeature f -> Text groupFeatureNameText' = groupFeatureNameText . toGroupFeature @@ -236,11 +239,8 @@ groupFeatureMemberAllowed' feature role prefs = let pref = getGroupPreference feature prefs in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref) --- TODO: some preferences are channel-only (e.g., comments) and should not generate --- UI items or be configurable in regular groups. Currently they are simply excluded --- from this list. When more channel-only or group-only preferences are added, --- consider adding a scope property to GroupFeatureI (e.g., GFScopeAll | GFScopeChannel | GFScopeGroup) --- and filtering at the call sites in createGroupFeatureItems_ / createGroupFeatureChangedItems. +-- Sessions and comments are channel-only features not shown in any client yet, +-- so they are omitted from generated feature items entirely. allGroupFeatures :: [AGroupFeature] allGroupFeatures = [ AGF SGFTimedMessages, @@ -252,11 +252,54 @@ allGroupFeatures = AGF SGFSimplexLinks, AGF SGFReports, AGF SGFHistory, - AGF SGFSupport + AGF SGFSupport, + AGF SGFSignMessages ] +-- Channels (public groups) show a subset of group features. Direct messages, voice, +-- files, SimpleX links and member reports are group-only and excluded in channels. +channelGroupFeatures :: [AGroupFeature] +channelGroupFeatures = filter (\(AGF f) -> groupFeatureInChannel (toGroupFeature f)) allGroupFeatures + +groupFeatureInChannel :: GroupFeature -> Bool +groupFeatureInChannel = \case + GFTimedMessages -> True + GFDirectMessages -> False + GFFullDelete -> True + GFReactions -> True + GFVoice -> False + GFFiles -> False + GFSimplexLinks -> False + GFReports -> False + GFHistory -> True + GFSupport -> True + GFSessions -> False + GFComments -> False + GFSignMessages -> True + +-- Regular groups show a subset of group features. Signing is channel-only for now +-- (keys are not shared between members in regular groups), so it is excluded. +regularGroupFeatures :: [AGroupFeature] +regularGroupFeatures = filter (\(AGF f) -> groupFeatureInRegularGroup (toGroupFeature f)) allGroupFeatures + +groupFeatureInRegularGroup :: GroupFeature -> Bool +groupFeatureInRegularGroup = \case + GFTimedMessages -> True + GFDirectMessages -> True + GFFullDelete -> True + GFReactions -> True + GFVoice -> True + GFFiles -> True + GFSimplexLinks -> True + GFReports -> True + GFHistory -> True + GFSupport -> True + GFSessions -> False + GFComments -> False + GFSignMessages -> False + groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f) -groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of +groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments, signMessages} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete @@ -269,6 +312,7 @@ groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reac SGFSupport -> support SGFSessions -> sessions SGFComments -> comments + SGFSignMessages -> signMessages toGroupFeature :: SGroupFeature f -> GroupFeature toGroupFeature = \case @@ -284,6 +328,7 @@ toGroupFeature = \case SGFSupport -> GFSupport SGFSessions -> GFSessions SGFComments -> GFComments + SGFSignMessages -> GFSignMessages class GroupPreferenceI p where getGroupPreference :: SGroupFeature f -> p -> GroupFeaturePreference f @@ -295,7 +340,7 @@ instance GroupPreferenceI (Maybe GroupPreferences) where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) instance GroupPreferenceI FullGroupPreferences where - getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments} = case f of + getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, support, sessions, comments, signMessages} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete @@ -308,6 +353,7 @@ instance GroupPreferenceI FullGroupPreferences where SGFSupport -> support SGFSessions -> sessions SGFComments -> comments + SGFSignMessages -> signMessages {-# INLINE getGroupPreference #-} -- collection of optional group preferences @@ -324,6 +370,7 @@ data GroupPreferences = GroupPreferences support :: Maybe SupportGroupPreference, sessions :: Maybe SessionsGroupPreference, comments :: Maybe CommentsGroupPreference, + signMessages :: Maybe SignMessagesGroupPreference, commands :: Maybe [ChatBotCommand] } deriving (Eq, Show) @@ -376,6 +423,7 @@ setGroupPreference_ f pref prefs = SGFSupport -> prefs {support = pref} SGFSessions -> prefs {sessions = pref} SGFComments -> prefs {comments = pref} + SGFSignMessages -> prefs {signMessages = pref} setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences setGroupTimedMessagesPreference pref prefs_ = @@ -420,6 +468,7 @@ data FullGroupPreferences = FullGroupPreferences support :: SupportGroupPreference, sessions :: SessionsGroupPreference, comments :: CommentsGroupPreference, + signMessages :: SignMessagesGroupPreference, commands :: ListDef ChatBotCommand } deriving (Eq, Show) @@ -491,11 +540,12 @@ defaultGroupPrefs = support = SupportGroupPreference {enable = FEOn}, sessions = SessionsGroupPreference {enable = FEOff, role = Nothing}, comments = CommentsGroupPreference {enable = FEOff, duration = Nothing}, + signMessages = SignMessagesGroupPreference {enable = FEOff}, commands = ListDef [] } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing businessGroupPrefs :: Preferences -> GroupPreferences businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} = @@ -529,6 +579,7 @@ defaultBusinessGroupPrefs = support = Just $ SupportGroupPreference FEOn, sessions = Just $ SessionsGroupPreference FEOn Nothing, comments = Just $ CommentsGroupPreference FEOff Nothing, + signMessages = Just $ SignMessagesGroupPreference FEOff, commands = Nothing } @@ -655,6 +706,10 @@ data ReportsGroupPreference = ReportsGroupPreference {enable :: GroupFeatureEnabled} deriving (Eq, Show) +data SignMessagesGroupPreference = SignMessagesGroupPreference + {enable :: GroupFeatureEnabled} + deriving (Eq, Show) + data HistoryGroupPreference = HistoryGroupPreference {enable :: GroupFeatureEnabled} deriving (Eq, Show) @@ -712,6 +767,9 @@ instance HasField "enable" SimplexLinksGroupPreference GroupFeatureEnabled where instance HasField "enable" ReportsGroupPreference GroupFeatureEnabled where hasField p@ReportsGroupPreference {enable} = (\e -> p {enable = e}, enable) +instance HasField "enable" SignMessagesGroupPreference GroupFeatureEnabled where + hasField p@SignMessagesGroupPreference {enable} = (\e -> p {enable = e}, enable) + instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable) @@ -772,6 +830,12 @@ instance GroupFeatureI 'GFReports where groupPrefParam _ = Nothing groupPrefRole _ = Nothing +instance GroupFeatureI 'GFSignMessages where + type GroupFeaturePreference 'GFSignMessages = SignMessagesGroupPreference + sGroupFeature = SGFSignMessages + groupPrefParam _ = Nothing + groupPrefRole _ = Nothing + instance GroupFeatureI 'GFHistory where type GroupFeaturePreference 'GFHistory = HistoryGroupPreference sGroupFeature = SGFHistory @@ -804,6 +868,8 @@ instance GroupFeatureNoRoleI 'GFReactions instance GroupFeatureNoRoleI 'GFReports +instance GroupFeatureNoRoleI 'GFSignMessages + instance GroupFeatureNoRoleI 'GFHistory instance GroupFeatureNoRoleI 'GFSupport @@ -1003,6 +1069,7 @@ mergeGroupPreferences groupPreferences = support = pref SGFSupport, sessions = pref SGFSessions, comments = pref SGFComments, + signMessages = pref SGFSignMessages, commands = ListDef $ fromMaybe [] $ groupPreferences >>= commands_ } where @@ -1024,6 +1091,7 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd support = pref SGFSupport, sessions = pref SGFSessions, comments = pref SGFComments, + signMessages = pref SGFSignMessages, commands = Just cmds } where @@ -1150,6 +1218,12 @@ $(J.deriveJSON defaultJSON ''SimplexLinksGroupPreference) $(J.deriveJSON defaultJSON ''ReportsGroupPreference) +$(J.deriveToJSON defaultJSON ''SignMessagesGroupPreference) + +instance FromJSON SignMessagesGroupPreference where + parseJSON v = $(J.mkParseJSON defaultJSON ''SignMessagesGroupPreference) v + omittedField = Just SignMessagesGroupPreference {enable = FEOff} + $(J.deriveJSON defaultJSON ''HistoryGroupPreference) $(J.deriveToJSON defaultJSON ''SupportGroupPreference) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs index c71f7ce37a..d8917cc00c 100644 --- a/src/Simplex/Chat/Types/Shared.hs +++ b/src/Simplex/Chat/Types/Shared.hs @@ -11,8 +11,9 @@ import qualified Data.ByteString.Char8 as B import Data.Text (Text) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Messaging.Agent.Store.DB (fromTextField_) +import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, enumJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) import Simplex.Messaging.Util ((<$?>)) data GroupMemberRole @@ -57,6 +58,12 @@ instance ToJSON GroupMemberRole where toJSON = textToJSON toEncoding = textToEncoding +-- Binary encoding for the roster blob; delegates to the canonical TextEncoding +-- (same member/moderator/admin form JSON and the DB use). GRUnknown round-trips. +instance Encoding GroupMemberRole where + smpEncode = smpEncode . textEncode + smpP = maybe (fail "bad GroupMemberRole") pure . textDecode =<< smpP + data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show) instance StrEncoding GroupAcceptance where @@ -82,6 +89,7 @@ data RelayStatus = RSNew -- only for owner | RSInvited | RSAccepted + | RSAcknowledgedRoster | RSActive | RSInactive | RSRejected @@ -92,6 +100,7 @@ relayStatusText = \case RSNew -> "new" RSInvited -> "invited" RSAccepted -> "accepted" + RSAcknowledgedRoster -> "acknowledged_roster" RSActive -> "active" RSInactive -> "inactive" RSRejected -> "rejected" @@ -101,6 +110,7 @@ instance TextEncoding RelayStatus where RSNew -> "new" RSInvited -> "invited" RSAccepted -> "accepted" + RSAcknowledgedRoster -> "acknowledged_roster" RSActive -> "active" RSInactive -> "inactive" RSRejected -> "rejected" @@ -108,6 +118,7 @@ instance TextEncoding RelayStatus where "new" -> Just RSNew "invited" -> Just RSInvited "accepted" -> Just RSAccepted + "acknowledged_roster" -> Just RSAcknowledgedRoster "active" -> Just RSActive "inactive" -> Just RSInactive "rejected" -> Just RSRejected @@ -137,3 +148,25 @@ instance ToField MsgSigStatus where toField = toField . textEncode instance FromField MsgSigStatus where fromField = fromTextField_ textDecode $(JQ.deriveJSON (enumJSON $ dropPrefix "MSS") ''MsgSigStatus) + +data MsgVerified = MVSigned {sigStatus :: MsgSigStatus} | MVSigMissing + deriving (Eq, Show) + +instance TextEncoding MsgVerified where + textEncode = \case + MVSigned s -> textEncode s + MVSigMissing -> "sig_missing" + textDecode = \case + "sig_missing" -> Just MVSigMissing + s -> MVSigned <$> textDecode s + +instance ToField MsgVerified where toField = toField . textEncode + +instance FromField MsgVerified where fromField = fromTextField_ textDecode + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "MV") ''MsgVerified) + +toMsgVerified :: Bool -> Maybe MsgSigStatus -> Maybe MsgVerified +toMsgVerified signRequired = \case + Just s -> Just (MVSigned s) + Nothing -> if signRequired then Just MVSigMissing else Nothing diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 004f6af825..3cf7554cf5 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -52,6 +52,7 @@ import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types import Simplex.Chat.Store (AddressSettings (..), AutoAccept (..), StoreError (..), UserContactLink (..)) import Simplex.Chat.Styled +import Simplex.Chat.Names (SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared @@ -147,6 +148,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRContactRatchetSyncStarted {} -> ["connection synchronization started"] CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"] CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code] + CRContactDomainVerified u (Contact {profile = LocalProfile {contactDomain}}) result -> ttyUser u $ viewDomainVerified NTContact (claimDomain <$> contactDomain) result + CRGroupDomainVerified u g result -> ttyUser u $ viewDomainVerified NTPublicGroup (groupSimplexDomain g) result CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView @@ -201,7 +204,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink CRConnectionIncognitoUpdated u c customUserProfile -> ttyUser u $ viewConnectionIncognitoUpdated c customUserProfile testView CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' - CRConnectionPlan u connLink connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan + CRConnectionPlan u connLink _ otherSimplexName connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan <> otherSimplexNameNote otherSimplexName CRNewPreparedChat u (AChat _ (Chat cInfo _ _)) -> ttyUser u $ case cInfo of DirectChat ct -> [ttyContact' ct <> ": contact is prepared"] GroupChat g _ -> [ttyGroup' g <> ": group is prepared"] @@ -372,9 +375,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp _ -> Nothing testViewItem :: CChatItem c -> Maybe GroupMember -> Text - testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText, msgSigned}}) membership_ = + testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText, msgVerified}}) membership_ = let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_) - in itemText <> sigStatusStr msgSigned <> deleted_ + in itemText <> msgVerifiedStr msgVerified <> deleted_ unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString] unmuted u chat ci@ChatItem {chatDir} = unmuted' u chat chatDir $ isUserMention ci unmutedReaction :: User -> ChatInfo c -> CIReaction c d -> [StyledString] -> [StyledString] @@ -392,6 +395,12 @@ sigStatusStr = \case Just MSSSignedNoKey -> " (signed, no key to verify)" Nothing -> "" +msgVerifiedStr :: IsString a => Maybe MsgVerified -> a +msgVerifiedStr = \case + Just (MVSigned s) -> sigStatusStr (Just s) + Just MVSigMissing -> " (signature missing)" + Nothing -> "" + signedStr :: IsString a => Bool -> a signedStr signed = if signed then " (signed)" else "" @@ -482,7 +491,8 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtSubscriptionEnd u acEntity -> let Connection {connId} = entityConnection acEntity in ttyUser u [sShow connId <> ": END"] - CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> show (length conns) <> " connections on server " <> showSMPServer srv] + CEvtSubscriptionStatus srv status conns -> [plain $ subStatusStr status <> " " <> tshow (length conns) <> " connections on server " <> showSMPServer srv] + CEvtServiceSubStatus srv event -> [plain $ serviceSubEventStr srv event] CEvtReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r CEvtUserJoinedGroup u g m -> ttyUser u $ viewUserJoinedGroup g m CEvtGroupLinkDataUpdated u g groupLink relays relaysChanged @@ -619,13 +629,14 @@ viewUsersList us = in if null ss then ["no users"] else ss where ldn (UserInfo User {localDisplayName = n} _) = T.toLower n - userInfo (UserInfo User {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, peerType, localBadge}, activeUser, showNtfs, viewPwdHash} count) + userInfo (UserInfo User {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, peerType, localBadge}, activeUser, showNtfs, viewPwdHash, clientService} count) | activeUser || isNothing viewPwdHash = Just $ ttyFullNameBadge n fullName shortDescr localBadge <> infoStr <> bot | otherwise = Nothing where infoStr = if null info then "" else " (" <> mconcat (intersperse ", " info) <> ")" info = [highlight' "active" | activeUser] + <> [highlight' "service" | isTrue clientService] <> [highlight' "hidden" | isJust viewPwdHash] <> ["muted" | not showNtfs] <> [plain ("unread: " <> show count) | count /= 0] @@ -633,8 +644,8 @@ viewUsersList us = Just CPTBot -> " (bot)" _ -> "" -showSMPServer :: SMPServer -> String -showSMPServer ProtocolServer {host} = B.unpack $ strEncode host +showSMPServer :: SMPServer -> Text +showSMPServer ProtocolServer {host} = safeDecodeUtf8 $ strEncode host viewHostEvent :: AProtocolType -> TransportHost -> String viewHostEvent p h = map toUpper (B.unpack $ strEncode p) <> " host " <> B.unpack (strEncode h) @@ -672,7 +683,7 @@ viewChatItems ttyUser unmuted u chatItems ts tz testView | otherwise = ttyUser u [sShow (length chatItems) <> " new messages created"] viewChatItem :: forall c d. MsgDirectionI d => ChatInfo c -> ChatItem c d -> Bool -> CurrentTime -> TimeZone -> [StyledString] -viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember, userMention, msgSigned}, content, quotedItem, file} doShow ts tz = +viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwardedByMember, userMention, msgVerified}, content, quotedItem, file} doShow ts tz = withGroupMsgForwarded . withItemDeleted <$> viewCI where viewCI = case chat of @@ -758,8 +769,8 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa ("", Just _, []) -> [] ("", Just CIFile {fileName}, _) -> view dir context (MCText $ T.pack fileName) ts tz meta _ -> view dir context mc ts tz meta - showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> sigStatusStr msgSigned] meta - showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content <> sigStatusStr msgSigned] False + showSndItem to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> msgVerifiedStr msgVerified] meta + showRcvItem from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content <> msgVerifiedStr msgVerified] False showSndItemProhibited to = showItem $ sentWithTime_ ts tz [to <> plainContent content <> " " <> prohibited] meta showRcvItemProhibited from = showItem $ receivedWithTime_ ts tz from [] meta [plainContent content <> " " <> prohibited] False showItem ss = if doShow then ss else [] @@ -1123,6 +1134,31 @@ simplexChatContact' = \case CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat} l@(CLShort _) -> l +groupSimplexDomain :: GroupInfo -> Maybe SimplexDomain +groupSimplexDomain GroupInfo {groupProfile = GroupProfile {publicGroup}} = + claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim) + +viewDomainVerified :: SimplexNameType -> Maybe SimplexDomain -> Maybe Text -> [StyledString] +viewDomainVerified nameType domain_ result = + let nameStr = maybe "name" (\d -> "SimpleX name " <> shortNameInfoStr (SimplexNameInfo nameType d)) domain_ + in case result of + Nothing -> [plain nameStr <> " verified"] + Just reason -> [plain nameStr <> " not verified: " <> plain reason] + +-- §4.7: show a peer's claimed name only with its verification context — "verified" / "verification +-- failed" when a status is recorded, "unverified" when there is a proof but no status yet, and nothing +-- at all when there is neither (an unproven, unverifiable claim is not shown). +simplexDomainLine :: SimplexNameType -> Maybe SimplexDomainClaim -> Maybe Bool -> [StyledString] +simplexDomainLine _ Nothing _ = [] +simplexDomainLine nameType (Just SimplexDomainClaim {domain, proof}) status = case status of + Just True -> [line "verified"] + Just False -> [line "verification failed"] + Nothing + | isJust proof -> [line "unverified"] + | otherwise -> [] + where + line s = plain $ "SimpleX name: " <> shortNameInfoStr (SimplexNameInfo nameType (unStrJSON domain)) <> " (" <> s <> ")" + -- TODO [short links] show all settings viewAddressSettings :: AddressSettings -> [StyledString] viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = case autoAccept of @@ -1140,12 +1176,14 @@ groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString] groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} = [ intro, "", - plain $ maybe cReqStr strEncode shortLink, - "", - "Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c ", - "to show it again: " <> highlight ("/show link #" <> viewGroupName g), - "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" + plain $ maybe cReqStr strEncode shortLink ] + <> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]] + <> [ "", + "Anybody can connect to you and join group as " <> showRole acceptMemberRole <> " with: " <> highlight' "/c ", + "to show it again: " <> highlight ("/show link #" <> viewGroupName g), + "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" + ] <> ["The group link for old clients: " <> plain cReqStr | isJust shortLink] where cReqStr = strEncode $ simplexChatContact cReq @@ -1205,8 +1243,8 @@ viewReceivedContactRequest c Profile {fullName, shortDescr} = ] showRelay :: GroupRelay -> StyledString -showRelay GroupRelay {groupRelayId, relayStatus} = - " - relay id " <> sShow groupRelayId <> ": " <> plain (relayStatusText relayStatus) +showRelay GroupRelay {groupRelayId, relayStatus, relayCap = RelayCapabilities {webDomain}} = + " - relay id " <> sShow groupRelayId <> ": " <> plain (relayStatusText relayStatus) <> maybe "" (\d -> ", web: " <> plain d) webDomain viewGroupRelays :: GroupInfo -> [GroupRelay] -> [StyledString] viewGroupRelays g relays = @@ -1221,6 +1259,7 @@ viewGroupLinkRelaysUpdated g groupLink relays = [ "group link:", plain $ maybe cReqStr strEncode shortLink ] + <> [plain ("SimpleX name: " <> shortNameInfoStr (SimplexNameInfo NTPublicGroup d)) | Just d <- [groupSimplexDomain g]] where GroupLink {connLinkContact = CCLink cReq shortLink} = groupLink cReqStr = strEncode $ simplexChatContact cReq @@ -1327,7 +1366,7 @@ viewJoinedGroupMemberConnecting g@GroupInfo {groupId} host m@GroupMember {groupM [ (ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting and pending review...), ") <> ("use " <> highlight ("/_accept member #" <> show groupId <> " " <> show groupMemberId <> " ") <> " to accept member") ] - _ | useRelays' g -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group"] + _ | useRelays' g -> [ttyGroup' g <> ": " <> ttyMember host <> " introduced " <> ttyFullMember m <> " in the channel"] | otherwise -> [ttyGroup' g <> ": " <> ttyMember host <> " added " <> ttyFullMember m <> " to the group (connecting...)"] viewConnectedToGroupMember :: GroupInfo -> GroupMember -> [StyledString] @@ -1494,7 +1533,7 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil viewNewMemberContactReceivedInv :: User -> Contact -> GroupInfo -> GroupMember -> [StyledString] viewNewMemberContactReceivedInv user ct@Contact {localDisplayName = c} g m - | isTrue (autoAcceptMemberContacts user) = + | 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", @@ -1580,13 +1619,23 @@ viewConnDiffIds userDiff connDiff where showIds = plain . T.intercalate ", " . map (tshow . unwrapId) -subStatusStr :: SubscriptionStatus -> String +subStatusStr :: SubscriptionStatus -> Text subStatusStr = \case SSActive -> "subscribed" SSPending -> "disconnected" - SSRemoved e -> "removed: " <> e + SSRemoved e -> "removed: " <> T.pack e SSNoSub -> "no subscription" +serviceSubEventStr :: SMPServer -> ServiceSubEvent -> Text +serviceSubEventStr srv = \case + ServiceSubUp e_ n -> "subscribed service " <> conns n <> srvStr <> ": " <> fromMaybe "ok" e_ + ServiceSubDown n -> "disconnected service " <> conns n <> srvStr + ServiceSubAll -> "received messages from service" <> srvStr -- "(" <> n <> "connections)" + ServiceSubEnd n -> "service subscription ended " <> conns n <> srvStr + where + conns n = "(" <> tshow n <> " connections)" + srvStr = " on server " <> showSMPServer srv + viewUserServers :: UserOperatorServers -> [StyledString] viewUserServers (UserOperatorServers _ [] [] []) = [] viewUserServers UserOperatorServers {operator, smpServers, xftpServers, chatRelays} = @@ -1610,12 +1659,12 @@ viewUserServers UserOperatorServers {operator, smpServers, xftpServers, chatRela testedInfo = maybe [] (\t -> ["test: " <> if t then "passed" else "failed"]) tested viewRoles op@ServerOperator {enabled} | not enabled = "disabled" - | storage rs && proxy rs = "enabled" - | storage rs = "enabled storage" - | proxy rs = "enabled proxy" + | rStorage && rProxy = "enabled" + | rStorage = "enabled storage" + | rProxy = "enabled proxy" | otherwise = "disabled (servers known)" where - rs = operatorRoles p op + ServerRoles {storage = rStorage, proxy = rProxy} = operatorRoles p op viewChatRelays :: [UserChatRelay] -> [StyledString] viewChatRelays [] = [] viewChatRelays cRelays @@ -1703,12 +1752,12 @@ viewOpEnabled ServerOperator {enabled, smpRoles, xftpRoles} | both smpRoles && both xftpRoles = "enabled" | otherwise = "SMP " <> viewRoles smpRoles <> ", XFTP " <> viewRoles xftpRoles where - no rs = not $ storage rs || proxy rs - both rs = storage rs && proxy rs - viewRoles rs + no ServerRoles {storage, proxy} = not $ storage || proxy + both ServerRoles {storage, proxy} = storage && proxy + viewRoles rs@ServerRoles {storage, proxy} | both rs = "enabled" - | storage rs = "enabled storage" - | proxy rs = "enabled proxy" + | storage = "enabled storage" + | proxy = "enabled proxy" | otherwise = "disabled (servers known)" viewConditionsAction :: UsageConditionsAction -> [StyledString] @@ -1766,11 +1815,13 @@ viewContactBadge = maybe [] $ \lb -> in [plain (textEncode badgeType <> " badge - " <> st), plain expiry] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge}, activeConn, uiThemes, customData} stats incognitoProfile = +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink, localBadge, contactDomain, contactDomainVerified, description}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] <> viewContactBadge localBadge + <> maybe [] ((bold' "description:" :) . map plain . T.lines) description <> maybe [] viewConnectionStats stats - <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink + <> maybe [] (\l -> ["contact address: " <> plain (strEncode (simplexChatContact' l))]) contactLink + <> simplexDomainLine NTContact contactDomain contactDomainVerified <> maybe ["you've shared main profile with this contact"] (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) @@ -1783,13 +1834,18 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta <> viewCustomData customData viewGroupInfo :: GroupInfo -> [StyledString] -viewGroupInfo gInfo@GroupInfo {groupId, uiThemes, customData, groupSummary = GroupSummary {currentMembers, publicMemberCount}} = +viewGroupInfo gInfo@GroupInfo {groupId, businessChat, groupDomainVerified, groupProfile = GroupProfile {publicGroup}, uiThemes, customData, groupSummary = GroupSummary {currentMembers, publicMemberCount}} = [ "group ID: " <> sShow groupId, memberCountLine ] + <> domainLine <> viewUITheme uiThemes <> viewCustomData customData where + -- a business presents as a contact (@-name); a public group/channel shows its #-name + domainLine = case businessChat of + Just bc -> simplexDomainLine NTContact (businessDomain bc) groupDomainVerified + Nothing -> simplexDomainLine NTPublicGroup (publicGroup >>= publicGroupAccess >>= groupDomainClaim) groupDomainVerified memberCountLine | useRelays' gInfo, Just count <- publicMemberCount = "subscribers: " <> sShow count | otherwise = "current members: " <> sShow currentMembers @@ -1801,16 +1857,19 @@ viewCustomData :: Maybe CustomData -> [StyledString] viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink, localBadge}, activeConn} stats = +viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink, localBadge, description}, activeConn} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> viewContactBadge localBadge + <> maybe [] ((bold' "description:" :) . map plain . T.lines) description <> maybe ["member not connected"] viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink <> ["alias: " <> plain localAlias | localAlias /= ""] - <> [viewConnectionVerified (memberSecurityCode m) | isJust stats] + <> [viewConnectionVerified mSecurityCode | isJust stats || isJust mSecurityCode] <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn + where + mSecurityCode = memberSecurityCode m viewConnectionVerified :: Maybe SecurityCode -> StyledString viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time? @@ -1825,7 +1884,7 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString -viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo +viewRcvQueuesInfo = plain . T.intercalate ", " . map showQueueInfo where showQueueInfo RcvQueueInfo {rcvServer, rcvSwitchStatus, canAbortSwitch} = let switchCanBeAborted = if canAbortSwitch then ", can be aborted" else "" @@ -1838,7 +1897,7 @@ viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo RSReceivedMessage -> "switch secured" viewSndQueuesInfo :: [SndQueueInfo] -> StyledString -viewSndQueuesInfo = plain . intercalate ", " . map showQueueInfo +viewSndQueuesInfo = plain . T.intercalate ", " . map showQueueInfo where showQueueInfo SndQueueInfo {sndServer, sndSwitchStatus} = showSMPServer sndServer @@ -1902,14 +1961,15 @@ viewSwitchPhase = \case SPCompleted -> "changed address" viewUserProfileUpdated :: Profile -> Profile -> UserProfileUpdateSummary -> [StyledString] -viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', image = image', contactLink = contactLink', preferences = prefs'} summary = +viewUserProfileUpdated Profile {displayName = n, fullName, shortDescr, description, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', shortDescr = shortDescr', description = description', image = image', contactLink = contactLink', preferences = prefs'} summary = profileUpdated <> viewPrefsUpdated preferences prefs' where UserProfileUpdateSummary {updateSuccesses = s, updateFailures = f} = summary profileUpdated - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = [if isNothing image' then "profile image removed" else "profile image updated"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = ["user description " <> (if maybe True T.null description' then "removed" else "changed to " <> maybe "" plain description') <> notified] | n == n' && fullName == fullName' = ["user bio " <> (if maybe True T.null shortDescr' then "removed" else "changed to " <> maybe "" plain shortDescr') <> notified] | n == n' = ["user full name " <> (if T.null fullName' || fullName' == n' then "removed" else "changed to " <> plain fullName') <> notified] | otherwise = ["user profile is changed to " <> ttyFullName n' fullName' shortDescr' <> notified] @@ -1970,10 +2030,10 @@ countactUserPrefText cup = case cup of viewGroupUpdated :: GroupInfo -> GroupInfo -> Maybe GroupMember -> Maybe MsgSigStatus -> [StyledString] viewGroupUpdated - GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, shortDescr, description, image, groupPreferences = gps, memberAdmission = ma}} - g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', shortDescr = shortDescr', description = description', image = image', groupPreferences = gps', memberAdmission = ma'}} + GroupInfo {localDisplayName = n, groupProfile = GroupProfile {fullName, shortDescr, description, image, groupPreferences = gps, memberAdmission = ma, publicGroup = pg}} + g'@GroupInfo {localDisplayName = n', groupProfile = GroupProfile {fullName = fullName', shortDescr = shortDescr', description = description', image = image', groupPreferences = gps', memberAdmission = ma', publicGroup = pg'}} m signed = do - let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated + let update = groupProfileUpdated <> groupPrefsUpdated <> memberAdmissionUpdated <> publicGroupAccessUpdated if null update then [] else memberUpdated <> update @@ -1989,7 +2049,7 @@ viewGroupUpdated | null prefs = [] | otherwise = bold' "updated group preferences:" : prefs where - prefs = mapMaybe viewPref allGroupFeatures + prefs = mapMaybe viewPref (if useRelays' g' then channelGroupFeatures else regularGroupFeatures) viewPref (AGF f) | pref gps == pref gps' = Nothing | otherwise = Just . plain $ groupPreferenceText (pref gps') @@ -1998,6 +2058,18 @@ viewGroupUpdated memberAdmissionUpdated | ma == ma' = [] | otherwise = ["changed member admission rules"] + publicGroupAccessUpdated + | access == access' = [] + | otherwise = ["updated public group access:" <> viewAccess access'] + where + access = pg >>= publicGroupAccess + access' = pg' >>= publicGroupAccess + viewAccess Nothing = " removed" + viewAccess (Just PublicGroupAccess {groupWebPage, groupDomainClaim, domainWebPage, allowEmbedding}) = + maybe "" (\u -> " web=" <> plain u) groupWebPage + <> maybe "" (\ni -> " domain=" <> plain (strEncode ni)) (claimDomain <$> groupDomainClaim) + <> (if domainWebPage then " domain_page=on" else "") + <> (if allowEmbedding then " embed=on" else "") viewGroupProfile :: GroupInfo -> [StyledString] viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {shortDescr, description, image, groupPreferences = gps}} = @@ -2005,7 +2077,7 @@ viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {shortDescr, descripti <> maybe [] (\sd -> ["description: " <> plain sd]) shortDescr <> maybe [] (const ["has profile image"]) image <> maybe [] ((bold' "welcome message:" :) . map plain . T.lines) description - <> (bold' "group preferences:" : map viewPref allGroupFeatures) + <> (bold' "group preferences:" : map viewPref (if useRelays' g then channelGroupFeatures else regularGroupFeatures)) where viewPref (AGF f) = plain $ groupPreferenceText (pref gps) where @@ -2088,6 +2160,12 @@ viewGroupUserChanged where userChangedStr = "group " <> ttyGroup' g <> " changed from user " <> plain un <> " to user " <> plain un' +otherSimplexNameNote :: Maybe SimplexNameInfo -> [StyledString] +otherSimplexNameNote = \case + Just ni@(SimplexNameInfo NTPublicGroup _) -> [plain $ "You can also join channel " <> shortNameInfoStr ni] + Just ni@(SimplexNameInfo NTContact _) -> [plain $ "You can also connect to " <> shortNameInfoStr ni <> " in direct chat"] + Nothing -> [] + viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString] viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case CPInvitationLink ilp -> case ilp of @@ -2096,12 +2174,12 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case ILPConnecting Nothing -> [invLink "connecting"] ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] ILPKnown ct - | nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] - | contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] + | nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct + | contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] <> contactDomainLine ct | otherwise -> - [ invLink ("known contact " <> ttyContact' ct), - "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" - ] + [invLink ("known contact " <> ttyContact' ct)] + <> contactDomainLine ct + <> ["use " <> ttyToContact' ct <> highlight' "" <> " to send messages"] where invLink = ("invitation link: " <>) invOrBiz = \case @@ -2114,12 +2192,12 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] CAPKnown ct - | nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)] + | nextConnectPrepared ct -> [ctAddr ("known prepared contact " <> ttyContact' ct)] <> contactDomainLine ct | otherwise -> - [ ctAddr ("known contact " <> ttyContact' ct), - "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" - ] - CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] + [ctAddr ("known contact " <> ttyContact' ct)] + <> contactDomainLine ct + <> ["use " <> ttyToContact' ct <> highlight' "" <> " to send messages"] + CAPContactViaAddress ct -> [ctAddr ("known contact without connection " <> ttyContact' ct)] <> contactDomainLine ct where ctAddr = ("contact address: " <>) addrOrBiz = \case @@ -2140,17 +2218,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case Just PreparedGroup {connLinkStartedConnection} -> case memberStatus m of GSMemUnknown | connLinkStartedConnection -> connecting g - | otherwise -> [knownGroup "prepared "] + | otherwise -> [knownGroup "prepared "] <> groupDomainLine g GSMemAccepted -> connecting g _ - | memberRemoved m -> [knownGroup "deleted "] -- it should not get here, as this plan is returned as GLPOk + | memberRemoved m -> [knownGroup "deleted "] <> groupDomainLine g -- it should not get here, as this plan is returned as GLPOk | otherwise -> knownActive _ -> knownActive where knownActive = - [ knownGroup "", - "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" - ] + [knownGroup ""] + <> groupDomainLine g + <> ["use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages"] knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g GLPNoRelays _ -> [grpLink "channel has no active relays, please try to join later"] GLPUpdateRequired _ -> [grpLink "this group requires a newer version of the app, please upgrade"] @@ -2168,6 +2246,13 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn _ -> False + contactDomainLine :: Contact -> [StyledString] + contactDomainLine Contact {profile = LocalProfile {contactDomain, contactDomainVerified}} = + simplexDomainLine NTContact contactDomain contactDomainVerified + groupDomainLine :: GroupInfo -> [StyledString] + groupDomainLine GroupInfo {groupDomainVerified, groupProfile = GroupProfile {publicGroup}} = do + let domain = publicGroup >>= publicGroupAccess >>= groupDomainClaim + in simplexDomainLine NTPublicGroup domain groupDomainVerified viewSigVerification = \case Just OVVerified -> ["owner signature: verified"] Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"] @@ -2175,13 +2260,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated - Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, contactLink}} - Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', contactLink = contactLink'}} - | n == n' && fullName == fullName' && shortDescr == shortDescr' && contactLink == contactLink' = [] - | n == n' && fullName == fullName' && shortDescr == shortDescr' = + Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, description, contactLink}} + Contact {localDisplayName = n', profile = LocalProfile {fullName = fullName', shortDescr = shortDescr', description = description', contactLink = contactLink'}} + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' && contactLink == contactLink' = [] + | n == n' && fullName == fullName' && shortDescr == shortDescr' && description == description' = if isNothing contactLink' then [ttyContact n <> " removed contact address"] else [ttyContact n <> " set new contact address, use " <> highlight ("/info " <> n) <> " to view"] + | n == n' && fullName == fullName' && shortDescr == shortDescr' = + if maybe True T.null description' + then ["contact " <> ttyContact n <> " removed description"] + else ["contact " <> ttyContact n <> " updated description: " <> maybe "" plain description'] | n == n' && fullName == fullName' = if maybe True T.null shortDescr' then ["contact " <> ttyContact n <> " removed bio"] @@ -2201,7 +2290,7 @@ viewReceivedUpdatedMessage :: StyledString -> [StyledString] -> MsgContent -> Cu viewReceivedUpdatedMessage = viewReceivedMessage_ True viewReceivedMessage_ :: Bool -> StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewReceivedMessage_ updated from context mc ts tz meta = receivedWithTime_ ts tz from context meta (ttyMsgContent mc) updated +viewReceivedMessage_ updated from context mc ts tz meta@CIMeta {msgVerified} = receivedWithTime_ ts tz from context meta (appendLast (msgVerifiedStr msgVerified) $ ttyMsgContent mc) updated viewReceivedReaction :: StyledString -> [StyledString] -> StyledString -> CurrentTime -> TimeZone -> UTCTime -> [StyledString] viewReceivedReaction from styledMsg reactionText ts tz reactionTs = @@ -2239,7 +2328,7 @@ recent now tz time = do || (localNow < currentDay12 && localTime >= previousDay18 && localTimeDay < localNowDay) viewSentMessage :: StyledString -> [StyledString] -> MsgContent -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewSentMessage to context mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive} = sentWithTime_ ts tz (prependFirst to $ context <> prependFirst (indent <> live) (ttyMsgContent mc)) meta +viewSentMessage to context mc ts tz meta@CIMeta {itemEdited, itemDeleted, itemLive, msgVerified} = sentWithTime_ ts tz (prependFirst to $ context <> prependFirst (indent <> live) (appendLast (msgVerifiedStr msgVerified) $ ttyMsgContent mc)) meta where indent = if null context then "" else " " live @@ -2296,6 +2385,10 @@ prependFirst :: StyledString -> [StyledString] -> [StyledString] prependFirst s [] = [s] prependFirst s (s' : ss) = (s <> s') : ss +appendLast :: StyledString -> [StyledString] -> [StyledString] +appendLast _ [] = [] +appendLast s ss = init ss <> [last ss <> s] + msgPlain :: Text -> [StyledString] msgPlain = map (styleMarkdownList . parseMarkdownList) . T.lines @@ -2600,7 +2693,6 @@ viewChatError isCmd logLevel testView = \case CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError] CENoSndFileUser aFileId -> ["error: snd file user not found, file id: " <> sShow aFileId | logLevel <= CLLError] CENoRcvFileUser aFileId -> ["error: rcv file user not found, file id: " <> sShow aFileId | logLevel <= CLLError] - CEActiveUserExists -> ["error: active user already exists"] CEUserExists name -> ["user with the name " <> ttyContact name <> " already exists"] CEChatRelayExists -> ["chat realy user already exists"] CEUserUnknown -> ["user does not exist or incorrect password"] @@ -2620,6 +2712,12 @@ viewChatError isCmd logLevel testView = \case CEChatNotStopped -> ["error: chat not stopped"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"] CEInvalidConnReq -> viewInvalidConnReq + CESimplexDomainNotReady domain domainErr -> + let reason = case domainErr of + SDENoValidLink -> "has no valid connection link" + SDEUnknownDomain -> "is not included in the connection link's profile" + in [plain $ "SimpleX name " <> strEncode domain <> " " <> reason] + CENotResolvedLocally -> ["no matching chat found, name resolution is disabled"] CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr] CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> [ plain $ diff --git a/src/Simplex/Chat/Web.hs b/src/Simplex/Chat/Web.hs new file mode 100644 index 0000000000..fc3e4b2a26 --- /dev/null +++ b/src/Simplex/Chat/Web.hs @@ -0,0 +1,435 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Web + ( WebChannelPreview (..), + WebMessage (..), + WebMemberProfile (..), + WebFileInfo (..), + webPreviewWorker, + writeCorsConfig, + removeStaleFiles, + channelContentChanged, + channelProfileUpdated, + channelRemoved, + extractOrigin, + ) +where + +import Control.Concurrent.STM (check, flushTQueue) +import Control.Exception (SomeException, catch) +import Control.Logger.Simple +import Control.Monad +import Control.Monad.Except (runExceptT) +import Data.Either (rights) +import Data.Int (Int64) +import qualified Data.Aeson as J +import qualified Data.Aeson.TH as JQ +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as LB +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Map.Strict as M +import qualified Data.Set as S +import Data.Maybe (isJust, mapMaybe, maybeToList) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as TIO +import Data.Time.Clock (UTCTime, getCurrentTime) +import Simplex.Chat.Controller (ChatController (..), CorsOrigin (..), PublishableGroup (..), WebPreviewConfig (..), WebPreviewState (..), mkStoreCxt) +import Simplex.Chat.Markdown (FormattedText (..), MarkdownList, parseMaybeMarkdownList) +import Simplex.Chat.Messages + ( CChatItem (..), + CIDirection (..), + CIFile (..), + CIMeta (..), + CIQDirection (..), + CIQuote (..), + CIReactionCount, + ChatItem (..), + ChatType (..), + ) +import Simplex.Chat.Messages.CIContent (ciMsgContent) +import Simplex.Chat.Protocol (MsgContent, MsgRef (..), QuotedMsg (..), isReport) +import Simplex.Chat.Store.Groups (getGroupOwners, getRelayPublishableGroups, updatePublicMemberCount) +import Simplex.Chat.Store.Messages (getGroupWebPreviewItems) +import Simplex.Chat.Store.Shared (getGroupInfo) +import Simplex.Chat.Types + ( B64UrlByteString, + GroupInfo (..), + GroupMember (..), + GroupProfile (..), + GroupSummary (..), + ImageData, + LocalProfile (..), + MemberId, + PublicGroupAccess (..), + PublicGroupProfile (..), + User (..), + ) +import Simplex.Messaging.Agent.Store.Common (withTransaction) +import Simplex.Messaging.Encoding.String (strEncode) +import Simplex.Messaging.Util (catchOwn, eitherToMaybe, safeDecodeUtf8, tshow) +import Simplex.Messaging.Parsers (defaultJSON) +import System.Directory (createDirectoryIfMissing, listDirectory, removeFile, renameFile) +import System.FilePath (dropExtension, takeExtension, ()) +import qualified URI.ByteString as U +import UnliftIO.STM + +data WebFileInfo = WebFileInfo + { fileName :: String, + fileSize :: Integer + } + deriving (Show) + +data WebMemberProfile = WebMemberProfile + { memberId :: MemberId, + displayName :: Text, + image :: Maybe ImageData + } + deriving (Show) + +data WebMessage = WebMessage + { sender :: Maybe MemberId, + ts :: UTCTime, + content :: MsgContent, + formattedText :: Maybe MarkdownList, + file :: Maybe WebFileInfo, + quote :: Maybe QuotedMsg, + reactions :: [CIReactionCount], + forward :: Maybe Bool, + edited :: Bool + } + deriving (Show) + +data WebChannelPreview = WebChannelPreview + { channel :: GroupProfile, + shortDescription :: Maybe MarkdownList, + welcomeMessage :: Maybe MarkdownList, + members :: [WebMemberProfile], + subscribers :: Maybe Int64, + messages :: [WebMessage], + updatedAt :: UTCTime + } + deriving (Show) + +$(JQ.deriveJSON defaultJSON ''WebFileInfo) + +$(JQ.deriveJSON defaultJSON ''WebMemberProfile) + +$(JQ.deriveJSON defaultJSON ''WebMessage) + +$(JQ.deriveJSON defaultJSON ''WebChannelPreview) + +webPreviewWorker :: WebPreviewConfig -> ChatController -> [User] -> IO () +webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterval} cc users = + forM_ (webPreviewState cc) $ \wps -> do + createDirectoryIfMissing True webJsonDir + initPublishableGroups wps + cleanStaleFiles wps + regenerateCors wps + seedRoutinePending wps + forever $ workerLoop wps `catchOwn` \e -> logError ("web preview worker error: " <> tshow e) + where + cxt = mkStoreCxt (config cc) + + workerLoop wps@WebPreviewState {priorityRender, filesToRemove, corsNeeded, routinePending, wakeSignal} = do + drainRemovals + drainPriority + handleCors + renderRoutine + noRoutine <- atomically $ S.null <$> readTVar routinePending + when noRoutine waitRefresh + where + drainRemovals = atomically (tryReadTQueue filesToRemove) >>= \case + Nothing -> pure () + Just f -> do + removeFile (webJsonDir f) `catch` \(_ :: SomeException) -> pure () + drainRemovals + + -- flush the whole queue and render each group once: a burst of changes in one + -- channel enqueues its id many times, but only needs a single render + drainPriority = do + gIds <- atomically $ flushTQueue priorityRender + forM_ (S.fromList gIds) $ renderOneGroup wps + + handleCors = do + needed <- atomically $ swapTVar corsNeeded False + when needed $ regenerateCors wps + + -- render a single routine item; the main loop calls this once per iteration + renderRoutine = do + mGId <- atomically $ do + pending <- readTVar routinePending + case S.minView pending of + Nothing -> pure Nothing + Just (gId, rest) -> writeTVar routinePending rest >> pure (Just gId) + forM_ mGId $ renderOneGroup wps + + -- routine list drained: wait for the refresh timer or a change signal; only the timer + -- seeds the next full sweep, a change just returns to let the main loop service it + waitRefresh = do + delay <- registerDelay (webUpdateInterval * 1000000) + timerFired <- atomically $ + (True <$ (readTVar delay >>= check)) `orElse` (False <$ takeTMVar wakeSignal) + when timerFired $ seedRoutinePending wps + + initPublishableGroups WebPreviewState {publishableGroupIds} = do + rows <- withTransaction (chatStore cc) $ \db -> + concat <$> mapM (getRelayPublishableGroups db) users + let gIds = M.fromList [(gId, toPublishableGroup pgId access) | (gId, pgId, access) <- rows] + atomically $ writeTVar publishableGroupIds gIds + + cleanStaleFiles WebPreviewState {publishableGroupIds} = do + ids <- readTVarIO publishableGroupIds + let activeFiles = S.fromList $ map pgFileName $ M.elems ids + removeStaleFiles webJsonDir activeFiles + + regenerateCors WebPreviewState {publishableGroupIds} = do + ids <- readTVarIO publishableGroupIds + let entries = mapMaybe pgCorsEntry $ M.elems ids + forM_ webCorsFile $ writeCorsConfig entries + + seedRoutinePending WebPreviewState {publishableGroupIds, routinePending} = + atomically $ M.keysSet <$> readTVar publishableGroupIds >>= writeTVar routinePending + + renderOneGroup WebPreviewState {publishableGroupIds} gId = do + publishable <- atomically $ M.member gId <$> readTVar publishableGroupIds + when publishable $ + renderOrRemoveStale `catch` \(e :: SomeException) -> + logError $ "web preview: error rendering group " <> T.pack (show gId) <> ": " <> T.pack (show e) + where + renderOrRemoveStale = do + r <- withTransaction (chatStore cc) $ \db -> + findUser $ \u -> fmap (\g -> (u, g)) <$> runExceptT (getGroupInfo db cxt u gId) + case r of + Just (u, gInfo) | hasPublicGroup gInfo -> + void $ renderGroupPreview cfg cc u gInfo + _ -> do + fName <- atomically $ do + pg <- M.lookup gId <$> readTVar publishableGroupIds + modifyTVar' publishableGroupIds (M.delete gId) + pure $ pgFileName <$> pg + forM_ fName $ \f -> + removeFile (webJsonDir f) `catch` \(_ :: SomeException) -> pure () + logInfo $ "web preview: group " <> T.pack (show gId) <> " no longer publishable" + + findUser f = go users + where + go [] = pure Nothing + go (u : us) = f u >>= \case + Right a -> pure (Just a) + Left _ -> go us + +renderGroupPreview :: WebPreviewConfig -> ChatController -> User -> GroupInfo -> IO (Maybe (Text, CorsOrigin)) +renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gInfo@GroupInfo {groupProfile = gp@GroupProfile {shortDescr = sd, description = wd, publicGroup}, groupSummary = GroupSummary {publicMemberCount}} = + case publicGroup of + Just PublicGroupProfile {publicGroupId, publicGroupAccess} -> do + let fName = publicGroupIdFileName publicGroupId <> ".json" + -- backfill the subscriber count for channels created before it was tracked + subscribers <- case publicMemberCount of + Just _ -> pure publicMemberCount + Nothing -> do + g_ <- withTransaction (chatStore cc) (\db -> runExceptT $ updatePublicMemberCount db cxt user gInfo) + pure $ eitherToMaybe g_ >>= \GroupInfo {groupSummary = GroupSummary {publicMemberCount = pmc}} -> pmc + (items, owners) <- withTransaction (chatStore cc) $ \db -> do + is <- getGroupWebPreviewItems db user gInfo webPreviewItemCount + os <- getGroupOwners db cxt user gInfo + pure (is, os) + ts <- getCurrentTime + let rendered = mapMaybe toRenderedItem $ rights items + msgs = map fst rendered + senders = collectSenders $ map memberToProfile owners <> concatMap snd rendered + preview = WebChannelPreview + { channel = gp, + shortDescription = toFormattedText =<< sd, + welcomeMessage = toFormattedText =<< wd, + members = senders, + subscribers, + messages = msgs, + updatedAt = ts + } + let destPath = webJsonDir fName + tmpPath = destPath <> ".tmp" + LB.writeFile tmpPath (J.encode preview) + renameFile tmpPath destPath + pure $ corsEntry publicGroupId <$> publicGroupAccess + Nothing -> pure Nothing + where + cxt = mkStoreCxt (config cc) + +channelContentChanged :: ChatController -> Int64 -> STM () +channelContentChanged cc gId = + forM_ (webPreviewState cc) $ \WebPreviewState {publishableGroupIds, priorityRender, routinePending, wakeSignal} -> do + ids <- readTVar publishableGroupIds + when (M.member gId ids) $ do + writeTQueue priorityRender gId + modifyTVar' routinePending (S.delete gId) + void $ tryPutTMVar wakeSignal () + +channelProfileUpdated :: ChatController -> Int64 -> GroupProfile -> STM () +channelProfileUpdated cc gId GroupProfile {publicGroup} = + forM_ (webPreviewState cc) $ \WebPreviewState {publishableGroupIds, priorityRender, filesToRemove, corsNeeded, routinePending, wakeSignal} -> + case publicGroup of + Just PublicGroupProfile {publicGroupId, publicGroupAccess} -> do + let pg = PublishableGroup + { pgFileName = publicGroupIdFileName publicGroupId <> ".json", + pgCorsEntry = corsEntry publicGroupId <$> publicGroupAccess + } + modifyTVar' publishableGroupIds (M.insert gId pg) + writeTQueue priorityRender gId + modifyTVar' routinePending (S.delete gId) + writeTVar corsNeeded True + void $ tryPutTMVar wakeSignal () + Nothing -> do + ids <- readTVar publishableGroupIds + forM_ (pgFileName <$> M.lookup gId ids) $ writeTQueue filesToRemove + modifyTVar' publishableGroupIds (M.delete gId) + modifyTVar' routinePending (S.delete gId) + writeTVar corsNeeded True + void $ tryPutTMVar wakeSignal () + +channelRemoved :: ChatController -> Int64 -> STM () +channelRemoved cc gId = + forM_ (webPreviewState cc) $ \WebPreviewState {publishableGroupIds, filesToRemove, corsNeeded, routinePending, wakeSignal} -> do + ids <- readTVar publishableGroupIds + forM_ (pgFileName <$> M.lookup gId ids) $ writeTQueue filesToRemove + modifyTVar' publishableGroupIds (M.delete gId) + modifyTVar' routinePending (S.delete gId) + writeTVar corsNeeded True + void $ tryPutTMVar wakeSignal () + +toRenderedItem :: CChatItem 'CTGroup -> Maybe (WebMessage, [WebMemberProfile]) +toRenderedItem (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemTimed, itemForwarded, itemEdited}, content, formattedText, quotedItem, reactions, file}) + | isJust itemTimed = Nothing + | otherwise = case ciMsgContent content of + Just mc | not (isReport mc) -> + let (sender, senderProfile) = case chatDir of + CIGroupRcv m@GroupMember {memberId} -> (Just memberId, [memberToProfile m]) + _ -> (Nothing, []) + quotedProfile = case quotedItem of + Just CIQuote {chatDir = CIQGroupRcv (Just m)} -> [memberToProfile m] + _ -> [] + in Just + ( WebMessage + { sender, + ts = itemTs, + content = mc, + formattedText, + file = webFileInfo <$> file, + quote = quotedItem >>= ciQuoteToQuotedMsg, + reactions, + forward = if isJust itemForwarded then Just True else Nothing, + edited = itemEdited + }, + senderProfile <> quotedProfile + ) + _ -> Nothing + +ciQuoteToQuotedMsg :: CIQuote c -> Maybe QuotedMsg +ciQuoteToQuotedMsg CIQuote {chatDir = qDir, sharedMsgId, sentAt, content = qContent} = + Just QuotedMsg + { msgRef = MsgRef + { msgId = sharedMsgId, + sentAt, + sent = case qDir of + CIQDirectSnd -> True + CIQGroupSnd -> True + _ -> False, + memberId = case qDir of + CIQGroupRcv (Just GroupMember {memberId}) -> Just memberId + _ -> Nothing + }, + content = qContent + } + +webFileInfo :: CIFile d -> WebFileInfo +webFileInfo CIFile {fileName, fileSize} = WebFileInfo {fileName, fileSize} + +collectSenders :: [WebMemberProfile] -> [WebMemberProfile] +collectSenders = M.elems . M.fromList . map (\p@WebMemberProfile {memberId} -> (memberId, p)) + +memberToProfile :: GroupMember -> WebMemberProfile +memberToProfile GroupMember {memberId, memberProfile = LocalProfile {displayName, image}} = + WebMemberProfile {memberId, displayName, image} + +toPublishableGroup :: B64UrlByteString -> Maybe PublicGroupAccess -> PublishableGroup +toPublishableGroup pgId access = + PublishableGroup + { pgFileName = publicGroupIdFileName pgId <> ".json", + pgCorsEntry = corsEntry pgId <$> access + } + +corsEntry :: B64UrlByteString -> PublicGroupAccess -> (Text, CorsOrigin) +corsEntry publicGroupId PublicGroupAccess {groupWebPage, allowEmbedding} = + let fName = T.pack $ publicGroupIdFileName publicGroupId <> ".json" + origin + | allowEmbedding = CorsAny + | otherwise = CorsOrigins $ mapMaybe extractOrigin $ maybeToList groupWebPage + in (fName, origin) + +extractOrigin :: Text -> Maybe Text +extractOrigin url = + case U.parseURI U.laxURIParserOptions (encodeUtf8 url) of + Right uri@U.URI {uriScheme = U.Scheme sch, uriAuthority = Just _} + | sch == "https" || sch == "http" -> + let originUri = uri {U.uriPath = "", U.uriQuery = U.Query [], U.uriFragment = Nothing} + origin = safeDecodeUtf8 $ U.serializeURIRef' originUri + in if T.all safeOriginChar origin then Just origin else Nothing + _ -> Nothing + where + -- percent-encoded bytes in the host (e.g. %22, %0a) are decoded by serializeURIRef', + -- so reject any origin with characters that could break out of the Caddy CORS config or header + safeOriginChar c = + (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c `elem` (".-:/[]" :: [Char]) + +channelPath :: Text +channelPath = "/channel/" + +writeCorsConfig :: [(Text, CorsOrigin)] -> FilePath -> IO () +writeCorsConfig entries path = + TIO.writeFile path $ T.unlines $ + ["map {path} {cors_origin} {"] + <> map corsLine entries + <> [ " default \"\"", + "}", + "header " <> channelPath <> "*.json Access-Control-Allow-Origin {cors_origin}", + "header " <> channelPath <> "*.json Access-Control-Allow-Methods \"GET, OPTIONS\"" + ] + where + corsLine (fName, origin) = case origin of + CorsAny -> " " <> channelPath <> fName <> " \"*\"" + CorsOrigins origins -> case origins of + [] -> " # " <> fName <> " (no origin configured)" + (o : _) -> " " <> channelPath <> fName <> " \"" <> o <> "\"" + +removeStaleFiles :: FilePath -> S.Set FilePath -> IO () +removeStaleFiles dir activeFiles = do + let -- matches ".json" and leftover ".json.tmp" from an interrupted write + isPreviewFile f = + let f' = if takeExtension f == ".tmp" then dropExtension f else f + base = dropExtension f' + in takeExtension f' == ".json" && not (null base) && all isBase64Url base + isBase64Url c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' + allFiles <- S.filter isPreviewFile . S.fromList <$> listDirectory dir + mapM_ (\f -> removeFile (dir f)) $ S.difference allFiles activeFiles + +toFormattedText :: Text -> Maybe MarkdownList +toFormattedText t = case parseMaybeMarkdownList t of + Just fts | any hasFormat fts -> Just fts + _ -> Nothing + where + hasFormat (FormattedText fmt _) = isJust fmt + +publicGroupIdFileName :: B64UrlByteString -> String +publicGroupIdFileName = B.unpack . strEncode + +hasPublicGroup :: GroupInfo -> Bool +hasPublicGroup GroupInfo {groupProfile = GroupProfile {publicGroup}} = isJust publicGroup + diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs index 051ee6b304..9edbb3cb73 100644 --- a/tests/Bots/BroadcastTests.hs +++ b/tests/Bots/BroadcastTests.hs @@ -33,7 +33,7 @@ withBroadcastBot opts test = bot = simplexChatCore testCfg (mkChatOpts opts) $ broadcastBot opts broadcastBotProfile :: Profile -broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing} +broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkBotOpts :: TestParams -> [KnownContact] -> BroadcastBotOpts mkBotOpts ps publishers = diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index a3a48e7d29..15f713cd1c 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -27,8 +27,12 @@ import Simplex.Chat.Controller (ChatConfig (..)) import qualified Simplex.Chat.Markdown as MD import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB +import Simplex.Chat.Protocol (memberSupportVoiceVersion) import Simplex.Chat.Types (ChatPeerType (..), Profile (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) +import Simplex.Messaging.Version +import NameResolver import System.FilePath (()) import Test.Hspec hiding (it) @@ -72,6 +76,9 @@ directoryServiceTests = do describe "list and promote groups" $ do it "should list and promote user's groups" $ testListUserGroups True describe "member admission" $ do + it "should require captcha by default for new groups" testCaptchaByDefault + it "should require captcha in all groups with --always-captcha" testAlwaysCaptcha + it "should require admin review in all groups with --knocking" testKnocking it "should ask member to pass captcha screen" testCapthaScreening it "should send voice captcha on /audio command" testVoiceCaptchaScreening it "should retry with voice captcha after switching to audio mode" testVoiceCaptchaRetry @@ -95,8 +102,14 @@ directoryServiceTests = do it "should handle re-registration when already listed" testReregistrationAlreadyListed it "should update subscriber count periodically" testLinkCheckUpdatesCount +-- separate spec from directoryServiceTests: these need a names-enabled SMP server (withSmpServerAndNames) +directoryNameTests :: SpecWith TestParams +directoryNameTests = do + it "should verify and show a channel's SimpleX name" testDirectoryChannelName + it "should mark an inconsistent SimpleX name as not verified" testDirectoryChannelNameNotVerified + directoryProfile :: Profile -directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing} +directoryProfile = Profile {displayName = "SimpleX Directory", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing} mkDirectoryOpts :: TestParams -> [KnownContact] -> Maybe KnownGroup -> Maybe FilePath -> DirectoryOpts mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = @@ -126,10 +139,14 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = directoryLog = Just $ ps "directory_service.log", migrateDirectoryLog = Nothing, serviceName = "SimpleX Directory", + clientService = True, runCLI = False, searchResults = 3, webFolder, linkCheckInterval = 0, + prohibitedToObserver = False, + alwaysCaptcha = False, + knocking = False, testing = True } @@ -166,6 +183,8 @@ testDirectoryService ps = bob <## "Please add it to the group welcome message." bob <## "For example, add:" welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine bob + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." -- putStrLn "*** update profile without link" updateGroupProfile bob "Welcome!" bob <# "'SimpleX Directory'> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." @@ -393,6 +412,14 @@ testSetRole ps = cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://localhost/g#" cath <## "#privacy: member bob (Bob) is connected" @@ -425,12 +452,18 @@ testJoinGroup ps = cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory_1'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory_1'> " . dropTime <$> getTermLine cath + cath <## "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'" + cath <## "use @'SimpleX Directory' to send messages" + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" - cath - <### [ "contact and member are merged: 'SimpleX Directory', #privacy 'SimpleX Directory_1'", - "use @'SimpleX Directory' to send messages", - Predicate (\l -> l == welcomeMsg || dropTime_ l == Just ("#privacy 'SimpleX Directory'> " <> welcomeMsg) || dropTime_ l == Just ("#privacy 'SimpleX Directory_1'> " <> welcomeMsg)) - ] + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" 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" @@ -785,7 +818,7 @@ testNotSentApprovalBadRoles ps = bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 bob ##> "/mr privacy 'SimpleX Directory' member" bob <## "#privacy: you changed the role of 'SimpleX Directory' to member" updateProfileWithLink bob "privacy" welcomeWithLink 1 @@ -808,7 +841,7 @@ testNotApprovedBadRoles ps = bob `connectVia` dsLink cath `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 updateProfileWithLink bob "privacy" welcomeWithLink 1 notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 bob ##> "/mr privacy 'SimpleX Directory' member" @@ -1016,14 +1049,14 @@ testDuplicateAskConfirmation ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - _ <- groupAccepted bob "privacy" + _ <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink <- groupAccepted cath "privacy" + welcomeWithLink <- groupAccepted cath "privacy" 1 groupNotFound bob "privacy" completeRegistrationId superUser cath "privacy" "Privacy" welcomeWithLink 2 1 groupFound bob "privacy" @@ -1047,7 +1080,7 @@ testDuplicateProhibitConfirmation ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." @@ -1066,14 +1099,14 @@ testDuplicateProhibitWhenUpdated ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink' <- groupAccepted cath "privacy" + welcomeWithLink' <- groupAccepted cath "privacy" 1 groupNotFound cath "privacy" completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 groupFound cath "privacy" @@ -1097,14 +1130,14 @@ testDuplicateProhibitApproval ps = withNewTestChat ps "cath" cathProfile $ \cath -> do bob `connectVia` dsLink submitGroup bob "privacy" "Privacy" - welcomeWithLink <- groupAccepted bob "privacy" + welcomeWithLink <- groupAccepted bob "privacy" 1 cath `connectVia` dsLink submitGroup cath "privacy" "Privacy" cath <# "'SimpleX Directory'> The group privacy (Privacy) is already submitted to the directory." cath <## "To confirm the registration, please send:" cath <# "'SimpleX Directory'> /confirm 1:privacy" cath #> "@'SimpleX Directory' /confirm 1:privacy" - welcomeWithLink' <- groupAccepted cath "privacy" + welcomeWithLink' <- groupAccepted cath "privacy" 1 updateProfileWithLink cath "privacy" welcomeWithLink' 1 notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2 groupNotFound cath "privacy" @@ -1191,6 +1224,102 @@ checkListings listed promoted = do map groupName gs `shouldBe` expected groupName DirectoryEntry {displayName} = displayName +testAlwaysCaptcha :: HasCallStack => TestParams -> IO () +testAlwaysCaptcha ps = + withDirectoryServiceOpts ps (\o -> o {alwaysCaptcha = True}) $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- disable the per-group captcha filter; --always-captcha must still force it + bob #> "@'SimpleX Directory' /filter 1 off" + bob <# "'SimpleX Directory'> > /filter 1 off" + bob <## " Spam filter settings for group privacy set to:" + bob <## "- reject long/inappropriate names: disabled" + bob <## "- pass captcha to join: disabled" + bob <## "" + bob <## "/'filter 1 name' - enable name filter" + bob <## "/'filter 1 captcha' - enable captcha challenge" + bob <## "/'filter 1 name captcha' - enable both" + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" + 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" + +testKnocking :: HasCallStack => TestParams -> IO () +testKnocking ps = + withDirectoryServiceOpts ps (\o -> o {knocking = True}) $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, connecting to group moderators for admission to group" + cath <## "#privacy: 'SimpleX Directory' accepted you to the group, pending review" + bob <## "#privacy: 'SimpleX Directory' added cath (Catherine) to the group (connecting and pending review...), use /_accept member #1 3 to accept member" + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: new member cath is connected and pending review, use /_accept member #1 3 to accept member" + +testCaptchaByDefault :: HasCallStack => TestParams -> IO () +testCaptchaByDefault ps = + withDirectoryService ps $ \superUser dsLink -> + withNewTestChat ps "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + -- the owner never ran /filter; captcha is on by default for new groups + bob #> "@'SimpleX Directory' /role 1" + bob <# "'SimpleX Directory'> > /role 1" + bob <## " The initial member role for the group privacy is set to member" + bob <## "Send /'role 1 observer' to change it." + bob <## "" + note <- getTermLine bob + let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "#privacy: joining the group..." + cath <## "#privacy: you joined the group, pending approval" + cath <# "#privacy (support) 'SimpleX Directory'> Captcha is generated by SimpleX Directory service." + cath <## "" + cath <## "Send captcha text to join the group privacy." + captcha <- dropStrPrefix "#privacy (support) 'SimpleX Directory'> " . dropTime <$> getTermLine cath + cath #> ("#privacy (support) " <> captcha) + cath <# ("#privacy (support) 'SimpleX Directory'!> > cath " <> captcha) + cath <## " Correct, you joined the group privacy" + cath <## "#privacy: you joined the group" + cath <#. "#privacy 'SimpleX Directory'> Link to join the group privacy: https://" + 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" + testCapthaScreening :: HasCallStack => TestParams -> IO () testCapthaScreening ps = withDirectoryService ps $ \superUser dsLink -> @@ -1206,16 +1335,6 @@ testCapthaScreening ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - -- enable captcha - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- connect with captcha screen _ <- join cath groupLink cath #> "#privacy (support) 123" -- sending incorrect captcha @@ -1304,16 +1423,6 @@ testVoiceCaptchaScreening ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - -- enable captcha - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" @@ -1373,15 +1482,6 @@ testVoiceCaptchaRetry ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- cath joins, receives text captcha with /audio hint cath ##> ("/c " <> groupLink) cath <## "connection request sent!" @@ -1434,15 +1534,6 @@ testVoiceCaptchaVoiceDisabled ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" @@ -1491,7 +1582,7 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do setPermissions mockScript $ setOwnerExecutable True $ setOwnerReadable True $ setOwnerWritable True emptyPermissions withDirectoryServiceVoiceCaptcha ps mockScript $ \superUser dsLink -> withNewTestChat ps "bob" bobProfile $ \bob -> - withNewTestChatCfg ps testCfgVPrev "cath" cathProfile $ \cath -> do + withNewTestChatCfg ps testCfg {chatVRange = (chatVRange testCfg) {maxVersion = prevVersion memberSupportVoiceVersion}} "cath" cathProfile $ \cath -> do bob `connectVia` dsLink registerGroup superUser bob "privacy" "Privacy" bob #> "@'SimpleX Directory' /role 1" @@ -1501,15 +1592,6 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" -- disable voice messages in the group bob ##> "/set voice #privacy off" bob <## "updated group preferences:" @@ -1540,20 +1622,24 @@ testVoiceCaptchaOldClient ps@TestParams {tmpPath} = do cath <## " Correct, you joined the group privacy" cath <## "#privacy: you joined the group" -withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO () -withDirectoryServiceVoiceCaptcha ps voiceScript test = do +withDirectoryServiceOpts :: HasCallStack => TestParams -> (DirectoryOpts -> DirectoryOpts) -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceOpts ps modOpts test = do dsLink <- withNewTestChatCfg ps testCfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps testCfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser ds ##> "/ad" getContactLink ds True - let opts = (mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing) {voiceCaptchaGenerator = Just voiceScript} + let opts = modOpts $ mkDirectoryOpts ps [KnownContact 2 "alice"] Nothing Nothing runDirectory testCfg opts $ withTestChatCfg ps testCfg "super_user" $ \superUser -> do superUser <## "subscribed 1 connections on server localhost" test superUser dsLink +withDirectoryServiceVoiceCaptcha :: HasCallStack => TestParams -> FilePath -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceVoiceCaptcha ps voiceScript = + withDirectoryServiceOpts ps (\o -> o {voiceCaptchaGenerator = Just voiceScript}) + testRestoreDirectory :: HasCallStack => TestParams -> IO () testRestoreDirectory ps = do testListUserGroups False ps @@ -1674,6 +1760,7 @@ withDirectoryServiceCfgOwnersGroup ps cfg createOwnersGroup webFolder test = do withNewTestChatCfg ps cfg serviceDbPrefix directoryProfile $ \ds -> withNewTestChatCfg ps cfg "super_user" aliceProfile $ \superUser -> do connectUsers ds superUser + enableNamesRole ds when createOwnersGroup $ do superUser ##> "/g owners" superUser <## "group #owners is created" @@ -1726,7 +1813,7 @@ registerGroup su u n fn = registerGroupId su u n fn 1 1 registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO () registerGroupId su u n fn gId ugId = do submitGroup u n fn - welcomeWithLink <- groupAccepted u n + welcomeWithLink <- groupAccepted u n ugId completeRegistrationId su u n fn welcomeWithLink gId ugId submitGroup :: TestCC -> String -> String -> IO () @@ -1737,8 +1824,8 @@ submitGroup u n fn = do u ##> ("/a " <> viewName n <> " 'SimpleX Directory' admin") u <## ("invitation to join the group #" <> viewName n <> " sent to 'SimpleX Directory'") -groupAccepted :: TestCC -> String -> IO String -groupAccepted u n = do +groupAccepted :: TestCC -> String -> Int -> IO String +groupAccepted u n ugId = do u <### [ WithTime ("'SimpleX Directory'> Joining the group " <> n <> "…"), ConsoleString ("#" <> viewName n <> ": 'SimpleX Directory' joined the group") @@ -1748,7 +1835,10 @@ groupAccepted u n = do u <## "" u <## "Please add it to the group welcome message." u <## "For example, add:" - dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u -- welcome message with link + welcomeWithLink <- dropStrPrefix "'SimpleX Directory'> " . dropTime <$> getTermLine u + u <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + u <## ("Captcha verification is enabled. Use /'filter " <> show ugId <> "' to change it.") + pure welcomeWithLink completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () completeRegistration su u n fn welcomeWithLink gId = @@ -1882,15 +1972,6 @@ testCaptchaTooManyAttempts ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." @@ -1929,15 +2010,6 @@ testCaptchaUnknownCommand ps = bob <## "" note <- getTermLine bob let groupLink = dropStrPrefix "Please note: it applies only to members joining via this link: " note - bob #> "@'SimpleX Directory' /filter 1 captcha" - bob <# "'SimpleX Directory'> > /filter 1 captcha" - bob <## " Spam filter settings for group privacy set to:" - bob <## "- reject long/inappropriate names: disabled" - bob <## "- pass captcha to join: enabled" - bob <## "" - bob <## "/'filter 1 name' - enable name filter" - bob <## "/'filter 1 name captcha' - enable both" - bob <## "/'filter 1 off' - disable filter" cath ##> ("/c " <> groupLink) cath <## "connection request sent!" cath <## "#privacy: joining the group..." @@ -1996,10 +2068,12 @@ testRegisterChannelViaCard ps = [ do relay <## "'SimpleX Directory': accepting request to join group #news..." relay <## "#news: 'SimpleX Directory' joined the group", - bob <## "#news: relay added 'SimpleX Directory_1' to the group" + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] -- owner sends a message to trigger member introduction bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2052,6 +2126,89 @@ testRegisterChannelViaCard ps = superUser <# "'SimpleX Directory'> The channel ID 1 (news) is de-listed (channel owner left)." relay <## "#news: 'SimpleX Directory' left the group (signed)" +-- owner sets a name; directory verifies name<->link consistency and shows the verified name to the admin +testDirectoryChannelName :: HasCallStack => TestParams -> IO () +testDirectoryChannelName ps = withSmpServerAndNames $ \reg -> + withDirectoryServiceCfg ps testCfg $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + enableNamesRole bob + bob `connectVia` dsLink + (shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + registerName reg newsName (channelNameRecord "news" (T.pack shortLink)) + bob ##> "/public group access #news domain=news.simplex" + bob <## "updated public group access: domain=news.simplex" + relay <## "bob updated group #news: (signed)" + relay <## "updated public group access: domain=news.simplex" + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" + ] + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." + -- the directory verified the name against the channel link and shows it to the admin + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <## "SimpleX name: #news" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + where + newsName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "news" []) + +-- registry re-pointed to a different link after the owner set the name: directory verification fails +testDirectoryChannelNameNotVerified :: HasCallStack => TestParams -> IO () +testDirectoryChannelNameNotVerified ps = withSmpServerAndNames $ \reg -> + withDirectoryServiceCfg ps testCfg $ \superUser dsLink -> + withNewTestChatCfg ps testCfg "bob" bobProfile $ \bob -> + withRelay ps $ \relay -> do + enableNamesRole bob + bob `connectVia` dsLink + (shortLink, _fullLink) <- prepareChannel1Relay "news" bob relay + registerName reg newsName (channelNameRecord "news" (T.pack shortLink)) + bob ##> "/public group access #news domain=news.simplex" + bob <## "updated public group access: domain=news.simplex" + relay <## "bob updated group #news: (signed)" + relay <## "updated public group access: domain=news.simplex" + -- the name is re-pointed to a different link after the owner set it + registerName reg newsName (channelNameRecord "news" "https://simplex.chat/other") + bob ##> "/share chat #news @'SimpleX Directory'" + bob <# "@'SimpleX Directory' link to join channel #news (signed):" + _ <- getTermLine bob -- short link + _ <- getTermLine bob -- ownerSig JSON + bob <# "'SimpleX Directory'> Joining the channel news…" + concurrentlyN_ + [ do + relay <## "'SimpleX Directory': accepting request to join group #news..." + relay <## "#news: 'SimpleX Directory' joined the group", + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" + ] + bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." + superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" + superUser <## "news" + superUser <## "SimpleX name: #news (NOT verified - will not be shown)" + superUser <##. "Link to join channel: " + superUser <## "You need SimpleX Chat app v6.5 to join." + superUser <## "1 subscribers" + superUser <## "" + superUser <## "To approve send:" + superUser <# "'SimpleX Directory'> /approve 1:news 1" + where + newsName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "news" []) + testLinkAsTextSearch :: HasCallStack => TestParams -> IO () testLinkAsTextSearch ps = withDirectoryServiceCfg ps testCfg $ \_superUser dsLink -> @@ -2095,9 +2252,11 @@ testDeleteChannelRegistration ps = [ do relay <## "'SimpleX Directory': accepting request to join group #news..." relay <## "#news: 'SimpleX Directory' joined the group", - bob <## "#news: relay added 'SimpleX Directory_1' to the group" + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2139,9 +2298,11 @@ testReregistrationAlreadyListed ps = [ do relay <## "'SimpleX Directory': accepting request to join group #news..." relay <## "#news: 'SimpleX Directory' joined the group", - bob <## "#news: relay added 'SimpleX Directory_1' to the group" + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " @@ -2198,9 +2359,11 @@ testLinkCheckUpdatesCount ps = do [ do relay <## "'SimpleX Directory': accepting request to join group #news..." relay <## "#news: 'SimpleX Directory' joined the group", - bob <## "#news: relay added 'SimpleX Directory_1' to the group" + bob <## "#news: relay introduced 'SimpleX Directory_1' in the channel" ] bob <# "'SimpleX Directory'> Joined the channel news. Registration is pending approval — it may take up to 48 hours." + bob <# "'SimpleX Directory'> We recommend allowing direct messages, media, voice, and SimpleX links only for group moderators and admins. Use group preferences to set them." + bob <## "Captcha verification is enabled. Use /'filter 1' to change it." superUser <# "'SimpleX Directory'> bob submitted the channel ID 1:" superUser <## "news" superUser <##. "Link to join channel: " diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 279a09e718..77d91bf522 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -1,4 +1,5 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} @@ -23,13 +24,17 @@ import Control.Monad.Except import Control.Monad.Reader import Data.Functor (($>)) import Data.List (dropWhileEnd, find) +import qualified Data.List.NonEmpty as L import Data.Maybe (isNothing) +import Data.Text (Text) import qualified Data.Text as T +import Data.Time.Clock (getCurrentTime) import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), ChatResponse (..), WebPreviewConfig (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Library.Commands +import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.Options.DB import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) @@ -48,6 +53,7 @@ import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval +import Simplex.Messaging.Agent.Store.Entity (SDBStored (..)) import Simplex.Messaging.Agent.Store.Interface (closeDBStore) import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -55,15 +61,17 @@ import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import Simplex.Messaging.Protocol (sndAuthKeySMPClientVersion) +import Simplex.Messaging.Protocol (ProtocolType (..)) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM (ServerConfig (..), ServerStoreCfg (..), StartOptions (..), StorePaths (..), defaultMessageExpiration, defaultIdleQueueInterval, defaultNtfExpiration, defaultInactiveClientExpiration) +import NameResolver (NameRegistry, resolverNamesConfig, withNameResolver) import Simplex.Messaging.Server.MsgStore.STM (STMMsgStore) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive) +import System.FilePath (()) import qualified System.Terminal as C import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal) import System.Timeout (timeout) @@ -77,7 +85,6 @@ import Data.ByteArray (ScrubbedBytes) import qualified Data.Map.Strict as M import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Store.Common (withConnection) -import System.FilePath (()) #endif #if defined(dbPostgres) @@ -119,7 +126,9 @@ testOpts = autoAcceptFileSize = 0, muteNotifications = True, markRead = True, - createBot = Nothing + createBot = Nothing, + userDisplayName = Nothing, + userImageFile = Nothing } testCoreOpts :: CoreChatOpts @@ -152,6 +161,9 @@ testCoreOpts = tbqSize = 16, deviceName = Nothing, chatRelay = False, + webPreviewConfig = Nothing, + chatRelayServer = Nothing, + headless = False, highlyAvailable = False, yesToUpMigrations = False, migrationBackupPath = Nothing, @@ -161,6 +173,9 @@ testCoreOpts = relayTestOpts :: ChatOpts relayTestOpts = testOpts {coreOptions = testCoreOpts {chatRelay = True}} +relayWebTestOpts :: Text -> FilePath -> Maybe FilePath -> ChatOpts +relayWebTestOpts webDomain webDir webCorsFile = testOpts {coreOptions = testCoreOpts {chatRelay = True, webPreviewConfig = Just WebPreviewConfig {webDomain, webJsonDir = webDir, webCorsFile, webUpdateInterval = 300, webPreviewItemCount = 50}}} + #if !defined(dbPostgres) getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts getTestOpts maintenance dbKey = testOpts {coreOptions = testCoreOpts {maintenance, dbOptions = (dbOptions testCoreOpts) {dbKey}}} @@ -196,13 +211,6 @@ testAgentCfg = where RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg -testAgentCfgNoShortLinks :: AgentConfig -testAgentCfgNoShortLinks = - testAgentCfg - { smpClientVRange = mkVersionRange (Version 1) sndAuthKeySMPClientVersion, -- v3 - smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion (Version 14)} -- before shortLinksSMPVersion - } - testCfg :: ChatConfig testCfg = defaultChatConfig @@ -211,13 +219,10 @@ testCfg = shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"], testView = True, tbqSize = 16, - channelSubscriberRole = GRMember, -- starting role is GRMember to test members sending messages + channelSubscriberRole = GRObserver, confirmMigrations = MCYesUp } -testCfgNoShortLinks :: ChatConfig -testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks} - testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg @@ -281,18 +286,19 @@ prevVersion (Version v) = Version (v - 1) nextVersion :: Version v -> Version v nextVersion (Version v) = Version (v + 1) -createTestChat :: TestParams -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC -createTestChat ps cfg opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {chatRelay}} dbPrefix profile = do +createTestChat :: TestParams -> ChatConfig -> ChatOpts -> String -> Bool -> Profile -> IO TestCC +createTestChat ps cfg opts@ChatOpts {coreOptions = coreOptions@CoreChatOpts {chatRelay}} dbPrefix clientService profile = do Right db@ChatDatabase {chatStore, agentStore} <- createDatabase ps coreOptions dbPrefix insertUser agentStore - Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile chatRelay True - startTestChat_ ps db cfg opts user + ts <- getCurrentTime + Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecordAt db' (AgentUserId 1) chatRelay clientService profile True ts + startTestChat_ ps db cfg opts dbPrefix user startTestChat :: TestParams -> ChatConfig -> ChatOpts -> String -> IO TestCC startTestChat ps cfg opts@ChatOpts {coreOptions} dbPrefix = do Right db@ChatDatabase {chatStore} <- createDatabase ps coreOptions dbPrefix Just user <- find activeUser <$> withTransaction chatStore getUsers - startTestChat_ ps db cfg opts user + startTestChat_ ps db cfg opts dbPrefix user createDatabase :: TestParams -> CoreChatOpts -> String -> IO (Either MigrationError ChatDatabase) #if defined(dbPostgres) @@ -309,12 +315,12 @@ insertUser :: DBStore -> IO () insertUser st = withTransaction st (`DB.execute_` "INSERT INTO users (user_id) VALUES (1)") #endif -startTestChat_ :: TestParams -> ChatDatabase -> ChatConfig -> ChatOpts -> User -> IO TestCC -startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {coreOptions = CoreChatOpts {maintenance}} user = do +startTestChat_ :: TestParams -> ChatDatabase -> ChatConfig -> ChatOpts -> String -> User -> IO TestCC +startTestChat_ TestParams {tmpPath, printOutput} db cfg opts@ChatOpts {coreOptions = CoreChatOpts {maintenance}} dbPrefix user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts - cc <- newChatController db (Just user) cfg opts False - void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") 0 `runReaderT` cc + Right cc <- newChatController db (Just user) cfg opts False + void $ execChatCommand' (SetTempFolder (tmpPath dbPrefix)) 0 `runReaderT` cc chatAsync <- async $ runSimplexChat cfg opts user cc $ \_u cc' -> runChatTerminal ct cc' opts unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO @@ -351,6 +357,9 @@ stopTestChat ps TestCC {chatController = cc@ChatController {smpAgent, chatStore} withNewTestChat :: HasCallStack => TestParams -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a withNewTestChat ps = withNewTestChatCfgOpts ps testCfg testOpts +withNewTestChat_ :: HasCallStack => TestParams -> String -> Bool -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChat_ ps = withNewTestChatCfgOpts_ ps testCfg testOpts + withNewTestChatV1 :: HasCallStack => TestParams -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a withNewTestChatV1 ps = withNewTestChatCfg ps testCfgV1 @@ -361,9 +370,12 @@ withNewTestChatOpts :: HasCallStack => TestParams -> ChatOpts -> String -> Profi withNewTestChatOpts ps = withNewTestChatCfgOpts ps testCfg withNewTestChatCfgOpts :: HasCallStack => TestParams -> ChatConfig -> ChatOpts -> String -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a -withNewTestChatCfgOpts ps cfg opts dbPrefix profile runTest = +withNewTestChatCfgOpts ps cfg opts dbPrefix = withNewTestChatCfgOpts_ ps cfg opts dbPrefix False + +withNewTestChatCfgOpts_ :: HasCallStack => TestParams -> ChatConfig -> ChatOpts -> String -> Bool -> Profile -> (HasCallStack => TestCC -> IO a) -> IO a +withNewTestChatCfgOpts_ ps cfg opts dbPrefix clientService profile runTest = bracket - (createTestChat ps cfg opts dbPrefix profile) + (createTestChat ps cfg opts dbPrefix clientService profile) (stopTestChat ps) (\cc -> runTest cc >>= ((cc )) @@ -387,6 +399,26 @@ withTestChatCfgOpts ps cfg opts dbPrefix = bracket (startTestChat ps cfg opts db withTestOutput :: HasCallStack => (HasCallStack => TestParams -> IO ()) -> TestParams -> IO () withTestOutput test ps = test ps {printOutput = True} +-- Opt the client's SMP servers into name resolution (self-hosted servers default names off). +enableNamesRole :: HasCallStack => TestCC -> IO () +enableNamesRole TestCC {chatController = cc} = do + r <- execChatCommand' (APIGetUserServers 1) 0 `runReaderT` cc + case r of + Right (CRUserServers _ uoss) -> do + r' <- execChatCommand' (APISetUserServers 1 (L.fromList (map toUpdated uoss))) 0 `runReaderT` cc + either (fail . show) (const $ pure ()) r' + Right other -> fail $ "enableNamesRole: unexpected response " <> show other + Left e -> fail $ "enableNamesRole: APIGetUserServers failed " <> show e + where + toUpdated UserOperatorServers {operator, smpServers, xftpServers, chatRelays} = + UpdatedUserOperatorServers + { operator, + smpServers = map (AUS SDBStored . enableNames) smpServers, + xftpServers = map (AUS SDBStored) xftpServers, + chatRelays = map (AUCR SDBStored) chatRelays + } + enableNames srv@UserServer {roles} = (srv :: UserServer 'PSMP) {roles = (roles :: ServerRolesOverride) {names = Just True}} + readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput t termQ = do let w = virtualWindow t @@ -420,9 +452,11 @@ testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStac testChatN cfg opts ps test params = bracket (getTestCCs $ zip ps [1 ..]) endTests test where + useClientServices = False + -- useClientServices = True getTestCCs :: [(Profile, Int)] -> IO [TestCC] getTestCCs [] = pure [] - getTestCCs ((p, db) : envs') = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' + getTestCCs ((p, db) : envs') = (:) <$> createTestChat params cfg opts (show db) useClientServices p <*> getTestCCs envs' endTests tcs = do mapConcurrently_ ( IO a -> IO a withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) +-- | SMP server with a local names resolver attached; the action gets the resolver +-- registry to map names to the addresses it creates. +withSmpServerAndNames :: (NameRegistry -> IO a) -> IO a +withSmpServerAndNames action = + withNameResolver $ \port reg -> + withSmpServer' smpServerCfg {namesConfig = Just (resolverNamesConfig port)} (action reg) + xftpTestPort :: ServiceName xftpTestPort = "7002" diff --git a/tests/ChatTests/ChatRelays.hs b/tests/ChatTests/ChatRelays.hs index e5c2598f41..5d1abcfe21 100644 --- a/tests/ChatTests/ChatRelays.hs +++ b/tests/ChatTests/ChatRelays.hs @@ -1,5 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} module ChatTests.ChatRelays where @@ -18,9 +20,14 @@ import ProtocolTests (testGroupProfile) import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..)) import Simplex.Chat.Types (GroupProfile (..)) +import Simplex.Chat.Controller (CorsOrigin (..)) +import Simplex.Chat.Web (WebChannelPreview (..), WebMessage (..), extractOrigin, removeStaleFiles, writeCorsConfig) import Simplex.Messaging.Crypto.BBS (bbsKeyGen) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Util (decodeJSON) +import qualified Data.Set as S +import System.Directory (createDirectoryIfMissing, doesFileExist, listDirectory) +import System.FilePath (takeExtension, ()) import Test.Hspec hiding (it) chatRelayTests :: SpecWith TestParams @@ -31,6 +38,19 @@ chatRelayTests = do it "re-add soft-deleted relay by same name" testReAddRelaySameName it "test chat relay" testChatRelayTest it "relay profile updated in address" testRelayProfileUpdateInAddress + describe "relay capabilities" $ do + it "relay sends webDomain in capabilities" testRelayWebCapabilities + describe "web preview" $ do + it "render messages and members" testWebPreviewRender + it "incremental render adds new messages" testWebPreviewIncremental + it "edited and deleted messages" testWebPreviewEditedDeleted + it "reactions in rendered messages" testWebPreviewReactions + it "non-public group produces no file" testWebPreviewNonPublic + it "multiple channels produce multiple files" testWebPreviewMultipleChannels + it "channel deletion removes preview file" testWebPreviewChannelDeleted + it "removeStaleFiles preserves non-base64url files" testWebPreviewStaleCleanup + it "generate CORS config" testWebPreviewCors + it "extractOrigin strips path from URL" testExtractOrigin describe "share channel card" $ do it "share channel card in direct chat" testShareChannelDirect it "share channel card in group" testShareChannelGroup @@ -362,6 +382,238 @@ testShareChannelChannel ps = getTermLine2 :: TestCC -> IO (String, String) getTermLine2 c = (,) <$> getTermLine c <*> getTermLine c +testRelayWebCapabilities :: HasCallStack => TestParams -> IO () +testRelayWebCapabilities ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" (tmpPath ps "web_cap") Nothing) "bob" bobProfile $ \relay -> do + rName <- userName relay + relay ##> "/ad" + (relaySLink, _cLink) <- getContactLinks relay True + alice ##> ("/relays name=" <> rName <> " " <> relaySLink) + alice <## "ok" + alice ##> "/public group relays=1 #news" + alice <## "group #news is created" + alice <## "wait for selected relay(s) to join, then you can invite members via group link" + concurrentlyN_ + [ do + alice <## "#news: group link relays updated, current relays:" + alice <### [EndsWith ": active, web: relay.example.com"] + alice <## "group link:" + _ <- getTermLine alice + pure (), + relay <## "#news: you joined the group as relay" + ] + +-- Helper: set up relay with web config + channel +withWebChannel :: TestParams -> String -> (TestCC -> TestCC -> FilePath -> IO ()) -> IO () +withWebChannel ps gName test = do + let webDir = tmpPath ps "web_" <> gName + corsFile = tmpPath ps "cors_" <> gName <> ".conf" + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir (Just corsFile)) "bob" bobProfile $ \relay -> do + _ <- setupRelay alice relay + createChannelWithRelayWeb gName alice relay + test alice relay webDir + +createChannelWithRelayWeb :: HasCallStack => String -> TestCC -> TestCC -> IO () +createChannelWithRelayWeb gName owner relay = do + owner ##> ("/public group relays=1 #" <> gName) + owner <## ("group #" <> gName <> " is created") + owner <## "wait for selected relay(s) to join, then you can invite members via group link" + concurrentlyN_ + [ do + owner <## ("#" <> gName <> ": group link relays updated, current relays:") + owner <### [EndsWith ": active, web: relay.example.com"] + owner <## "group link:" + _ <- getTermLine owner + pure (), + relay <## ("#" <> gName <> ": you joined the group as relay") + ] + +-- Poll for a JSON preview file written by the worker that satisfies predicate, with timeout +waitPreviewWith :: HasCallStack => FilePath -> (WebChannelPreview -> Bool) -> IO WebChannelPreview +waitPreviewWith webDir check = go 50 + where + go :: Int -> IO WebChannelPreview + go 0 = error "waitPreview: timed out waiting for matching JSON file" + go n = do + files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir + case files of + [f] -> do + jsonBytes <- LB.readFile (webDir f) + case J.eitherDecode jsonBytes of + Right p | check p -> pure p + _ -> threadDelay 100000 >> go (n - 1) + _ -> threadDelay 100000 >> go (n - 1) + +waitPreview :: HasCallStack => FilePath -> IO WebChannelPreview +waitPreview webDir = waitPreviewWith webDir (const True) + +testWebPreviewRender :: HasCallStack => TestParams -> IO () +testWebPreviewRender ps = + withWebChannel ps "news" $ \alice relay webDir -> do + alice #> "#news hello from the channel" + relay <# "#news> hello from the channel" + alice #> "#news second message" + relay <# "#news> second message" + wPreview <- waitPreviewWith webDir (\p -> length (messages p) >= 2) + let GroupProfile {displayName = chName} = channel wPreview + chName `shouldBe` "news" + length (messages wPreview) `shouldBe` 2 + content (messages wPreview !! 0) `shouldBe` MCText "hello from the channel" + content (messages wPreview !! 1) `shouldBe` MCText "second message" + length (members wPreview) `shouldSatisfy` (>= 1) + all (\m -> ts m > read "2020-01-01 00:00:00 UTC") (messages wPreview) `shouldBe` True + jsonFiles <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir + length jsonFiles `shouldBe` 1 + +testWebPreviewIncremental :: HasCallStack => TestParams -> IO () +testWebPreviewIncremental ps = + withWebChannel ps "inc" $ \alice relay webDir -> do + alice #> "#inc first" + relay <# "#inc> first" + p1 <- waitPreviewWith webDir (\p -> length (messages p) >= 1) + length (messages p1) `shouldBe` 1 + content (messages p1 !! 0) `shouldBe` MCText "first" + alice #> "#inc second" + relay <# "#inc> second" + alice #> "#inc third" + relay <# "#inc> third" + p2 <- waitPreviewWith webDir (\p -> length (messages p) >= 3) + length (messages p2) `shouldBe` 3 + content (messages p2 !! 0) `shouldBe` MCText "first" + content (messages p2 !! 1) `shouldBe` MCText "second" + content (messages p2 !! 2) `shouldBe` MCText "third" + +testWebPreviewEditedDeleted :: HasCallStack => TestParams -> IO () +testWebPreviewEditedDeleted ps = + withWebChannel ps "ed" $ \alice relay webDir -> do + alice #> "#ed msg one" + relay <# "#ed> msg one" + alice #> "#ed msg two" + relay <# "#ed> msg two" + msgId2 <- lastItemId alice + alice #> "#ed msg three" + relay <# "#ed> msg three" + msgId3 <- lastItemId alice + alice ##> ("/_update item #1 " <> msgId2 <> " text msg two edited") + alice <# "#ed [edited] msg two edited" + relay <# "#ed> [edited] msg two edited" + alice #$> ("/_delete item #1 " <> msgId3 <> " broadcast", id, "message marked deleted") + relay <# "#ed> [marked deleted] msg three" + p <- waitPreviewWith webDir (\p -> length (messages p) == 2 && any edited (messages p)) + length (messages p) `shouldBe` 2 + content (messages p !! 0) `shouldBe` MCText "msg one" + content (messages p !! 1) `shouldBe` MCText "msg two edited" + edited (messages p !! 0) `shouldBe` False + edited (messages p !! 1) `shouldBe` True + +testWebPreviewReactions :: HasCallStack => TestParams -> IO () +testWebPreviewReactions ps = + withWebChannel ps "react" $ \alice relay webDir -> do + alice #> "#react hello" + relay <# "#react> hello" + alice ##> "+1 #react hello" + alice <## "added 👍" + relay <# "#react alice> > hello" + relay <## " + 👍" + p <- waitPreviewWith webDir (\p -> not (null (messages p)) && not (null (reactions (head (messages p))))) + length (messages p) `shouldBe` 1 + length (reactions (messages p !! 0)) `shouldSatisfy` (>= 1) + +testWebPreviewNonPublic :: HasCallStack => TestParams -> IO () +testWebPreviewNonPublic ps = do + let webDir = tmpPath ps "web_nonpub" + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir Nothing) "bob" bobProfile $ \relay -> do + _ <- setupRelay alice relay + alice ##> "/g private" + alice <## "group #private is created" + alice <## "to add members use /a private or /create link #private" + alice #> "#private hello" + threadDelay 2000000 + files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir + length files `shouldBe` 0 + +testWebPreviewMultipleChannels :: HasCallStack => TestParams -> IO () +testWebPreviewMultipleChannels ps = do + let webDir = tmpPath ps "web_multi" + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps (relayWebTestOpts "relay.example.com" webDir Nothing) "bob" bobProfile $ \relay -> do + _ <- setupRelay alice relay + createChannelWithRelayWeb "ch1" alice relay + createChannelWithRelayWeb "ch2" alice relay + alice #> "#ch1 msg in ch1" + relay <# "#ch1> msg in ch1" + alice #> "#ch2 msg in ch2" + relay <# "#ch2> msg in ch2" + threadDelay 2000000 + files <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir + length files `shouldBe` 2 + +testWebPreviewChannelDeleted :: HasCallStack => TestParams -> IO () +testWebPreviewChannelDeleted ps = + withWebChannel ps "del" $ \alice relay webDir -> do + alice #> "#del hello" + relay <# "#del> hello" + _ <- waitPreviewWith webDir (\p -> not (null (messages p))) + jsonFiles <- filter (\f -> takeExtension f == ".json") <$> listDirectory webDir + length jsonFiles `shouldBe` 1 + let previewFile = webDir head jsonFiles + alice ##> "/d #del" + alice <## "#del: you deleted the group (signed)" + relay <## "#del: alice deleted the group (signed)" + relay <## "use /d #del to delete the local copy of the group" + waitFileDeleted previewFile 50 + +testWebPreviewStaleCleanup :: HasCallStack => TestParams -> IO () +testWebPreviewStaleCleanup ps = do + let webDir = tmpPath ps "web_stale_unit" + activeFile = "abc123.json" + staleFile = "AAAA_stale.json" + safeFile = "my.config.json" + createDirectoryIfMissing True webDir + writeFile (webDir activeFile) "{}" + writeFile (webDir staleFile) "{}" + writeFile (webDir safeFile) "{}" + removeStaleFiles webDir (S.singleton activeFile) + doesFileExist (webDir staleFile) `shouldReturn` False + doesFileExist (webDir safeFile) `shouldReturn` True + doesFileExist (webDir activeFile) `shouldReturn` True + +waitFileDeleted :: HasCallStack => FilePath -> Int -> IO () +waitFileDeleted _ 0 = error "waitFileDeleted: timed out" +waitFileDeleted path n = + doesFileExist path >>= \case + False -> pure () + True -> threadDelay 100000 >> waitFileDeleted path (n - 1) + +testWebPreviewCors :: HasCallStack => TestParams -> IO () +testWebPreviewCors ps = do + let corsFile = tmpPath ps "simplex-cors.conf" + entries = + [ ("abc123.json", CorsAny), + ("def456.json", CorsOrigins ["https://owner-site.com"]), + ("ghi789.json", CorsOrigins []) + ] + writeCorsConfig entries corsFile + corsContent <- readFile corsFile + corsContent `shouldContain` "/channel/abc123.json \"*\"" + corsContent `shouldContain` "/channel/def456.json \"https://owner-site.com\"" + corsContent `shouldContain` "# ghi789.json (no origin configured)" + corsContent `shouldContain` "Access-Control-Allow-Origin" + corsContent `shouldContain` "Access-Control-Allow-Methods" + +testExtractOrigin :: HasCallStack => TestParams -> IO () +testExtractOrigin _ps = do + extractOrigin "https://owner.example.com/channel.html" `shouldBe` Just "https://owner.example.com" + extractOrigin "https://owner.example.com/path/to/page?q=1#frag" `shouldBe` Just "https://owner.example.com" + extractOrigin "https://owner.example.com:8443/page" `shouldBe` Just "https://owner.example.com:8443" + extractOrigin "https://owner.example.com" `shouldBe` Just "https://owner.example.com" + extractOrigin "http://localhost:3000/preview" `shouldBe` Just "http://localhost:3000" + extractOrigin "ftp://example.com/file" `shouldBe` Nothing + extractOrigin "not-a-url" `shouldBe` Nothing + -- Create a public group with relay=1, wait for relay to join createChannelWithRelay :: HasCallStack => String -> TestCC -> TestCC -> IO () createChannelWithRelay gName owner relay = do diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 740e757ed8..e34f071fd0 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -122,6 +122,7 @@ chatDirectTests = do it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "delete user with chat tags" testDeleteUserChatTags + it "rejects raw chat TTL updates for another user's chat" testRejectCrossUserChatTTL it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration it "chat items only expire for users who configured expiration" testEnableCIExpirationOnlyForOneUser @@ -1209,20 +1210,20 @@ testOperators = alice <##. "Current conditions: 2." alice ##> "/_operators" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" - alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required" alice <##. "The new conditions will be accepted for SimpleX Chat Ltd, InFlux Technologies Limited at " -- set conditions notified alice ##> "/_conditions_notified 2" alice <## "ok" alice ##> "/_operators" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: required" - alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: required" + alice <## "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: required" alice ##> "/_conditions" alice <##. "Current conditions: 2 (notified)." -- accept conditions alice ##> "/_accept_conditions 2 1,2" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" - alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled proxy, conditions: accepted (" + alice <##. "2 (flux). Flux (InFlux Technologies Limited), domains: simplexonflux.com, servers: SMP enabled proxy, XFTP enabled, conditions: accepted (" -- update operators alice ##> "/operators 2:on:smp=proxy:xftp=off" alice <##. "1 (simplex). SimpleX Chat (SimpleX Chat Ltd), domains: simplex.im, servers: enabled, conditions: accepted (" @@ -1554,11 +1555,12 @@ testConnSyncExtraAgentUsers ps = do DB.execute_ db "UPDATE connections_sync SET should_sync = 1 WHERE connections_sync_id = 1" withTestChat ps "alice" $ \alice -> do - alice <## "connections difference summary:" - alice <## "number of extra users in agent: 1" - alice <## "removed extra users in agent" - - alice <## "subscribed 1 connections on server localhost" + alice <### + [ "connections difference summary:", + "number of extra users in agent: 1", + "removed extra users in agent", + "subscribed 1 connections on server localhost" + ] threadDelay 100000 agentUserCount <- withCCAgentTransaction alice $ \db -> @@ -1918,14 +1920,14 @@ testMultipleUserAddresses = cLinkAlisa <- getContactLink alice True bob ##> ("/c " <> cLinkAlisa) alice <#? bob - alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", "Audio/video calls: enabled"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", "Audio/video calls: enabled"), ("@Ask SimpleX Team", ""), ("*", "")]) alice ##> "/ac bob" alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alisa: contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@Ask SimpleX Team", ""), ("*", "")]) alice <##> bob bob #> "@alice hey alice" @@ -1956,7 +1958,7 @@ testMultipleUserAddresses = (cath <## "alisa: contact is connected") (alice <## "cath (Catherine): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@Ask SimpleX Team", ""), ("*", "")]) alice <##> cath -- first user doesn't have cath as contact @@ -2096,6 +2098,25 @@ testDeleteUserChatTags = alice ##> "/users" alice <## "alisa (active)" +testRejectCrossUserChatTTL :: HasCallStack => TestParams -> IO () +testRejectCrossUserChatTTL = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #$> ("/_ttl 1 @2 2", id, "ok") + alice #$> ("/ttl @bob", id, "old messages are set to be deleted after: 2 second(s)") + + alice ##> "/create user alisa" + showActiveUser alice "alisa" + + alice ##> "/_ttl 2 @2 9" + alice <##. "chat db error:" + + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + alice #$> ("/ttl @bob", id, "old messages are set to be deleted after: 2 second(s)") + testUsersDifferentCIExpirationTTL :: HasCallStack => TestParams -> IO () testUsersDifferentCIExpirationTTL ps = do withNewTestChat ps "bob" bobProfile $ \bob -> do @@ -2150,7 +2171,7 @@ testUsersDifferentCIExpirationTTL ps = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -2163,11 +2184,11 @@ testUsersDifferentCIExpirationTTL ps = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2100000 - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -2233,7 +2254,7 @@ testUsersRestartCIExpiration ps = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -2246,11 +2267,11 @@ testUsersRestartCIExpiration ps = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 4000000 - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -2292,7 +2313,7 @@ testEnableCIExpirationOnlyForOneUser ps = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 @@ -2304,14 +2325,14 @@ testEnableCIExpirationOnlyForOneUser ps = do -- messages are not deleted for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) withTestChatCfg ps cfg "alice" $ \alice -> do alice <## "subscribed 1 connections on server localhost" alice <## "subscribed 1 connections on server localhost" -- messages are not deleted for second user after restart - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #> "@bob alisa 5" bob <# "alisa> alisa 5" @@ -2321,7 +2342,7 @@ testEnableCIExpirationOnlyForOneUser ps = do threadDelay 2000000 -- new messages are not deleted for second user - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -2355,12 +2376,12 @@ testDisableCIExpirationOnlyForOneUser ps = do bob #> "@alisa alisa 2" alice <# "bob> alisa 2" - alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @5 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) withTestChatCfg ps cfg "alice" $ \alice -> do alice <## "subscribed 1 connections on server localhost" @@ -2374,12 +2395,12 @@ testDisableCIExpirationOnlyForOneUser ps = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2500000 + threadDelay 3000000 -- second user messages are deleted - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -2394,7 +2415,7 @@ testUsersTimedMessages ps' = do alice ##> "/create user alisa" showActiveUser alice "alisa" connectUsers alice bob - configureTimedMessages alice bob "6" "3" + configureTimedMessages alice bob "5" "3" -- first user messages alice ##> "/user alice" @@ -2423,7 +2444,7 @@ testUsersTimedMessages ps' = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner"), (1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -2436,7 +2457,7 @@ testUsersTimedMessages ps' = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner"), (1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -2445,7 +2466,7 @@ testUsersTimedMessages ps' = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) -- first user messages alice ##> "/user alice" @@ -2475,7 +2496,7 @@ testUsersTimedMessages ps' = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) -- messages are deleted after restart threadDelay 1000000 @@ -2489,7 +2510,7 @@ testUsersTimedMessages ps' = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 1000000 @@ -2498,7 +2519,7 @@ testUsersTimedMessages ps' = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) + alice #$> ("/_get chat @5 count=100", chat, [(1,"chat banner")]) where ps = ps' {printOutput = True} :: TestParams configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index c8cd1c5f30..37827ab2ad 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -3,6 +3,7 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -16,31 +17,41 @@ import ChatTests.DBUtils import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) +import Control.Concurrent.STM (atomically) import Control.Monad (forM_, void, when) +import Control.Monad.Except (runExceptT) import Data.Bifunctor (second) -import Data.Maybe (fromMaybe, maybeToList) +import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B +import Data.Maybe (fromMaybe, isJust, maybeToList) +import Data.Time (UTCTime, getCurrentTime) import Data.Int (Int64) -import Data.List (intercalate, isInfixOf) +import Data.List (intercalate, isInfixOf, isSuffixOf) import qualified Data.Map.Strict as M import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), ChatHooks (..), defaultChatHooks) +import Simplex.Chat.Controller (ChatController (ChatController, smpAgent), ChatConfig (..), ChatHooks (..), ChatLogLevel (..), defaultChatHooks) import Simplex.Chat.Library.Internal (uniqueMsgMentions, updatedMentionNames) import Simplex.Chat.Markdown (parseMaybeMarkdownList) import Simplex.Chat.Messages (CIMention (..), CIMentionMember (..), ChatItemId) +import Simplex.Chat.Messages.Batch (encodeBinaryBatch, encodeFwdElement) import Simplex.Chat.Messages.CIContent (publicGroupNoE2EText) import Simplex.Chat.Options -import Simplex.Chat.Protocol (MsgMention (..), MsgContent (..), msgContentText) +import Simplex.Chat.Protocol (ChatMessage (ChatMessage), ChatMsgEvent (XGrpMemNew, XMsgUpdate, XMsgNew, XMsgDel), FwdSender (FwdMember, FwdChannel), GrpMsgForward (GrpMsgForward), MsgContainer (..), MsgMention (..), MsgContent (..), VerifiedMsg (VMUnsigned), mcSimple, msgContentText) import Simplex.Chat.Types -import Simplex.Chat.Types.MemberRelations (MemberRelation (..), setRelation) +import Simplex.Chat.Types.MemberRelations (MemberRelation (..), getRelation, setRelation) import Simplex.Chat.Types.Shared (GroupMemberRole (..), GroupAcceptance (..)) +import Simplex.Messaging.Agent (sendMessages, vrValue) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.DB (Binary (..)) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff) +import Simplex.Messaging.Protocol (MsgFlags (..)) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport import Simplex.Messaging.Version +import System.Directory (copyFile, doesFileExist) import Test.Hspec hiding (it) #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..)) @@ -49,7 +60,6 @@ import Database.PostgreSQL.Simple.SqlQQ (sql) import Database.SQLite.Simple (Only (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Options.DB -import System.Directory (copyFile) import System.FilePath (()) #endif @@ -203,6 +213,8 @@ chatGroupTests = do it "repeat block, unblock" testBlockForAllRepeat it "block multiple members" testBlockForAllMultipleMembers it "block left/removed members" testBlockForAllLeftRemoved + it "mentions of blocked member are ignored" testBlockForAllMentionsIgnored + it "replies of blocked member are not mentions" testBlockedMemberReplyNotMention describe "group member inactivity" $ do it "mark member inactive on reaching quota" testGroupMemberInactive describe "group member reports" $ do @@ -230,6 +242,7 @@ chatGroupTests = do it "should correctly maintain unread stats for support chats on reading chat items" testScopedSupportUnreadStatsOnRead it "should correctly maintain unread stats for support chats on deleting chat items" testScopedSupportUnreadStatsOnDelete it "should correct member attention stat for support chat on opening it" testScopedSupportUnreadStatsCorrectOnOpen + it "should not read support chat items when reading group without scope" testScopedSupportUnreadStatsGroupReadNoScope it "should remove support chat with member when member is removed" testScopedSupportMemberRemoved it "should remove support chat with member when user removes member" testScopedSupportUserRemovesMember it "should remove support chat with member when member leaves" testScopedSupportMemberLeaves @@ -253,6 +266,15 @@ chatGroupTests = do describe "multiple relays" $ do it "2 relays: should deliver messages to members" testChannels2RelaysDeliver it "should share same incognito profile with all relays" testChannels2RelaysIncognito + it "should connect to channel via /c (CLI)" testConnectChannelCLI + it "should connect to channel via /c incognito (CLI)" testConnectChannelCLIIncognito + describe "deliver member profiles via relay" $ do + it "late joiner (no prior history) learns sender on first forward" testChannelLateJoinerReceivesProfile + it "2 relays: deduplicate member announcement" testChannel2RelaysDeduplicateProfile + it "multi senders disseminate independently" testChannelMultiSendersIndependent + it "large profile fits in body" testChannelLargeProfileFits + it "multiple large profiles pack across batches in one multi-sender job" testChannelMultipleLargeProfiles + it "profile update reuses existing announcement (no re-prepend)" testChannelProfileUpdateNoRePrepend describe "channel operations" $ do it "should update channel profile (signed)" testChannelUpdateProfileSigned it "should preserve working link after profile update" testChannelLinkAfterProfileUpdate @@ -262,6 +284,7 @@ chatGroupTests = do it "should change member role (signed)" testChannelChangeRoleSigned it "should block member for all (signed)" testChannelBlockMemberSigned it "should remove member (signed)" testChannelRemoveMemberSigned + it "should verify member security code via membership keys" testChannelMemberSecurityCode it "should delete channel (signed)" testChannelDeleteGroupSigned it "should delete channel and clean up relay connections" testChannelDeleteGroupCleanup it "owner should leave channel (signed)" testChannelOwnerLeave @@ -279,6 +302,21 @@ chatGroupTests = do it "operator allow clears rejection and relay accepts again" testRelayAllowAcceptsAgain it "rejection on channel A does not affect unrelated channel B" testRelayDoesNotRejectUnrelatedChannel it "concurrent fresh invitations both rejected" testRelayRejectRaceConcurrentInvitations + describe "promoted members roster" $ do + it "moderator action verifies via owner-signed roster" testChannelModeratorActionViaRoster + it "subscriber recovers a missed roster member after a version gap" testChannelSubscriberRosterCatchUp + it "2 relays: subscriber recovers a missed roster member after a version gap" testChannel2RelaysSubscriberRosterCatchUp + it "removed moderator drops from the roster cache" testChannelRemovedModeratorRefreshesRoster + it "role transitions update the roster (mod <-> admin, admin -> non-roster)" testChannelRoleTransitionsUpdateRoster + it "malicious relay cannot downgrade or re-key a roster-established moderator via XGrpMemNew" testChannelRelayCannotDowngradeRosterMember + it "malicious relay cannot forge a privileged member via XGrpMemNew forwarded as the owner" testChannelRelayCannotForgePrivilegedMember + it "should add relay to channel with roster (relay caches roster before joinable)" testChannelAddRelayWithRoster + it "roster blob spanning multiple chunks reassembles" testChannelRosterMultipartReassembly + it "corrupted roster blob is rejected on digest mismatch" testChannelRosterDigestMismatchRejected + it "promoted member enters the roster and can post" testChannelPromotedMemberCanPost + it "observer cannot post until promoted" testChannelObserverCannotPost + it "promoted member re-connecting via a new relay is accepted via the roster-pinned key" testChannelPromotedMemberRejoinViaRelay + it "2 relays: multi-chunk roster reassembles per source (no stream interleaving)" testChannelRosterMultiRelayMultipart describe "channel message operations" $ do it "should update channel message" testChannelMessageUpdate it "should delete channel message" testChannelMessageDelete @@ -298,6 +336,18 @@ chatGroupTests = do it "should compute sendAsGroup in CLI forward" testForwardCLISendAsGroup it "should update member message in channel" testChannelMemberMessageUpdate it "should delete member message in channel" testChannelMemberMessageDelete + describe "channel message signing" $ do + it "should sign member message and reuse signature on edit" testChannelMemberMessageSign + it "should reject unsigned update of a signed item" testChannelMemberUpdateEnforcement + it "should sign as-channel post and keep it displayed as the channel" testChannelAsGroupSign + it "should reject a non-owner posting as the channel" testChannelAsGroupSpoof + it "should sign self-delete of a signed item" testChannelMemberSelfDeleteSign + it "should reject unsigned delete of a signed item" testChannelMemberDeleteEnforcement + it "should always sign moderation delete" testChannelModerationDeleteSign + it "should verify signed file digest" testChannelSignedFile + it "should warn on missing signature when signing is required" testChannelSignMessagesRequired + it "should preserve signatures in history for catch-up subscribers" testChannelSignedHistory + it "should forward unsigned channel history for catch-up subscribers" testChannelUnsignedHistory testGroupCheckMessages :: HasCallStack => TestParams -> IO () testGroupCheckMessages = @@ -1943,7 +1993,7 @@ testGroupDelayedModerationFullDelete ps = do testDeleteMemberWithMessages :: HasCallStack => TestParams -> IO () testDeleteMemberWithMessages = testChat3 aliceProfile bobProfile cathProfile $ - \alice bob cath -> do + \alice bob cath -> withXFTPServer $ do createGroup3' "team" alice (bob, GRMember) (cath, GRMember) threadDelay 750000 alice ##> "/set delete #team on" @@ -1961,22 +2011,61 @@ testDeleteMemberWithMessages = cath <## "Full deletion: on" ] threadDelay 750000 - bob #> "#team hello" - concurrently_ - (alice <# "#team bob> hello") - (cath <# "#team bob> hello") - alice #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) - bob #$> ("/_get chat #1 count=1", chat, [(1, "hello")]) - cath #$> ("/_get chat #1 count=1", chat, [(0, "hello")]) + + alice #$> ("/_files_folder ./tests/tmp/alice_app_files", id, "ok") + bob #$> ("/_files_folder ./tests/tmp/bob_app_files", id, "ok") + cath #$> ("/_files_folder ./tests/tmp/cath_app_files", id, "ok") + copyFile "./tests/fixtures/test.jpg" "./tests/tmp/bob_app_files/test.jpg" + + bob ##> "/_send #1 json [{\"filePath\": \"test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"file from bob\"}}]" + bob <# "#team file from bob" + bob <# "/f #team test.jpg" + bob <## "use /fc 1 to cancel sending" + + alice <# "#team bob> file from bob" + alice <# "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes)" + alice <## "use /fr 1 [/ | ] to receive it" + + cath <# "#team bob> file from bob" + cath <# "#team bob> sends file test.jpg (136.5 KiB / 139737 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + + bob <## "completed uploading file 1 (test.jpg) for #team" + + alice ##> "/fr 1" + alice + <### [ "saving file 1 from bob to test.jpg", + "started receiving file 1 (test.jpg) from bob" + ] + alice <## "completed receiving file 1 (test.jpg) from bob" + + cath ##> "/fr 1" + cath + <### [ "saving file 1 from bob to test.jpg", + "started receiving file 1 (test.jpg) from bob" + ] + cath <## "completed receiving file 1 (test.jpg) from bob" + + src <- B.readFile "./tests/fixtures/test.jpg" + B.readFile "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` src + B.readFile "./tests/tmp/bob_app_files/test.jpg" `shouldReturn` src + B.readFile "./tests/tmp/cath_app_files/test.jpg" `shouldReturn` src + threadDelay 1000000 alice ##> "/rm #team bob messages=on" alice <## "#team: you removed bob from the group with all messages" bob <## "#team: alice removed you from the group with all messages" bob <## "use /d #team to delete the group" cath <## "#team: alice removed bob from the group with all messages" - alice #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by you]"), (1, "removed bob")]) - bob #$> ("/_get chat #1 count=2", chat, [(1, "moderated [deleted by alice]"), (0, "removed you")]) - cath #$> ("/_get chat #1 count=2", chat, [(0, "moderated [deleted by alice]"), (0, "removed bob")]) + + doesFileExist "./tests/tmp/alice_app_files/test.jpg" `shouldReturn` False + doesFileExist "./tests/tmp/bob_app_files/test.jpg" `shouldReturn` False + doesFileExist "./tests/tmp/cath_app_files/test.jpg" `shouldReturn` False + + -- Under fullDelete, bob's items are physically deleted on all sides; only the system event remains. + alice #$> ("/_get chat #1 count=1", chat, [(1, "removed bob")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "removed you")]) + cath #$> ("/_get chat #1 count=1", chat, [(0, "removed bob")]) testDeleteMemberMarkMessagesDeleted :: HasCallStack => TestParams -> IO () testDeleteMemberMarkMessagesDeleted = @@ -6834,6 +6923,73 @@ testBlockForAllMarkedBlocked = ) bob #$> ("/_get chat #1 count=4", chat, [(1, "1"), (1, "2"), (1, "3"), (1, "4")]) +testBlockForAllMentionsIgnored :: HasCallStack => TestParams -> IO () +testBlockForAllMentionsIgnored = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + + -- mention of user is shown as mention ("!" after member name) + bob #> "#team hello @alice" + alice <# "#team bob!> hello @alice" + cath <# "#team bob> hello @alice" + + threadDelay 1000000 + + alice ##> "/block for all #team bob" + alice <## "#team: you blocked bob" + cath <## "#team: alice blocked bob" + bob "#team hello again @alice" + alice <# "#team bob> hello again @alice [blocked by admin] " + cath <# "#team bob> hello again @alice [blocked by admin] " + +testBlockedMemberReplyNotMention :: HasCallStack => TestParams -> IO () +testBlockedMemberReplyNotMention = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + + bob #> "#team hi" + alice <# "#team bob> hi" + cath <# "#team bob> hi" + + threadDelay 1000000 + + -- reply to user message is shown as mention ("!" after member name) + alice `send` "> #team @bob (hi) hey bob!" + alice <# "#team > bob hi" + alice <## " hey bob!" + bob <# "#team alice!> > bob hi" + bob <## " hey bob!" + cath <# "#team alice> > bob hi" + cath <## " hey bob!" + + threadDelay 1000000 + + -- admins can only block for all, blocking for self via api + bob ##> "/_member settings #1 1 {\"showMessages\": false}" + bob <## "ok" + + threadDelay 1000000 + + -- reply of blocked member is ignored (no "!" after member name) + alice `send` "> #team @bob (hi) hey again!" + alice <# "#team > bob hi" + alice <## " hey again!" + bob <#. "#team alice> > bob hi" + bob <##. " hey again!" + cath <# "#team alice> > bob hi" + cath <## " hey again!" + testBlockForAllFullDelete :: HasCallStack => TestParams -> IO () testBlockForAllFullDelete = testChat3 aliceProfile bobProfile cathProfile $ @@ -8376,6 +8532,43 @@ testScopedSupportUnreadStatsCorrectOnOpen = { markRead = False } +testScopedSupportUnreadStatsGroupReadNoScope :: HasCallStack => TestParams -> IO () +testScopedSupportUnreadStatsGroupReadNoScope = + testChatOpts2 opts aliceProfile bobProfile $ \alice bob -> do + createGroup2 "team" alice bob + + bob #> "#team (support) 1" + alice <# "#team (support: bob) bob> 1" + -- capture the support item id directly: lastItemId returns the latest item by + -- item_ts, which right after createGroup2 can be a group event ("connected") + -- rather than the support message, making the per-item read below target the + -- wrong item. + bobItemId <- + withCCTransaction alice $ \db -> do + rows <- DB.query_ db "SELECT chat_item_id FROM chat_items WHERE group_scope_tag = 'member_support' ORDER BY chat_item_id DESC LIMIT 1" :: IO [Only Int] + case rows of + Only iId : _ -> pure $ show iId + _ -> error "testScopedSupportUnreadStatsGroupReadNoScope: no member_support item" + + alice ##> "/member support chats #team" + alice <## "members require attention: 1" + alice <## "bob (Bob) (id 2): unread: 1, require attention: 1, mentions: 0" + + -- reading the group without scope must not mark support scope items read + alice #$> ("/_read chat #1", id, "ok") + + -- the support item was left unread, so reading it in scope still decrements the stats + alice #$> ("/_read chat items #1(_support:2) " <> bobItemId, id, "items read for chat") + + alice ##> "/member support chats #team" + alice <## "members require attention: 0" + alice <## "bob (Bob) (id 2): unread: 0, require attention: 0, mentions: 0" + where + opts = + testOpts + { markRead = False + } + testScopedSupportMemberRemoved :: HasCallStack => TestParams -> IO () testScopedSupportMemberRemoved = testChatOpts3 opts aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -8454,6 +8647,7 @@ testScopedSupportMemberLeaves = alice <## "members require attention: 1" alice <## "bob (Bob) (id 2): unread: 2, require attention: 2, mentions: 0" + threadDelay 100000 bob ##> "/l team" concurrentlyN_ [ do @@ -8584,6 +8778,61 @@ testSupportPreferenceChannel ps = bob <# "#team (support) alice> yes [>>]" ] +testConnectChannelCLI :: HasCallStack => TestParams -> IO () +testConnectChannelCLI ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, _fullLink) <- prepareChannel2Relays "team" alice bob cath + relayNames <- mapM userName [bob, cath] + mName <- userName dan + mFullName <- showName dan + dan ##> ("/c " <> shortLink) + dan <## "#team: connection started" + concurrentlyN_ $ + [ dan + <### concat + [ [ ConsoleString ("#team: joining the group (connecting to relay " <> rName <> ")..."), + ConsoleString ("#team: you joined the group (connected to relay " <> rName <> ")") + ] + | rName <- relayNames + ] + ] + <> [ do + relay <## (mFullName <> ": accepting request to join group #team...") + relay <## ("#team: " <> mName <> " joined the group") + | relay <- [bob, cath] + ] + <> [alice <### [EndsWith ("introduced " <> mFullName <> " in the channel")]] + +testConnectChannelCLIIncognito :: HasCallStack => TestParams -> IO () +testConnectChannelCLIIncognito ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, _fullLink) <- prepareChannel2Relays "team" alice bob cath + relayNames <- mapM userName [bob, cath] + dan ##> ("/c i " <> shortLink) + danIncognito <- getTermLine dan + dan <## "#team: connection started incognito" + concurrentlyN_ $ + [ dan + <### concat + [ [ ConsoleString ("#team: joining the group (connecting to relay " <> rName <> ")..."), + ConsoleString ("#team: you joined the group (connected to relay " <> rName <> ") incognito as " <> danIncognito) + ] + | rName <- relayNames + ] + ] + <> [ do + relay <## (danIncognito <> ": accepting request to join group #team...") + relay <## ("#team: " <> danIncognito <> " joined the group") + | relay <- [bob, cath] + ] + <> [alice <### [EndsWith ("introduced " <> danIncognito <> " in the channel")]] + testChannels1RelayDeliver :: HasCallStack => TestParams -> IO () testChannels1RelayDeliver ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -8604,10 +8853,11 @@ testChannels1RelayDeliver ps = -- alice knows cath via XGrpMemNew announcement from relay alice <# "#team cath> > hi" alice <## " + 👍" - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + -- dan/eve learn cath via prepended XGrpMemNew before the forwarded reaction + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> > hi" dan <## " + 👍" - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> > hi" eve <## " + 👍" @@ -8627,6 +8877,20 @@ createChannel1Relay gName owner relay cath dan eve = do forM_ [cath, dan, eve] $ \member -> memberJoinChannel gName [relay] [owner] shortLink fullLink member +-- Promote a fresh channel subscriber (observer default) to member so it can post; the roster bump +-- re-serves to the other (still-unknown) subscribers, who see the change rendered by member id hash. +promoteChannelMember :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> [TestCC] -> IO () +promoteChannelMember gName owner relay member others = do + mName <- userName member + oName <- userName owner + owner ##> ("/mr #" <> gName <> " " <> mName <> " member") + owner <## ("#" <> gName <> ": you changed the role of " <> mName <> " to member (signed)") + concurrentlyN_ $ + [ relay <## ("#" <> gName <> ": " <> oName <> " changed the role of " <> mName <> " from observer to member (signed)"), + member <## ("#" <> gName <> ": " <> oName <> " changed your role from observer to member (signed)") + ] + <> [o <### [EndsWith "from observer to member (signed)"] | o <- others] + setupRelay :: TestCC -> TestCC -> IO String setupRelay owner relay = do rName <- userName relay @@ -8636,6 +8900,43 @@ setupRelay owner relay = do owner <## "ok" pure relaySLink +testChannelMemberSecurityCode :: HasCallStack => TestParams -> IO () +testChannelMemberSecurityCode ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- a channel message lets the relay-forwarded member keys settle on both sides + alice #> "#team hi" + bob <# "#team> hi" + cath <# "#team> hi [>>]" + threadDelay 1000000 + -- owner and subscriber derive the same code from their membership keys + alice ##> "/code #team cath" + aCode <- getTermLine alice + cath ##> "/code #team alice" + cCode <- getTermLine cath + aCode `shouldBe` cCode + -- a wrong code does not verify + alice ##> "/verify #team cath 123" + alice <##. "connection not verified, current code is " + -- the correct code verifies and the verification persists + alice ##> ("/verify #team cath " <> aCode) + alice <## "connection verified" + alice ##> "/i #team cath" + alice <## "group ID: 1" + alice <##. "member ID: " + alice <## "member not connected" + alice <## "connection verified" + -- verification can be cleared + alice ##> "/verify #team cath" + alice <##. "connection not verified, current code is " + alice ##> "/i #team cath" + alice <## "group ID: 1" + alice <##. "member ID: " + alice <## "member not connected" + prepareChannel1Relay :: String -> TestCC -> TestCC -> IO (String, String) prepareChannel1Relay gName owner relay = do _ <- setupRelay owner relay @@ -8661,7 +8962,7 @@ prepareChannel' relayId gName owner relay = do ] owner ##> ("/show link #" <> gName) - getGroupLinks owner gName GRMember False + getGroupLinks owner gName GRObserver False createChannel2Relays :: String -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO () createChannel2Relays gName owner relay1 relay2 dan eve frank = do @@ -8692,7 +8993,7 @@ prepareChannel2Relays gName owner relay1 relay2 = do owner <## ("#" <> gName <> ": group link relays updated, current relays:") owner <### [ EndsWith ": active", - EndsWith ": accepted" + Predicate (\l -> ": invited" `isSuffixOf` l || ": accepted" `isSuffixOf` l || ": acknowledged_roster" `isSuffixOf` l) ] owner <## "group link:" void $ getTermLine owner -- consume group link line @@ -8709,7 +9010,7 @@ prepareChannel2Relays gName owner relay1 relay2 = do ] owner ##> ("/show link #" <> gName) - getGroupLinks owner gName GRMember False + getGroupLinks owner gName GRObserver False memberJoinChannel :: String -> [TestCC] -> [TestCC] -> String -> String -> TestCC -> IO () memberJoinChannel gName = memberJoinChannel' gName 1 0 0 0 @@ -8747,7 +9048,7 @@ memberJoinChannel' gName gId relaySfx ownerSfx memberRelaySfx relays owners shor relay <## ("#" <> gName <> ": " <> sfxMName relaySfx <> " joined the group") | relay <- relays ] - <> [ owner <### [EndsWith ("added " <> sfxName ownerSfx <> " to the group")] + <> [ owner <### [EndsWith ("introduced " <> sfxName ownerSfx <> " in the channel")] | owner <- owners ] @@ -8779,11 +9080,29 @@ memberJoinChannelIncognito gName relays owners shortLink fullLink member = do relay <## ("#" <> gName <> ": " <> memIncognito <> " joined the group") | relay <- relays ] - <> [ owner <### [EndsWith ("added " <> memIncognito <> " to the group")] + <> [ owner <### [EndsWith ("introduced " <> memIncognito <> " in the channel")] | owner <- owners ] pure memIncognito +-- | Assert that sender's member_relations_vector has 'MRIntroduced' at +-- the recipient's index, looked up by display name on the same DB. +memberIntroducedTo :: HasCallStack => TestCC -> T.Text -> T.Text -> IO () +memberIntroducedTo cc senderName recipientName = do + rows <- withCCTransaction cc $ \db -> + DB.query + db + [sql| + SELECT s.member_relations_vector, r.index_in_group + FROM group_members s, group_members r + WHERE s.local_display_name = ? AND r.local_display_name = ? + |] + (senderName, recipientName) :: + IO [(Maybe ByteString, Int64)] + case rows of + [(mv, idx)] -> getRelation idx (fromMaybe B.empty mv) `shouldBe` MRIntroduced + _ -> expectationFailure $ "memberIntroducedTo: expected exactly one row for " <> show (senderName, recipientName) <> ", got " <> show (length rows) + testChannels1RelayDeliverLoop :: HasCallStack => Int -> TestParams -> IO () testChannels1RelayDeliverLoop deliveryBucketSize ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -8803,10 +9122,10 @@ testChannels1RelayDeliverLoop deliveryBucketSize ps = bob <## " + 👍" alice <# "#team cath> > hi" alice <## " + 👍" - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> > hi" dan <## " + 👍" - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> > hi" eve <## " + 👍" where @@ -8820,6 +9139,9 @@ testChannelsSenderDeduplicateOwn ps = do withNewTestChat ps "eve" eveProfile $ \eve -> do withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath and dan while the relay is online, so their buffered posts replay as members + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] -- chat relay bob is offline alice #> "#team 1" @@ -8845,14 +9167,16 @@ testChannelsSenderDeduplicateOwn ps = do WithTime "#team dan> 6 [>>]" ] cath - <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record dan", + <### [ EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", WithTime "#team> 3 [>>]", WithTime "#team dan> 6 [>>]" ] dan - <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record cath", + <### [ EndsWith "updated to cath", + "#team: bob introduced cath (Catherine) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", WithTime "#team> 3 [>>]", @@ -8860,8 +9184,10 @@ testChannelsSenderDeduplicateOwn ps = do WithTime "#team cath> 5 [>>]" ] eve - <### [ "#team: bob forwarded a message from an unknown member, creating unknown member record cath", - "#team: bob forwarded a message from an unknown member, creating unknown member record dan", + <### [ EndsWith "updated to cath", + EndsWith "updated to dan", + "#team: bob introduced cath (Catherine) in the channel", + "#team: bob introduced dan (Daniel) in the channel", WithTime "#team> 1 [>>]", WithTime "#team> 2 [>>]", WithTime "#team> 3 [>>]", @@ -8872,6 +9198,260 @@ testChannelsSenderDeduplicateOwn ps = do where cfg = testCfg {deliveryWorkerDelay = 250000} +testChannelLateJoinerReceivesProfile :: HasCallStack => TestParams -> IO () +testChannelLateJoinerReceivesProfile ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] + + -- first forward: dan resolves cath (roster-known by id hash) on the prepended XGrpMemNew. + cath #> "#team hi" + bob <# "#team cath> hi" + alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi [>>]" + + -- second forward: dan's bit is set, no prepend, no view event. + cath #> "#team hi again" + bob <# "#team cath> hi again" + alice <# "#team cath> hi again [>>]" + dan <# "#team cath> hi again [>>]" + + memberIntroducedTo bob "cath" "alice" + memberIntroducedTo bob "cath" "dan" + + -- profile update: rename piggybacks on next send; no re-prepend, bits stay set. + cath ##> "/p kate Kate" + cath <## "user profile is changed to kate (Kate) (your 0 contacts are notified)" + + cath #> "#team renamed" + bob <# "#team kate> renamed" + alice <# "#team kate> renamed [>>]" + dan <# "#team kate> renamed [>>]" + threadDelay 500000 + memberIntroducedTo bob "kate" "alice" + memberIntroducedTo bob "kate" "dan" + +testChannel2RelaysDeduplicateProfile :: HasCallStack => TestParams -> IO () +testChannel2RelaysDeduplicateProfile ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink dan + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink eve + + -- promote dan (observer default) so it can post; eve learns dan via the roster (id hash) + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"] + ] + + -- first forward: both relays prepend XGrpMemNew(dan) for eve; + -- second hits xGrpMemNew's "already created via another relay" branch. + dan #> "#team hi" + bob <# "#team dan> hi" + cath <# "#team dan> hi" + alice <# "#team dan> hi [>>]" + eve <### [EndsWith "updated to dan"] + eve .<## " introduced dan (Daniel) in the channel" + eve <# "#team dan> hi [>>]" + + -- second forward: eve's bit is set on both relays, no prepend. + dan #> "#team hi again" + bob <# "#team dan> hi again" + cath <# "#team dan> hi again" + alice <# "#team dan> hi again [>>]" + eve <# "#team dan> hi again [>>]" + + -- both relays independently mark eve in dan's vector; + -- alice's bit was set at join via introduceInChannel and stays set. + memberIntroducedTo bob "dan" "alice" + memberIntroducedTo bob "dan" "eve" + memberIntroducedTo cath "dan" "alice" + memberIntroducedTo cath "dan" "eve" + + -- profile update: rename piggybacks on next send; no re-prepend, bits stay set. + dan ##> "/p dean Dean" + dan <## "user profile is changed to dean (Dean) (your 0 contacts are notified)" + + dan #> "#team renamed" + bob <# "#team dean> renamed" + cath <# "#team dean> renamed" + alice <# "#team dean> renamed [>>]" + eve <# "#team dean> renamed [>>]" + threadDelay 500000 + memberIntroducedTo bob "dean" "alice" + memberIntroducedTo bob "dean" "eve" + memberIntroducedTo cath "dean" "alice" + memberIntroducedTo cath "dean" "eve" + +testChannelLargeProfileFits :: HasCallStack => TestParams -> IO () +testChannelLargeProfileFits ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] + + -- ~14000 chars: profile fits in a singleton batch AND packs + -- inline with the forwarded body (exercises the in-body path). + let bigImage = T.pack ("data:image/png;base64," <> replicate 14000 'A') + withCCTransaction bob $ \db -> + DB.execute db "UPDATE contact_profiles SET image = ? WHERE display_name = ?" (bigImage, "cath" :: T.Text) + + cath #> "#team hi" + bob <# "#team cath> hi" + alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi [>>]" + + memberIntroducedTo bob "cath" "dan" + +testChannelMultipleLargeProfiles :: HasCallStack => TestParams -> IO () +testChannelMultipleLargeProfiles ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] + + -- ~14500 chars each: one rides inline with the body, + -- the other spills into a standalone overflow batch. + let cathImage = T.pack ("data:image/png;base64," <> replicate 14500 'A') + danImage = T.pack ("data:image/png;base64," <> replicate 14500 'B') + withCCTransaction bob $ \db -> do + DB.execute db "UPDATE contact_profiles SET image = ? WHERE display_name = ?" (cathImage, "cath" :: T.Text) + DB.execute db "UPDATE contact_profiles SET image = ? WHERE display_name = ?" (danImage, "dan" :: T.Text) + + -- deliveryWorkerDelay=250ms lets the relay coalesce cath's and + -- dan's sends into one multi-sender job. + cath #> "#team from cath" + bob <# "#team cath> from cath" + dan #> "#team from dan" + bob <# "#team dan> from dan" + + alice + <### [ WithTime "#team cath> from cath [>>]", + WithTime "#team dan> from dan [>>]" + ] + cath + <### [ EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", + WithTime "#team dan> from dan [>>]" + ] + dan + <### [ EndsWith "updated to cath", + "#team: bob introduced cath (Catherine) in the channel", + WithTime "#team cath> from cath [>>]" + ] + eve + <### [ EndsWith "updated to cath", + EndsWith "updated to dan", + "#team: bob introduced dan (Daniel) in the channel", + "#team: bob introduced cath (Catherine) in the channel", + WithTime "#team cath> from cath [>>]", + WithTime "#team dan> from dan [>>]" + ] + + memberIntroducedTo bob "cath" "eve" + memberIntroducedTo bob "dan" "eve" + where + cfg = testCfg {deliveryWorkerDelay = 250000} + +-- Asserted via SQL on the relay's DB rather than terminal output: the +-- "updated profile" chat item rendering on relays/owners is order-sensitive. +testChannelProfileUpdateNoRePrepend :: HasCallStack => TestParams -> IO () +testChannelProfileUpdateNoRePrepend ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + promoteChannelMember "team" alice bob cath [dan] + + cath #> "#team hi" + bob <# "#team cath> hi" + alice <# "#team cath> hi [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi [>>]" + + memberIntroducedTo bob "cath" "dan" + + -- /p only delivers XInfo to direct contacts; for group members it + -- piggybacks on the next group send via shouldSendProfileUpdate. + cath ##> "/p kate Kate" + cath <## "user profile is changed to kate (Kate) (your 0 contacts are notified)" + + cath #> "#team hi again" + bob <# "#team kate> hi again" + alice <# "#team kate> hi again [>>]" + dan <# "#team kate> hi again [>>]" + threadDelay 500000 + memberIntroducedTo bob "kate" "dan" + +testChannelMultiSendersIndependent :: HasCallStack => TestParams -> IO () +testChannelMultiSendersIndependent ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> do + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> do + withNewTestChat ps "cath" cathProfile $ \cath -> do + withNewTestChat ps "dan" danProfile $ \dan -> do + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + promoteChannelMember "team" alice bob dan [cath, eve] + + -- cath posts: dan and eve resolve cath on the prepended XGrpMemNew + cath #> "#team from cath" + bob <# "#team cath> from cath" + alice <# "#team cath> from cath [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> from cath [>>]" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> from cath [>>]" + + -- dan posts: cath and eve resolve dan independently of cath's vector + dan #> "#team from dan" + bob <# "#team dan> from dan" + alice <# "#team dan> from dan [>>]" + cath <### [EndsWith "updated to dan"] + cath <## "#team: bob introduced dan (Daniel) in the channel" + cath <# "#team dan> from dan [>>]" + eve <### [EndsWith "updated to dan"] + eve <## "#team: bob introduced dan (Daniel) in the channel" + eve <# "#team dan> from dan [>>]" + + -- second post from cath: all recipients have cath marked, no prepend + cath #> "#team again from cath" + bob <# "#team cath> again from cath" + alice <# "#team cath> again from cath [>>]" + dan <# "#team cath> again from cath [>>]" + eve <# "#team cath> again from cath [>>]" + testChannels2RelaysDeliver :: HasCallStack => TestParams -> IO () testChannels2RelaysDeliver ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -8882,6 +9462,17 @@ testChannels2RelaysDeliver ps = withNewTestChat ps "frank" frankProfile $ \frank -> do createChannel2Relays "team" alice bob cath dan eve frank + -- promote dan (observer default) so it can send; eve/frank learn dan via the roster + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + alice #> "#team hi" [bob, cath] *<# "#team> hi" [dan, eve, frank] *<# "#team> hi [>>]" @@ -8894,18 +9485,15 @@ testChannels2RelaysDeliver ps = cath <## " + 👍" alice <# "#team dan> > hi" alice <## " + 👍" - eve .<## " forwarded a message from an unknown member, creating unknown member record dan" + eve .<##. ("#team: unknown member ", " updated to dan") + eve .<## " introduced dan (Daniel) in the channel" eve <# "#team dan> > hi" eve <## " + 👍" - frank .<## " forwarded a message from an unknown member, creating unknown member record dan" + frank .<##. ("#team: unknown member ", " updated to dan") + frank .<## " introduced dan (Daniel) in the channel" frank <# "#team dan> > hi" frank <## " + 👍" - -- remove below if default role is changed to observer - dan #> "#team hey" - [bob, cath] *<# "#team dan> hey" - [alice, eve, frank] *<# "#team dan> hey [>>]" - testChannels2RelaysIncognito :: HasCallStack => TestParams -> IO () testChannels2RelaysIncognito ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do @@ -8919,6 +9507,17 @@ testChannels2RelaysIncognito ps = forM_ [eve, frank] $ \member -> memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + -- promote dan (observer default) so it can send; eve/frank learn dan via the roster + alice ##> ("/mr #team " <> danIncognito <> " member") + alice <## ("#team: you changed the role of " <> danIncognito <> " to member (signed)") + concurrentlyN_ + [ bob <## ("#team: alice changed the role of " <> danIncognito <> " from observer to member (signed)"), + cath <## ("#team: alice changed the role of " <> danIncognito <> " from observer to member (signed)"), + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + alice #> "#team hi" [bob, cath] *<# "#team> hi" dan ?<# "#team> hi [>>]" @@ -8932,17 +9531,21 @@ testChannels2RelaysIncognito ps = cath <## " + 👍" alice <# ("#team " <> danIncognito <> "> > hi") alice <## " + 👍" - eve .<## (" forwarded a message from an unknown member, creating unknown member record " <> danIncognito) + eve .<##. ("#team: unknown member ", (" updated to " <> danIncognito)) + eve .<## (" introduced " <> danIncognito <> " in the channel") eve <# ("#team " <> danIncognito <> "> > hi") eve <## " + 👍" - frank .<## (" forwarded a message from an unknown member, creating unknown member record " <> danIncognito) + frank .<##. ("#team: unknown member ", (" updated to " <> danIncognito)) + frank .<## (" introduced " <> danIncognito <> " in the channel") frank <# ("#team " <> danIncognito <> "> > hi") frank <## " + 👍" - -- remove below if default role is changed to observer - dan ?#> "#team hey" - [bob, cath] *<# ("#team " <> danIncognito <> "> hey") - [alice, eve, frank] *<# ("#team " <> danIncognito <> "> hey [>>]") + alice `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "eve", "frank"] + bob `hasContactProfiles` ["alice", "bob", T.pack danIncognito, "eve", "frank"] + cath `hasContactProfiles` ["alice", "cath", T.pack danIncognito, "eve", "frank"] + dan `hasContactProfiles` ["alice", "bob", "cath", "dan", T.pack danIncognito] + eve `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "eve"] + frank `hasContactProfiles` ["alice", "bob", "cath", T.pack danIncognito, "frank"] testChannelUpdateProfileSigned :: HasCallStack => TestParams -> IO () testChannelUpdateProfileSigned ps = @@ -9001,7 +9604,7 @@ testChannelLinkAfterProfileUpdate ps = -- late subscriber joins via the same channel link after profile update threadDelay 100000 alice ##> "/show link #my_team" - (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan @@ -9038,7 +9641,7 @@ testChannelLinkAfterWelcomeUpdate ps = -- re-fetch updated link, late subscriber joins threadDelay 100000 alice ##> "/show link #team" - (shortLink', fullLink') <- getGroupLinks alice "team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "team" [bob] [alice] shortLink' fullLink' dan @@ -9075,7 +9678,7 @@ testChannelOwnerKeyAfterLinkUpdate ps = -- Late subscriber joins via the same channel link after profile update. alice ##> "/show link #my_team" - (shortLink', fullLink') <- getGroupLinks alice "my_team" GRMember False + (shortLink', fullLink') <- getGroupLinks alice "my_team" GRObserver False shortLink' `shouldBe` shortLink fullLink' `shouldBe` fullLink memberJoinChannel "my_team" [bob] [alice] shortLink' fullLink' dan @@ -9142,16 +9745,23 @@ testChannelChangeRoleSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + + threadDelay 1000000 + -- other members discover cath cath #> "#team hello from cath" bob <# "#team cath> hello from cath" concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9165,27 +9775,25 @@ testChannelChangeRoleSigned ps = dan <## "#team: alice changed the role of cath from member to admin (signed)", eve <## "#team: alice changed the role of cath from member to admin (signed)" ] + -- chat item is not created for other members alice #$> ("/_get chat #1 count=1", chat, [(1, "changed role of cath to admin (signed)")]) - bob #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")]) cath #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) - dan #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) - eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) + dan #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")]) + eve #$> ("/_get chat #1 count=1", chat, [(0, "hello from cath")]) - -- change role of silent member (other members don't know about member) + -- change role of silent member threadDelay 1000000 alice ##> "/mr #team dan admin" alice <## "#team: you changed the role of dan to admin (signed)" - bob <## "#team: alice changed the role of dan from member to admin (signed)" concurrentlyN_ - [ dan <## "#team: alice changed your role from member to admin (signed)", - cath <## "error: x.grp.mem.role with unknown member ID", - eve <## "error: x.grp.mem.role with unknown member ID" + [ bob <## "#team: alice changed the role of dan from observer to admin (signed)", + dan <## "#team: alice changed your role from observer to admin (signed)", + cath .<##. ("#team: alice changed the role of ", " from observer to admin (signed)"), + eve .<##. ("#team: alice changed the role of ", " from observer to admin (signed)") ] alice #$> ("/_get chat #1 count=1", chat, [(1, "changed role of dan to admin (signed)")]) - bob #$> ("/_get chat #1 count=1", chat, [(0, "changed role of dan to admin (signed)")]) - cath #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) -- now new chat item dan #$> ("/_get chat #1 count=1", chat, [(0, "changed your role to admin (signed)")]) - eve #$> ("/_get chat #1 count=1", chat, [(0, "changed role of cath to admin (signed)")]) -- now new chat item testChannelBlockMemberSigned :: HasCallStack => TestParams -> IO () testChannelBlockMemberSigned ps = @@ -9196,6 +9804,9 @@ testChannelBlockMemberSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- other members discover cath threadDelay 1000000 cath #> "#team hello from cath" @@ -9203,10 +9814,12 @@ testChannelBlockMemberSigned ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9251,6 +9864,388 @@ testChannelBlockMemberSigned ps = r2 `shouldStartWith` "blocked" r2 `shouldEndWith` "(signed)" +checkMemberRow :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO () +checkMemberRow cc name expectedRole = do + roles <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only r) -> r) roles `shouldBe` maybeToList expectedRole + +-- The wire member id for a named member (look it up on a client that knows the name, e.g. the owner), used to +-- find a member by id on a subscriber that only knows it by member-id hash (e.g. after roster recovery). +getMemberIdByName :: TestCC -> T.Text -> IO ByteString +getMemberIdByName cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_id FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only ByteString] + case rows of + [Only mid] -> pure mid + _ -> fail $ "expected one group_members row for " <> T.unpack name + +getMemberRoleKey :: TestCC -> ByteString -> IO (T.Text, Maybe ByteString) +getMemberRoleKey cc mid = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role, member_pub_key FROM group_members WHERE member_id = ?" (Only mid) :: IO [(T.Text, Maybe ByteString)] + case rows of + [r] -> pure r + _ -> fail "expected one group_members row by member id" + +testChannelModeratorActionViaRoster :: HasCallStack => TestParams -> IO () +testChannelModeratorActionViaRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + + -- promote dan (observer default) so it can post; cath and eve then discover dan + threadDelay 1000000 + promoteChannelMember "team" alice bob dan [cath, eve] + dan #> "#team hello from dan" + bob <# "#team dan> hello from dan" + concurrentlyN_ + [ alice <# "#team dan> hello from dan [>>]", + do + cath <### [EndsWith "updated to dan"] + cath <## "#team: bob introduced dan (Daniel) in the channel" + cath <# "#team dan> hello from dan [>>]", + do + eve <### [EndsWith "updated to dan"] + eve <## "#team: bob introduced dan (Daniel) in the channel" + eve <# "#team dan> hello from dan [>>]" + ] + + -- cath promoted observer -> moderator; dan/eve learn cath via the roster re-serve + -- (no name yet -> rendered by member id hash) + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + eve <### [EndsWith "to moderator (signed)"] + ] + + -- cath (moderator) blocks dan; profile prepend carries cath's full profile to dan/eve + threadDelay 1000000 + cath ##> "/block for all #team dan" + cath <## "#team: you blocked dan (signed)" + bob <## "#team: cath blocked dan (signed)" + alice <## "#team: cath blocked dan (signed)" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <## "#team: cath blocked dan (signed)" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + + -- frank joins after the roster update; cached roster gives him cath as moderator. + -- both alice (owner) and cath (mod) receive XGrpMemNew(frank) via introduceInChannel. + -- the roster apply also emits the role-change chat item on frank's side (owner + -- profile may not be loaded yet, so the actor renders by memberId hash) + threadDelay 1000000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink frank + -- the late joiner learns the roster from the served snapshot (verified below); under the + -- no-broadcast model the apply finds no role change to surface, so no item here + threadDelay 1000000 -- the served roster arrives async + checkMemberRole frank "cath" "moderator" + where + checkMemberRole :: HasCallStack => TestCC -> T.Text -> T.Text -> IO () + checkMemberRole cc name expectedRole = do + roles <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_role FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only T.Text] + map (\(Only r) -> r) roles `shouldBe` [expectedRole] + +testChannelSubscriberRosterCatchUp :: HasCallStack => TestParams -> IO () +testChannelSubscriberRosterCatchUp ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve, frank] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + -- promote dan (roster v0) then eve (v1) into the owner-signed roster; cath learns both with their keys + threadDelay 1000000 + promoteChannelMember "team" alice bob dan [cath, eve, frank] + threadDelay 1000000 + promoteChannelMember "team" alice bob eve [cath, dan, frank] + threadDelay 1000000 + -- simulate cath having fallen behind and lost dan: capture dan's member id (from the owner, which + -- knows the name) and cath's owner-pinned key for dan, then delete dan's record and rewind cath's + -- applied frontier so the next delta arrives as a gap (v2 > applied 0 + 1) + danId <- getMemberIdByName alice "dan" + (_, danKey) <- getMemberRoleKey cath danId + withCCTransaction cath $ \db -> do + DB.execute db "DELETE FROM group_members WHERE member_id = ?" (Only danId) + DB.execute db "UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ?" (0 :: Int64, 1 :: Int64) + -- the next privileged change (frank -> v2) reaches cath at a jumped version, triggering catch-up: + -- cath requests the roster from the forwarding relay, which re-serves the current snapshot + promoteChannelMember "team" alice bob frank [cath, dan, eve] + threadDelay 2000000 -- wait for the gap request + relay re-serve to recover dan + -- cath recovered dan from the re-served roster: same member id, role, and owner-pinned key + (recRole, recKey) <- getMemberRoleKey cath danId + recRole `shouldBe` "member" + recKey `shouldBe` danKey + +-- Same recovery, but the subscriber (frank) is connected to two relays: the request goes to whichever relay +-- forwarded the gapping delta, and only an observation that catch-up works in a 2-relay channel (not the race). +testChannel2RelaysSubscriberRosterCatchUp :: HasCallStack => TestParams -> IO () +testChannel2RelaysSubscriberRosterCatchUp ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + forM_ [dan, eve, frank] $ \member -> + memberJoinChannel "team" [bob, cath] [alice] shortLink fullLink member + -- promote dan (v0) then eve (v1) into the owner-signed roster, forwarded by both relays; frank + -- (the subscriber) learns both with their keys + threadDelay 1000000 + alice ##> "/mr #team dan member" + alice <## "#team: you changed the role of dan to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of dan from observer to member (signed)", + cath <## "#team: alice changed the role of dan from observer to member (signed)", + dan <## "#team: alice changed your role from observer to member (signed)", + eve <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + threadDelay 1000000 + alice ##> "/mr #team eve member" + alice <## "#team: you changed the role of eve to member (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from observer to member (signed)", + cath <## "#team: alice changed the role of eve from observer to member (signed)", + eve <## "#team: alice changed your role from observer to member (signed)", + dan <### [EndsWith "from observer to member (signed)"], + frank <### [EndsWith "from observer to member (signed)"] + ] + threadDelay 1000000 + -- simulate frank having fallen behind and lost dan: delete dan's record and rewind frank's complete + -- frontier so the next delta (eve -> v2) arrives as a gap (2 > applied 0 + 1) + danId <- getMemberIdByName alice "dan" + (_, danKey) <- getMemberRoleKey frank danId + withCCTransaction frank $ \db -> do + DB.execute db "DELETE FROM group_members WHERE member_id = ?" (Only danId) + DB.execute db "UPDATE groups SET applied_complete_roster_version = ? WHERE group_id = ?" (0 :: Int64, 1 :: Int64) + -- eve -> moderator (v2) reaches frank at a jumped version; it requests the roster from the relay that + -- forwarded the delta, which re-serves the current snapshot (including dan), recovering dan + alice ##> "/mr #team eve moderator" + alice <## "#team: you changed the role of eve to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from member to moderator (signed)", + cath <## "#team: alice changed the role of eve from member to moderator (signed)", + eve <## "#team: alice changed your role from member to moderator (signed)", + dan <### [EndsWith "from member to moderator (signed)"], + frank <### [EndsWith "from member to moderator (signed)"] + ] + threadDelay 2000000 -- wait for the gap request + relay re-serve to recover dan + (recRole, recKey) <- getMemberRoleKey frank danId + recRole `shouldBe` "member" + recKey `shouldBe` danKey + +testChannelRemovedModeratorRefreshesRoster :: HasCallStack => TestParams -> IO () +testChannelRemovedModeratorRefreshesRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + forM_ [cath, dan, eve] $ \member -> + memberJoinChannel "team" [bob] [alice] shortLink fullLink member + -- cath promoted observer -> moderator; dan/eve learn cath via the roster (id hash) + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + eve <### [EndsWith "to moderator (signed)"] + ] + threadDelay 1000000 + alice ##> "/rm #team cath" + alice <## "#team: you removed cath from the group (signed)" + -- the relay applies the removal via the roster (revert to observer) before the delete delta + bob <## "#team: alice changed the role of cath from moderator to observer (signed)" + bob <## "#team: alice removed cath from the group (signed)" + cath <## "#team: alice removed you from the group (signed)" + cath <## "use /d #team to delete the group" + dan <### [EndsWith "from the group (signed)"] + eve <### [EndsWith "from the group (signed)"] + + -- frank joins after the removal; cached roster has dropped cath + threadDelay 1000000 + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + threadDelay 100000 + checkMemberRow frank "cath" Nothing + +testChannelRoleTransitionsUpdateRoster :: HasCallStack => TestParams -> IO () +testChannelRoleTransitionsUpdateRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- observer -> moderator + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + -- dan joins; cached roster has cath as moderator (learned from the served snapshot, + -- no separate role-change item under the no-broadcast model) + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink dan + threadDelay 1000000 -- the served roster arrives async; wait before reading the applied state + checkMemberRow dan "cath" (Just "moderator") + -- moderator -> admin: dan now knows cath, role event lands cleanly + threadDelay 100000 + alice ##> "/mr #team cath admin" + alice <## "#team: you changed the role of cath to admin (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from moderator to admin (signed)", + cath <## "#team: alice changed your role from moderator to admin (signed)", + dan <## "#team: alice changed the role of cath from moderator to admin (signed)" + ] + -- eve joins; cached roster has cath as admin (learned from the served snapshot) + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink eve + threadDelay 1000000 -- the served roster arrives async; wait before reading the applied state + checkMemberRow eve "cath" (Just "admin") + -- admin -> observer (crossing out of roster, since member is now in-roster): roster drops cath + threadDelay 100000 + alice ##> "/mr #team cath observer" + alice <## "#team: you changed the role of cath to observer (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from admin to observer (signed)", + cath <## "#team: alice changed your role from admin to observer (signed)", + dan <## "#team: alice changed the role of cath from admin to observer (signed)", + eve <## "#team: alice changed the role of cath from admin to observer (signed)" + ] + -- frank joins; cath isn't in the roster, so frank has no record of her + threadDelay 100000 + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + threadDelay 100000 + checkMemberRow frank "cath" Nothing + +testChannelRelayCannotDowngradeRosterMember :: HasCallStack => TestParams -> IO () +testChannelRelayCannotDowngradeRosterMember ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChatOpts ps (testOpts {coreOptions = testCoreOpts {logLevel = CLLWarning}}) "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink frank + -- promote cath; roster TOFU-creates cath on frank as moderator with the real key + threadDelay 1000000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)", + frank <### [EndsWith "to moderator (signed)"] + ] + threadDelay 100000 + realKey <- getMemberPubKey bob "cath" + -- malicious relay: corrupt bob's local record of cath so its XGrpMemNew dissemination + -- carries a downgraded role + no key + withCCTransaction bob $ \db -> + DB.execute + db + "UPDATE group_members SET member_role = ?, member_pub_key = NULL WHERE local_display_name = ?" + ("member" :: T.Text, "cath" :: T.Text) + -- cath posts; bob prepends XGrpMemNew(cath, member, NULL) to the delivery (frank not yet introduced) + threadDelay 100000 + cath #> "#team hello from cath" + bob <# "#team cath> hello from cath" + concurrentlyN_ + [ alice <# "#team cath> hello from cath [>>]", + do + frank <##. "warning: x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" + frank <### [EndsWith "updated to cath"] + frank <## "#team: bob introduced cath (Catherine) in the channel" + frank <# "#team cath> hello from cath [>>]" + ] + threadDelay 100000 + checkMemberRow frank "cath" (Just "moderator") + frankKey <- getMemberPubKey frank "cath" + frankKey `shouldBe` realKey + where + getMemberPubKey :: TestCC -> T.Text -> IO (Maybe ByteString) + getMemberPubKey cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_pub_key FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only (Maybe ByteString)] + case rows of + [Only k] -> pure k + _ -> fail $ "expected one row for " <> T.unpack name + +testChannelRelayCannotForgePrivilegedMember :: HasCallStack => TestParams -> IO () +testChannelRelayCannotForgePrivilegedMember ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 1000000 + -- the forged attribution only resolves to a privileged author if the victim already holds the + -- owner at GROwner (established via the group link on join) - this documents and guards that premise + checkMemberRow cath "alice" (Just "owner") + ownerMemId <- ownerMemberId bob + connId <- relayConnIdToMember bob "cath" + -- the malicious relay forges the announcement, choosing the new member's role and signing key + g <- C.newRandom + kp <- atomically $ C.generateKeyPair g + ts <- getCurrentTime + let ChatController {smpAgent = bobAgent} = chatController bob + attackerPub = fst kp :: C.PublicKeyEd25519 + forgedMemId = MemberId "forgedadmin1" + forgedProfile = (aliceProfile :: Profile) {displayName = "forgery", fullName = "Forgery"} + memInfo = + MemberInfo + { memberId = forgedMemId, + memberRole = GRAdmin, + v = Nothing, + profile = forgedProfile, + memberKey = Just (MemberKey attackerPub) + } + chatMsg = ChatMessage chatInitialVRange Nothing (XGrpMemNew memInfo Nothing) + fwd = GrpMsgForward (FwdMember ownerMemId "alice") ts + body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)] + sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)] + either (fail . show) (const $ pure ()) sent + -- secure: the victim rejects the forged privileged announcement instead of storing it + cath <##. "error: x.grp.mem.new: privileged member not established by roster" + forged <- forgedMemberRows cath "forgery" + forged `shouldBe` [] + where + ownerMemberId :: TestCC -> IO MemberId + ownerMemberId cc = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1" (Only ("owner" :: T.Text)) :: IO [Only ByteString] + case rows of + [Only mid] -> pure (MemberId mid) + _ -> fail "expected exactly one owner member on the relay" + forgedMemberRows :: TestCC -> T.Text -> IO [(T.Text, Maybe ByteString)] + forgedMemberRows cc name = + withCCTransaction cc $ \db -> + DB.query db "SELECT member_role, member_pub_key FROM group_members WHERE local_display_name = ?" (Only name) + testChannelRemoveMemberSigned :: HasCallStack => TestParams -> IO () testChannelRemoveMemberSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9260,16 +10255,21 @@ testChannelRemoveMemberSigned ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote eve to member (observer default) so it can post + promoteChannelMember "team" alice bob eve [cath, dan] + -- other members discover eve eve #> "#team hello from eve" bob <# "#team eve> hello from eve" concurrentlyN_ [ alice <# "#team eve> hello from eve [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record eve" + dan <### [EndsWith "updated to eve"] + dan <## "#team: bob introduced eve (Eve) in the channel" dan <# "#team eve> hello from eve [>>]", do - cath <## "#team: bob forwarded a message from an unknown member, creating unknown member record eve" + cath <### [EndsWith "updated to eve"] + cath <## "#team: bob introduced eve (Eve) in the channel" cath <# "#team eve> hello from eve [>>]" ] @@ -9286,6 +10286,8 @@ testChannelRemoveMemberSigned ps = threadDelay 1000000 alice ##> "/rm #team eve" alice <## "#team: you removed eve from the group (signed)" + -- the relay applies the removal via the roster (revert to observer) before the delete delta + bob <## "#team: alice changed the role of eve from member to observer (signed)" bob <## "#team: alice removed eve from the group (signed)" concurrentlyN_ [ cath <## "#team: alice removed eve from the group (signed)", @@ -9300,6 +10302,9 @@ testChannelRemoveMemberSigned ps = dan #$> ("/_get chat #1 count=1", chat, [(0, "removed eve (signed)")]) eve #$> ("/_get chat #1 count=1", chat, [(0, "removed you (signed)")]) + -- eve had items (posted "hello from eve") -> kept as permanent GSMemRemoved records, removed_at NULL + checkRemovedMember alice "eve" False + -- after first removal alice ##> "/_info #1" alice <## "group ID: 1" @@ -9326,6 +10331,9 @@ testChannelRemoveMemberSigned ps = dan #$> ("/_get chat #1 count=1", chat, [(0, "removed you (signed)")]) eve #$> ("/_get chat #1 count=1", chat, [(0, "removed you (signed)")]) -- no new chat item + -- dan had no items -> kept as GSMemRemoved record with removed_at set (TTL cleanup path) + checkRemovedMember alice "dan" True + -- after second removal alice ##> "/_info #1" alice <## "group ID: 1" @@ -9335,6 +10343,14 @@ testChannelRemoveMemberSigned ps = cath <## "group ID: 1" cath <## "subscribers: 2" +-- asserts the member row is GSMemRemoved, with removed_at set (TTL tombstone) or NULL (permanent) +checkRemovedMember :: HasCallStack => TestCC -> String -> Bool -> Expectation +checkRemovedMember cc name removedAtSet = do + rows <- + withCCTransaction cc $ \db -> + DB.query db "SELECT member_status, removed_at FROM group_members WHERE local_display_name = ?" (Only name) :: IO [(String, Maybe UTCTime)] + map (\(status, removedAt) -> (status, isJust removedAt)) rows `shouldBe` [("removed", removedAtSet)] + testChannelDeleteGroupSigned :: HasCallStack => TestParams -> IO () testChannelDeleteGroupSigned ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -9427,6 +10443,9 @@ testChannelSubscriberLeave ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- other members discover cath threadDelay 1000000 cath #> "#team hello from cath" @@ -9434,10 +10453,12 @@ testChannelSubscriberLeave ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9488,7 +10509,8 @@ testChannelSubscriberLeave ps = dan <## "use /d #team to delete the group" bob <## "#team: dan left the group (signed)" alice <## "#team: dan left the group (signed)" - -- eve doesn't know dan - no unknown member record created (skipped for XGrpLeave) + -- dan never sent before leaving and is now left, so the relay does not prepend + -- his XGrpMemNew; eve receives the bare XGrpLeave and does not create a record (allowCreate=False) alice #$> ("/_get chat #1 count=1", chat, [(0, "left (signed)")]) bob #$> ("/_get chat #1 count=1", chat, [(0, "left (signed)")]) dan #$> ("/_get chat #1 count=1", chat, [(1, "left (signed)")]) @@ -9507,8 +10529,10 @@ testChannelSubscriberLeave ps = checkMemberStatus alice "dan" (Just "left") checkMemberStatus bob "dan" (Just "left") checkMemberStatus dan "dan" (Just "left") - -- eve doesn't know dan - no member record (XGrpLeave skips unknown member creation) + -- the relay did not announce left dan, and the bare XGrpLeave does not create a + -- record (allowCreate=False), so eve never learned dan checkMemberStatus eve "dan" Nothing + -- cath left earlier and was excluded from the forward; no record on cath checkMemberStatus cath "dan" Nothing where checkMemberStatus :: HasCallStack => TestCC -> T.Text -> Maybe T.Text -> IO () @@ -9659,6 +10683,9 @@ testChannelSubscriberProfileUpdate ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote dan to member early (observer default) so its role-change item precedes the messages + promoteChannelMember "team" alice bob dan [cath, eve] + -- enable support and create support chat for cath (but not dan) threadDelay 1000000 alice ##> "/set support #team on" @@ -9678,6 +10705,9 @@ testChannelSubscriberProfileUpdate ps = (dan "#team hello from cath" @@ -9685,10 +10715,12 @@ testChannelSubscriberProfileUpdate ps = concurrentlyN_ [ alice <# "#team cath> hello from cath [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello from cath [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello from cath [>>]" ] @@ -9717,9 +10749,8 @@ testChannelSubscriberProfileUpdate ps = cath #$> ("/_get chat #1 count=2", chat, [(1, "hello from cath"), (1, "hello from kate")]) -- verify profiles are updated correctly forM_ [alice, bob] $ \cc -> cc `hasContactProfiles` ["alice", "bob", "kate", "dan", "eve"] - cath `hasContactProfiles` ["alice", "bob", "kate"] dan `hasContactProfiles` ["alice", "bob", "kate", "dan"] - eve `hasContactProfiles` ["alice", "bob", "kate", "eve"] + -- cath/eve also know dan by id hash now (roster-learned before dan posts); not asserted -- previously silent subscriber updates profile -- dan has no support chat -> no profile update item created @@ -9731,10 +10762,12 @@ testChannelSubscriberProfileUpdate ps = concurrentlyN_ [ alice <# "#team dave> hello from dave [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record dave" + eve <### [EndsWith "updated to dave"] + eve <## "#team: bob introduced dave in the channel" eve <# "#team dave> hello from dave [>>]", do - cath <## "#team: bob forwarded a message from an unknown member, creating unknown member record dave" + cath <### [EndsWith "updated to dave"] + cath <## "#team: bob introduced dave in the channel" cath <# "#team dave> hello from dave [>>]" ] -- no profile update items in main scope (dan has no support chat, item not created) @@ -9819,6 +10852,250 @@ testChannelAddRelay ps = [bob, cath] *<# "#team> hello" [dan, eve] *<# "#team> hello [>>]" +testChannelAddRelayWithRoster :: HasCallStack => TestParams -> IO () +testChannelAddRelayWithRoster ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "dan" danProfile $ \dan -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "eve" eveProfile $ \_eve -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + + -- promote cath observer -> moderator: the roster is created (bob caches it) + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + + -- add dan as a 2nd relay; with a roster present it must cache the roster and ack + -- (XGrpRosterAck) before alice publishes it as joinable + dan ##> "/ad" + (danSLink, _cLink) <- getContactLinks dan True + alice ##> ("/relays name=dan " <> danSLink) + alice <## "ok" + alice ##> "/_add relays #1 2" + alice <## "#team: group relays:" + alice <## " - relay id 1: active" + alice <## " - relay id 2: invited" + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice + <### [ " - relay id 1: active", + " - relay id 2: active" + ] + alice <## "group link:" + void $ getTermLine alice, + dan <## "#team: you joined the group as relay" + ] + + -- cath (an existing member) connects to the new relay and is attached to her roster + -- record, kept as moderator (the relay learned cath from the cached roster snapshot, so + -- it surfaces no role-change item for her) + concurrentlyN_ + [ do + cath <## "#team: joining the group (connecting to relay dan)..." + cath <## "#team: you joined the group (connected to relay dan)", + dan + <### [ EndsWith "accepting request to join group #team...", + EndsWith "is connected" + ] + ] + + threadDelay 100000 + -- the new relay holds the roster (cath is moderator) and learns her name when she connects + checkMemberRow dan "cath" (Just "moderator") + +testChannelRosterMultipartReassembly :: HasCallStack => TestParams -> IO () +testChannelRosterMultipartReassembly ps = + withNewTestChatCfgOpts ps cfg testOpts "alice" aliceProfile $ \alice -> + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatCfgOpts ps cfg testOpts "cath" cathProfile $ \cath -> + withNewTestChatCfgOpts ps cfg testOpts "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink dan + -- dan reassembles the multi-chunk roster from the served snapshot (arrives async) + threadDelay 1000000 + checkMemberRow dan "cath" (Just "moderator") + where + cfg = testCfg {fileChunkSize = 30} + +testChannelRosterDigestMismatchRejected :: HasCallStack => TestParams -> IO () +testChannelRosterDigestMismatchRejected ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "frank" frankProfile $ \frank -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + threadDelay 100000 + alice ##> "/mr #team cath moderator" + alice <## "#team: you changed the role of cath to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of cath from observer to moderator (signed)", + cath <## "#team: alice changed your role from observer to moderator (signed)" + ] + threadDelay 100000 + -- corrupt the relay's stored blob (same length, different content) so its digest no + -- longer matches the signed header (DB-agnostic: read it, overwrite with zeroed bytes) + withCCTransaction bob $ \db -> do + rows <- DB.query_ db "SELECT roster_blob FROM groups WHERE roster_blob IS NOT NULL" :: IO [Only (Binary ByteString)] + forM_ rows $ \(Only (Binary blob)) -> + DB.execute db "UPDATE groups SET roster_blob = ? WHERE roster_blob IS NOT NULL" (Only (Binary (B.replicate (B.length blob) '\NUL'))) + -- frank joins; bob re-serves the valid header with the corrupted blob, frank rejects it + threadDelay 100000 + memberJoinChannel "team" [bob] [alice, cath] shortLink fullLink frank + threadDelay 1000000 + -- the rejected roster never elevates cath: the intro caps her to the channel default, so she + -- stays observer (not moderator), and the version must not advance to the corrupted roster's version 1 + checkMemberRow frank "cath" (Just "observer") + checkRosterNotApplied frank + where + -- the version is the second guarantee (the role is asserted above): frank holds exactly the team + -- group with no roster applied, so roster_version is NULL - it never advanced to the corrupted version 1 + checkRosterNotApplied :: HasCallStack => TestCC -> IO () + checkRosterNotApplied cc = do + vs <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT roster_version FROM groups" :: IO [Only (Maybe Int64)] + map (\(Only v) -> v) vs `shouldBe` [Nothing] + +testChannelPromotedMemberCanPost :: HasCallStack => TestParams -> IO () +testChannelPromotedMemberCanPost ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + -- promote cath to member: cath enters the owner-signed roster (dan learns cath by id hash) + promoteChannelMember "team" alice bob cath [dan] + -- the promoted member can now post; dan resolves cath on the first forward + cath #> "#team hi from cath" + bob <# "#team cath> hi from cath" + alice <# "#team cath> hi from cath [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi from cath [>>]" + checkMemberRow dan "cath" (Just "member") + +testChannelObserverCannotPost :: HasCallStack => TestParams -> IO () +testChannelObserverCannotPost ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + memberJoinChannel "team" [bob] [alice] shortLink fullLink dan + -- cath is an observer (default): its own post is rejected locally and never reaches the relay + cath ##> "#team observer attempt" + cath <## "#team: you don't have permission to send messages" + -- promote cath to member; the post is now accepted and delivered, dan resolves cath + promoteChannelMember "team" alice bob cath [dan] + cath #> "#team member post" + bob <# "#team cath> member post" + alice <# "#team cath> member post [>>]" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> member post [>>]" + +testChannelPromotedMemberRejoinViaRelay :: HasCallStack => TestParams -> IO () +testChannelPromotedMemberRejoinViaRelay ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOpts "dan" danProfile $ \dan -> + withNewTestChat ps "cath" cathProfile $ \cath -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice bob + memberJoinChannel "team" [bob] [alice] shortLink fullLink cath + -- promote cath to member: cath enters the owner-signed roster with her pinned key + threadDelay 100000 + promoteChannelMember "team" alice bob cath [] + threadDelay 100000 + -- add dan as a 2nd relay; it caches the roster (incl. member cath) before joinable + dan ##> "/ad" + (danSLink, _cLink) <- getContactLinks dan True + alice ##> ("/relays name=dan " <> danSLink) + alice <## "ok" + alice ##> "/_add relays #1 2" + alice <## "#team: group relays:" + alice <## " - relay id 1: active" + alice <## " - relay id 2: invited" + concurrentlyN_ + [ do + alice <## "#team: group link relays updated, current relays:" + alice + <### [ " - relay id 1: active", + " - relay id 2: active" + ] + alice <## "group link:" + void $ getTermLine alice, + dan <## "#team: you joined the group as relay" + ] + -- cath (a promoted member) connects to the new relay; the widened join gate + -- (verifyKey over the roster-pinned key) accepts her and keeps her as member + concurrentlyN_ + [ do + cath <## "#team: joining the group (connecting to relay dan)..." + cath <## "#team: you joined the group (connected to relay dan)", + dan + <### [ EndsWith "accepting request to join group #team...", + EndsWith "is connected" + ] + ] + threadDelay 100000 + checkMemberRow dan "cath" (Just "member") + +testChannelRosterMultiRelayMultipart :: HasCallStack => TestParams -> IO () +testChannelRosterMultiRelayMultipart ps = + withNewTestChatCfgOpts ps cfg testOpts "alice" aliceProfile $ \alice -> + withNewTestChatCfgOpts ps cfg relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChatCfgOpts ps cfg relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChatCfgOpts ps cfg testOpts "dan" danProfile $ \dan -> + withNewTestChatCfgOpts ps cfg testOpts "eve" eveProfile $ \eve -> + withNewTestChatCfgOpts ps cfg testOpts "frank" frankProfile $ \frank -> do + createChannel2Relays "team" alice bob cath dan eve frank + + -- promote eve to moderator: the owner-signed roster broadcasts through BOTH relays to dan and + -- frank (each connected to both). At fileChunkSize=30 the blob spans multiple chunks, so each + -- member receives two interleaved multi-chunk streams (one per relay) for the same roster. + threadDelay 1000000 + alice ##> "/mr #team eve moderator" + alice <## "#team: you changed the role of eve to moderator (signed)" + concurrentlyN_ + [ bob <## "#team: alice changed the role of eve from observer to moderator (signed)", + cath <## "#team: alice changed the role of eve from observer to moderator (signed)", + eve <## "#team: alice changed your role from observer to moderator (signed)", + dan <### [EndsWith "to moderator (signed)"], + frank <### [EndsWith "to moderator (signed)"] + ] + threadDelay 1000000 -- let both relays' interleaved multipart streams settle + + -- per-source transfers keep the streams independent, so each member reassembles the blob and pins + -- eve as the single moderator WITH her owner-attested key (role + key both come from the blob) + checkOneModeratorWithKey dan + checkOneModeratorWithKey frank + where + cfg = testCfg {fileChunkSize = 30} + checkOneModeratorWithKey cc = do + rows <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT member_pub_key FROM group_members WHERE member_role = 'moderator'" :: IO [Only (Maybe ByteString)] + map (\(Only k) -> isJust k) rows `shouldBe` [True] + testChannelRemoveRelay :: HasCallStack => TestParams -> IO () testChannelRemoveRelay ps = withNewTestChat ps "alice" aliceProfile $ \alice -> @@ -10399,42 +11676,48 @@ testChannelMessageFile ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - + -- the roster arrives as a file before this one; Postgres assigns it a new id and does not + -- reuse it on delete (SQLite does), so the received message file is id 2 here, 1 on SQLite. +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as channel message alice #> "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- all members receive the file concurrently src <- B.readFile "./tests/fixtures/test.jpg" concurrentlyN_ - [ receiveFile bob "bob" src, - receiveFile cath "cath" src, - receiveFile dan "dan" src, - receiveFile eve "eve" src + [ receiveFile bob "bob" rcvFileId src, + receiveFile cath "cath" rcvFileId src, + receiveFile dan "dan" rcvFileId src, + receiveFile eve "eve" rcvFileId src ] where - receiveFile cc name src = do + receiveFile cc name fileId src = do let path = "./tests/tmp/test_" <> name <> ".jpg" - cc ##> ("/fr 1 " <> path) + cc ##> ("/fr " <> show fileId <> " " <> path) cc - <### [ ConsoleString ("saving file 1 from #team to " <> path), - "started receiving file 1 (test.jpg) from #team" + <### [ ConsoleString ("saving file " <> show fileId <> " from #team to " <> path), + ConsoleString ("started receiving file " <> show fileId <> " (test.jpg) from #team") ] - cc <## "completed receiving file 1 (test.jpg) from #team" + cc <## ("completed receiving file " <> show fileId <> " (test.jpg) from #team") B.readFile path >>= (`shouldBe` src) testChannelMessageFileCancel :: HasCallStack => TestParams -> IO () @@ -10445,33 +11728,37 @@ testChannelMessageFileCancel ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as channel message alice #> "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- owner cancels file alice ##> "/fc 1" alice <## "cancelled sending file 1 (test.jpg) to bob" - bob <## "team cancelled sending file 1 (test.jpg)" + bob <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)") concurrentlyN_ - [ cath <## "team cancelled sending file 1 (test.jpg)", - dan <## "team cancelled sending file 1 (test.jpg)", - eve <## "team cancelled sending file 1 (test.jpg)" + [ cath <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + dan <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + eve <## ("team cancelled sending file " <> show rcvFileId <> " (test.jpg)") ] testChannelMessageQuote :: HasCallStack => TestParams -> IO () @@ -10488,6 +11775,9 @@ testChannelMessageQuote ps = bob <# "#team> hello from channel" [cath, dan, eve] *<# "#team> hello from channel [>>]" + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member quotes channel message cath `send` "> #team (hello from) replying to channel" cath <# "#team > hello from channel" @@ -10499,11 +11789,13 @@ testChannelMessageQuote ps = alice <# "#team cath> > hello from channel [>>]" alice <## " replying to channel [>>]", do - dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> > hello from channel [>>]" dan <## " replying to channel [>>]", do - eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> > hello from channel [>>]" eve <## " replying to channel [>>]" ] @@ -10616,43 +11908,47 @@ testChannelOwnerFileTransferAsMember ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as member (not as channel) alice ##> "/_send #1(as_group=off) json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- all members receive the file src <- B.readFile "./tests/fixtures/test.jpg" concurrentlyN_ - [ receiveFile bob "bob" src, - receiveFile cath "cath" src, - receiveFile dan "dan" src, - receiveFile eve "eve" src + [ receiveFile bob "bob" rcvFileId src, + receiveFile cath "cath" rcvFileId src, + receiveFile dan "dan" rcvFileId src, + receiveFile eve "eve" rcvFileId src ] where - receiveFile cc name src = do + receiveFile cc name fileId src = do let path = "./tests/tmp/test_" <> name <> ".jpg" - cc ##> ("/fr 1 " <> path) + cc ##> ("/fr " <> show fileId <> " " <> path) cc - <### [ ConsoleString ("saving file 1 from alice to " <> path), - "started receiving file 1 (test.jpg) from alice" + <### [ ConsoleString ("saving file " <> show fileId <> " from alice to " <> path), + ConsoleString ("started receiving file " <> show fileId <> " (test.jpg) from alice") ] - cc <## "completed receiving file 1 (test.jpg) from alice" + cc <## ("completed receiving file " <> show fileId <> " (test.jpg) from alice") B.readFile path >>= (`shouldBe` src) testChannelOwnerFileCancelAsMember :: HasCallStack => TestParams -> IO () @@ -10663,34 +11959,38 @@ testChannelOwnerFileCancelAsMember ps = withNewTestChat ps "dan" danProfile $ \dan -> withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do createChannel1Relay "team" alice bob cath dan eve - +#if defined(dbPostgres) + let rcvFileId = 2 :: Int +#else + let rcvFileId = 1 :: Int +#endif -- owner sends file as member (not as channel) alice ##> "/_send #1(as_group=off) json [{\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"file\", \"text\": \"\"}}]" alice <# "/f #team ./tests/fixtures/test.jpg" alice <## "use /fc 1 to cancel sending" alice <## "completed uploading file 1 (test.jpg) for #team" bob <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes)" - bob <## "use /fr 1 [/ | ] to receive it" + bob <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it") concurrentlyN_ [ do cath <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - cath <## "use /fr 1 [/ | ] to receive it [>>]", + cath <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do dan <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - dan <## "use /fr 1 [/ | ] to receive it [>>]", + dan <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]"), do eve <# "#team alice> sends file test.jpg (136.5 KiB / 139737 bytes) [>>]" - eve <## "use /fr 1 [/ | ] to receive it [>>]" + eve <## ("use /fr " <> show rcvFileId <> " [/ | ] to receive it [>>]") ] -- owner cancels file alice ##> "/fc 1" alice <## "cancelled sending file 1 (test.jpg) to bob" - bob <## "alice cancelled sending file 1 (test.jpg)" + bob <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)") concurrentlyN_ - [ cath <## "alice cancelled sending file 1 (test.jpg)", - dan <## "alice cancelled sending file 1 (test.jpg)", - eve <## "alice cancelled sending file 1 (test.jpg)" + [ cath <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + dan <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)"), + eve <## ("alice cancelled sending file " <> show rcvFileId <> " (test.jpg)") ] testChannelReactionAttribution :: HasCallStack => TestParams -> IO () @@ -10854,14 +12154,19 @@ testChannelMemberMessageUpdate ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member sends a message cath #> "#team hello" bob <# "#team cath> hello" concurrentlyN_ [ alice <# "#team cath> hello [>>]", - do dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello [>>]", - do eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello [>>]" ] @@ -10885,14 +12190,19 @@ testChannelMemberMessageDelete ps = withNewTestChat ps "eve" eveProfile $ \eve -> do createChannel1Relay "team" alice bob cath dan eve + -- promote cath to member (observer default) so it can post + promoteChannelMember "team" alice bob cath [dan, eve] + -- member sends a message cath #> "#team hello" bob <# "#team cath> hello" concurrentlyN_ [ alice <# "#team cath> hello [>>]", - do dan <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" dan <# "#team cath> hello [>>]", - do eve <## "#team: bob forwarded a message from an unknown member, creating unknown member record cath" + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" eve <# "#team cath> hello [>>]" ] @@ -10906,6 +12216,637 @@ testChannelMemberMessageDelete ps = eve <# "#team cath> [marked deleted] hello" ] +memberIdByName :: TestCC -> T.Text -> IO MemberId +memberIdByName cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT member_id FROM group_members WHERE local_display_name = ?" (Only name) :: IO [Only ByteString] + case rows of + (Only mid : _) -> pure (MemberId mid) + _ -> fail $ "no member " <> T.unpack name + +relayConnIdToMember :: TestCC -> T.Text -> IO ByteString +relayConnIdToMember cc name = do + rows <- withCCTransaction cc $ \db -> + DB.query + db + "SELECT c.agent_conn_id FROM connections c JOIN group_members m ON m.group_member_id = c.group_member_id WHERE m.local_display_name = ?" + (Only name) :: + IO [Only ByteString] + case rows of + (Only connId : _) -> pure connId + _ -> fail $ "no relay connection to member " <> T.unpack name + +itemSharedMsgId :: TestCC -> IO SharedMsgId +itemSharedMsgId cc = do + rows <- withCCTransaction cc $ \db -> + DB.query_ db "SELECT shared_msg_id FROM chat_items WHERE shared_msg_id IS NOT NULL ORDER BY chat_item_id DESC LIMIT 1" :: IO [Only ByteString] + case rows of + (Only smid : _) -> pure (SharedMsgId smid) + _ -> fail "no shared_msg_id" + +testChannelMemberMessageSign :: HasCallStack => TestParams -> IO () +testChannelMemberMessageSign ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- member sends a signed message + cath ##> "/_send #1 sign=on text signed hello" + cath <# "#team signed hello (signed)" + bob <# "#team cath> signed hello (signed)" + concurrentlyN_ + [ alice <# "#team cath> signed hello (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> signed hello (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> signed hello (signed) [>>]" + ] + -- sender and recipient hold it signed + cath #$> ("/_get chat #1 count=100 search=signed hello", chat, [(1, "signed hello (signed)")]) + dan #$> ("/_get chat #1 count=100 search=signed hello", chat, [(0, "signed hello (signed)")]) + + -- editing a signed item reuses the signature + cathMsgId <- lastItemId cath + cath ##> ("/_update item #1 " <> cathMsgId <> " text signed hello edited") + cath <# "#team [edited] signed hello edited (signed)" + bob <# "#team cath> [edited] signed hello edited (signed)" + concurrentlyN_ + [ alice <# "#team cath> [edited] signed hello edited (signed)", + dan <# "#team cath> [edited] signed hello edited (signed)", + eve <# "#team cath> [edited] signed hello edited (signed)" + ] + cath #$> ("/_get chat #1 count=100 search=signed hello edited", chat, [(1, "signed hello edited (signed)")]) + dan #$> ("/_get chat #1 count=100 search=signed hello edited", chat, [(0, "signed hello edited (signed)")]) + + -- default send is unsigned, and holds no signature + cath #> "#team plain hello" + bob <# "#team cath> plain hello" + concurrentlyN_ + [ alice <# "#team cath> plain hello [>>]", + dan <# "#team cath> plain hello [>>]", + eve <# "#team cath> plain hello [>>]" + ] + cath #$> ("/_get chat #1 count=100 search=plain hello", chat, [(1, "plain hello")]) + dan #$> ("/_get chat #1 count=100 search=plain hello", chat, [(0, "plain hello")]) + +testChannelSignedFile :: HasCallStack => TestParams -> IO () +testChannelSignedFile ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> withXFTPServer $ do + xftpCLI ["rand", "./tests/tmp/testfile", "1mb"] `shouldReturn` ["File created: ./tests/tmp/testfile"] + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + -- roster serves arrive as files that Postgres deletes without reusing the id (SQLite reuses + -- it), so ids run higher here. cath's send and the subscribers' (dan, eve) receive both + -- follow only their join roster (id 2). The relay (bob) also received the roster re-served + -- on cath's promotion, so its file is id 3. The owner (alice) has no roster file, so id 1. +#if defined(dbPostgres) + let fileId = 2 :: Int + relayFileId = 3 :: Int +#else + let fileId = 1 :: Int + relayFileId = 1 :: Int +#endif + + -- cath's first (signed) message introduces her to dan/eve + cath ##> "/_send #1 sign=on text hi" + cath <# "#team hi (signed)" + bob <# "#team cath> hi (signed)" + concurrentlyN_ + [ alice <# "#team cath> hi (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> hi (signed) [>>]" + ] + + -- cath sends a signed file + cath ##> "/_send #1 sign=on json [{\"filePath\": \"./tests/tmp/testfile\", \"msgContent\": {\"text\":\"signed file\",\"type\":\"file\"}}]" + cath <# "#team signed file (signed)" + cath <# "/f #team ./tests/tmp/testfile" + cath <## ("use /fc " <> show fileId <> " to cancel sending") + cath <## ("completed uploading file " <> show fileId <> " (testfile) for #team") + + bob <# "#team cath> signed file (signed)" + bob <# "#team cath> sends file testfile (1.0 MiB / 1048576 bytes)" + bob <## ("use /fr " <> show relayFileId <> " [/ | ] to receive it") + + concurrentlyN_ + [ do alice <# "#team cath> signed file (signed) [>>]" + alice <# "#team cath> sends file testfile (1.0 MiB / 1048576 bytes) [>>]" + alice <## "use /fr 1 [/ | ] to receive it [>>]", + do dan <# "#team cath> signed file (signed) [>>]" + dan <# "#team cath> sends file testfile (1.0 MiB / 1048576 bytes) [>>]" + dan <## ("use /fr " <> show fileId <> " [/ | ] to receive it [>>]"), + do eve <# "#team cath> signed file (signed) [>>]" + eve <# "#team cath> sends file testfile (1.0 MiB / 1048576 bytes) [>>]" + eve <## ("use /fr " <> show fileId <> " [/ | ] to receive it [>>]") + ] + + -- dan downloads: the signed digest is verified and the file completes + dan ##> ("/fr " <> show fileId <> " ./tests/tmp") + dan + <### [ ConsoleString ("saving file " <> show fileId <> " from cath to ./tests/tmp/testfile_1"), + ConsoleString ("started receiving file " <> show fileId <> " (testfile) from cath") + ] + dan <## ("completed receiving file " <> show fileId <> " (testfile) from cath") + src <- B.readFile "./tests/tmp/testfile" + destDan <- B.readFile "./tests/tmp/testfile_1" + destDan `shouldBe` src + -- the signed digest was carried to dan and stored, so verification ran (not skipped) and passed + digestCount <- withCCTransaction dan $ \db -> + DB.query_ db "SELECT count(1) FROM files WHERE file_digest IS NOT NULL" :: IO [[Int]] + digestCount `shouldBe` [[1]] + +testChannelMemberUpdateEnforcement :: HasCallStack => TestParams -> IO () +testChannelMemberUpdateEnforcement ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- cath posts a signed message; dan holds it verified + cath ##> "/_send #1 sign=on text secret" + cath <# "#team secret (signed)" + bob <# "#team cath> secret (signed)" + concurrentlyN_ + [ alice <# "#team cath> secret (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> secret (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> secret (signed) [>>]" + ] + dan #$> ("/_get chat #1 count=100 search=secret", chat, [(0, "secret (signed)")]) + + -- the malicious relay forges an unsigned XMsgUpdate of cath's signed item to dan + cathMemId <- memberIdByName bob "cath" + sharedId <- itemSharedMsgId cath + connId <- relayConnIdToMember bob "dan" + ts <- getCurrentTime + let ChatController {smpAgent = bobAgent} = chatController bob + chatMsg = ChatMessage chatInitialVRange Nothing (XMsgUpdate sharedId (MCText "forged") M.empty Nothing Nothing Nothing Nothing) + fwd = GrpMsgForward (FwdMember cathMemId "cath") ts + body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)] + sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)] + either (fail . show) (const $ pure ()) sent + -- dan rejects the unsigned mutation of the held-signed item (RGEMsgBadSignature, stored not shown live), + -- and the original signed content is not overwritten + threadDelay 2000000 + -- (critical) the forged content did NOT overwrite the original signed item + dan #$> ("/_get chat #1 count=100 search=secret", chat, [(0, "secret (signed)")]) + -- the rejection is recorded as a bad-signature item + dan #$> ("/_get chat #1 count=100 search=bad signature", chat, [(0, "message rejected: bad signature")]) + + -- a legitimate signed edit by cath is accepted + cathMsgId <- lastItemId cath + cath ##> ("/_update item #1 " <> cathMsgId <> " text secret edited") + cath <# "#team [edited] secret edited (signed)" + bob <# "#team cath> [edited] secret edited (signed)" + concurrentlyN_ + [ alice <# "#team cath> [edited] secret edited (signed)", + dan <# "#team cath> [edited] secret edited (signed)", + eve <# "#team cath> [edited] secret edited (signed)" + ] + dan #$> ("/_get chat #1 count=100 search=secret edited", chat, [(0, "secret edited (signed)")]) + dan #$> ("/_get chat #1 count=100 search=bad signature", chat, [(0, "message rejected: bad signature")]) + +testChannelAsGroupSign :: HasCallStack => TestParams -> IO () +testChannelAsGroupSign ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + + -- owner posts as the channel, signed: verifiable AND displayed as the channel + alice ##> "/_send #1(as_group=on) sign=on text signed channel post" + alice <# "#team signed channel post (signed)" + bob <# "#team> signed channel post (signed)" + [cath, dan, eve] *<# "#team> signed channel post (signed) [>>]" + alice #$> ("/_get chat #1 count=100 search=signed channel post", chat, [(1, "signed channel post (signed)")]) + cath #$> ("/_get chat #1 count=100 search=signed channel post", chat, [(0, "signed channel post (signed)")]) + + -- owner posts as the channel, unsigned: anonymous (FwdChannel), no signature, still as the channel + alice ##> "/_send #1(as_group=on) text plain channel post" + alice <# "#team plain channel post" + bob <# "#team> plain channel post" + [cath, dan, eve] *<# "#team> plain channel post [>>]" + alice #$> ("/_get chat #1 count=100 search=plain channel post", chat, [(1, "plain channel post")]) + cath #$> ("/_get chat #1 count=100 search=plain channel post", chat, [(0, "plain channel post")]) + +testChannelSignMessagesRequired :: HasCallStack => TestParams -> IO () +testChannelSignMessagesRequired ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- owner requires signatures + alice ##> "/set signatures #team on" + alice <## "updated group preferences:" + alice <## "Sign messages: on" + concurrentlyN_ + [ do + bob <## "alice updated group #team: (signed)" + bob <## "updated group preferences:" + bob <## "Sign messages: on", + do + cath <## "alice updated group #team: (signed)" + cath <## "updated group preferences:" + cath <## "Sign messages: on", + do + dan <## "alice updated group #team: (signed)" + dan <## "updated group preferences:" + dan <## "Sign messages: on", + do + eve <## "alice updated group #team: (signed)" + eve <## "updated group preferences:" + eve <## "Sign messages: on" + ] + + -- owner posts as the channel, signed: verified for everyone, held signed in db + alice ##> "/_send #1(as_group=on) sign=on text signed by owner" + alice <# "#team signed by owner (signed)" + bob <# "#team> signed by owner (signed)" + [cath, dan, eve] *<# "#team> signed by owner (signed) [>>]" + alice #$> ("/_get chat #1 count=100 search=signed by owner", chat, [(1, "signed by owner (signed)")]) + dan #$> ("/_get chat #1 count=100 search=signed by owner", chat, [(0, "signed by owner (signed)")]) + + -- owner posts as the channel without explicit sign: auto-signed because signing is required + alice ##> "/_send #1(as_group=on) text plain from owner" + alice <# "#team plain from owner (signed)" + bob <# "#team> plain from owner (signed)" + [cath, dan, eve] *<# "#team> plain from owner (signed) [>>]" + alice #$> ("/_get chat #1 count=100 search=plain from owner", chat, [(1, "plain from owner (signed)")]) + dan #$> ("/_get chat #1 count=100 search=plain from owner", chat, [(0, "plain from owner (signed)")]) + + -- promoted subscriber posts signed: verified for recipients (members are required to sign too) + cath ##> "/_send #1 sign=on text signed by member" + cath <# "#team signed by member (signed)" + bob <# "#team cath> signed by member (signed)" + concurrentlyN_ + [ alice <# "#team cath> signed by member (signed) [>>]", + do + dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> signed by member (signed) [>>]", + do + eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> signed by member (signed) [>>]" + ] + cath #$> ("/_get chat #1 count=100 search=signed by member", chat, [(1, "signed by member (signed)")]) + dan #$> ("/_get chat #1 count=100 search=signed by member", chat, [(0, "signed by member (signed)")]) + + -- promoted subscriber posts without explicit sign: auto-signed + cath ##> "/_send #1 text plain from member" + cath <# "#team plain from member (signed)" + bob <# "#team cath> plain from member (signed)" + concurrentlyN_ + [ alice <# "#team cath> plain from member (signed) [>>]", + dan <# "#team cath> plain from member (signed) [>>]", + eve <# "#team cath> plain from member (signed) [>>]" + ] + cath #$> ("/_get chat #1 count=100 search=plain from member", chat, [(1, "plain from member (signed)")]) + dan #$> ("/_get chat #1 count=100 search=plain from member", chat, [(0, "plain from member (signed)")]) + + -- a malicious relay forwards an unsigned channel message; the recipient holds it with a missing-signature warning + danConnId <- relayConnIdToMember bob "dan" + forgeTs <- getCurrentTime + let ChatController {smpAgent = bobAgent} = chatController bob + forgedMsg = ChatMessage chatInitialVRange (Just (SharedMsgId "forged-unsigned-01")) (XMsgNew $ mcSimple (MCText "forged unsigned")) + forgedBody = encodeBinaryBatch [encodeFwdElement (GrpMsgForward FwdChannel forgeTs) (VMUnsigned forgedMsg)] + sentForged <- runExceptT $ sendMessages bobAgent [(danConnId, PQEncOff, MsgFlags False, vrValue forgedBody)] + either (fail . show) (const $ pure ()) sentForged + dan <# "#team> forged unsigned (signature missing) [>>]" + dan #$> ("/_get chat #1 count=100 search=forged unsigned", chat, [(0, "forged unsigned (signature missing)")]) + +testChannelSignedHistory :: HasCallStack => TestParams -> IO () +testChannelSignedHistory ps = + testChat4 aliceProfile bobProfile cathProfile danProfile test ps + where + test alice bob cath dan = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice relay + memberJoinChannel "team" [relay] [alice] shortLink fullLink bob + memberJoinChannel "team" [relay] [alice] shortLink fullLink cath + -- require signatures + alice ##> "/set signatures #team on" + alice <## "updated group preferences:" + alice <## "Sign messages: on" + concurrentlyN_ + [ do + relay <## "alice updated group #team: (signed)" + relay <## "updated group preferences:" + relay <## "Sign messages: on", + do + bob <## "alice updated group #team: (signed)" + bob <## "updated group preferences:" + bob <## "Sign messages: on", + do + cath <## "alice updated group #team: (signed)" + cath <## "updated group preferences:" + cath <## "Sign messages: on" + ] + + -- signed post that is edited: history must carry the current, signed content + alice ##> "/_send #1(as_group=on) text history one" + alice <# "#team history one (signed)" + relay <# "#team> history one (signed)" + [bob, cath] *<# "#team> history one (signed) [>>]" + editId <- lastItemId alice + alice ##> ("/_update item #1 " <> editId <> " text history one edited") + alice <# "#team [edited] history one edited (signed)" + relay <# "#team> [edited] history one edited (signed)" + [bob, cath] *<# "#team> [edited] history one edited (signed)" + + -- signed post that is deleted from history: excluded from history + alice ##> "/_send #1(as_group=on) text history two" + alice <# "#team history two (signed)" + relay <# "#team> history two (signed)" + [bob, cath] *<# "#team> history two (signed) [>>]" + delId <- lastItemId alice + alice #$> ("/_delete item #1 " <> delId <> " history", id, "message marked deleted") + relay <# "#team> [marked deleted] history two (signed)" + + -- promoted member posts a signed message (from-member): history attributes it to the member, still verified + promoteChannelMember "team" alice relay cath [bob] + cath ##> "/_send #1 text from cath member" + cath <# "#team from cath member (signed)" + relay <# "#team cath> from cath member (signed)" + concurrentlyN_ + [ alice <# "#team cath> from cath member (signed) [>>]", + do + bob <### [EndsWith "updated to cath"] + bob <## "#team: relay introduced cath (Catherine) in the channel" + bob <# "#team cath> from cath member (signed) [>>]" + ] + + -- catch-up subscriber joins late: as-group edited content + signed member post (author unknown to dan, shown by id hash), deleted excluded + memberJoinChannel "team" [relay] [alice] shortLink fullLink dan + dan <# "#team> [edited] history one edited (signed)" + dan .<## "> from cath member (signed) [>>]" + dan #$> ("/_get chat #1 count=100 search=history one edited", chat, [(0, "history one edited (signed)")]) + dan #$> ("/_get chat #1 count=100 search=from cath member", chat, [(0, "from cath member (signed)")]) + dan #$> ("/_get chat #1 count=100 search=history two", chat, []) + +testChannelUnsignedHistory :: HasCallStack => TestParams -> IO () +testChannelUnsignedHistory ps = + testChat4 aliceProfile bobProfile cathProfile danProfile test ps + where + test alice bob cath dan = withRelay ps $ \relay -> do + (shortLink, fullLink) <- prepareChannel1Relay "team" alice relay + memberJoinChannel "team" [relay] [alice] shortLink fullLink bob + memberJoinChannel "team" [relay] [alice] shortLink fullLink cath + + -- unsigned as-group post that is edited: history carries the current content + alice ##> "/_send #1(as_group=on) text history one" + alice <# "#team history one" + relay <# "#team> history one" + [bob, cath] *<# "#team> history one [>>]" + editId <- lastItemId alice + alice ##> ("/_update item #1 " <> editId <> " text history one edited") + alice <# "#team [edited] history one edited" + relay <# "#team> [edited] history one edited" + [bob, cath] *<# "#team> [edited] history one edited" + + -- unsigned as-group post deleted from history: excluded + alice ##> "/_send #1(as_group=on) text history two" + alice <# "#team history two" + relay <# "#team> history two" + [bob, cath] *<# "#team> history two [>>]" + delId <- lastItemId alice + alice #$> ("/_delete item #1 " <> delId <> " history", id, "message marked deleted") + relay <# "#team> [marked deleted] history two" + + -- promoted member posts an unsigned message (from-member): history attributes it to the member + promoteChannelMember "team" alice relay cath [bob] + cath ##> "/_send #1 text from cath member" + cath <# "#team from cath member" + relay <# "#team cath> from cath member" + concurrentlyN_ + [ alice <# "#team cath> from cath member [>>]", + do + bob <### [EndsWith "updated to cath"] + bob <## "#team: relay introduced cath (Catherine) in the channel" + bob <# "#team cath> from cath member [>>]" + ] + + -- catch-up subscriber joins late: unsigned edits re-encode as current content (no [edited] marker, unlike signed); + -- member post shown by id hash (author unknown to dan); deleted excluded + memberJoinChannel "team" [relay] [alice] shortLink fullLink dan + dan <# "#team> history one edited [>>]" + dan .<## "> from cath member [>>]" + dan #$> ("/_get chat #1 count=100 search=history one edited", chat, [(0, "history one edited")]) + dan #$> ("/_get chat #1 count=100 search=from cath member", chat, [(0, "from cath member")]) + dan #$> ("/_get chat #1 count=100 search=history two", chat, []) + +testChannelAsGroupSpoof :: HasCallStack => TestParams -> IO () +testChannelAsGroupSpoof ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- cath posts legitimately (introduces cath to dan as a member) + cath #> "#team hi from cath" + bob <# "#team cath> hi from cath" + concurrentlyN_ + [ alice <# "#team cath> hi from cath [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> hi from cath [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> hi from cath [>>]" + ] + + -- the relay forges an asGroup=True post attributed to non-owner cath; dan rejects (owner guard, §2) + cathMemId <- memberIdByName bob "cath" + connId <- relayConnIdToMember bob "dan" + ts <- getCurrentTime + let ChatController {smpAgent = bobAgent} = chatController bob + container = (mcSimple (MCText "fake channel announcement")) {asGroup = Just True} + chatMsg = ChatMessage chatInitialVRange Nothing (XMsgNew container) + fwd = GrpMsgForward (FwdMember cathMemId "cath") ts + body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)] + sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)] + either (fail . show) (const $ pure ()) sent + dan <##. "error: x.msg.new: member is not allowed to send as group" + -- not rendered as the channel: dan still holds only the legitimate member message + threadDelay 1000000 + dan #$> ("/_get chat #1 count=100 search=hi from cath", chat, [(0, "hi from cath")]) + +testChannelMemberSelfDeleteSign :: HasCallStack => TestParams -> IO () +testChannelMemberSelfDeleteSign ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- member sends a signed message; dan holds it verified + cath ##> "/_send #1 sign=on text signed hello" + cath <# "#team signed hello (signed)" + bob <# "#team cath> signed hello (signed)" + concurrentlyN_ + [ alice <# "#team cath> signed hello (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> signed hello (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> signed hello (signed) [>>]" + ] + dan #$> ("/_get chat #1 count=100 search=signed hello", chat, [(0, "signed hello (signed)")]) + + -- self-delete of the signed item: signed delete, dan (holding it signed) accepts + cathMsgId <- lastItemId cath + cath #$> ("/_delete item #1 " <> cathMsgId <> " broadcast", id, "message marked deleted") + bob <# "#team cath> [marked deleted] signed hello (signed)" + concurrentlyN_ + [ alice <# "#team cath> [marked deleted] signed hello (signed)", + dan <# "#team cath> [marked deleted] signed hello (signed)", + eve <# "#team cath> [marked deleted] signed hello (signed)" + ] + + -- self-delete of an unsigned item: unsigned delete, accepted (no enforcement) + cath #> "#team plain hello" + bob <# "#team cath> plain hello" + concurrentlyN_ + [ alice <# "#team cath> plain hello [>>]", + dan <# "#team cath> plain hello [>>]", + eve <# "#team cath> plain hello [>>]" + ] + cathMsgId2 <- lastItemId cath + cath #$> ("/_delete item #1 " <> cathMsgId2 <> " broadcast", id, "message marked deleted") + bob <# "#team cath> [marked deleted] plain hello" + concurrentlyN_ + [ alice <# "#team cath> [marked deleted] plain hello", + dan <# "#team cath> [marked deleted] plain hello", + eve <# "#team cath> [marked deleted] plain hello" + ] + +testChannelMemberDeleteEnforcement :: HasCallStack => TestParams -> IO () +testChannelMemberDeleteEnforcement ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- cath posts a signed message; dan holds it verified + cath ##> "/_send #1 sign=on text secret" + cath <# "#team secret (signed)" + bob <# "#team cath> secret (signed)" + concurrentlyN_ + [ alice <# "#team cath> secret (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> secret (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> secret (signed) [>>]" + ] + dan #$> ("/_get chat #1 count=100 search=secret", chat, [(0, "secret (signed)")]) + + -- the relay forges an unsigned XMsgDel of cath's signed item to dan + cathMemId <- memberIdByName bob "cath" + sharedId <- itemSharedMsgId cath + connId <- relayConnIdToMember bob "dan" + ts <- getCurrentTime + let ChatController {smpAgent = bobAgent} = chatController bob + chatMsg = ChatMessage chatInitialVRange Nothing (XMsgDel sharedId Nothing Nothing False) + fwd = GrpMsgForward (FwdMember cathMemId "cath") ts + body = encodeBinaryBatch [encodeFwdElement fwd (VMUnsigned chatMsg)] + sent <- runExceptT $ sendMessages bobAgent [(connId, PQEncOff, MsgFlags False, vrValue body)] + either (fail . show) (const $ pure ()) sent + -- dan rejects the unsigned delete of the held-signed item; item not deleted, rejection recorded + threadDelay 2000000 + dan #$> ("/_get chat #1 count=100 search=secret", chat, [(0, "secret (signed)")]) + dan #$> ("/_get chat #1 count=100 search=bad signature", chat, [(0, "message rejected: bad signature")]) + + -- a legitimate signed self-delete by cath is accepted + cathMsgId <- lastItemId cath + cath #$> ("/_delete item #1 " <> cathMsgId <> " broadcast", id, "message marked deleted") + bob <# "#team cath> [marked deleted] secret (signed)" + concurrentlyN_ + [ alice <# "#team cath> [marked deleted] secret (signed)", + dan <# "#team cath> [marked deleted] secret (signed)", + eve <# "#team cath> [marked deleted] secret (signed)" + ] + +testChannelModerationDeleteSign :: HasCallStack => TestParams -> IO () +testChannelModerationDeleteSign ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> + withNewTestChat ps "cath" cathProfile $ \cath -> + withNewTestChat ps "dan" danProfile $ \dan -> + withNewTestChat ps "eve" eveProfile $ \eve -> do + createChannel1Relay "team" alice bob cath dan eve + promoteChannelMember "team" alice bob cath [dan, eve] + + -- cath posts a signed message; dan holds it verified + cath ##> "/_send #1 sign=on text moderated post" + cath <# "#team moderated post (signed)" + bob <# "#team cath> moderated post (signed)" + concurrentlyN_ + [ alice <# "#team cath> moderated post (signed) [>>]", + do dan <### [EndsWith "updated to cath"] + dan <## "#team: bob introduced cath (Catherine) in the channel" + dan <# "#team cath> moderated post (signed) [>>]", + do eve <### [EndsWith "updated to cath"] + eve <## "#team: bob introduced cath (Catherine) in the channel" + eve <# "#team cath> moderated post (signed) [>>]" + ] + dan #$> ("/_get chat #1 count=100 search=moderated post", chat, [(0, "moderated post (signed)")]) + + -- owner moderation-deletes cath's signed post; the always-signed delete is accepted by dan (holding it signed) + -- resolve alice's item id by text (not lastItemId) so a racing trailing event can't select the wrong item + catItemIdOnAlice <- itemIdByText alice "moderated post" + alice ##> ("/_delete member item #1 " <> catItemIdOnAlice) + alice <## "message marked deleted by you" + concurrentlyN_ + [ bob <# "#team cath> [marked deleted by alice] moderated post (signed)", + cath <# "#team cath> [marked deleted by alice] moderated post (signed)", + dan <# "#team cath> [marked deleted by alice] moderated post (signed)", + eve <# "#team cath> [marked deleted by alice] moderated post (signed)" + ] + where + itemIdByText :: TestCC -> T.Text -> IO String + itemIdByText cc t = do + rows <- withCCTransaction cc $ \db -> + DB.query db "SELECT chat_item_id FROM chat_items WHERE item_text LIKE '%' || ? || '%' ORDER BY chat_item_id DESC LIMIT 1" (Only t) :: IO [Only Int64] + case rows of + (Only i : _) -> pure (show i) + _ -> fail $ "no item with text " <> T.unpack t + testGroupLinkContentFilter :: HasCallStack => TestParams -> IO () testGroupLinkContentFilter = testChat3 aliceProfile bobProfile cathProfile $ diff --git a/tests/ChatTests/Names.hs b/tests/ChatTests/Names.hs new file mode 100644 index 0000000000..46b62801fe --- /dev/null +++ b/tests/ChatTests/Names.hs @@ -0,0 +1,310 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module ChatTests.Names where + +import ChatClient +import ChatTests.DBUtils +import ChatTests.Groups (memberJoinChannel, prepareChannel1Relay) +import ChatTests.Utils +import Control.Concurrent.Async (concurrently_) +import qualified Data.Text as T +import NameResolver +import Simplex.Messaging.SimplexName (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) +import Test.Hspec hiding (it) + +chatNamesTests :: SpecWith TestParams +chatNamesTests = do + it "connect by resolved name" testConnectByName + it "connect by name not claimed in link profile is rejected" testConnectByNameNotClaimed + it "connect by name to a known contact not claimed in profile is rejected" testConnectByNameKnownContactNotClaimed + it "connect by unregistered name fails to resolve" testConnectByNameNotFound + it "set name not resolving to own address is rejected" testSetNameNotOwnAddress + it "channel name is not verified just by joining via link" testChannelDomainLinkJoinUnverified + it "verify channel name, fail on re-point, retain status on refresh" testChannelDomainVerify + it "connect by channel name" testConnectByChannelName + it "connect by name resolving to channel (primary) and direct contact" testConnectByNameChannelAndContact + it "connect by name resolving to direct contact (primary) and channel" testConnectByNameContactAndChannel + it "connect by name resolving to business (primary) and channel" testConnectByNameBusinessAndChannel + +testConnectByName :: HasCallStack => TestParams -> IO () +testConnectByName ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + mapM_ enableNamesRole [alice, bob] + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + alice ##> "/_set domain 1 alice.simplex" + alice <## "new contact address set" + bob ##> "/c @alice.simplex" + bob <## "alice: connection started" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + bob ##> "/i alice" + bob <## "contact ID: 2" + bob <## "receiving messages via: localhost" + bob <## "sending messages via: localhost" + _ <- getTermLine bob + bob <## "SimpleX name: @alice.simplex (verified)" + bob <## "you've shared main profile with this contact" + bob <## "connection not verified, use /code command to see security code" + bob <## "quantum resistant end-to-end encryption" + _ <- getTermLine bob + pure () + +testConnectByNameNotClaimed :: HasCallStack => TestParams -> IO () +testConnectByNameNotClaimed ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + mapM_ enableNamesRole [alice, bob] + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + bob ##> "/c @alice.simplex" + bob <## "SimpleX name alice.simplex is not included in the connection link's profile" + +testConnectByNameKnownContactNotClaimed :: HasCallStack => TestParams -> IO () +testConnectByNameKnownContactNotClaimed ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + mapM_ enableNamesRole [alice, bob] + alice ##> "/ad" + (shortLink, _) <- getContactLinks alice True + bob ##> ("/c " <> shortLink) + bob <## "connection request sent!" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + registerName reg aliceName (contactNameRecord "alice" (T.pack shortLink)) + bob ##> "/c @alice.simplex" + bob <## "SimpleX name alice.simplex is not included in the connection link's profile" + +testConnectByNameNotFound :: HasCallStack => TestParams -> IO () +testConnectByNameNotFound ps = withSmpServerAndNames $ \_reg -> + testChat2 aliceProfile bobProfile test ps + where + test _alice bob = do + enableNamesRole bob + bob ##> "/c @nobody.simplex" + bob .<## "smpErr = NAME {nameErr = NOT_FOUND}}" + +testSetNameNotOwnAddress :: HasCallStack => TestParams -> IO () +testSetNameNotOwnAddress ps = withSmpServerAndNames $ \reg -> + testChat2 aliceProfile bobProfile (test reg) ps + where + aliceName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "alice" []) + test reg alice bob = do + mapM_ enableNamesRole [alice, bob] + bob ##> "/ad" + (bobShortLink, _) <- getContactLinks bob True + registerName reg aliceName (contactNameRecord "alice" (T.pack bobShortLink)) + alice ##> "/ad" + _ <- getContactLinks alice True + alice ##> "/_set domain 1 alice.simplex" + alice <## "SimpleX name alice.simplex has no valid connection link" + +-- a self-claimed name is never auto-verified from link data: the claim is not proof of ownership +testChannelDomainLinkJoinUnverified :: HasCallStack => TestParams -> IO () +testChannelDomainLinkJoinUnverified ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (shortLink, fullLink) <- prepareChannel1Relay "team" alice cath + registerName reg teamName (channelNameRecord "team" (T.pack shortLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + memberJoinChannel "team" [cath] [alice] shortLink fullLink bob + -- a link-data refresh must not mark the self-claimed name verified + bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups") + bob <## "group link: known group #team" + bob <## "use #team to send messages" -- no "SimpleX name" line: status stays unknown + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +testChannelDomainVerify :: HasCallStack => TestParams -> IO () +testChannelDomainVerify ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (shortLink, fullLink) <- prepareChannel1Relay "team" alice cath + registerName reg teamName (channelNameRecord "team" (T.pack shortLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + -- setting the name resolved it, so the owner's channel is verified + alice ##> "/_verify domain #1" + alice <## "SimpleX name #team verified" + memberJoinChannel "team" [cath] [alice] shortLink fullLink bob + bob ##> "/_verify domain #1" + bob <## "SimpleX name #team verified" + -- the name is re-pointed to a different link: verification fails + registerName reg teamName (channelNameRecord "team" "https://simplex.chat/other") + bob ##> "/_verify domain #1" + bob <## "SimpleX name #team not verified: the name does not resolve to the link in the group profile" + -- a link-data refresh keeps the failed status, not overwritten with verified + bob ##> ("/_connect plan 1 " <> shortLink <> " resolve=allGroups") + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verification failed)" + bob <## "use #team to send messages" + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +testConnectByChannelName :: HasCallStack => TestParams -> IO () +testConnectByChannelName ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (shortLink, _) <- prepareChannel1Relay "team" alice cath + registerName reg teamName (channelNameRecord "team" (T.pack shortLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/c #team.simplex" + bob <## "#team: connection started" + concurrentlyN_ + [ bob + <### [ "#team: joining the group (connecting to relay cath)...", + "#team: you joined the group (connected to relay cath)" + ] + , do + cath <## "bob (Bob): accepting request to join group #team..." + cath <## "#team: bob joined the group" + , alice <### [EndsWith "introduced bob (Bob) in the channel"] + ] + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verified)" + bob <## "use #team to send messages" + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +-- The bare name "team.simplex" resolves to both a channel and a direct contact. The channel is tried +-- first and succeeds (bob has joined #team), so it is the primary (planSimplexName); otherSimplexName +-- is the direct contact @team.simplex, shown as "You can also connect to @team.simplex in direct chat". +testConnectByNameChannelAndContact :: HasCallStack => TestParams -> IO () +testConnectByNameChannelAndContact ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (channelLink, _) <- prepareChannel1Relay "team" alice cath + alice ##> "/ad" + (contactLink, _) <- getContactLinks alice True + registerName reg teamName (contactAndChannelNameRecord "team" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/public group access #team domain=team.simplex" + alice <## "updated public group access: domain=team.simplex" + cath <## "alice updated group #team: (signed)" + cath <## "updated public group access: domain=team.simplex" + bob ##> "/c #team.simplex" + bob <## "#team: connection started" + concurrentlyN_ + [ bob + <### [ "#team: joining the group (connecting to relay cath)...", + "#team: you joined the group (connected to relay cath)" + ] + , do + cath <## "bob (Bob): accepting request to join group #team..." + cath <## "#team: bob joined the group" + , alice <### [EndsWith "introduced bob (Bob) in the channel"] + ] + bob ##> "/_connect plan 1 team.simplex" + bob <## "group link: known group #team" + bob <## "SimpleX name: #team (verified)" + bob <## "use #team to send messages" + bob <## "You can also connect to @team.simplex in direct chat" + where + teamName = SimplexNameInfo NTPublicGroup (SimplexDomain TLDSimplex "team" []) + +-- The bare name "acme.simplex" resolves to both a channel and a direct contact. The channel is tried +-- first but its group profile does not claim the domain, so the channel side of the plan fails; the +-- plan falls back to the direct contact as primary (planSimplexName) while otherSimplexName is the +-- channel #acme, shown as "You can also join channel #acme". The channel link is a real, fetchable +-- #acme channel, so the failure is the faithful "channel does not claim this domain" case, not a broken link. +testConnectByNameContactAndChannel :: HasCallStack => TestParams -> IO () +testConnectByNameContactAndChannel ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (channelLink, _) <- prepareChannel1Relay "acme" alice cath + alice ##> "/ad" + (contactLink, _) <- getContactLinks alice True + registerName reg acmeName (contactAndChannelNameRecord "acme" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/_set domain 1 acme.simplex" + alice <## "new contact address set" + bob ##> "/_connect plan 1 acme.simplex" + bob <## "contact address: ok to connect" + _ <- getTermLine bob -- contact short link data (JSON, printed in test view) + bob <## "You can also join channel #acme" + where + acmeName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "acme" []) + +testConnectByNameBusinessAndChannel :: HasCallStack => TestParams -> IO () +testConnectByNameBusinessAndChannel ps = withSmpServerAndNames $ \reg -> + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + mapM_ enableNamesRole [alice, cath, bob] + (channelLink, _) <- prepareChannel1Relay "biz" alice cath + alice ##> "/ad" + (contactLink, fullLink) <- getContactLinks alice True + registerName reg bizName (contactAndChannelNameRecord "biz" (T.pack contactLink) (T.pack channelLink)) + alice ##> "/auto_accept on business" + alice <## "auto_accept on, business" + alice ##> "/_set domain 1 biz.simplex" + alice <## "new contact address set" + bob ##> "/_connect plan 1 biz.simplex" + bob <## "business address: ok to connect" + contactSLinkData <- getTermLine bob -- contact short link data (JSON, printed in test view) + bob <## "You can also join channel #biz" + -- preparing the business by name saves its domain on the group, so it is then found by local name search + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> contactLink <> " domain=biz.simplex " <> contactSLinkData) + bob <## "#alice: group is prepared" + -- host changes its profile so the handshake's group-profile write fires; it must not wipe the saved domain + alice ##> "/p alice Alice Biz" + alice <## "user bio changed to Alice Biz (your 0 contacts are notified)" + bob ##> "/_connect plan 1 @biz.simplex resolve=never" + bob <## "business address: known prepared business #alice" + bob ##> "/_connect group #1" + bob <## "#alice: connection started" + alice <## "#bob (Bob): accepting business address request..." + bob <## "#alice: joining the group..." + alice <## "#bob: bob_1 joined the group" + bob <## "#alice: you joined the group" + -- after fully connecting, the business must still be found by local name search + bob ##> "/_connect plan 1 @biz.simplex resolve=never" + bob <## "business address: known business #alice" + bob <## "use #alice to send messages" + -- the business's verified domain survives the handshake and is shown in group info + bob ##> "/i #alice" + bob <## "group ID: 1" + bob <## "current members: 2" + bob <## "SimpleX name: @biz.simplex (verified)" + where + bizName = SimplexNameInfo NTContact (SimplexDomain TLDSimplex "biz" []) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 0efdd6baa2..4b82779b0e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -26,9 +26,9 @@ import qualified Data.Map.Strict as M import Simplex.Chat.Badges (BadgeCredential, BadgeInfo (..), BadgePurchase (..), BadgeRequest (..), BadgeType (..), generateMasterKey, issueBadge, verifyPayment) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatHooks (..), defaultChatHooks, mkStoreCxt) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) -import Simplex.Chat.Protocol (currentChatVersion) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..), currentChatVersion) import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..), profileFromName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) @@ -38,7 +38,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport -import Simplex.Messaging.Util (encodeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -46,7 +46,11 @@ chatProfileTests :: SpecWith TestParams chatProfileTests = do describe "user profiles" $ do it "update user profile and notify contacts" testUpdateProfile + it "profile description round-trips and shows in contact info" testProfileDescriptionShown + it "member profile description is redacted for members without a direct contact" testMemberDescriptionRedacted it "update user profile with image" testUpdateProfileImage + it "reject profile image that is too large" testSetProfileImageTooLarge + it "set profile image from file" testSetProfileImageFromFile it "use multiword profile names" testMultiWordProfileNames it "present supporter badge to contacts" testUserBadgeBroadcast it "supporter badge sent to contact connecting after attach" testUserBadgeOnConnect @@ -57,6 +61,7 @@ chatProfileTests = do it "supporter badge sent to contact connecting via address" testUserBadgeContactAddress describe "user contact link" $ do it "create and connect via contact link" testUserContactLink + it "create address on specified server" testCreateAddressOnServer it "retry connecting via contact link" testRetryConnectingViaContactLink it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept @@ -65,12 +70,9 @@ chatProfileTests = do it "reject contact and delete contact link" testRejectContactAndDeleteUserContact it "keep connection requests when contact link deleted" testKeepConnectionRequests it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks - -- TODO [short links] test auto-reply with current version, with connecting client not preparing contact it "auto-reply message" testAutoReplyMessage - it "auto-reply message in incognito" testAutoReplyMessageInIncognito describe "business address" $ do it "create and connect via business address" testBusinessAddress - -- TODO [short links] test business auto-reply with current version, with connecting client not preparing contact it "update profiles with business address" testBusinessUpdateProfiles describe "contact address connection plan" $ do it "contact address ok to connect; known contact" testPlanAddressOkKnown @@ -82,7 +84,6 @@ chatProfileTests = do describe "incognito" $ do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress - it "accept contact request incognito" testAcceptContactRequestIncognito it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation @@ -123,8 +124,12 @@ chatProfileTests = do it "should connect via one-time invitation" testShortLinkInvitation it "should plan and connect via one-time invitation" testPlanShortLinkInvitation it "should connect via contact address" testShortLinkContactAddress + it "should share contact address via chat" testShareAddressViaChat it "should join group" testShortLinkJoinGroup describe "short links with attached data" shortLinkTests + describe "client services" $ do + it "should create user as a service, disable and re-enable" testClientService + it "should create user without a service, enable and disable" testSwitchClientService shortLinkTests :: SpecWith TestParams shortLinkTests = do @@ -200,6 +205,69 @@ testUpdateProfile = bob <## "use @cat to send messages" ] +-- Profile.description survives the connect-time round-trip and is shown in the contact /i view. +testProfileDescriptionShown :: HasCallStack => TestParams -> IO () +testProfileDescriptionShown = + testChat2 aliceProfile bobWithDescr $ + \alice bob -> do + connectUsers alice bob + alice ##> "/i @bob" + alice <## "contact ID: 2" + alice <## "description:" + alice <## "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out" + alice <##. "receiving messages via" + alice <##. "sending messages via" + alice <## "you've shared main profile with this contact" + alice <## "connection not verified, use /code command to see security code" + alice <## "quantum resistant end-to-end encryption" + alice <##. "peer chat protocol version range" + where + bobWithDescr = bobProfile {description = Just "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out"} + +-- for a member without a direct contact, the description is redacted per the group's link/name policy +testMemberDescriptionRedacted :: HasCallStack => TestParams -> IO () +testMemberDescriptionRedacted = + testChat3 aliceProfile bobProfile cathWithDescr $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + -- prohibit direct messages (and thus simplex links) before members join + alice ##> "/set direct #team off" + alice <## "updated group preferences:" + alice <## "Direct messages: off" + addMember "team" alice bob GRAdmin + bob ##> "/j team" + concurrently_ + (alice <## "#team: bob joined the group") + (bob <## "#team: you joined the group") + -- cath joins and is introduced to bob with a redacted profile (link stripped) + addMember "team" alice cath GRAdmin + cath ##> "/j team" + concurrentlyN_ + [ alice <## "#team: cath joined the group", + do + cath <## "#team: you joined the group" + cath <## "#team: member bob (Bob) is connected", + do + bob <## "#team: alice added cath (Catherine) to the group (connecting...)" + bob <## "#team: new member cath is connected" + ] + -- bob has no direct contact to cath, so his stored member profile has the link stripped + bob ##> "/i #team cath" + bob <## "group ID: 1" + bob <##. "member ID:" + bob <## "description:" + bob <## "check out" + bob <##. "receiving messages via" + bob <##. "sending messages via" + bob <## "connection not verified, use /code command to see security code" + bob <##. "peer chat protocol version range" + where + cathWithDescr = cathProfile {description = Just "check [this link](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) out"} + -- the test issuer key under index 1 in the test config testBadgeKeys :: BBSPublicKey -> M.Map Int BBSPublicKey testBadgeKeys = M.singleton 1 @@ -424,6 +492,52 @@ testUpdateProfileImage = bob <## "use @alice2 to send messages" (bob TestParams -> IO () +testSetProfileImageTooLarge = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + -- image within the size limit is accepted + alice ##> "/set profile image data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=" + alice <## "profile image updated" + -- image over the size limit is rejected; + -- the long command wraps in the virtual terminal, so drain echo lines until the error + alice `send` ("/set profile image data:image/png;base64," <> replicate 12500 'A') + let errPrefix = "bad chat command: Profile image is too large" + expectError = do + l <- getTermLine alice + unless (take (length errPrefix) l == errPrefix) expectError + expectError + (bob TestParams -> IO () +testSetProfileImageFromFile ps = testChat aliceProfile test ps + where + tmp = tmpPath ps + pngPath = tmp <> "/avatar.png" + gifPath = tmp <> "/avatar.gif" + missingPath = tmp <> "/missing.png" + emptyPath = tmp <> "/empty.png" + test alice = do + B.writeFile pngPath "fake png bytes" -- content is not validated, only the extension + -- set profile image from a .png file + alice ##> ("/set profile image file " <> pngPath) + alice <## "profile image updated" + alice ##> "/show profile image" + alice <## "Profile image:" + alice <##. "data:image/png;base64," + -- unsupported extension is rejected + B.writeFile gifPath "GIF89a" + alice ##> ("/set profile image file " <> gifPath) + alice <##. "bad chat command: unsupported image extension" + -- missing file is rejected + alice ##> ("/set profile image file " <> missingPath) + alice <##. "bad chat command: image file not found" + -- empty file is rejected + B.writeFile emptyPath "" + alice ##> ("/set profile image file " <> emptyPath) + alice <##. "bad chat command: image file is empty" + testMultiWordProfileNames :: HasCallStack => TestParams -> IO () testMultiWordProfileNames = testChat3 aliceProfile' bobProfile' cathProfile' $ @@ -498,7 +612,7 @@ testMultiWordProfileNames = aliceProfile' = baseProfile {displayName = "Alice Jones"} bobProfile' = baseProfile {displayName = "Bob James"} cathProfile' = baseProfile {displayName = "Cath Johnson"} - baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing} + baseProfile = Profile {displayName = "", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} testUserContactLink :: HasCallStack => TestParams -> IO () testUserContactLink = @@ -530,6 +644,32 @@ testUserContactLink = alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath +testCreateAddressOnServer :: HasCallStack => TestParams -> IO () +testCreateAddressOnServer ps = testChat aliceProfile test ps + where + tmp = tmpPath ps + -- second SMP server, distinct from alice's configured server (localhost:7001) + altServer = "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003" + altServerCfg = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + serverStoreCfg = persistentServerStoreCfg tmp + } + test alice = do + withSmpServer' altServerCfg $ do + -- without a server the address is created on the configured server (7001) + alice ##> "/_address 1" + (_, defaultLink) <- getContactLinks alice True + defaultLink `shouldContain` "localhost%3A7001" -- server is URL-encoded in the link + alice ##> "/_delete_address 1" + alice <## "Your chat address is deleted - accepted contacts will remain connected." + alice <## "To create a new chat address use /ad" + -- with a server the address is pinned to the requested server (7003) + alice ##> ("/_address 1 " <> altServer) + (_, pinnedLink) <- getContactLinks alice True + pinnedLink `shouldContain` "localhost%3A7003" + alice <## "disconnected 1 connections on server localhost" + testRetryConnectingViaContactLink :: HasCallStack => TestParams -> IO () testRetryConnectingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test ps where @@ -971,10 +1111,10 @@ testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile bob @@@ [("@alice", "hey")] testAutoReplyMessage :: HasCallStack => TestParams -> IO () -testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $ +testAutoReplyMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True + cLink <- getContactLink alice True alice ##> "/auto_accept on incognito=off text hello!" alice <## "auto_accept on" alice <## "auto reply:" @@ -992,31 +1132,6 @@ testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile alice <## "bob (Bob): contact is connected" ] -testAutoReplyMessageInIncognito :: HasCallStack => TestParams -> IO () -testAutoReplyMessageInIncognito = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $ - \alice bob -> do - alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True - alice ##> "/auto_accept on incognito=on text hello!" - alice <## "auto_accept on, incognito" - alice <## "auto reply:" - alice <## "hello!" - - bob ##> ("/c " <> cLink) - bob <## "connection request sent!" - alice <## "bob (Bob): accepting contact request..." - alice <## "bob (Bob): you can send messages to contact" - alice <# "i @bob hello!" - aliceIncognito <- getTermLine alice - concurrentlyN_ - [ do - bob <# (aliceIncognito <> "> hello!") - bob <## (aliceIncognito <> ": contact is connected"), - do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice <## "use /i bob to print out this incognito profile again" - ] - testBusinessAddress :: HasCallStack => TestParams -> IO () testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile $ \biz alice bob -> do @@ -1072,10 +1187,10 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice (biz <# "#bob bob_1> hey there") testBusinessUpdateProfiles :: HasCallStack => TestParams -> IO () -testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile aliceProfile bobProfile cathProfile $ +testBusinessUpdateProfiles = testChat4 businessProfile aliceProfile bobProfile cathProfile $ \biz alice bob cath -> do biz ##> "/ad" - cLink <- getContactLinkNoShortLink biz True + cLink <- getContactLink biz True biz ##> "/auto_accept on business text Welcome" biz <## "auto_accept on, business" biz <## "auto reply:" @@ -1105,7 +1220,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al biz ##> "/mr alisa alisa_1 admin" biz <## "#alisa: you changed the role of alisa_1 to admin" alice <## "#biz: biz_1 changed your role from member to admin" - connectUsersNoShortLink alice bob + connectUsers alice bob alice ##> "/a #biz bob" alice <## "invitation to join the group #biz sent to bob" bob <## "#biz (Biz Inc): alisa invites you to join the group as member" @@ -1136,7 +1251,7 @@ testBusinessUpdateProfiles = testChatCfg4 testCfgNoShortLinks businessProfile al alice <# "#biz robert> hi there" biz <# "#alisa robert> hi there" -- add business team member - connectUsersNoShortLink biz cath + connectUsers biz cath biz ##> "/a #alisa cath" biz <## "invitation to join the group #alisa sent to cath" cath <## "#alisa: biz invites you to join the group as member" @@ -1625,54 +1740,6 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ (bob TestParams -> IO () -testAcceptContactRequestIncognito = testChatCfg3 testCfgNoShortLinks aliceProfile bobProfile cathProfile $ - \alice bob cath -> do - alice ##> "/ad" - cLink <- getContactLinkNoShortLink alice True - -- GUI /_accept api - bob ##> ("/c " <> cLink) - alice <#? bob - alice ##> "/_accept incognito=on 1" - alice <## "bob (Bob): accepting contact request, you can send messages to contact" - aliceIncognitoBob <- getTermLine alice - concurrentlyN_ - [ bob <## (aliceIncognitoBob <> ": contact is connected"), - do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob) - alice <## "use /i bob to print out this incognito profile again" - ] - -- conversation is incognito - alice ?#> "@bob my profile is totally inconspicuous" - bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous") - bob #> ("@" <> aliceIncognitoBob <> " I know!") - alice ?<# "bob> I know!" - -- list contacts - alice ##> "/contacts" - alice <## "i bob (Bob)" - alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob] - -- delete contact, incognito profile is deleted - alice ##> "/d bob" - alice <## "bob: contact is deleted" - bob <## (aliceIncognitoBob <> " deleted contact with you") - alice ##> "/contacts" - (alice ("/c " <> cLink) - alice <#? cath - alice ##> "/accept incognito cath" - alice <## "cath (Catherine): accepting contact request, you can send messages to contact" - aliceIncognitoCath <- getTermLine alice - concurrentlyN_ - [ cath <## (aliceIncognitoCath <> ": contact is connected"), - do - alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath) - alice <## "use /i cath to print out this incognito profile again" - ] - alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath] - cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath] - testSetConnectionIncognito :: HasCallStack => TestParams -> IO () testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -3035,6 +3102,38 @@ testPlanShortLinkInvitation = slSimplexScheme :: String -> String slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost" +testShareAddressViaChat :: HasCallStack => TestParams -> IO () +testShareAddressViaChat = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + alice ##> "/ad" + _ <- getContactLinks alice True + connectUsers alice bob + connectUsers bob cath + -- alice shares her signed address card to bob + alice ##> "/share address @bob" + alice <# "@bob contact address of @alice (signed):" + _ <- getTermLine alice -- link + _ <- getTermLine alice -- owner signature (testView) + bob <# "alice> contact address of @alice (signed):" + bLink <- getTermLine bob + bSig <- getTermLine bob + -- bob verifies alice's owner signature + bob ##> ("/_connect plan 1 " <> bLink <> " sig=" <> bSig) + bob <## "contact address: ok to connect" + bob <## "owner signature: verified" + _ <- getTermLine bob -- link data + -- bob replays alice's signed card to cath: the binding was alice->bob, so cath strips the signature + let sig = maybe (error "bad sig") id (decodeJSON (T.pack bSig) :: Maybe LinkOwnerSig) + cLink = either error id $ strDecode (B.pack bLink) + mc = MCChat (T.pack bLink) (MCLContact cLink (profileFromName "alice") False) (Just sig) + cm = "{\"msgContent\":" <> T.unpack (encodeJSON mc) <> "}" + bob ##> ("/_send @3 json [" <> cm <> "]") + bob <# "@cath contact address of @alice (signed):" + _ <- getTermLine bob -- link + _ <- getTermLine bob -- owner signature (bob's sent view) + cath <# "bob> contact address of @alice:" + void $ getTermLine cath -- link (signature stripped) + testShortLinkContactAddress :: HasCallStack => TestParams -> IO () testShortLinkContactAddress = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do @@ -3835,14 +3934,14 @@ testShortLinkChangePreparedContactUser = testChat2 aliceProfile bobProfile test bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) bob <## "alice: contact is prepared" - -- 2 ids are for "user contacts", 2 ids are for second user contact cards, so alice is id 5 - bob ##> "/_set contact user @5 2" + -- 2 ids are for "user contacts", 1 id is for second user contact card, so alice is id 4 + bob ##> "/_set contact user @4 2" bob <## "contact alice changed from user bob to user robert" bob ##> "/user robert" showActiveUser bob "robert" - bob ##> "/_connect contact @5 text hello" + bob ##> "/_connect contact @4 text hello" bob <### [ "alice: connection started", WithTime "@alice hello" @@ -3856,8 +3955,8 @@ testShortLinkChangePreparedContactUser = testChat2 aliceProfile bobProfile test alice @@@ [("@robert", "hey")] alice `hasContactProfiles` ["alice", "robert"] - bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) - bob `hasContactProfiles` ["robert", "alice", "Ask SimpleX Team", "SimpleX Status"] + bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@Ask SimpleX Team", ""), ("*", "")]) + bob `hasContactProfiles` ["robert", "alice", "Ask SimpleX Team"] bob ##> "/user bob" showActiveUser bob "bob (Bob)" bob @@@ [] @@ -3885,16 +3984,16 @@ testShortLinkChangePreparedContactUserDuplicate = testChat2 aliceProfile bobProf bob <## "alice: contact is prepared" -- 2 ids are for "user contacts" - -- 2 ids are for second user contact cards + -- 1 id is for second user contact card -- 1 for second user's alice - -- so this alice is id 6 - bob ##> "/_set contact user @6 2" + -- so this alice is id 5 + bob ##> "/_set contact user @5 2" bob <## "contact alice changed from user bob to user robert, new local name: alice_1" bob ##> "/user robert" showActiveUser bob "robert" - bob ##> "/_connect contact @6 text hello" + bob ##> "/_connect contact @5 text hello" bob <### [ "alice_1: connection started", WithTime "@alice_1 hello" @@ -3913,8 +4012,8 @@ testShortLinkChangePreparedContactUserDuplicate = testChat2 aliceProfile bobProf alice @@@ [("@robert", "hey"), ("@robert_1", "hey")] alice `hasContactProfiles` ["alice", "robert", "robert"] - bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@alice_1", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) - bob `hasContactProfiles` ["robert", "alice", "alice", "Ask SimpleX Team", "SimpleX Status"] + bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@alice_1", "hey"), ("@Ask SimpleX Team", ""), ("*", "")]) + bob `hasContactProfiles` ["robert", "alice", "alice", "Ask SimpleX Team"] bob ##> "/user bob" showActiveUser bob "bob (Bob)" bob @@@ [] @@ -4007,8 +4106,8 @@ testShortLinkChangePreparedGroupUser = testChat3 aliceProfile bobProfile cathPro alice @@@ [("#team", "3"), ("@cath","sent invitation to join group team as admin")] alice `hasContactProfiles` ["alice", "cath", "robert"] - bob #$> ("/_get chats 2 pcc=on", chats, [("#team", "3"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")]) - bob `hasContactProfiles` ["robert", "alice", "cath", "Ask SimpleX Team", "SimpleX Status"] + bob #$> ("/_get chats 2 pcc=on", chats, [("#team", "3"), ("@Ask SimpleX Team", ""), ("*", "")]) + bob `hasContactProfiles` ["robert", "alice", "cath", "Ask SimpleX Team"] cath @@@ [("#team", "3"), ("@alice","received invitation to join group team as admin")] cath `hasContactProfiles` ["cath", "alice", "robert"] bob ##> "/user bob" @@ -4121,7 +4220,7 @@ testShortLinkChangePreparedGroupUserDuplicate = testChat3 aliceProfile bobProfil alice @@@ [("#team", "7"), ("@cath","sent invitation to join group team as admin")] alice `hasContactProfiles` ["alice", "cath", "robert", "robert"] - bob `hasContactProfiles` ["robert", "robert", "robert", "alice", "alice", "cath", "cath", "Ask SimpleX Team", "SimpleX Status"] + bob `hasContactProfiles` ["robert", "robert", "robert", "alice", "alice", "cath", "cath", "Ask SimpleX Team"] cath @@@ [("#team", "7"), ("@alice","received invitation to join group team as admin")] cath `hasContactProfiles` ["cath", "alice", "robert", "robert"] bob ##> "/user bob" @@ -4362,3 +4461,83 @@ testShortLinkGroupChangeProfileReceived = testChat3 aliceProfile bobProfile cath [alice, cath] *<# "#club bob> 2" cath #> "#club 3" [alice, bob] *<# "#club cath> 3" + +testClientService :: HasCallStack => TestParams -> IO () +testClientService ps = + withNewTestChat ps "alice" aliceProfile $ \alice -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + -- create user as service + withNewTestChat_ ps "service" True serviceProfile $ \service -> do + connectUsers alice service + alice <##> service + service ##> "/set client service 1:service_user off" + service <## "error: chat not stopped" + service ##> "/users" + service <## "service_user (Service user) (active, service)" + -- connect as service + withTestChat ps "service" $ \service -> do + subscribeClientService service 1 + alice <##> service + setClientService ps "off" + -- connect without service + withTestChat ps "service" $ \service -> do + service <## "subscribed 1 connections on server localhost" + alice <##> service + connectUsers bob service + bob <##> service + setClientService ps "on" + -- connect as service, queue associated + withTestChat ps "service" $ \service -> do + service <## "subscribed 2 connections on server localhost" + alice <##> service + bob <##> service + -- connect as service + withTestChat ps "service" $ \service -> do + subscribeClientService service 2 + alice <##> service + bob <##> service + +testSwitchClientService :: HasCallStack => TestParams -> IO () +testSwitchClientService ps = + withNewTestChat ps "user" aliceProfile $ \alice -> + withNewTestChat ps "bob" bobProfile $ \bob -> do + -- create user without service + withNewTestChat_ ps "service" False serviceProfile $ \service -> do + connectUsers alice service + alice <##> service + -- connect without service + withTestChat ps "service" $ \service -> do + service <## "subscribed 1 connections on server localhost" + alice <##> service + setClientService ps "on" + -- connect as service, queue associated + withTestChat ps "service" $ \service -> do + service <## "subscribed 1 connections on server localhost" + alice <##> service + connectUsers bob service + bob <##> service + -- connect as service + withTestChat ps "service" $ \service -> do + subscribeClientService service 2 + alice <##> service + bob <##> service + -- connect without service + setClientService ps "off" + withTestChat ps "service" $ \service -> do + service <## "subscribed 2 connections on server localhost" + alice <##> service + bob <##> service + +setClientService :: TestParams -> String -> IO () +setClientService ps onOff = + withTestChatCfgOpts ps testCfg testOpts {coreOptions = testCoreOpts {maintenance = True}} "service" $ \service -> do + service ##> ("/set client service 1:service_user " <> onOff) + service <## "ok" + +subscribeClientService :: TestCC -> Int -> IO () +subscribeClientService service n = + service + <### + [ ConsoleString $ "subscribed service (" <> show n <> " connections) on server localhost: ok", + "received messages from service on server localhost" + ] diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index b83b79c3a9..df2bff8ab6 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -24,6 +24,7 @@ import Data.Maybe (fromMaybe) import Data.String import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), mkStoreCxt) +import Simplex.Chat.Library.Commands (maxProfileImageSize) import Simplex.Chat.Markdown (viewName) import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText) import Simplex.Chat.Protocol @@ -84,8 +85,11 @@ businessProfile = mkProfile "biz" "Biz Inc" Nothing chatRelayProfile :: Profile chatRelayProfile = mkProfile "relay" "Relay" Nothing +serviceProfile :: Profile +serviceProfile = mkProfile "service_user" "Service user" Nothing + mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile -mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing} +mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, description = Nothing, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs, badge = Nothing, contactDomain = Nothing} it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) it name test = @@ -120,7 +124,7 @@ skip = before_ . pendingWith versionTestMatrix2 :: (HasCallStack => Bool -> Bool -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams versionTestMatrix2 runTest = do it "current" $ testChat2 aliceProfile bobProfile (runTest True True) - it "prev" $ testChatCfg2 testCfgVPrev aliceProfile bobProfile (runTest False True) + it "prev" $ runTestCfg2 testCfgVPrev testCfgVPrev (runTest False True) it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest False True) it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest False True) it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest False False) @@ -130,7 +134,7 @@ versionTestMatrix2 runTest = do versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams versionTestMatrix3 runTest = do it "current" $ testChat3 aliceProfile bobProfile cathProfile runTest - it "prev" $ testChatCfg3 testCfgVPrev aliceProfile bobProfile cathProfile runTest + it "prev" $ runTestCfg3 testCfgVPrev testCfgVPrev testCfgVPrev runTest it "prev to curr" $ runTestCfg3 testCfg testCfgVPrev testCfgVPrev runTest it "curr+prev to curr" $ runTestCfg3 testCfg testCfg testCfgVPrev runTest it "curr to prev" $ runTestCfg3 testCfgVPrev testCfg testCfg runTest @@ -238,7 +242,9 @@ genProfileImg = do g <- C.newRandom atomically $ B64.encode <$> C.randomBytes lrgLen g where - lrgLen = maxEncodedInfoLength * 3 `div` 4 - 420 + -- raw bytes that base64-encode to fit maxProfileImageSize when prefixed with "data:image/png;base64," + lrgLen = (maxProfileImageSize - imagePrefixLen) * 3 `div` 4 - 1 + imagePrefixLen = 22 -- PQ combinators / @@ -309,17 +315,17 @@ groupFeatures'' dir = ((1, "chat banner"), Nothing, Nothing) : ((dir, e2eeInfoNo groupFeatures_ :: Int -> Bool -> [((Int, String), Maybe (Int, String), Maybe String)] groupFeatures_ dir isChannel = - [ ((dir, "Disappearing messages: off"), Nothing, Nothing), - ((dir, "Direct messages: on"), Nothing, Nothing), - ((dir, "Full deletion: off"), Nothing, Nothing), - ((dir, "Message reactions: on"), Nothing, Nothing), - ((dir, "Voice messages: on"), Nothing, Nothing), - ((dir, "Files and media: on"), Nothing, Nothing), - ((dir, "SimpleX links: on"), Nothing, Nothing), - ((dir, "Member reports: on"), Nothing, Nothing), - ((dir, "Recent history: on"), Nothing, Nothing), - ((dir, "Chat with admins: " <> (if isChannel then "off" else "on")), Nothing, Nothing) - ] + [((dir, "Disappearing messages: off"), Nothing, Nothing)] + <> [((dir, "Direct messages: on"), Nothing, Nothing) | not isChannel] + <> [((dir, "Full deletion: off"), Nothing, Nothing)] + <> [((dir, "Message reactions: on"), Nothing, Nothing)] + <> [((dir, "Voice messages: on"), Nothing, Nothing) | not isChannel] + <> [((dir, "Files and media: on"), Nothing, Nothing) | not isChannel] + <> [((dir, "SimpleX links: on"), Nothing, Nothing) | not isChannel] + <> [((dir, "Member reports: on"), Nothing, Nothing) | not isChannel] + <> [((dir, "Recent history: on"), Nothing, Nothing)] + <> [((dir, "Chat with admins: " <> (if isChannel then "off" else "on")), Nothing, Nothing)] + <> [((dir, "Sign messages: off"), Nothing, Nothing) | isChannel] businessGroupFeatures :: [(Int, String)] businessGroupFeatures = map (\(a, _, _) -> a) $ businessGroupFeatures'' 0 diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index d611df8867..37fab0e4f0 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\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false}}}}" +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\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false,\"clientService\":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\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false}}}" +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\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"userChatRelay\":false,\"clientService\":false}}}" chatStartedSwift :: LB.ByteString chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}" diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index 2a5328ff26..e315b59f5e 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -10,7 +10,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Markdown -import Simplex.Messaging.Agent.Protocol (SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) +import Simplex.Messaging.Agent.Protocol (SimplexDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$$>)) import System.Console.ANSI.Types @@ -400,7 +400,7 @@ command' :: Text -> Text -> FormattedText command' = FormattedText . Just . Command sname :: SimplexNameType -> SimplexTLD -> Text -> [Text] -> Text -> Markdown -sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexNameDomain ns dom sub)) (pfx <> txt) +sname nt ns dom sub txt = markdown (SimplexName $ SimplexNameInfo nt (SimplexDomain ns dom sub)) (pfx <> txt) where pfx = case nt of NTPublicGroup -> "#"; NTContact -> "@" diff --git a/tests/MessageBatching.hs b/tests/MessageBatching.hs index 05322a0834..00cbbd757b 100644 --- a/tests/MessageBatching.hs +++ b/tests/MessageBatching.hs @@ -12,14 +12,31 @@ import qualified Data.ByteString as B import Data.ByteString.Internal (c2w) import Data.Either (partitionEithers) import Data.Int (Int64) +import Data.List.NonEmpty (NonEmpty (..)) import Data.String (IsString (..)) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) +import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) +import Simplex.Chat.Delivery + ( DeliveryJobScope (DJSGroup, jobSpec), + DeliveryJobSpec (DJDeliveryJob, includePending), + MessageDeliveryTask (MessageDeliveryTask, brokerTs, fwdSender, jobScope, senderGMId, taskId, verifiedMsg), + deliveryTaskId, + ) import Simplex.Chat.Messages.Batch import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..)) import Simplex.Chat.Messages (SndMessage (..)) -import Simplex.Chat.Protocol (maxEncodedMsgLength) -import Simplex.Chat.Types (SharedMsgId (..)) +import Simplex.Chat.Protocol + ( ChatMessage (ChatMessage), + ChatMsgEvent (XMsgNew), + FwdSender (FwdChannel), + GrpMsgForward (GrpMsgForward), + MsgContent (MCText), + VerifiedMsg (VMUnsigned), + maxEncodedMsgLength, + mcSimple, + ) +import Simplex.Chat.Types (SharedMsgId (..), chatInitialVRange) import Simplex.Messaging.Encoding (Large (..), smpEncodeList) import Test.Hspec @@ -28,6 +45,8 @@ batchingTests = describe "message batching tests" $ do testBatchingCorrectness testBinaryBatchingCorrectness it "image x.msg.new and x.msg.file.descr should fit into single batch" testImageFitsSingleBatch + it "does not create a relay delivery body when every task is oversized" testRelayBatchAllLarge + it "classifies a task that fits raw but not as a framed singleton as large" testRelayBatchSingletonOverflow instance IsString SndMessage where fromString s = SndMessage {msgId, sharedMsgId = SharedMsgId "", msgBody = s', signedMsg_ = Nothing} @@ -131,6 +150,37 @@ testImageFitsSingleBatch = do runBatcherTest' BMJson maxEncodedMsgLength [msg xMsgNewStr, msg descrStr] [] [batched] +testRelayBatchAllLarge :: IO () +testRelayBatchAllLarge = do + let task1 = deliveryTask 1 "one" + task2 = deliveryTask 2 "two" + (body_, accepted, large) = batchDeliveryTasks1 chatInitialVRange 1 (task1 :| [task2]) + body_ `shouldBe` Nothing + map deliveryTaskId accepted `shouldBe` [] + map deliveryTaskId large `shouldBe` [1, 2] + +deliveryTask :: Int64 -> T.Text -> MessageDeliveryTask +deliveryTask taskId text = + MessageDeliveryTask + { taskId, + jobScope = DJSGroup {jobSpec = DJDeliveryJob {includePending = False}}, + senderGMId = 1, + fwdSender = FwdChannel, + brokerTs = systemToUTCTime $ MkSystemTime 0 0, + verifiedMsg = + VMUnsigned + (ChatMessage chatInitialVRange Nothing $ XMsgNew $ mcSimple $ MCText text) + } + +testRelayBatchSingletonOverflow :: IO () +testRelayBatchSingletonOverflow = do + let task = deliveryTask 1 "overflow" + elemLen = B.length $ encodeFwdElement (GrpMsgForward (fwdSender task) (brokerTs task)) (verifiedMsg task) + (body_, accepted, large) = batchDeliveryTasks1 chatInitialVRange (elemLen + 2) (task :| []) + body_ `shouldBe` Nothing + map deliveryTaskId accepted `shouldBe` [] + map deliveryTaskId large `shouldBe` [1] + runBatcherTest :: BatchMode -> Int -> [SndMessage] -> [ChatError] -> [ByteString] -> Spec runBatcherTest mode maxLen msgs expectedErrors expectedBatches = it diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index bc0cc30a78..4e3ddbc0fa 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -22,6 +22,7 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BS import Data.ByteString.Internal (create) import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Time.Clock (getCurrentTime) import Data.Word (Word8, Word32) import Foreign.C import Foreign.Marshal.Alloc (mallocBytes) @@ -151,7 +152,8 @@ testChatApi ps = do dbPrefix = tmp "1" Right ChatDatabase {chatStore, agentStore} <- createChatDatabase (ChatDbOpts dbPrefix "myKey" DB.TQOff True) (MigrationConfig MCYesUp Nothing) insertUser agentStore - Right _ <- withTransaction chatStore $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} False True + ts <- getCurrentTime + Right _ <- withTransaction chatStore $ \db -> runExceptT $ createUserRecordAt db (AgentUserId 1) False False aliceProfile {preferences = Nothing} True ts Right cc <- chatMigrateInit dbPrefix "myKey" "yesUp" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" "yesUp" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey" "yesUp" diff --git a/tests/NameResolver.hs b/tests/NameResolver.hs new file mode 100644 index 0000000000..35f5cd2496 --- /dev/null +++ b/tests/NameResolver.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Local HTTP names resolver for chat tests, copied from simplexmq's +-- NamesResolverServer and made dynamic: it answers /resolve/ from a +-- mutable name -> NameRecord registry, so a test can resolve a name to the +-- address it just created. +module NameResolver + ( NameRegistry, + withNameResolver, + registerName, + contactNameRecord, + channelNameRecord, + contactAndChannelNameRecord, + resolverNamesConfig, + ) +where + +import Control.Concurrent.STM +import qualified Data.Aeson as J +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Text (Text) +import Network.HTTP.Types (hContentType, notFound404, ok200) +import Network.Wai (Application, pathInfo, responseLBS) +import qualified Network.Wai.Handler.Warp as Warp +import Simplex.Messaging.Names.Record (NameRecord (..)) +import Simplex.Messaging.Server.Names (NamesConfig (..)) +import Simplex.Messaging.SimplexName (SimplexNameInfo (..), fullDomainName) + +type NameRegistry = TVar (Map Text NameRecord) + +-- | Run an action with a local resolver on a free port and its registry (keyed +-- by full domain name, what the resolver looks the name up by). +withNameResolver :: (Int -> TVar (Map Text NameRecord) -> IO a) -> IO a +withNameResolver action = do + reg <- newTVarIO M.empty + Warp.withApplication (pure (app reg)) $ \port -> action port reg + where + app :: TVar (Map Text NameRecord) -> Application + app reg req send = do + (st, body) <- case pathInfo req of + ["health"] -> pure (ok200, "{}") + ["resolve", d] -> maybe (notFound404, "{}") (\r -> (ok200, J.encode r)) . M.lookup d <$> readTVarIO reg + _ -> pure (notFound404, "{}") + send $ responseLBS st [(hContentType, "application/json")] body + +-- | Register a name's domain to resolve to the given record. +registerName :: TVar (Map Text NameRecord) -> SimplexNameInfo -> NameRecord -> IO () +registerName reg SimplexNameInfo {nameDomain} r = + atomically $ modifyTVar' reg $ M.insert (fullDomainName nameDomain) r + +contactNameRecord :: Text -> Text -> NameRecord +contactNameRecord name link = (emptyRecord name) {nrSimplexContact = [link]} + +channelNameRecord :: Text -> Text -> NameRecord +channelNameRecord name link = (emptyRecord name) {nrSimplexChannel = [link]} + +-- | A record whose domain resolves to both a direct contact link and a channel link. +contactAndChannelNameRecord :: Text -> Text -> Text -> NameRecord +contactAndChannelNameRecord name contactLink channelLink = + (emptyRecord name) {nrSimplexContact = [contactLink], nrSimplexChannel = [channelLink]} + +emptyRecord :: Text -> NameRecord +emptyRecord name = + NameRecord + { nrName = name, + nrNickname = "", + nrWebsite = "", + nrLocation = "", + nrSimplexContact = [], + nrSimplexChannel = [], + nrEth = Nothing, + nrBtc = Nothing, + nrXmr = Nothing, + nrDot = Nothing, + nrOwner = "", + nrResolver = "" + } + +-- | NamesConfig for a chat test SMP server pointing at this resolver. +resolverNamesConfig :: Int -> NamesConfig +resolverNamesConfig port = + NamesConfig + { resolverEndpoint = "http://127.0.0.1:" <> show port, + resolverAuth = Nothing, + resolverTimeoutMs = 1000, + resolverMaxResponseBytes = 65536 + } diff --git a/tests/OperatorTests.hs b/tests/OperatorTests.hs index 5e659dd82c..c7ce5e8206 100644 --- a/tests/OperatorTests.hs +++ b/tests/OperatorTests.hs @@ -23,7 +23,7 @@ import Simplex.Chat.Operators.Presets import Simplex.Chat.Protocol (RelayProfile (..), mkRelayProfile) import Simplex.Chat.Types import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Encoding.String import Simplex.Messaging.Protocol @@ -33,14 +33,15 @@ operatorTests :: Spec operatorTests = describe "managing server operators" $ do validateServersTest updatedServersTest + perServerRolesTest validateServersTest :: Spec validateServersTest = describe "validate user servers" $ do it "should pass valid user servers" $ validateUserServers [valid] [] `shouldBe` ([], []) it "should fail without servers" $ do - validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], []) - validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], []) - validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing]) + validateUserServers [invalidNoServers] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing]) + validateUserServers [invalidDisabled] [] `shouldBe` ([USENoServers aSMP Nothing], [USWNoNamesServers Nothing]) + validateUserServers [invalidDisabledOp] [] `shouldBe` ([USENoServers aSMP Nothing, USENoServers aXFTP Nothing], [USWNoChatRelays Nothing, USWNoNamesServers Nothing]) it "should fail without servers with storage role" $ do validateUserServers [invalidNoStorage] [] `shouldBe` ([USEStorageMissing aSMP Nothing], []) it "should fail with duplicate host" $ do @@ -122,6 +123,84 @@ updatedServersTest = describe "validate user servers" $ do PresetServers {operators} = presetServers defaultChatConfig customRelayAddr = either error id $ strDecode "https://relay.example.im/r#Pz9qz7ZVljMofoRxiDDpL_w2DZSazK8IgafxqnWKv6Y" +perServerRolesTest :: Spec +perServerRolesTest = describe "per-server roles" $ do + describe "agentServerCfgs resolution" $ do + it "self-hosted server keeps its per-server roles" $ + case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride (Just True) (Just False) (Just True))] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Nothing + rolesTuple roles `shouldBe` (True, False, True) + cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs + it "self-hosted server without roles falls back to default roles" $ + case agentServerCfgs SPSMP opDomains [selfHostedSMP emptyServerRolesOverride] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Nothing + rolesTuple roles `shouldBe` (True, True, False) + cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs + it "self-hosted partial override keeps defaults for unset roles" $ + case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride Nothing Nothing (Just True))] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Nothing + rolesTuple roles `shouldBe` (True, True, True) + cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs + it "self-hosted explicit No overrides a default-yes role" $ + case agentServerCfgs SPSMP opDomains [selfHostedSMP (ServerRolesOverride (Just False) Nothing Nothing)] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Nothing + rolesTuple roles `shouldBe` (False, True, False) + cfgs -> expectationFailure $ "expected one self-hosted ServerCfg, got: " <> show cfgs + it "operator-matched server without override inherits operator roles" $ + -- operator roles are all on; no override -> all inherited, including names + case agentServerCfgs SPSMP opDomains [opMatchedSMP emptyServerRolesOverride] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Just 1 + rolesTuple roles `shouldBe` (True, True, True) + cfgs -> expectationFailure $ "expected one operator-matched ServerCfg, got: " <> show cfgs + it "operator-matched server applies its override over operator roles" $ + -- operator roles are all on; override turns storage/names off, proxy inherits (on) + case agentServerCfgs SPSMP opDomains [opMatchedSMP (ServerRolesOverride (Just False) Nothing (Just False))] of + [ServerCfg {operator, roles}] -> do + operator `shouldBe` Just 1 + rolesTuple roles `shouldBe` (False, True, False) + cfgs -> expectationFailure $ "expected one operator-matched ServerCfg, got: " <> show cfgs + it "two self-hosted servers resolve their three roles independently" $ + -- agentServerCfgs preserves input order + let a = (newUserServer "smp://abcd@self.example.com" :: NewUserServer 'PSMP) {roles = ServerRolesOverride (Just False) (Just True) (Just True)} + b = (newUserServer "smp://abcd@self2.example.com" :: NewUserServer 'PSMP) {roles = ServerRolesOverride (Just True) (Just False) Nothing} + in case agentServerCfgs SPSMP opDomains [a, b] of + [ServerCfg {operator = opA, roles = rolesA}, ServerCfg {operator = opB, roles = rolesB}] -> do + (opA, opB) `shouldBe` (Nothing, Nothing) + rolesTuple rolesA `shouldBe` (False, True, True) + rolesTuple rolesB `shouldBe` (True, False, False) + cfgs -> expectationFailure $ "expected two self-hosted ServerCfgs, got: " <> show cfgs + describe "validateUserServers per-server names coverage" $ do + it "self-hosted-only user without names servers warns USWNoNamesServers" $ do + let (_errs, warns) = validateUserServers [selfHostedUser emptyServerRolesOverride] [] + warns `shouldSatisfy` elem (USWNoNamesServers Nothing) + it "self-hosted user with a names server does not warn USWNoNamesServers" $ do + let (_errs, warns) = validateUserServers [selfHostedUser (ServerRolesOverride Nothing Nothing (Just True))] [] + warns `shouldSatisfy` notElem (USWNoNamesServers Nothing) + where + testOp = operatorSimpleXChat {operatorId = DBEntityId 1} + opDomains = operatorDomains [testOp] + -- host matches no operator domain -> self-hosted + selfHostedSMP :: ServerRolesOverride -> NewUserServer 'PSMP + selfHostedSMP r = (newUserServer "smp://abcd@self.example.com" :: NewUserServer 'PSMP) {roles = r} + -- host matches operator domain simplex.im + opMatchedSMP :: ServerRolesOverride -> NewUserServer 'PSMP + opMatchedSMP r = (newUserServer "smp://abcd@smp8.simplex.im" :: NewUserServer 'PSMP) {roles = r} + selfHostedUser :: ServerRolesOverride -> UpdatedUserOperatorServers + selfHostedUser r = + UpdatedUserOperatorServers + { operator = Nothing, + smpServers = [AUS SDBNew $ selfHostedSMP r], + xftpServers = [], + chatRelays = [] + } + rolesTuple :: ServerRoles -> (Bool, Bool, Bool) + rolesTuple ServerRoles {storage, proxy, names} = (storage, proxy, names) + deriving instance Eq User deriving instance Eq UserServersError diff --git a/tests/PostgresSchemaDump.hs b/tests/PostgresSchemaDump.hs index 7df0beb2fa..b36b7faeaf 100644 --- a/tests/PostgresSchemaDump.hs +++ b/tests/PostgresSchemaDump.hs @@ -78,5 +78,9 @@ skipComparisonForDownMigrations = [ -- via_group field moves "20250922_remove_unused_connections", -- group_member_intro_id field moves - "20251128_migrate_member_relations" + "20251128_migrate_member_relations", + -- on down migration single_sender_group_member_id column is re-added at the end of the table + "20260529_delivery_job_senders", + -- group_domain is removed + "20260603_simplex_name" ] diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 10f8808015..798ab3c4d7 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -3,12 +3,14 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ProtocolTests where import qualified Data.Aeson as J import Data.ByteString.Char8 (ByteString) import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) +import Simplex.Chat.Library.Internal (decodeLinkUserData, encodeShortLinkData) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -22,7 +24,9 @@ import Simplex.Messaging.Version import Test.Hspec protocolTests :: Spec -protocolTests = decodeChatMessageTest +protocolTests = do + decodeChatMessageTest + shortLinkDataTests srv :: SMPServer srv = SMPServer "smp.simplex.im" "5223" (C.KeyHash "\215m\248\251") @@ -101,14 +105,33 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing} testProfile :: Profile -testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing} +testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, description = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} testGroupProfile :: GroupProfile testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", description = Nothing, shortDescr = Nothing, image = Nothing, publicGroup = Nothing, groupPreferences = testGroupPreferences, memberAdmission = Nothing} +shortLinkDataTests :: Spec +shortLinkDataTests = describe "Short link data encoding/decoding" $ do + it "decodes compressed short-link user data below the decompressed size limit" $ do + let value = replicate 11000 'a' + decodeLinkUserData (linkData value) `shouldReturn` Just value + it "rejects compressed short-link user data above the decompressed size limit" $ do + let value = replicate (maxDecompressedMsgLength + 1) 'a' + decodeLinkUserData (linkData value) `shouldReturn` (Nothing :: Maybe String) + where + linkData value = + ContactLinkData + supportedSMPAgentVRange + UserContactData + { direct = True, + owners = [], + relays = [], + userData = encodeShortLinkData (value :: String) + } + decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ @@ -133,7 +156,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-17\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-19\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -218,7 +241,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XInfo testProfile it "x.info with empty full name" $ "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" - #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing} + #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} it "x.contact with xContactId" $ "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing @@ -244,13 +267,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" @@ -265,7 +288,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-17\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -278,7 +301,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" - #==# XGrpMemDel (MemberId "\1\2\3\4") False + #==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing it "x.grp.leave" $ "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 2c4ba05ce6..783f6587bf 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -141,7 +141,13 @@ skipComparisonForDownMigrations = -- indexes move down to the end of the file "20250922_remove_unused_connections", -- group_member_intros table moves down to the end of the file - "20251128_migrate_member_relations" + "20251128_migrate_member_relations", + -- on down migration single_sender_group_member_id column and its index + -- are re-added at the end of the table / file (ALTER TABLE ADD COLUMN + -- appends; CREATE INDEX appends). + "20260529_delivery_job_senders", + -- group_domain is removed + "20260603_simplex_name" ] getSchema :: FilePath -> FilePath -> IO String diff --git a/tests/Test.hs b/tests/Test.hs index 874428bc1f..84a019c21c 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -8,6 +8,7 @@ import Bots.DirectoryTests import ChatClient import ChatTests import ChatTests.DBUtils +import ChatTests.Names (chatNamesTests) import ChatTests.Utils (xdescribe'') import Control.Logger.Simple import Data.Time.Clock.System @@ -71,6 +72,10 @@ main = do describe "Message batching" batchingTests describe "Operators" operatorTests describe "Random servers" randomServersTests +#if !defined(dbPostgres) + around (tmpTestBracket chatQueryStats agentQueryStats) $ describe "names tests" chatNamesTests + around (tmpTestBracket chatQueryStats agentQueryStats) $ xdescribe'' "SimpleX Directory names" directoryNameTests +#endif #if defined(dbPostgres) createdDropDb . around testBracket #else @@ -96,6 +101,8 @@ main = do #else testBracket chatQueryStats agentQueryStats test = withSmpServer $ tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False} + tmpTestBracket chatQueryStats agentQueryStats test = + tmpBracket $ \tmpPath -> test TestParams {tmpPath, chatQueryStats, agentQueryStats, printOutput = False} #endif tmpBracket test = do t <- getSystemTime diff --git a/website/.eleventy.js b/website/.eleventy.js index f0310c5665..a96614e4ba 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -21,10 +21,50 @@ linksData.forEach(entry => { entry.imageExists = entry.image && fs.existsSync(path.join(linkImagesDir, entry.image)) }) +// Rewrites relative markdown links to website permalinks or GitHub URLs. +// Shared by the main markdown renderer (markdownLib) and the glossary renderer +// below, so glossary tooltips injected into pages get absolute, working links. +function replaceLink(link, _env) { + let parsed = uri.parse(link) + if (parsed.scheme || parsed.host) return link + + let hostFile = path.resolve(_env.page.inputPath) + let linkFile = path.resolve(hostFile, '..', parsed.path) + if (parsed.path.startsWith('/')) { + let srcIndex = hostFile.indexOf("/src") + if (srcIndex !== -1) { + linkFile = path.join(hostFile.slice(0, srcIndex + 4), parsed.path) + } + } + + if (fs.existsSync(linkFile) && fs.statSync(linkFile).isFile()) { + // this condition works if the link is a valid website file + const fileContent = fs.readFileSync(linkFile, 'utf8') + parsed.path = (matter(fileContent).data?.permalink || parsed.path).replace(/\.md$/, ".html").toLowerCase() + } else if (!fs.existsSync(linkFile)) { + linkFile = linkFile.replace('/website/src', '') + if (fs.existsSync(linkFile)) { + // this condition works if the link is a valid project file + const githubUrl = "https://github.com/simplex-chat/simplex-chat/blob/stable" + const repoRoot = hostFile.slice(0, hostFile.indexOf('/website/src')) + const repoPath = linkFile.slice(repoRoot.length) + return `${githubUrl}${repoPath}` + } else { + // if the link is not a valid website file or project file + throw new Error(`Broken link: ${parsed.path} in ${hostFile}`) + } + } + + return uri.serialize(parsed) +} + // The implementation of Glossary feature -const md = new markdownIt() +const md = new markdownIt({ replaceLink }).use(markdownItReplaceLink) +// Resolve the glossary's relative links (e.g. ./SIMPLEX.md) as if rendered from +// its built location, so tooltips embedded on other pages get absolute URLs. +const glossaryInputPath = path.resolve(__dirname, 'src/docs/GLOSSARY.md') const glossaryMarkdownContent = fs.readFileSync(path.resolve(__dirname, '../docs/GLOSSARY.md'), 'utf8') -const glossaryHtmlContent = md.render(glossaryMarkdownContent) +const glossaryHtmlContent = md.render(glossaryMarkdownContent, { page: { inputPath: glossaryInputPath } }) const glossaryDOM = new JSDOM(glossaryHtmlContent) const glossaryDocument = glossaryDOM.window.document const glossary = require('./src/_data/glossary.json') @@ -63,7 +103,7 @@ const globalConfig = { } const translationsDirectoryPath = './langs' -const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", "file", ""] +const supportedRoutes = ["blog", "contact", "invitation", "messaging", "docs", "fdroid", "why", "file", "downloads", "faq", "reproduce", "transparency", "security", "jobs", ""] let supportedLangs = [] fs.readdir(translationsDirectoryPath, (err, files) => { if (err) { @@ -310,7 +350,7 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/img") ty.addPassthroughCopy("src/video") ty.addPassthroughCopy("src/css") - ty.addPassthroughCopy("src/js") + ty.addPassthroughCopy("src/js/**/*.js") ty.addPassthroughCopy("src/lottie_file") ty.addPassthroughCopy("src/contact/*.js") ty.addPassthroughCopy("src/call") @@ -320,12 +360,16 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/blog/images") ty.addPassthroughCopy("src/docs/*.png") ty.addPassthroughCopy("src/docs/images") + ty.addPassthroughCopy("src/docs/guide/images") + ty.addPassthroughCopy("src/docs/guide/diagrams") + ty.addPassthroughCopy("src/docs/themes") ty.addPassthroughCopy("src/docs/protocol/diagrams") ty.addPassthroughCopy("src/docs/protocol/*.json") ty.addPassthroughCopy("src/images") ty.addPassthroughCopy("src/CNAME") ty.addPassthroughCopy("src/.well-known") ty.addPassthroughCopy("src/file-assets") + ty.addPassthroughCopy("src/credits") ty.addCollection('blogs', function (collection) { return collection.getFilteredByGlob('src/blog/*.md').reverse() @@ -417,40 +461,7 @@ module.exports = function (ty) { html: true, breaks: true, linkify: true, - replaceLink: function (link, _env) { - let parsed = uri.parse(link) - if (parsed.scheme || parsed.host) return link - - let hostFile = path.resolve(_env.page.inputPath) - let linkFile = path.resolve(hostFile, '..', parsed.path) - if (parsed.path.startsWith('/')) { - let srcIndex = hostFile.indexOf("/src") - if (srcIndex !== -1) { - linkFile = path.join(hostFile.slice(0, srcIndex + 4), parsed.path) - } - } - - if (fs.existsSync(linkFile) && fs.statSync(linkFile).isFile()) { - // this condition works if the link is a valid website file - const fileContent = fs.readFileSync(linkFile, 'utf8') - parsed.path = (matter(fileContent).data?.permalink || parsed.path).replace(/\.md$/, ".html").toLowerCase() - } else if (!fs.existsSync(linkFile)) { - linkFile = linkFile.replace('/website/src', '') - if (fs.existsSync(linkFile)) { - // this condition works if the link is a valid project file - const githubUrl = "https://github.com/simplex-chat/simplex-chat/blob/stable" - const keyword = "/simplex-chat" - index = linkFile.indexOf(keyword) - linkFile = linkFile.substring(index + keyword.length) - parsed.path = `${githubUrl}${linkFile}` - } else { - // if the link is not a valid website file or project file - throw new Error(`Broken link: ${parsed.path} in ${hostFile}`) - } - } - - return uri.serialize(parsed) - } + replaceLink: replaceLink }).use(markdownItAnchor, { slugify: (str) => slugify(str, { diff --git a/website/channel_sample.html b/website/channel_sample.html new file mode 100644 index 0000000000..169db55599 --- /dev/null +++ b/website/channel_sample.html @@ -0,0 +1,28 @@ + + + + + + SimpleX Channel Preview + + + + +
+ + + diff --git a/website/langs/cs.json b/website/langs/cs.json index 0805173d37..fbcfc430cd 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -127,7 +127,7 @@ "to-make-a-connection": "K vytvoření připojení:", "install-simplex-app": "Instalace aplikace SimpleX", "connect-in-app": "Se připojit v aplikaci", - "open-simplex-app": "Otevřete aplikaci Simplex", + "open-simplex-app": "Otevřete aplikaci SimpleX", "tap-the-connect-button-in-the-app": "Klepněte na tlačítko \"připojit\" v aplikaci", "scan-the-qr-code-with-the-simplex-chat-app": "Naskenujte QR kód pomocí aplikace SimpleX Chat", "installing-simplex-chat-to-terminal": "Instalace SimpleX chat do terminálu", @@ -175,9 +175,9 @@ "comparison-section-list-point-7": "P2P sítě mají buď centrální autoritu, nebo může být ohrožena celá síť", "see-here": "viz zde", "simplex-network-overlay-card-1-li-5": "Všechny známé P2P sítě mohou být zranitelné vůči Sybil útoku, protože každý uzel je zjistitelný a síť funguje jako celek. Známá opatření ke zmírnění tohoto problému vyžadují buď centralizovanou součást, nebo drahé prokázání práce. Síť SimpleX nemá možnost zjistitelnosti serveru, je fragmentovaná a funguje jako několik izolovaných podsítí, což znemožňuje útoky v celé síti.", - "simplex-network-overlay-card-1-li-6": "Sítě P2P mohou být zranitelné vůči útoku DRDoS, kdy klienti mohou znovu vysílat a zesílit provoz, což vede k odmítnutí služby v celé síti. SimpleX klienti pouze přenášejí provoz ze známého spojení a nemohou být zneužiti útočníkem k zesílení provozu v celé síti.", + "simplex-network-overlay-card-1-li-6": "Sítě P2P mohou být zranitelné vůči útoku DRDoS, kdy klienti mohou znovu vysílat a zesílit provoz, což vede k odmítnutí služby v celé síti. SimpleX klienti pouze přenášejí provoz ze známých spojení a nemohou být zneužiti útočníkem k zesílení provozu v celé síti.", "privacy-matters-overlay-card-1-p-2": "Internetoví prodejci vědí, že lidé s nižšími příjmy častěji provádějí urgentní nákupy, takže mohou účtovat vyšší ceny nebo odebírat slevy.", - "privacy-matters-overlay-card-1-p-4": "SimpleX síť chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery přednastavené v SimpleX Chat apce, operátoři serverů neznají počet uživatelů ani jejich připojení.", + "privacy-matters-overlay-card-1-p-4": "SimpleX síť chrání soukromí vašich připojení lépe než jakákoli jiná alternativa a plně zabraňuje tomu, aby byl váš sociální graf dostupný všem společnostem nebo organizacím. I když lidé používají servery přednastavené v aplikaci SimpleX Chat, operátoři serverů neznají počet uživatelů ani jejich připojení.", "privacy-matters-overlay-card-2-p-2": "Chcete-li být objektivní a činit nezávislá rozhodnutí, musíte mít svůj informační prostor pod kontrolou. Je to možné pouze v případě, že používáte soukromou komunikační síť, která nemá přístup k vašemu sociálnímu grafu.", "simplex-unique-overlay-card-1-p-2": "K doručování zpráv SimpleX používá párové anonymní adresy jednosměrných front zpráv, oddělených pro přijaté a odeslané zprávy, obvykle přes různé servery.", "privacy-matters-overlay-card-3-p-2": "Jedním z nejvíce šokujících příběhů je zkušenost Mohamedoua Oulda Salahiho popsaná v jeho pamětech a zobrazená v Mauritánském filmu. Byl umístěn do tábora na Guantánamu bez soudu a byl tam 15 let mučen po telefonátu svému příbuznému v Afghánistánu pro podezření z účasti na útocích z 11. září, i když předchozích 10 let žil v Německu.", @@ -191,7 +191,7 @@ "simplex-network": "SimpleX síť", "simplex-explained-tab-2-p-1": "Pro každé připojení používáte dvě samostatné fronty zasílání zpráv k odesílání a přijímání zpráv prostřednictvím různých serverů.", "simplex-explained-tab-1-p-1": "Můžete vytvářet kontakty a skupiny a vést obousměrné konverzace, stejně jako v jakémkoli jiném messengeru.", - "simplex-explained-tab-3-p-2": "Uživatelé mohou dále zlepšit soukromí metadat pomocí Tor pro přístup k serverům, což zabraňuje korelaci podle IP adresy.", + "simplex-explained-tab-3-p-2": "Uživatelé mohou dále zlepšit soukromí z pohledu metadat tím, že budou přistupovat k serverům skrze Tor, což zabrání korelaci podle IP adresy.", "hero-p-1": "Jiné aplikace mají uživatelská ID: Signal, Matrix, Session, Briar, Jami, Cwtch atd.
SimpleX ne, ani náhodná čísla.
To radikálně zlepšuje vaše soukromí.", "hero-2-header-desc": "Video ukazuje, jak se spojit se svým přítelem prostřednictvím jeho jednorázového QR kódu, osobně nebo prostřednictvím QR kódu ve videu. Můžete se také připojit sdílením odkazu pozvánky.", "feature-2-title": "E2E šifrované
obrázky, videa a soubory", @@ -369,5 +369,8 @@ "file-proto-spec": "Přečtěte si specifikaci XFTP protokolu →", "file-proto-p-2": "Šifrovací klíč souboru je obsažen pouze v části hash adresy URL – váš prohlížeč jej nikdy neodesílá na server. Existují 3 úrovně šifrování: přenos přes protokol TLS, šifrování pro každého příjemce (jedinečný dočasný klíč pro každý přenos) a end-to-end šifrování souborů.", "file-proto-p-4": "Když je soubor rozdělen na části, je odeslán přes síťové směrovače provozované nezávislými stranami. Žádný operátor nemůže vidět aktuální velikost nebo jméno souboru. I kdyby byl směrovač ohrožen, může vidět pouze šifrované části s pevně stanovenou velikosti. Části souboru jsou v mezipaměti síťových směrovačů uchovávány přibližně 48 hodin.", - "send-file": "Odeslat soubor" + "send-file": "Odeslat soubor", + "links": "Odkazy", + "links-title": "Komunitní odkazy", + "links-all-languages": "Všechny jazyky" } diff --git a/website/langs/de.json b/website/langs/de.json index 7c52be6498..e3a3bafbfc 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -200,7 +200,7 @@ "privacy-matters-overlay-card-1-p-4": "Das SimpleX-Netzwerk schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen einsehbar wird. Selbst wenn Anwender die in der SimpleX Chat-App vorkonfigurierten Server verwenden, kennen die Server-Betreiber die Anzahl der Benutzer oder deren Verbindungen nicht.", "contact-hero-header": "Sie haben eine Adresse zur Verbindung mit SimpleX Chat erhalten", "invitation-hero-header": "Sie haben einen Einmal-Link zur Verbindung mit SimpleX Chat erhalten", - "privacy-matters-overlay-card-3-p-3": "Selbst in demokratischen Ländern werden normale Menschen, auch unter Nutzung ihrer „anonymen“ Benutzerkennungen, für das, was sie online teilen, verhaftet.", + "privacy-matters-overlay-card-3-p-3": "Selbst in demokratischen Ländern werden normale Menschen, auch unter Nutzung ihrer „anonymen“ Benutzerkonten, für das, was sie online teilen, verhaftet.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat speichert alle Benutzerdaten ausschließlich auf den Endgeräten in einem portablen und verschlüsselten Datenbankformat, welches exportiert und auf jedes unterstützte Gerät übertragen werden kann.", "simplex-unique-overlay-card-2-p-2": "Auch wenn die optionale Benutzeradresse zum Versenden von Spam-Kontaktanfragen verwendet werden kann, können Sie sie ändern oder ganz löschen, ohne dass Ihre Verbindungen verloren gehen.", "simplex-unique-overlay-card-4-p-2": "Das SimpleX-Netzwerk verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an. Dies ermöglicht die Erstellung von Diensten, mit denen Nutzer über SimpleX Chat-Apps interagieren können — wir freuen uns sehr darauf zu sehen, welche SimpleX-Dienste Sie entwickeln werden.", @@ -274,11 +274,11 @@ "index-publications-kuketz-title": "Überprüfung von Mike Kuketz", "index-publications-optout-title": "Podcast-Interview von OptOut", "worlds-most-secure-messaging": "Niemand kann sehen, mit wem Sie kommunizieren", - "index-messaging-p1": "Nicht einmal Server – alle Nachrichten sehen aus wie zufälliges Rauschen.", + "index-messaging-p1": "Alle Nachrichten sehen aus wie zufälliges Rauschen - auch für die Server.", "index-messaging-p2": "Täglich werden Dutzende Millionen Nachrichten privat zugestellt.", "index-messaging-cta": "Lernen Sie mehr über SimpleX-Messaging", "index-nextweb-h2": "Das Netzwerk
gehört Ihnen", - "index-nextweb-p1": "Jeder Kontakt und jede Gruppe liegt auf Ihrem Gerät, nicht in einer Server-Datenbank.", + "index-nextweb-p1": "Jeder Kontakt und jede Gruppe wird auf Ihrem Gerät gespeichert, nicht in einer Server-Datenbank.", "index-nextweb-p2": "Keine einzelne Instanz kontrolliert das Netzwerk – jeder kann Server betreiben.", "index-token-h2": "Finanziert von seinen Nutzern", "index-token-p1": "Um unabhängig zu bleiben, werden große Kanäle und Communitys für ihre Server bezahlen.", @@ -296,7 +296,7 @@ "index-roadmap-3-title": "Lassen Sie Ihre Communitys wachsen", "index-roadmap-3-desc": "Tools zur Förderung Ihrer Communitys", "index-directory-h2": "Treten Sie SimpleX-Communitys bei", - "index-directory-p1": "Mehr als 2 Millionen Menschen haben SimpleX-Apps heruntergeladen.", + "index-directory-p1": "Mehr als 2 Millionen Nutzer haben schon SimpleX-Apps heruntergeladen.", "index-directory-p2": "Finden Sie Kanäle und Communitys im Verzeichnis oder erstellen Sie Ihre Eigenen!", "index-directory-cta": "SimpleX-Verzeichnis anzeigen", "index-directory-users-group-title": "SimpleX-Nutzergruppe", @@ -316,10 +316,10 @@ "navbar-token": "Token", "navbar-old-site": "Alte Webseite", "docs-dropdown-15": "Builds überprüfen und reproduzieren", - "why-p1": "Sie wurden ohne eine Benutzerkennung geboren.", + "why-p1": "Sie wurden ohne ein Benutzerkonto geboren.", "why-p2": "Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature — sie war selbstverständlich.", "why-p3": "Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen — Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so — Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein.", - "why-p4": "Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzernamen, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist.", + "why-p4": "Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzerkonten, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist.", "why-p5": "Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten — Sie sind souverän.", "why-p6": "Ihre Kommunikation gehört Ihnen, so wie es immer war, bevor es das Internet gab. Das Netzwerk ist kein Ort, den Sie besuchen. Es ist ein Ort, den Sie erschaffen und besitzen und Niemand kann es Ihnen nehmen, egal ob Sie es privat oder öffentlich machen.", "why-p7": "Die älteste Freiheit des Menschen — mit einem anderen Menschen sprechen zu können, ohne beobachtet zu werden — gestützt auf einer Infrastruktur, die Sie nicht verraten kann.", @@ -327,12 +327,12 @@ "why-tagline": "Genießen Sie die Freiheit in Ihrem Netzwerk.", "why-footer-link": "Warum wir es erschaffen haben", "file": "Datei", - "file-desc": "Versenden Sie Dateien via Ende-zu-Ende-Verschlüsselung — ohne Benutzerkennungen, ohne Tracking.", + "file-desc": "Versenden Sie Dateien via Ende-zu-Ende-Verschlüsselung — ohne Benutzerkonten und ohne Tracking.", "file-noscript": "Für den Datei-Transfer wird JavaScript benötigt.", "file-e2e-note": "Ende-zu-Ende-verschlüsselt — der Server bekommt Ihre Datei nie zu sehen.", "file-learn-more": "Erfahren Sie mehr über das XFTP-Protokoll", "file-cta-heading": "Laden Sie sich die SimpleX Chat App herunter — die sicherste & private Messenger-App", - "file-cta-subheading": "Die Dateiübertragung, die Sie gerade verwendet haben, nutzt dasselbe Datenweiterleitungsprotokoll wie SimpleX Chat. Die App bietet Ende‑zu‑Ende‑verschlüsselte Nachrichten, Sprach‑ und Videoanrufe, Gruppen sowie das Senden von Dateien. Keine Benutzerkennung. Kein Telefon. Keine E‑Mail. Keine Benutzerprofil‑IDs.", + "file-cta-subheading": "Die Dateiübertragung, die Sie gerade verwendet haben, nutzt dasselbe Datenweiterleitungsprotokoll wie SimpleX Chat. Die App bietet Ende‑zu‑Ende‑verschlüsselte Nachrichten, Sprach‑ und Videoanrufe, Gruppen sowie das Senden von Dateien. Kein Benutzerkonto. Keine Telefonnummer. Keine E‑Mail. Keine Benutzerprofil‑IDs.", "file-title": "SimpleX Dateiübertragung", "file-drop-text": "Datei per Drag & Drop hinzufügen", "file-drop-hint": "oder", @@ -362,12 +362,16 @@ "file-dl-sec-1": "Diese Datei ist verschlüsselt — Datenrouter sehen weder Inhalt, Namen noch Größe der Datei.", "file-workers-required": "Web Workers werden benötigt — aktualisieren Sie Ihren Browser", "file-protocol-title": "XFTP-Protokoll: Der sicherste Dateitransfer", - "file-proto-h-1": "Es wird keine Benutzerkennung benötigt", + "file-proto-h-1": "Es wird kein Benutzerkonto benötigt", "file-proto-p-1": "Jedes Dateifragment verwendet einen neuen zufälligen Schlüssel. Datenrouter kennen keine \"Benutzer\" oder \"Dateien\" — sie übertragen nur verschlüsselte Dateifragmente fester Größe.", "file-proto-h-2": "Dreifach direkt in Ihrem Browser verschlüsselt", "file-proto-p-2": "Der für die Dateiverschlüsselung genutzte Schlüssel befindet sich ausschließlich im Hash‑Fragment der URL — Ihr Browser sendet ihn niemals an einen Server. Es gibt drei Verschlüsselungsebenen: TLS‑Transport, empfängerbezogene Verschlüsselung (ein eindeutiger, flüchtiger Schlüssel pro Transfer) und Ende‑zu‑Ende‑Verschlüsselung der Datei.", "file-proto-h-4": "Unabhängig voneinander arbeitende Datenrouter", "file-proto-p-4": "Wenn die Datei in Fragmente aufgeteilt wurde, wird sie über Netzwerkrouter übertragen, die von unabhängigen Parteien betrieben werden. Kein Betreiber kann die tatsächliche Dateigröße oder den Dateinamen sehen. Selbst wenn ein Router kompromittiert wird, sieht er nur verschlüsselte Fragmente fester Größe. Die Fragmente werden von den Netzwerkroutern für etwa 48 Stunden zwischengespeichert.", - "file-proto-spec": "Lesen Sie die XFTP‑Protokollspezifikation durch →", - "send-file": "Datei senden" + "file-proto-spec": "Lesen Sie sich die XFTP‑Protokollspezifikation durch →", + "send-file": "Datei senden", + "links": "Links", + "links-title": "Community-Links", + "links-all-languages": "Alle Sprachen", + "docs-dropdown-16": "Ein Chat-Relais hosten" } diff --git a/website/langs/en.json b/website/langs/en.json index 2e860d76d4..482fe50042 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -242,6 +242,7 @@ "docs-dropdown-12": "Security", "docs-dropdown-14": "SimpleX for business", "docs-dropdown-15": "Verify & reproduce builds", + "docs-dropdown-16": "Host Chat Relay", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", @@ -263,6 +264,8 @@ "index-hero-h1": "Be
Free", "index-hero-h2": "In Your Network", "index-hero-p1": "The first network without user IDs.
You own your contacts, groups and channels.", + "index-hero-invest": "Invest in SimpleX Chat.", + "index-hero-invest-cta": "Learn more on Wefunder.", "index-hero-download-desktop-btn-title": "Download SimpleX Desktop App", "index-testflight-title": "SimpleX iOS beta-release on TestFlight", "index-f-droid-title": "SimpleX app via F-Droid", diff --git a/website/langs/es.json b/website/langs/es.json index a294c683f6..cff716c8d0 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -369,5 +369,8 @@ "file-proto-p-4": "Cuando un archivo se divide en fragmentos, se envía a través de routers en la red gestionados por terceros independientes. Ningún operador puede ver el nombre o el tamaño real del archivo. Incluso si un enrutador se ve comprometido, solo puede ver fragmentos cifrados de tamaño fijo. Los fragmentos de los archivos se almacenan por los routers de la red durante aproximadamente 48 horas.", "file-proto-spec": "Sobre las especificaciones del protocolo XFTP →", "file-cta-subheading": "La transferencia de archivos que acabas de usar emplea el mismo protocolo de enrutamiento de datos que SimpleX Chat. La aplicación ofrece mensajería, llamadas de voz y video, grupos y envío de archivos con cifrado de extremo a extremo. Sin cuentas. Sin teléfono. Sin correo electrónico. Sin identificadores de usuario.", - "send-file": "Enviar archivo" + "send-file": "Enviar archivo", + "links": "Enlaces", + "links-title": "Enlaces de la comunidad", + "links-all-languages": "Todos los idiomas" } diff --git a/website/langs/fa.json b/website/langs/fa.json index 250d1800ca..9fe7656d51 100644 --- a/website/langs/fa.json +++ b/website/langs/fa.json @@ -257,5 +257,9 @@ "simplex-network-overlay-card-1-li-3": "P2P مشکل حملات MITM را حل نمی‌کند و بیشتر پیاده‌سازی‌های موجود از پیام‌های خارج از باند برای تبادل کلید اولیه استفاده نمی‌کنند. SimpleX از پیام‌های خارج از باند یا در برخی موارد، از اتصالات امن و مورد اعتماد پیشین برای تبادل کلید اولیه استفاده می‌کند.", "simplex-network-overlay-card-1-li-4": "پیاده‌سازی‌های P2P می‌توانند توسط برخی از ارائه‌دهندگان اینترنت (مانند BitTorrent) مسدود شوند. SimpleX مستقل از نوع حمل و نقل است — این امکان را دارد که بر روی پروتکل‌های وب استاندارد، مانند WebSockets، کار کند.", "directory": "دایرکتوری", - "about-and-contact-us": "درباره ما و تماس با ما" + "about-and-contact-us": "درباره ما و تماس با ما", + "navbar-token": "توکن", + "docs-dropdown-15": "بررسی و بازتولید ساخت‌ها", + "index-hero-h1": "آزاد
باش", + "index-hero-h2": "در شبکه شما" } diff --git a/website/langs/fr.json b/website/langs/fr.json index 765e3feec8..e71bdc2207 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -16,7 +16,7 @@ "simplex-explained-tab-2-p-1": "Pour chaque connexion, vous utilisez deux files d'attente de messages distinctes pour envoyer et recevoir des messages via des serveurs différents.", "simplex-explained-tab-2-p-2": "Les serveurs ne transmettent les messages que dans une direction, sans connaître la totalité de la conversation ou des connexions de l'utilisateur.", "simplex-explained-tab-3-p-1": "Les serveurs disposent d'identifiants anonymes distincts pour chaque file d'attente, et ne savent pas à quels utilisateurs ils appartiennent.", - "simplex-explained-tab-3-p-2": "Les utilisateurs peuvent améliorer davantage leur protection des métadonnées en utilisant Tor pour accéder aux serveurs, ce qui empêche la corrélation par adresse IP.", + "simplex-explained-tab-3-p-2": "Les utilisateurs peuvent améliorer davantage leur protection de leurs métadonnées en utilisant Tor pour accéder aux serveurs, ce qui empêche la corrélation par adresse IP.", "chat-bot-example": "Exemple de chatbot", "smp-protocol": "Protocole SMP", "chat-protocol": "Protocole de chat", @@ -71,7 +71,7 @@ "simplex-private-card-9-point-1": "Chaque file d'attente de messages transmet les messages dans une direction, avec des adresses d'envoi et de réception différentes.", "simplex-private-card-9-point-2": "Il réduit les vecteurs d'attaque, par rapport aux agents de messagerie traditionnels, et les métadonnées disponibles.", "simplex-private-card-10-point-1": "SimpleX utilise des adresses et des informations d'identification anonymes temporaires par paires pour chaque contact utilisateur ou membre de groupe.", - "simplex-private-card-10-point-2": "Il permet la distribution de messages sans identifiants de profil utilisateur, offrant une meilleure confidentialité des métadonnées que les alternatives.", + "simplex-private-card-10-point-2": "Il permet la distribution des messages sans identifiants de profil utilisateur, offrant une meilleure confidentialité des métadonnées que les alternatives.", "privacy-matters-1-title": "La publicité et la discrimination par les prix", "privacy-matters-1-overlay-1-title": "Protéger votre vie privée peut vous faire économiser de l'argent", "privacy-matters-1-overlay-1-linkText": "Protéger votre vie privée peut vous faire économiser de l'argent", @@ -94,7 +94,7 @@ "hero-overlay-card-1-p-3": "Vous définissez le ou les serveurs que vous souhaitez utiliser pour recevoir les messages et vos contacts — les serveurs que vous utilisez pour leur envoyer des messages. Chaque conversation est susceptible d'utiliser deux serveurs différents.", "hero-overlay-card-1-p-4": "Cette méthode empêche la fuite de métadonnées des utilisateurs au niveau de l'application. Pour améliorer encore la protection de votre vie privée et protéger votre adresse IP, vous pouvez vous connecter aux serveurs de messagerie via Tor.", "hero-overlay-card-1-p-5": "Seuls les appareils clients stockent les profils des utilisateurs, les contacts et les groupes ; les messages sont envoyés avec un chiffrement de bout en bout à deux couches.", - "hero-overlay-card-1-p-6": "En savoir plus sur le SimpleX Whitepaper.", + "hero-overlay-card-1-p-6": "En savoir plus sur le livre blanc de SimpleX.", "hero-overlay-card-2-p-1": "Lorsque les utilisateurs ont des identités persistantes, même s'il ne s'agit que d'un nombre aléatoire, comme un ID de session, il y a un risque que le fournisseur ou un attaquant puisse observer comment les utilisateurs sont connectés et combien de messages ils envoient.", "hero-overlay-card-2-p-2": "Ils pourraient ensuite corréler ces informations avec les réseaux sociaux publics existants, et déterminer de véritables identités.", "hero-overlay-card-2-p-3": "Même avec les applications les plus privées qui utilisent les services Tor v3, si vous parlez à deux contacts différents via le même profil, ils peuvent prouver qu'ils sont connectés à la même personne.", @@ -291,5 +291,33 @@ "index-roadmap-3": "Déc 2027", "index-roadmap-3-title": "Faites grandir vos communautés", "index-roadmap-3-desc": "Outils pour promouvoir vos communautés", - "send-file": "Envoyer un fichier" + "send-file": "Envoyer un fichier", + "index-publications-optout-title": "Entretien avec le podcast OptOut", + "index-messaging-p2": "Des dizaines de millions de messages envoyés en privé chaque jour.", + "index-nextweb-p1": "Chaque contact et chaque groupe reste sur votre appareil, et non sur un serveur.", + "index-token-p1": "Pour rester indépendants, les grands canaux et les communautés paieront leurs propres serveurs.", + "index-token-p2": "Cela couvrira l'infrastructure, le développement logiciel et la gouvernance du réseau.", + "index-directory-h2": "Rejoindre les communautés SimpleX", + "index-directory-p1": "Plus de 2 millions de personnes ont téléchargé les applications SimpleX.", + "index-directory-p2": "Trouvez vos chaînes et communautés dans l'annuaire et créez-en les vôtres !", + "index-directory-cta": "Afficher le répertoire SimpleX", + "index-directory-users-group-title": "Groupe d'utilisateurs SimpleX", + "how-secure-comparison-title": "Comparaison de la sécurité du chiffrement de bout en bout dans différentes applications de messagerie", + "how-secure-repudiation-deniability": "Dénégation (possibilité de nier)", + "how-secure-break-in-recovery": "Sécurité après une compromission", + "how-secure-two-factor-key-exchange": "Échange de clés à deux facteurs", + "how-secure-post-quantum-hybrid-crypto": "Cryptographie hybride post-quantique", + "messengers-comparison-section-list-point-1": "Briar complète les messages à une taille arrondie à 1 024 octets, tandis que Signal les complète à 160 octets", + "messengers-comparison-section-list-point-2": "La répudiation ne concerne pas la connexion client-serveur.", + "messengers-comparison-section-list-point-3": "Il semblerait que l'utilisation de signatures cryptographiques compromette la possibilité de nier l'auteur (déni), mais cela doit être clarifié.", + "messengers-comparison-section-list-point-4": "La mise en œuvre multi-appareils compromet la sécurité post-compromission de Double Ratchet", + "messengers-comparison-section-list-point-5": "L'échange de clés à deux facteurs est facultatif et peut être remplacé par une vérification par code de sécurité.", + "navbar-old-site": "Ancien site", + "why-p1": "Vous êtes né sans compte.", + "why-p2": "Personne ne surveillait vos conversations. Personne ne dressait de carte des endroits où vous étiez allés. La vie privée n’était pas une fonctionnalité, c’était un mode de vie.", + "why-p3": "Puis nous sommes passés au numérique, et chaque plateforme nous demandait de lui livrer une partie de nous-mêmes : notre nom, notre numéro, nos amis. Nous avons accepté que le prix à payer pour communiquer avec les autres soit de révéler à qui nous parlions. À chaque génération, tant sur le plan humain que technologique, cela s'est passé ainsi : le téléphone, les e-mails, les messageries instantanées, les réseaux sociaux. Cela semblait être la seule voie possible.", + "why-p4": "Il existe une autre solution. Un réseau sans numéros de téléphone, sans noms d'utilisateur, sans comptes, sans aucune forme d'identité d'utilisateur. Un réseau qui met les gens en relation et transmet des messages cryptés sans que l'on sache qui est connecté.", + "why-p5": "Ça n’est pas une meilleure serrure sur la porte de quelqu’un d’autre. Ça n’est pas un propriétaire plus aimable qui respecte votre vie privée, mais qui tient tout de même un registre de tous les visiteurs. Vous n’êtes pas un invité, vous êtes chez vous. Aucun roi ne peut y entrer : c’est vous le souverain.", + "docs-dropdown-15": "Vérifier et reproduire les builds", + "why-p6": "Vos conversations vous appartiennent, comme cela a toujours été le cas avant Internet. Le réseau n'est pas un endroit que vous visitez. C'est un espace que vous créez et qui vous appartient. Et personne ne peut vous l'enlever, que vous le rendiez privé ou public." } diff --git a/website/langs/hu.json b/website/langs/hu.json index 67133b8b80..9775e2da57 100644 --- a/website/langs/hu.json +++ b/website/langs/hu.json @@ -20,7 +20,7 @@ "smp-protocol": "SMP-protokoll", "chat-protocol": "Csevegési protokoll", "donate": "Adományozás", - "copyright-label": "© 2020-2025 SimpleX Chat | Nyílt forráskódú projekt", + "copyright-label": "© 2020-2026 SimpleX Chat | Nyílt forráskódú projekt", "simplex-chat-protocol": "SimpleX Chat protokoll", "terminal-cli": "Terminál CLI", "terms-and-privacy-policy": "Adatvédelmi irányelvek", @@ -259,7 +259,7 @@ "directory": "Csoportjegyzék", "about-and-contact-us": "Névjegy és kapcsolat", "index-hero-h1": "
Legyen
szabad
", - "index-hero-p1": "Az első hálózat felhasználói azonosítók nélkül.
Az Ön névjegyei, csoportjai és csatornái az Öné.", + "index-hero-p1": "Az első hálózat felhasználói azonosítók nélkül.
A saját partnerei, csoportjai és csatornái felett Ön rendelkezik.", "index-hero-download-desktop-btn-title": "SimpleX számítógépes alkalmazásának letöltése", "index-security-assessment-title": "Biztonsági auditok", "index-security-review-2022-title": "Biztonsági audit 2022", @@ -275,15 +275,15 @@ "index-publications-optout-title": "OptOut podcast interjú", "worlds-most-secure-messaging": "Senki sem láthatja, kivel beszélget", "index-messaging-p1": "Még a kiszolgálók sem – az összes üzenet véletlenszerű zajnak tűnik.", - "index-messaging-p2": "Naponta több tízmillió üzenetet kézbesítünk bizalmasan.", + "index-messaging-p2": "Naponta több tízmillió üzenet kerül kézbesítésre privát módon.", "index-messaging-cta": "Tudjon meg többet a SimpleX üzenetváltó alkalmazásról", "index-nextweb-h2": "A hálózat
az Öné", - "index-nextweb-p1": "Minden névjegy és csoport az Ön eszközén van, nem egy kiszolgáló adatbázisában.", + "index-nextweb-p1": "Minden partnere és csoportja az Ön eszközén van tárolva, nem pedig egy ismeretlen kiszolgáló adatbázisában.", "index-nextweb-p2": "Egyetlen szervezet sem irányítja a hálózatot – bárki üzemeltethet kiszolgálókat.", - "index-token-h2": "A felhasználói finanszírozzák", + "index-token-h2": "A felhasználók finanszírozzák", "index-token-p1": "A függetlenség megőrzéséhez a nagy csatornák és közösségek fizetni fognak a kiszolgálóikért.", "index-token-p2": "Ez fedezi az infrastruktúrát, a szoftverfejlesztést és a hálózat irányítását.", - "index-token-cta": "Tudjon meg többet a Community Credits-ről", + "index-token-cta": "Tudjon meg többet a közösségi kreditekről", "index-roadmap-h2": "A SimpleX ütemterve a szabad internethez", "index-roadmap-now": "Most", "index-roadmap-1": "2026", @@ -291,13 +291,13 @@ "index-roadmap-1-desc": "Központosított platformok elhagyása", "index-roadmap-2": "2027. jún.", "index-roadmap-2-title": "Fenntartható közösségek és kiszolgálók", - "index-roadmap-2-desc": "Community Credits elindítása", + "index-roadmap-2-desc": "Közösségi kreditek elindítása", "index-roadmap-3": "2027. dec.", "index-roadmap-3-title": "Közösségek növelése", "index-roadmap-3-desc": "Eszközök biztosítása a közösségek népszerűsítéséhez", "index-directory-h2": "Csatlakozzon a SimpleX közösségekhez", - "index-directory-p1": "Több mint 2 millió ember töltötte le a SimpleX alkalmazásokat.", - "index-directory-p2": "Találja meg csatornáit és közösségeit a csoportjegyzékben, vagy hozza létre a sajátját!", + "index-directory-p1": "Több mint 2 millió ember töltötte le a SimpleX alkalmazások egyikét.", + "index-directory-p2": "Találja meg a kedvenc csatornáit és közösségeit a csoportjegyzékben, vagy hozza létre a sajátját!", "index-directory-cta": "SimpleX-csoportjegyzék megtekintése", "index-directory-users-group-title": "SimpleX felhasználók csoportja", "how-secure-comparison-title": "A végpontok közötti titkosítás összehasonlítása más üzenetváltó alkalmazásokkal", @@ -369,5 +369,9 @@ "file-proto-h-4": "Független útválasztók", "file-proto-p-4": "Amikor a fájl töredékekre oszlik, akkor a független felek által üzemeltetett hálózati útválasztókon keresztül kerül továbbításra. Egyetlen üzemeltető sem láthatja a fájl tényleges méretét és nevét. Még ha egy útválasztó biztonsága meg is sérül, csak a rögzített méretű titkosított töredékeket „láthatja”. A fájltöredékeket a hálózati útválasztók körülbelül 48 órán át tárolják a gyorsítótárban.", "file-proto-spec": "Olvassa el az XFTP-protokoll leírását →", - "send-file": "Fájl küldése" + "send-file": "Fájl küldése", + "links": "Hivatkozások", + "links-title": "Közösségi hivatkozások", + "links-all-languages": "Összes nyelv", + "docs-dropdown-16": "Csevegési átjátszó üzemeltetése" } diff --git a/website/langs/id.json b/website/langs/id.json index c766929416..bb80effd21 100644 --- a/website/langs/id.json +++ b/website/langs/id.json @@ -22,7 +22,7 @@ "simplex-private-1-title": "2 lapisan
enkripsi end-to-end", "simplex-private-card-4-point-1": "Untuk melindungi alamat IP Anda, Anda dapat mengakses server melalui Tor atau lapisan jaringan transport lainnya.", "simplex-unique-3-overlay-1-title": "Kepemilikan, kontrol, dan keamanan data Anda", - "simplex-private-card-10-point-2": "Mengirim pesan tanpa pengenal profil pengguna, menyediakan privasi meta-data yang lebih baik daripada alternatif lain.", + "simplex-private-card-10-point-2": "Fitur ini mengirim pesan tanpa identifikasi profil pengguna, memberikan privasi metadata lebih baik daripada alternatif lainnya.", "terminal-cli": "Terminal CLI", "hero-overlay-3-textlink": "Penilaian keamanan", "chat-protocol": "Protokol obrolan", @@ -61,7 +61,7 @@ "simplex-unique-4-title": "Anda memiliki jaringan SimpleX", "simplex-unique-4-overlay-1-title": "Sepenuhnya terdesentralisasi — pengguna memiliki jaringan SimpleX", "hero-overlay-card-1-p-2": "Untuk mengirim pesan, alih-alih ID pengguna yang digunakan oleh semua jaringan lain, SimpleX menggunakan pengenal bersifat anonim sementara dari antrean pesan, terpisah untuk setiap koneksi — tidak ada pengenal jangka panjang.", - "hero-overlay-card-1-p-1": "Banyak pengguna bertanya: jika SimpleX tidak ada ID pengguna, bagaimana itu mengetahui ke mana pesan dikirim?", + "hero-overlay-card-1-p-1": "Banyak pengguna bertanya: jika SimpleX tidak ada ID pengguna, bagaimana mengetahui ke mana pesan dikirim?", "sign-up-to-receive-our-updates": "Daftar untuk menerima pembaruan kami", "enter-your-email-address": "Masukkan alamat email Anda", "learn-more": "Lebih lanjut", @@ -70,11 +70,11 @@ "contact-hero-header": "Anda menerima alamat untuk terhubung di SimpleX Chat", "invitation-hero-header": "Anda menerima tautan 1 kali untuk terhubung di SimpleX Chat", "simplex-explained-tab-1-p-1": "Anda dapat membuat kontak dan grup, dan melakukan percakapan dua arah, seperti pada aplikasi perpesanan lainnya.", - "simplex-explained-tab-1-p-2": "Bagaimana cara kerjanya dengan antrean searah dan tanpa ID profil pengguna?", + "simplex-explained-tab-1-p-2": "Bagaimana cara kerjanya dengan antrean searah dan tanpa pengenal profil pengguna?", "simplex-explained-tab-2-p-1": "Untuk setiap koneksi, Anda menggunakan dua antrean pesan terpisah untuk mengirim dan menerima pesan melalui server yang berbeda.", "simplex-explained-tab-2-p-2": "Server hanya menyampaikan pesan satu arah, tanpa memiliki gambaran lengkap tentang percakapan atau koneksi pengguna.", "simplex-explained-tab-3-p-1": "Server memiliki kredensial anonim terpisah untuk setiap antrean, dan tidak mengetahui pengguna mana yang menjadi milik mereka.", - "simplex-explained-tab-3-p-2": "Pengguna dapat tingkatkan privasi metadata dengan memakai Tor untuk akses server, mencegah korelasi berdasarkan alamat IP.", + "simplex-explained-tab-3-p-2": "Pengguna dapat tingkatkan privasi metadata dengan Tor untuk akses server, mencegah korelasi berdasarkan alamat IP.", "chat-bot-example": "Contoh chat bot", "hero-header": "Privasi diredefinisikan", "hero-subheader": "Perpesanan pertama
tanpa ID pengguna", @@ -155,7 +155,7 @@ "join-the-REDDIT-community": "Bergabung dengan komunitas REDDIT", "join-us-on-GitHub": "Gabung dengan kami di GitHub", "donate-here-to-help-us": "Donasi untuk bantu kami", - "why-simplex-is-unique": "Mengapa SimpleX unik", + "why-simplex-is-unique": "Mengapa SimpleX unik", "simplex-unique-card-1-p-2": "Tidak seperti jaringan perpesanan lain yang ada, SimpleX tidak memiliki ID tetap kepada pengguna — bahkan nomor acak.", "simplex-unique-card-2-p-1": "Karena Anda tidak memiliki ID atau alamat tetap di jaringan SimpleX, tidak seorang pun dapat menghubungi Anda kecuali Anda membagikan alamat pengguna 1-kali atau sementara, seperti kode QR atau tautan.", "simplex-unique-card-3-p-1": "SimpleX menyimpan semua data pengguna pada perangkat klien dalam format basis data terenkripsi portabel — data tersebut dapat ditransfer ke perangkat lain.", @@ -171,7 +171,7 @@ "install-simplex-app": "Instal aplikasi SimpleX", "connect-in-app": "Hubungkan di aplikasi", "open-simplex-app": "Buka aplikasi SimpleX", - "tap-the-connect-button-in-the-app": "Ketuk ‘hubungkan’ di aplikasi", + "tap-the-connect-button-in-the-app": "Ketuk tombol \"connect\" di aplikasi", "scan-the-qr-code-with-the-simplex-chat-app": "Pindai kode QR dengan aplikasi SimpleX Chat", "scan-the-qr-code-with-the-simplex-chat-app-description": "Kunci publik dan alamat antrean pesan dalam tautan ini TIDAK dikirim melalui jaringan saat Anda melihat halaman ini —
keduanya terdapat dalam fragmen hash URL tautan.", "installing-simplex-chat-to-terminal": "Menginstal SimpleX Chat ke terminal", @@ -181,19 +181,19 @@ "the-instructions--source-code": "untuk petunjuk cara mengunduh atau mengompilasinya dari kode sumber.", "if-you-already-installed-simplex-chat-for-the-terminal": "Jika Anda sudah menginstal SimpleX Chat untuk terminal", "if-you-already-installed": "Jika Anda sudah menginstal", - "privacy-matters-section-header": "Mengapa privasi penting", - "privacy-matters-section-subheader": "Menjaga privasi metadata Anda — dengan siapa Anda berbicara — melindungi Anda dari:", + "privacy-matters-section-header": "Mengapa privasi penting", + "privacy-matters-section-subheader": "Menjaga privasi metadata Anda — dengan siapa Anda berbicara — melindungi Anda dari:", "privacy-matters-section-label": "Pastikan messenger Anda tidak dapat mengakses data Anda!", - "simplex-private-section-header": "Apa yang membuat SimpleX privat", + "simplex-private-section-header": "Apa yang membuat SimpleX privat", "tap-to-close": "Ketuk untuk tutup", - "simplex-network-section-header": "Jaringan SimpleX", - "simplex-network-section-desc": "SimpleX Chat memberikan privasi terbaik dengan menggabungkan keunggulan P2P dan jaringan terfederasi.", + "simplex-network-section-header": "Jaringan SimpleX", + "simplex-network-section-desc": "SimpleX Chat memberikan privasi terbaik dengan menggabungkan keunggulan jaringan P2P dan federasi.", "simplex-network-1-header": "Tidak seperti jaringan P2P", "simplex-network-1-desc": "Semua pesan dikirim melalui server, keduanya memberikan privasi metadata yang lebih baik dan pengiriman pesan asinkron yang andal, sekaligus menghindari banyak", "simplex-network-1-overlay-linktext": "masalah jaringan P2P", "simplex-network-2-header": "Tidak seperti jaringan terfederasi", "simplex-network-2-desc": "Server relay SimpleX TIDAK menyimpan profil pengguna, kontak dan pesan yang terkirim, TIDAK terhubung satu sama lain, dan TIDAK ada direktori server.", - "simplex-network-3-desc": "server menyediakan antrian searah untuk hubungkan pengguna, tetapi mereka tidak dapat melihat grafik koneksi jaringan — hanya pengguna yang dapat melihatnya.", + "simplex-network-3-desc": "server menyediakan antrean satu arah untuk menghubungkan pengguna, tetapi mereka tidak memiliki visibilitas terhadap grafik koneksi jaringan — hanya pengguna yang memilikinya.", "comparison-section-header": "Perbandingan dengan protokol lain", "comparison-point-3-text": "Ketergantungan pada DNS", "comparison-point-4-text": "Jaringan tunggal atau terpusat", @@ -201,7 +201,7 @@ "yes": "Ya", "see-here": "lihat disini", "comparison-section-list-point-4a": "Relay SimpleX tidak dapat membahayakan enkripsi e2e. Verifikasi kode keamanan untuk memitigasi serangan pada saluran out-of-band", - "comparison-section-list-point-4": "Jika server operator disusupi. Verifikasi kode keamanan di Signal dan beberapa aplikasi lain untuk mengatasinya", + "comparison-section-list-point-4": "Jika server operator dikompromikan. Verifikasi kode keamanan di Signal dan beberapa aplikasi lain untuk memitigasinya", "comparison-section-list-point-5": "Tidak melindungi privasi metadata pengguna", "comparison-section-list-point-6": "Meskipun P2P didistribusikan, mereka tidak terfederasi — mereka beroperasi sebagai jaringan tunggal", "comparison-section-list-point-7": "Jaringan P2P memiliki otoritas pusat atau seluruh jaringan dapat terkompromi", @@ -216,12 +216,12 @@ "simplex-chat-via-f-droid": "SimpleX Chat melalui F-Droid", "simplex-chat-repo": "Repo SimpleX Chat", "stable-and-beta-versions-built-by-developers": "Versi stable dan beta yang dibuat oleh pengembang", - "f-droid-page-simplex-chat-repo-section-text": "Untuk menambahkannya ke klien F-Droid Anda, pindai kode QR atau gunakan URL ini:", + "f-droid-page-simplex-chat-repo-section-text": "Untuk menambahkannya ke klien F-Droid Anda, pindai kode QR atau gunakan URL ini:", "signing-key-fingerprint": "Penandatanganan sidikjari kunci (SHA-256)", "f-droid-org-repo": "Repo F-Droid.org", "stable-versions-built-by-f-droid-org": "Versi stable yang dibuat oleh F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "Rilisan ke repo ini dilakukan beberapa hari kemudian", - "f-droid-page-f-droid-org-repo-section-text": "Repositori SimpleX Chat dan F-Droid.org menandatangani build dengan kunci berbeda. Untuk beralih, silakan ekspor basis data obrolan dan instal ulang aplikasi.", + "f-droid-page-f-droid-org-repo-section-text": "Repositori SimpleX Chat dan F-Droid.org menandatangani build dengan kunci yang berbeda. Untuk beralih, silakan ekspor basis data chat dan pasang ulang aplikasi.", "hero-overlay-card-2-p-4": "SimpleX melindungi dari serangan ini dengan tidak memiliki ID pengguna dalam desainnya. Dan, jika Anda gunakan mode Samaran, Anda akan memiliki nama tampilan berbeda untuk setiap kontak, sehingga mencegah data dibagikan di antara mereka.", "hero-overlay-card-3-p-1": "Trail of Bits adalah konsultan keamanan dan teknologi terkemuka yang kliennya meliputi perusahaan teknologi besar, lembaga pemerintah, dan proyek blockchain besar.", "hero-overlay-card-3-p-2": "Trail of Bits meninjau kriptografi jaringan SimpleX dan komponen jaringan pada November 2022. Baca selengkapnya.", @@ -263,16 +263,16 @@ "index-hero-p1": "Jaringan pertama tanpa ID pengguna.
Anda pemilik kontak, grup, dan kanal Anda.", "index-hero-download-desktop-btn-title": "Unduh Aplikasi Desktop SimpleX", "index-testflight-title": "Pratinjau iOS publik di TestFlight", - "index-f-droid-title": "Repositori SimpleX F-Droid", - "index-security-assessment-title": "penilaian keamanan", - "index-security-review-2022-title": "Tinjauan Keamanan 2022", - "index-security-review-2024-title": "Tinjauan Keamanan 2024", + "index-f-droid-title": "Aplikasi SimpleX via F-Droid", + "index-security-assessment-title": "Audit Keamanan", + "index-security-review-2022-title": "Audit Keamanan 2022", + "index-security-review-2024-title": "Audit Keamanan 2024", "index-security-audits-label": "Audit
Keamanan", - "index-publications-privacy-guides-title": "rekomendasi perpesanan", + "index-publications-privacy-guides-title": "Rekomendasi messenger Privacy Guides", "index-publications-whonix-title": "Rekomendasi perpesanan Whonix", - "index-publications-heise-title": "publikasi", - "index-publications-kuketz-title": "tinjauan", - "index-publications-optout-title": "wawancara podcast", + "index-publications-heise-title": "Publikasi Heise Online", + "index-publications-kuketz-title": "Ulasan oleh Mike Kuketz", + "index-publications-optout-title": "Wawancara podcast OptOut", "worlds-most-secure-messaging": "Tidak Ada yang Bisa Melihat dengan Siapa Anda Bicara", "index-messaging-p1": "Bahkan server pun tidak bisa – semua pesan terlihat seperti derau acak.", "index-messaging-p2": "Puluhan juta pesan dikirim secara privat setiap hari.", @@ -300,7 +300,7 @@ "index-directory-p2": "Temukan kanal dan komunitas Anda di direktori dan buat milik Anda sendiri!", "index-directory-cta": "Lihat Direktori SimpleX", "index-directory-users-group-title": "Grup pengguna SimpleX", - "how-secure-comparison-title": "Seberapa amankah enkripsi end-to-end di berbagai aplikasi perpesanan?", + "how-secure-comparison-title": "Perbandingan keamanan enkripsi end-to-end di berbagai messenger", "how-secure-message-padding": "Lapisan pesan", "how-secure-repudiation-deniability": "Penolakan (penyangkalan)", "how-secure-forward-secrecy": "Forward secrecy", @@ -315,5 +315,61 @@ "messengers-comparison-section-list-point-6": "Kesepakatan kunci Post-quantum \"jarang\" — hanya melindungi beberapa langkah ratchet.", "navbar-token": "Token", "navbar-old-site": "Situs lama", - "send-file": "Kirim file" + "send-file": "Kirim file", + "docs-dropdown-15": "Verifikasi & reproduksi build", + "why-p1": "Anda lahir tanpa akun.", + "why-p2": "Tidak ada yang melacak percakapan Anda. Tidak ada yang membuat peta ke mana Anda pernah pergi. Privasi tidak pernah menjadi fitur — itu adalah cara hidup.", + "why-p3": "Lalu kita berpindah ke dunia online, dan setiap platform meminta sebagian dari diri Anda — nama, nomor, teman-teman Anda. Kita menerima bahwa harga untuk berbicara dengan orang lain adalah membiarkan seseorang tahu dengan siapa kita berbicara. Dari generasi ke generasi, manusia dan teknologi selalu seperti ini — telepon, email, messenger, media sosial. Tampaknya itu satu-satunya cara yang mungkin.", + "why-p4": "Ada cara lain. Sebuah jaringan tanpa nomor telepon. Tanpa nama pengguna. Tanpa akun. Tanpa identitas pengguna dalam bentuk apa pun. Sebuah jaringan yang menghubungkan orang dan membawa pesan terenkripsi tanpa mengetahui siapa yang terhubung.", + "why-p5": "Bukan kunci yang lebih baik di pintu milik orang lain. Bukan pemilik properti yang lebih baik yang menghormati privasi Anda, tetapi tetap menyimpan catatan semua pengunjung. Anda bukan tamu. Anda berada di rumah. Tidak ada raja yang bisa memasukinya — Anda berdaulat.", + "why-p6": "Percakapan Anda adalah milik Anda, seperti yang selalu terjadi sebelum Internet. Jaringan bukanlah tempat yang Anda kunjungi. Jaringan adalah tempat yang Anda ciptakan dan miliki. Dan tidak seorang pun dapat merampasnya dari Anda, entah Anda menjadikannya privat atau publik.", + "why-p7": "Kebebasan manusia yang paling tua — berbicara dengan orang lain tanpa diawasi — dibangun di atas infrastruktur yang tidak dapat mengkhianatinya.", + "why-p8": "Karena kami menghancurkan kekuatan untuk mengetahui siapa Anda. Agar kekuatan Anda tidak pernah bisa dirampas.", + "why-tagline": "Bebaslah di jaringan Anda.", + "why-footer-link": "Mengapa kami membangunnya", + "file-desc": "Kirim file dengan aman menggunakan enkripsi end-to-end — tanpa akun, tanpa pelacakan.", + "file-noscript": "JavaScript diperlukan untuk transfer file.", + "file-e2e-note": "Terenkripsi end-to-end — server tidak pernah melihat file Anda.", + "file-learn-more": "Pelajari lebih lanjut tentang protokol XFTP", + "file-cta-heading": "Dapatkan SimpleX Chat — messenger paling aman & privat", + "file-cta-subheading": "Transfer file yang baru saja Anda gunakan memakai protokol perutean data yang sama dengan SimpleX Chat. Aplikasi ini memiliki pesan terenkripsi end-to-end, panggilan suara dan video, grup, serta pengiriman file. Tanpa akun. Tanpa telepon. Tanpa email. Tanpa ID profil pengguna.", + "file-title": "Transfer File SimpleX", + "file-drop-text": "Seret & lepas file di sini", + "file-drop-hint": "atau", + "file-choose": "Pilih file", + "file-max-size": "Maks. 100 MB - aplikasi SimpleX Chat mendukung file hingga 1 GB", + "file-encrypting": "Mengenkripsi…", + "file-uploading": "Mengunggah…", + "file-cancel": "Batal", + "file-uploaded": "File terunggah", + "file-copy": "Salin", + "file-copied": "Tersalin!", + "file-share": "Bagikan", + "file-expiry": "File biasanya tersedia selama 48 jam.", + "file-sec-1": "File Anda dienkripsi di browser - router data tidak pernah melihat isi, nama, atau ukuran file.", + "file-sec-2": "Kunci enkripsi ada di fragmen hash tautan - tidak pernah dikirim ke server mana pun.", + "file-sec-3": "Untuk keamanan yang lebih baik, gunakan aplikasi SimpleX Chat.", + "file-retry": "Coba lagi", + "file-downloading": "Mengunduh…", + "file-decrypting": "Mendekripsi…", + "file-download-complete": "Unduhan selesai", + "file-download-btn": "Unduh", + "file-too-large": "File terlalu besar (%size%). Maksimum adalah 100 MB. Aplikasi SimpleX mendukung file hingga 1 GB.", + "file-empty": "File kosong.", + "file-invalid-link": "Tautan tidak valid atau rusak.", + "file-init-error": "Gagal menginisialisasi: %error%", + "file-available": "File tersedia (~%size%)", + "file-dl-sec-1": "File ini dienkripsi - router data tidak pernah melihat isi, nama, atau ukuran file.", + "file-workers-required": "Web Workers diperlukan — perbarui browser Anda", + "file-protocol-title": "Protokol XFTP: transfer file paling aman", + "file-proto-h-1": "Tidak memerlukan akun", + "file-proto-p-1": "Setiap fragmen file menggunakan kunci acak baru. Router data tidak memiliki \"pengguna\" atau \"file\" - mereka mentransfer fragmen file terenkripsi dengan ukuran tetap.", + "file-proto-h-2": "Dienkripsi tiga lapis di browser Anda", + "file-proto-p-2": "Kunci enkripsi file hanya ada di fragmen hash URL - browser Anda tidak pernah mengirimkannya ke server. Ada 3 lapisan enkripsi: transport TLS, enkripsi per penerima (kunci ephemeral unik untuk setiap transfer), dan enkripsi file end-to-end.", + "file-proto-h-4": "Router data independen", + "file-proto-p-4": "Saat file dipecah menjadi fragmen, file tersebut dikirim melalui router jaringan yang dioperasikan oleh pihak independen. Tidak ada operator yang dapat melihat ukuran atau nama file yang sebenarnya. Bahkan jika sebuah router dikompromikan, router itu hanya dapat melihat fragmen terenkripsi berukuran tetap. Fragmen file di-cache oleh router jaringan selama sekitar 48 jam.", + "file-proto-spec": "Baca spesifikasi protokol XFTP →", + "links": "Tautan", + "links-title": "Tautan Komunitas", + "links-all-languages": "Semua Bahasa" } diff --git a/website/langs/it.json b/website/langs/it.json index a3ced52c8e..8970706295 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -274,15 +274,15 @@ "index-publications-kuketz-title": "Recensione di Mike Kuketz", "index-publications-optout-title": "Intervista podcast di OptOut", "worlds-most-secure-messaging": "Nessuno può vedere con chi parli", - "index-messaging-p1": "Nemmeno i server – tutti i messaggi appaiono come rumore casuale.", - "index-messaging-p2": "Decine di milioni di messaggi recapitati privatamente ogni giorno.", + "index-messaging-p1": "Nemmeno i server: tutti i messaggi appaiono come rumore casuale.", + "index-messaging-p2": "Decine di milioni di messaggi recapitati in modo privato ogni giorno.", "index-messaging-cta": "Scopri di più sui messaggi di SimpleX", "index-nextweb-h2": "La rete
è tua", "index-nextweb-p1": "Ogni contatto e gruppo è sul tuo dispositivo, non nel database di un server.", - "index-nextweb-p2": "Nessuna singola entità controlla la rete – chiunque può gestire server.", + "index-nextweb-p2": "Non c'è una singola entità che controlla la rete: chiunque può gestire i server.", "index-token-h2": "Finanziato dai suoi utenti", "index-token-p1": "Per restare indipendenti, i grandi canali e le comunità pagheranno per i propri server.", - "index-token-p2": "Questo coprirà infrastruttura, sviluppo software e governance della rete.", + "index-token-p2": "Ciò coprirà infrastruttura, sviluppo software e gestione della rete.", "index-token-cta": "Scopri di più sui Crediti Comunitari", "index-roadmap-h2": "Tabella di marcia per un internet libero", "index-roadmap-now": "Ora", @@ -369,5 +369,9 @@ "file-proto-h-4": "Instradatori indipendenti di dati", "file-proto-p-4": "Quando il file è diviso in frammenti, viene inviato tramite instradatori di rete operati da parti indipendenti. Nessun operatore può vedere la vera dimensione o il nome del file. Anche se un instradatore venisse compromesso, potrà vedere solo frammenti cifrati di dimensione fissa. I frammenti di file restano in cache dagli instradatori di rete per circa 48 ore.", "file-proto-spec": "Leggi le specifiche del protocollo XFTP →", - "send-file": "Invia file" + "send-file": "Invia file", + "links": "Collegamenti", + "links-title": "Link della comunità", + "links-all-languages": "Tutte le lingue", + "docs-dropdown-16": "Ospita un relay di chat" } diff --git a/website/langs/ja.json b/website/langs/ja.json index 8d8baee0c5..08807fcbb0 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -93,7 +93,7 @@ "docs-dropdown-1": "SimpleXネットワーク", "hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。", "simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット", - "simplex-network-overlay-card-1-li-3": "P2P は MITM 攻撃 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", + "simplex-network-overlay-card-1-li-3": "P2P は MITM 攻撃 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", "the-instructions--source-code": "ソースコードからダウンロードまたはコンパイルする方法を説明します。", "simplex-network-section-desc": "SimpleX Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。", "privacy-matters-section-subheader": "メタデータのプライバシーを保護する — 話す相手 — 以下のことからあなたを守ります:", @@ -127,7 +127,7 @@ "comparison-point-1-text": "グローバル ID が必要", "comparison-section-list-point-5": "ユーザーのメタデータのプライバシーを保護しない", "hero-overlay-card-2-p-2": "その後、この情報を既存の公開ソーシャル ネットワークと関連付けて、本当の身元を特定することができます。", - "privacy-matters-overlay-card-1-p-3": "一部の金融会社や保険会社は、ソーシャル グラフを使用して金利や保険料を決定しています。 多くの場合、収入の低い人にはより多くの料金を支払わなければなりません。 —これは、「貧困プレミアム」 として知られています。", + "privacy-matters-overlay-card-1-p-3": "一部の金融会社や保険会社は、ソーシャル グラフを使用して金利や保険料を決定しています。 多くの場合、収入の低い人にはより多くの料金を支払わなければなりません。 —これは、\"「貧困プレミアム」\" として知られています。", "comparison-point-3-text": "DNS への依存", "yes": "はい", "docs-dropdown-6": "WebRTC サーバー", @@ -150,7 +150,7 @@ "privacy-matters-2-overlay-1-title": "プライバシーはあなたに力を与えます", "simplex-unique-overlay-card-2-p-2": "オプションのユーザー アドレスを使用しても、スパムの連絡先リクエストの送信に使用される可能性がありますが、接続を失うことなく変更または完全に削除できます。", "simplex-unique-4-overlay-1-title": "完全に分散化されています — ユーザーは SimpleX ネットワークを所有します", - "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、Sybil 攻撃に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な作業証明が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", + "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、Sybil 攻撃に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な作業証明が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", "simplex-private-2-title": "追加レイヤーの
サーバー暗号化", "hero-overlay-card-1-p-4": "この設計により、ユーザーの情報の漏洩が防止されます' アプリケーションレベルのメタデータ。 プライバシーをさらに向上させ、IP アドレスを保護するために、Tor 経由でメッセージング サーバーに接続できます。", "f-droid-org-repo": "F-Droid.org リポジトリ", @@ -182,7 +182,7 @@ "simplex-private-card-2-point-1": "TLSが侵害された場合、受信したサーバー・トラフィックと送信したサーバー・トラフィックの相関を防ぐため、受信者に配信するサーバー暗号化レイヤーを追加します。", "f-droid-page-simplex-chat-repo-section-text": "F-Droid クライアントに追加するには、QR コードをスキャンするか、次の URL を使用してください:", "join-the-REDDIT-community": "REDDITコミュニティに参加する", - "simplex-private-card-10-point-2": "ユーザー プロファイル識別子なしでメッセージを配信できるため、他の方法よりも優れたメタデータ プライバシーが提供されます。", + "simplex-private-card-10-point-2": "ユーザー プロファイル識別子なしでメッセージが配信されるため、他の方法よりも優れたメタデータ プライバシーが提供されます。", "privacy-matters-2-title": "選挙操作", "simplex-private-card-5-point-2": "これにより、異なるサイズのメッセージがサーバーやネットワーク オブザーバーには同じように見えます。", "hero-overlay-card-1-p-1": "多くのユーザーは、SimpleX にユーザー識別子がない場合、メッセージの配信先をどのようにして知ることができるのでしょうか? と質問しました", @@ -204,7 +204,7 @@ "simplex-unique-1-title": "プライバシーが完全に守られます", "protocol-2-text": "XMPP、Matrix", "guide": "ガイド", - "simplex-network-overlay-card-1-li-4": "P2P の実装は、一部のインターネット プロバイダー (BitTorrent など) によってブロックされる場合があります。 SimpleX はトランスポートに依存しません — WebSocketのような標準的な Web プロトコル上で動作します。", + "simplex-network-overlay-card-1-li-4": "P2P の実装は、一部のインターネット プロバイダー (BitTorrent など) によってブロックされる場合があります。 SimpleX はトランスポートに依存しません — WebSocketのような標準的な Web プロトコル上で動作します。", "hero-overlay-2-title": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", "docs-dropdown-4": "ホストSMPサーバー", "feature-4-title": "E2E暗号化された音声メッセージ", @@ -218,7 +218,7 @@ "more-info": "詳細情報", "no-decentralized": "いいえ - 分散型", "protocol-1-text": "Signal、大きなプラットフォーム", - "simplex-network-overlay-card-1-li-6": "P2P ネットワークは、DRDoS 攻撃に対して脆弱になる可能性があります。 クライアントがトラフィックを再ブロードキャストして増幅する可能性があり、その結果、ネットワーク全体のサービス拒否が発生する可能性があります。 SimpleX クライアントは既知の接続からのトラフィックのみを中継するため、攻撃者がネットワーク全体のトラフィックを増幅するために使用することはできません。", + "simplex-network-overlay-card-1-li-6": "P2P ネットワークは、DRDoS 攻撃に対して脆弱になる可能性があります。 クライアントがトラフィックを再ブロードキャストして増幅する可能性があり、その結果、ネットワーク全体のサービス拒否が発生する可能性があります。 SimpleX クライアントは既知の接続からのトラフィックのみを中継するため、攻撃者がネットワーク全体のトラフィックを増幅するために使用することはできません。", "if-you-already-installed-simplex-chat-for-the-terminal": "すでにターミナルに SimpleX Chat をインストールしている場合", "docs-dropdown-8": "SimpleX ディレクトリ", "simplex-private-card-1-point-1": "ダブルラチェットプロトコル —
完全な前方秘匿性と侵入回復機能を備えたOTRメッセージング。", @@ -232,16 +232,16 @@ "simplex-unique-overlay-card-1-p-3": "この設計により、通信相手のプライバシーが保護され、SimpleX ネットワークサーバや監視者からプライバシーが隠されます。 IP アドレスをサーバから隠すには、Tor 経由で SimpleX サーバーに接続します。", "simplex-private-7-title": "メッセージの整合性
検証", "privacy-matters-overlay-card-1-p-4": "SimpleXネットワークは、他のどのプラットフォームよりも接続のプライバシーを保護し、ソーシャル グラフが企業や組織に利用されることを完全に防ぎます。 SimpleX Chatアプリに予め設定されたサーバを利用している場合でも、サーバオペレータはユーザーの数や接続数を知ることはできません。", - "hero-overlay-card-1-p-6": "詳細については、SimpleX ホワイトペーパーをご覧ください。", + "hero-overlay-card-1-p-6": "詳細については、SimpleX ホワイトペーパーをご覧ください。", "simplex-network-overlay-card-1-p-1": "P2P メッセージング プロトコルとアプリには、SimpleX よりも信頼性が低く、分析がより複雑になるさまざまな問題があり、また、いくつかの種類の攻撃に対して脆弱です。", - "simplex-network-overlay-card-1-li-1": "P2P ネットワークは、メッセージをルーティングするために DHT の一部の変種に依存します。 DHT の設計では、配信保証と遅延のバランスを取る必要があります。 SimpleX は、受信者が選択したサーバーを使用して、メッセージを複数のサーバーを介して並行して冗長的に渡すことができるため、P2P よりも優れた配信保証と低い遅延の両方を備えています。 P2P ネットワークでは、メッセージはアルゴリズムによって選択されたノードを使用して、O(log N) 個のノードを順番に通過します。", + "simplex-network-overlay-card-1-li-1": "P2P ネットワークは、メッセージをルーティングするために DHT の一部の変種に依存します。 DHT の設計では、配信保証と遅延のバランスを取る必要があります。 SimpleX は、受信者が選択したサーバーを使用して、メッセージを複数のサーバーを介して並行して冗長的に渡すことができるため、P2P よりも優れた配信保証と低い遅延の両方を備えています。 P2P ネットワークでは、メッセージはアルゴリズムによって選択されたノードを使用して、O(log N) 個のノードを順番に通過します。", "privacy-matters-section-label": "メッセージアプリがあなたのデータにアクセスできないようにしてください!", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat は、サポートされているデバイスにエクスポートして転送できるポータブル暗号化データベース形式を使用して、すべてのユーザー データをクライアント デバイスにのみ保存します。", - "simplex-network-3-desc": "サーバーはユーザーを接続するための一方向キューを提供しますが、ネットワーク接続グラフは表示されません— ユーザーだけがそうします。", + "simplex-network-3-desc": "サーバーはユーザーを接続するための一方向キューを提供しますが、ネットワーク接続グラフは表示されません— ユーザーだけがそうします。", "simplex-private-card-3-point-1": "クライアント/サーバー接続には、強力なアルゴリズムを備えた TLS 1.2/1.3 のみが使用されます。", "hero-overlay-card-1-p-3": "メッセージの受信に使用するサーバー、連絡先を定義します —メッセージを送信するために使用するサーバー。 すべての会話では 2 つの異なるサーバーが使用される可能性があります。", "simplex-unique-overlay-card-1-p-1": "他のメッセージングネットワークとは異なり、SimpleX にはユーザーに割り当てられる識別子がありません。 ユーザーを識別するために、電話番号、ドメインベースのアドレス (電子メールや XMPP など)、ユーザー名、公開キー、さらには乱数にも依存しません。 — サーバオペレータはどれだけの人が利用しているかも知ることはありません。", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat と F-Droid.org リポジトリは、異なるキーを使用してビルドに署名します。 切り替えるには、チャット データベースをエクスポートし、アプリを再インストールしてください。", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat と F-Droid.org リポジトリは、異なるキーを使用してビルドに署名します。 切り替えるには、チャット データベースをエクスポートし、アプリを再インストールしてください。", "simplex-private-5-title": "何レイヤーもの
コンテンツパディング", "hero-overlay-card-3-p-1": "Trail of Bitsは、大手ハイテク企業、政府機関、主要なブロックチェーン・プロジェクトなどを顧客に持つ、セキュリティとテクノロジーの大手コンサルタント会社です。", "jobs": "チームに参加する", @@ -302,5 +302,8 @@ "navbar-token": "トークン", "docs-dropdown-15": "認証と再ビルド", "index-f-droid-title": "F-Droid経由のSimpleXアプリ", - "how-secure-forward-secrecy": "前方秘匿性" + "how-secure-forward-secrecy": "前方秘匿性", + "how-secure-two-factor-key-exchange": "2ファクタ鍵交換", + "how-secure-post-quantum-hybrid-crypto": "ポスト量子ハイブリッド暗号", + "messengers-comparison-section-list-point-2": "否認可能性の対象には、クライアントとサーバー間の接続は含まれません。" } diff --git a/website/langs/ru.json b/website/langs/ru.json index ab968446af..fe8aee606e 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -44,7 +44,7 @@ "guide-dropdown-9": "Установление соединений", "simplex-unique-1-overlay-1-title": "Полная конфиденциальность Вашей личности, профиля, контактов и метаданных", "hero-overlay-card-2-p-4": "SimpleX защищает от этих атак, поскольку он не использует никакие идентификаторы профилей пользователей. И, если Вы используете режим инкогнито, у Вас будет другое отображаемое имя для каждого контакта, что позволит избежать какого-либо пересечения между ними.", - "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, необходимо контролировать свое информационное пространство. Это возможно только, если Вы используете конфиденциальную коммуникационную сеть, которая не имеет доступа к контактам Вашей социальной сети.", + "privacy-matters-overlay-card-2-p-2": "Чтобы быть объективным и принимать независимые решения, необходимо контролировать своё информационное пространство. Это возможно только, если Вы используете конфиденциальную коммуникационную сеть, которая не имеет доступа к контактам Вашей социальной сети.", "hero-overlay-card-2-p-1": "Когда у пользователя есть постоянный идентификатор, даже если это просто случайное число, например Session ID, существует риск того, что провайдер или злоумышленник могут наблюдать за тем, как пользователи соединены и сколько сообщений они отправляют.", "feature-3-title": "Децентрализованные группы — известные только участникам", "glossary": "Глоссарий", @@ -60,7 +60,7 @@ "simplex-network-section-desc": "SimpleX Chat обеспечивает наилучшую конфиденциальность, сочетая преимущества P2P и федеративных сетей.", "privacy-matters-section-subheader": "Сохранение конфиденциальности Ваших метаданных — с кем Вы общаетесь — защищает Вас от:", "if-you-already-installed": "Если Вы уже установили", - "simplex-explained-tab-3-p-2": "Пользователи могут повысить свою конфиденциальность используя сеть Tor для доступа к серверам.", + "simplex-explained-tab-3-p-2": "Пользователи могут дополнительно повысить конфиденциальность метаданных, используя Tor для подключения к серверам. Это предотвращает сопоставление действий по IP-адресу.", "join": "Присоединяйтесь к", "privacy-matters-section-header": "Почему конфиденциальность важна", "hero-overlay-1-textlink": "Почему идентификаторы пользователя уменьшают конфиденциальность?", @@ -184,7 +184,7 @@ "simplex-unique-overlay-card-3-p-2": "Сквозные зашифрованные сообщения временно хранятся на серверах SimpleX до получения, после чего они удаляются безвозвратно.", "blog": "Блог", "simplex-private-card-7-point-1": "Для обеспечения неизменности, сообщения нумеруются по порядку и содержат хеш предыдущего сообщения.", - "simplex-unique-overlay-card-4-p-2": "Сеть SimpleX использует открытый протокол и предоставляет SDK для создания чат-ботов, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем сервисы SimpleX, которые Вы создадите.", + "simplex-unique-overlay-card-4-p-2": "Сеть SimpleX использует открытый протокол и предоставляет SDK для создания чат-ботов, позволяя внедрять сервисы, с которыми пользователи могут взаимодействовать через приложение SimpleX Chat — мы с нетерпением ждем сервисы SimpleX, которые Вы создадите.", "simplex-explained-tab-1-p-1": "Вы можете создавать контакты и группы, а также вести двусторонние беседы, как и в любом другом мессенджере.", "contact-hero-p-2": "Еще не скачали SimpleX Chat?", "why-simplex-is-unique": "Почему SimpleX уникален", @@ -216,7 +216,7 @@ "no-decentralized": "Нет - децентрализованный", "protocol-1-text": "Signal, большие платформы", "hero-2-header-desc": "В видео показано, как подключиться к Вашему другу через одноразовый QR-код, при встрече или во время видеосвязи. Вы также можете соединится, поделившись ссылкой-приглашением.", - "simplex-network-overlay-card-1-li-6": "Сети P2P могут быть уязвимы для DRDoS атаки, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известного соединения и не могут быть использованы злоумышленником для создания трафика во всей сети.", + "simplex-network-overlay-card-1-li-6": "P2P сети могут быть уязвимы для DRDoS атаки, когда клиенты могут ретранслировать и увеличивать трафик, что приводит к отказу всей сети. Клиенты SimpleX ретранслируют трафик только из известных соединений и не могут быть использованы злоумышленником для усиления трафика во всей сети.", "if-you-already-installed-simplex-chat-for-the-terminal": "Если Вы уже установили SimpleX Chat для терминала", "docs-dropdown-8": "Каталог SimpleX", "simplex-private-card-1-point-1": "Протокол двойного обновления ключей —
\"отрицаемые\" сообщения с идеальной прямой секретностью и восстановлением после взлома.", @@ -232,7 +232,7 @@ "simplex-unique-overlay-card-1-p-3": "Этот дизайн защищает конфиденциальность Ваших контактов, скрывая их от серверов SimpleX и от любых внешних наблюдателей. Чтобы скрыть свой IP-адрес от серверов, Вы можете подключиться к серверам SimpleX через сеть Tor.", "developers": "Разработчики", "simplex-private-7-title": "Проверка неизменности
сообщений", - "privacy-matters-overlay-card-1-p-4": "Сеть SimpleX защищает конфиденциальность Ваших контактов лучше, чем альтернативы, предотвращая доступ к Вашей социальной сети каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", + "privacy-matters-overlay-card-1-p-4": "Сеть SimpleX обеспечивает более высокий уровень конфиденциальности ваших связей, чем любое другое решение, полностью предотвращая доступ компаний и организаций к вашему социальному графу. Даже если пользователи используют серверы, предварительно настроенные в приложениях SimpleX Chat, операторы этих серверов не знают ни количества пользователей, ни их связей.", "hero-overlay-card-1-p-6": "Подробнее читайте в техническом описании SimpleX.", "simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями P2P имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.", "terms-and-privacy-policy": "Политика Конфиденциальности", @@ -325,5 +325,9 @@ "why-p8": "Потому что мы разрушили саму возможность узнать, кто вы. Чтобы вашу свободу невозможно было отнять.", "why-tagline": "Будь свободен в своей сети.", "why-footer-link": "Почему мы это строим", - "send-file": "Отправить файл" + "send-file": "Отправить файл", + "file-desc": "Отправьте файлы безопасно со сквозным шифрованием — без учётных записей, без отслеживания.", + "file-noscript": "JavaScript необходим для передачи файлов.", + "file-e2e-note": "Сквозное шифрование — сервер никогда не видит ваш файл.", + "file-learn-more": "Узнайте больше о протоколе XFTP" } diff --git a/website/langs/tr.json b/website/langs/tr.json index 6c9849306f..c832a80f74 100644 --- a/website/langs/tr.json +++ b/website/langs/tr.json @@ -258,5 +258,13 @@ "please-use-link-in-mobile-app": "Lütfen bağlantıyı mobil uygulamada kullanın", "directory": "Dizin", "navbar-token": "Token", - "about-and-contact-us": "Hakkımızda & İletişim" + "about-and-contact-us": "Hakkımızda & İletişim", + "links-all-languages": "Bütün diller", + "index-hero-h2": "Ağınızda", + "index-hero-download-desktop-btn-title": "SimpleX Masaüstü Uygulamasını İndir", + "index-testflight-title": "SimpleX iOS beta sürümü TestFlight'ta", + "index-f-droid-title": "SimpleX uygulaması F-Droid'de", + "index-security-assessment-title": "Güvenlik Denetimleri", + "index-security-review-2022-title": "2022 Güvenlik Denetimleri", + "index-security-review-2024-title": "2024 Güvenlik Denetimleri" } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 70f87ca79e..f32e63556d 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -75,7 +75,7 @@ "simplex-private-10-title": "临时匿名成对标识符", "simplex-private-8-title": "通过消息混合减少相关性", "simplex-private-card-1-point-2": "每个队列中的网络与密码学库加密盒(NaCL cryptobox)可防止 TLS 受到威胁时消息队列之间的流量关联。", - "simplex-private-card-10-point-2": "它让在没有用户标识符的情况下传递消息成为可能,并提供比替代方案更好的元数据隐私。", + "simplex-private-card-10-point-2": "它让消息能在没有用户标识符的情况下传递,并提供比替代方案更好的元数据隐私。", "privacy-matters-1-title": "广告和价格歧视", "privacy-matters-1-overlay-1-linkText": "隐私为您省钱", "privacy-matters-2-title": "对选举的操纵", @@ -121,7 +121,7 @@ "simplex-network-overlay-card-1-li-6": "P2P 网络可能受到 分布式反射拒绝服务攻击 。客户端有能力重新广播和放大流量,从而导致整个网络范围内的服务中断。 SimpleX 客户端仅中继来自已知连接的流量,因此不能被攻击者用来放大整个网络的流量。", "privacy-matters-overlay-card-1-p-2": "在线零售商知道收入较低的人更有可能在紧急情况下购买商品,因此他们可能会收取更高的价格或取消折扣。", "privacy-matters-overlay-card-1-p-3": "一些金融和保险公司使用社交图谱来确定利率和保费。 它通常会让收入较低的人支付更多—它被称为“贫困溢价”。", - "privacy-matters-overlay-card-1-p-4": "SimpleX 网络比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 应用预配置的服务器,服务器运营方也不知道用户数量或他们的连接数。", + "privacy-matters-overlay-card-1-p-4": "SimpleX 网络比任何替代方案都能更好地保护您人际关系层面的隐私,彻底防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 应用预配置的服务器,服务器运营方也不知道用户数量或他们的连接数。", "privacy-matters-overlay-card-2-p-1": "不久前,我们观察到几次大选被一家知名咨询公司操纵,该公司使用我们的社交图谱扭曲我们对现实世界的看法并操纵我们的选票。", "privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信网络时,这才有可能。", "privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的网络,这样能比任何已知的替代方案都更好地保护您的连接图谱。", @@ -152,7 +152,7 @@ "invitation-hero-header": "您收到了一个连接 SimpleX Chat 的一次性链接", "contact-hero-subheader": "使用手机或平板电脑上的 SimpleX Chat 应用程序扫描二维码。", "contact-hero-p-1": "当您查看此页面时,此链接中的公钥和消息队列地址不会通过网络发送 ——它们包含在链接 URL 的哈希片段中。", - "open-simplex-app": "打开 Simplex 应用程序", + "open-simplex-app": "打开 SimpleX 应用程序", "to-make-a-connection": "要建立连接:", "see-simplex-chat": "查看 SimpleX 聊天", "install-simplex-app": "安装 SimpleX 应用程序", @@ -283,8 +283,8 @@ "index-nextweb-p2": "没有任何单一实体控制网络 – 任何人都可以运行服务器。", "index-token-h2": "由用户资助", "index-token-p1": "为保持独立性,大型频道和社区将为其服务器付费。", - "index-token-p2": "这将用于支付基础设施、软件开发和网络治理费用。", - "index-token-cta": "了解更多关于 Community Credits", + "index-token-p2": "这会承担基础设施、软件开发和网络治理费用。", + "index-token-cta": "了解更多关于 Community Credits 的信息", "index-roadmap-h2": "SimpleX 通往自由互联网的路线图", "index-roadmap-1-title": "扩展到大型社区", "index-roadmap-1-desc": "逃离中心化平台", diff --git a/website/langs/zh_Hant.json b/website/langs/zh_Hant.json index 88dbdf107b..d1f25cc1e9 100644 --- a/website/langs/zh_Hant.json +++ b/website/langs/zh_Hant.json @@ -202,5 +202,7 @@ "docs-dropdown-11": "常見問題", "docs-dropdown-12": "安全性", "docs-dropdown-7": "翻譯SimpleX", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和F-Droid.org 儲存庫使用不同的金鑰為安裝包簽名。若要切換,請匯出聊天資料庫並重新安裝應用程式。" + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和F-Droid.org 儲存庫使用不同的金鑰為安裝包簽名。若要切換,請匯出聊天資料庫並重新安裝應用程式。", + "directory": "目錄", + "file-retry": "重試" } diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index 172145d86a..75959386d3 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -24,6 +24,10 @@ "title": "docs-dropdown-5", "url": "/docs/xftp-server.html" }, + { + "title": "docs-dropdown-16", + "url": "/docs/chat-relay.html" + }, { "title": "docs-dropdown-6", "url": "/docs/webrtc.html" diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index f9b4d15b54..aab46dfa28 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -6,6 +6,7 @@ "README.md", "send-messages.md", "secret-groups.md", + "channel-webpage.md", "chat-profiles.md", "managing-data.md", "audio-video-calls.md", @@ -27,6 +28,7 @@ "TRANSLATIONS.md", "WEBRTC.md", "XFTP-SERVER.md", + "CHAT-RELAY.md", "DOWNLOADS.md", "REPRODUCE.md", "TRANSPARENCY.md", diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index 34ee893dd3..cec2aa0a01 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -148,7 +148,7 @@ - {% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('credits' not in page.url) and ('file' not in page.url) and ('links' not in page.url) %} + {% if ('blog' not in page.url) and ('about' not in page.url) and ('donate' not in page.url) and ('privacy' not in page.url) and ('directory' not in page.url) and ('credits' not in page.url) and ('file' not in page.url) and ('links' not in page.url) and ('news' not in page.url) %}