diff --git a/.github/workflows/reproduce-schedule.yml b/.github/workflows/reproduce-schedule.yml index e86a471782..7d28d6f70c 100644 --- a/.github/workflows/reproduce-schedule.yml +++ b/.github/workflows/reproduce-schedule.yml @@ -33,7 +33,7 @@ jobs: user: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_USER }} pass: ${{ secrets.STATUS_SIMPLEX_WEBHOOK_PASS }} run: | - if [ -f "${GITHUB_WORKSPACE}/$TAG/_sha256sums" ]; then + if [ -f "${GITHUB_WORKSPACE}/${TAG}-simplex-chat/_sha256sums" ]; then exit 0 else curl --proto '=https' --tlsv1.2 -sSf \ diff --git a/README.md b/README.md index 1aa3b649a0..dc4b9658f7 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ You must: Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment. -You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FiBkJE72asZX1NUZaYFIeKRVk6oVjb-iv%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAinqu3j74AMjODLoIRR487ZW6ysip_dlpD6Zxk18SPFY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22groupLinkId%22%3A%223wAFGCLygQHR5AwynZOHlQ%3D%3D%22%7D) +You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://smp4.simplex.im/g#hr4lvFeBmndWMKTwqiodPz3VBo_6UmdGWocXd1SupsM) -There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform: +There is also a group [#simplex-devs](https://smp6.simplex.im/g#Drx3efC-n418AuSpzTspw9SER0iJwrQTmKBafQHwkKM) for developers who build on SimpleX platform: - chat bots and automations - integrations with other apps @@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users: -[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking). +[\#SimpleX-DE](https://smp6.simplex.im/g#V6tQ-lJqsdgJJdJiLPtP326oQFKHvwinIbgruZ9K2oU) (German-speaking), [\#SimpleX-ES](https://smp5.simplex.im/g#xJ5kwDLq2305O5FmpUzvgRIXXAcAJ9S5BItCd2Wmloc) (Spanish-speaking), [\#SimpleX-FR](https://smp6.simplex.im/g#cVOpB0CKd6hEf2aWQ6sJ22E2DVgQLtdHoiSdKxXeKqk) (French-speaking), [\#SimpleX-RU](https://smp5.simplex.im/g#vwXRdfG5SgtaG6aVcITiUGd--Ux0rY1IuH4QXYxlq3U) (Russian-speaking), [\#SimpleX-IT](https://smp5.simplex.im/g#BtRcjsl29ULFNBSE2OPhp1UwZfW7PW9gUYFQTKHdjqU) (Italian-speaking). You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code. diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 02cdad715e..b9bf6dd63a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -30,7 +30,7 @@ struct CILinkView: View { VStack(alignment: .leading, spacing: 6) { Text(linkPreview.title) .lineLimit(3) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) @@ -44,25 +44,71 @@ struct CILinkView: View { } } -func openBrowserAlert(uri: URL) { +func openBrowserAlert(uri: String) { + let (url, err) = sanitizeUri(uri) + if let url { + let uriStr = url.uri.absoluteString + showAlert( + NSLocalizedString("Open link?", comment: "alert title"), + message: uriStr.count > 160 ? "\(uriStr.prefix(160))…" : uriStr, + actions: { + if let sanitizedUri = url.sanitizedUri { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open full link", comment: "alert action"), + style: .destructive, + handler: { _ in UIApplication.shared.open(url.uri) } + ), + UIAlertAction( + title: NSLocalizedString("Open clean link", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(sanitizedUri) } + ) + ] + } else { + [ + cancelAlertAction, + UIAlertAction( + title: NSLocalizedString("Open", comment: "alert action"), + style: .default, + handler: { _ in UIApplication.shared.open(url.uri) } + ) + ] + } + } + ) + } else { + showInvalidLinkAlert(uri, error: err) + } +} + +func showInvalidLinkAlert(_ uri: String, error: String? = nil) { + let message = if let error, !error.isEmpty { + error + "\n" + uri + } else { + uri + } showAlert( - NSLocalizedString("Open link?", comment: "alert title"), - message: uri.absoluteString, - actions: {[ - cancelAlertAction, - UIAlertAction( - title: NSLocalizedString("Open", comment: "alert action"), - style: .default, - handler: { _ in UIApplication.shared.open(uri) } - ) - ]} + NSLocalizedString("Invalid link", comment: "alert title"), + message: message, + actions: {[okAlertAction]} ) } +func sanitizeUri(_ s: String) -> (url: (uri: URL, sanitizedUri: URL?)?, error: String?) { + let parsed = parseSanitizeUri(s) + return if let uri = URL(string: s), let uriInfo = parsed?.uriInfo { + (url: (uri: uri, sanitizedUri: uriInfo.sanitized.flatMap { URL(string: $0) }), error: nil) + } else { + (url: nil, error: parsed?.parseError) + } +} + struct LargeLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,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" diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 30ed3fa1a4..2a1b526893 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -93,7 +93,18 @@ struct MsgContentView: View { @inline(__always) private func msgContentView() -> some View { - let r = messageText(text, formattedText, textStyle: textStyle, sender: sender, mentions: mentions, userMemberId: userMemberId, showSecrets: showSecrets, backgroundColor: containerBackground, prefix: prefix) + let r = messageText( + text, + formattedText, + textStyle: textStyle, + sender: sender, + mentions: mentions, + userMemberId: userMemberId, + showSecrets: showSecrets, + commands: chat.chatInfo.useCommands && chat.chatInfo.sndReady, + backgroundColor: containerBackground, + prefix: prefix + ) let s = r.string let t: Text if let mt = meta { @@ -104,7 +115,7 @@ struct MsgContentView: View { } else { t = Text(AttributedString(s)) } - return msgTextResultView(r, t, showSecrets: $showSecrets) + return msgTextResultView(r, t, showSecrets: $showSecrets, sendCommand: { cmd in sendCommandMsg(chat, cmd) }) } @inline(__always) @@ -120,14 +131,27 @@ struct MsgContentView: View { } } -func msgTextResultView(_ r: MsgTextResult, _ t: Text, showSecrets: Binding>? = nil, centered: Bool = false, smallFont: Bool = false) -> some View { +func msgTextResultView( + _ r: MsgTextResult, + _ t: Text, + showSecrets: Binding>? = nil, + sendCommand: ((String) -> 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, centered: centered, smallFont: smallFont)) } + .if(r.handleTaps) { $0.overlay(handleTextTaps(r.string, showSecrets: showSecrets, sendCommand: sendCommand, centered: centered, smallFont: smallFont)) } } // smallFont parameter is used to pad height, otherwise CTFrameGetLines fails to see them as lines - it's needed if font is not .body @inline(__always) -private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding>? = nil, centered: Bool, smallFont: Bool) -> some View { +private func handleTextTaps( + _ s: NSAttributedString, + showSecrets: Binding>? = nil, + sendCommand: ((String) -> Void)? = nil, + centered: Bool, + smallFont: Bool +) -> some View { return GeometryReader { g in Rectangle() .fill(Color.clear) @@ -163,23 +187,25 @@ private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding (URL, Bool)? { - var linkURL: URL? + func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (String, Bool)? { + var linkURL: String? var browser: 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 url = attrs[linkAttrKey] as? NSURL { - linkURL = url.absoluteURL + if let url = attrs[linkAttrKey] as? String { + linkURL = url browser = attrs[webLinkAttrKey] != nil } else if let showSecrets, let i = attrs[secretAttrKey] as? Int { if showSecrets.wrappedValue.contains(i) { @@ -187,6 +213,8 @@ private func handleTextTaps(_ s: NSAttributedString, showSecrets: Binding?, + commands: Bool = false, backgroundColor: UIColor, prefix: NSAttributedString? = nil ) -> MsgTextResult { @@ -326,22 +358,41 @@ func messageText( case .uri: attrs = linkAttrs() if !preview { - let s = t.lowercased() - let link = s.hasPrefix("http://") || s.hasPrefix("https://") + let link = t.hasPrefix("http://") || t.hasPrefix("https://") ? t : "https://" + t - attrs[linkAttrKey] = NSURL(string: link) + attrs[linkAttrKey] = link attrs[webLinkAttrKey] = true handleTaps = true } - case let .simplexLink(linkType, simplexUri, smpHosts): + case let .hyperLink(text, uri): attrs = linkAttrs() + if let text { t = text } if !preview { - attrs[linkAttrKey] = NSURL(string: simplexUri) + attrs[linkAttrKey] = uri + attrs[webLinkAttrKey] = true handleTaps = true } - if case .description = privacySimplexLinkModeDefault.get() { - t = simplexLinkText(linkType, smpHosts) + case let .simplexLink(text, linkType, simplexUri, smpHosts): + attrs = linkAttrs() + if !preview { + attrs[linkAttrKey] = simplexUri + handleTaps = true + } + if let s = text ?? (privacySimplexLinkModeDefault.get() == .description ? linkType.description : nil) { + res.append(NSAttributedString(string: s + " ", attributes: attrs)) + italic = italic ?? UIFont(descriptor: descr.withSymbolicTraits(.traitItalic) ?? descr, size: descr.pointSize) + attrs[.font] = italic + t = viaHost(smpHosts) + } + case let .command(cmdStr): + snippet = snippet ?? UIFont.monospacedSystemFont(ofSize: descr.pointSize, weight: .regular) + attrs[.font] = snippet + t = "/" + cmdStr + if !preview && commands { + attrs[.foregroundColor] = uiLinkColor + attrs[commandAttrKey] = t + handleTaps = true } case let .mention(memberName): if let m = mentions?[memberName] { @@ -364,13 +415,13 @@ func messageText( case .email: attrs = linkAttrs() if !preview { - attrs[linkAttrKey] = NSURL(string: "mailto:" + ft.text) + attrs[linkAttrKey] = "mailto:" + ft.text handleTaps = true } case .phone: attrs = linkAttrs() if !preview { - attrs[linkAttrKey] = NSURL(string: "tel:" + t.replacingOccurrences(of: " ", with: "")) + attrs[linkAttrKey] = "tel:" + t.replacingOccurrences(of: " ", with: "") handleTaps = true } case .unknown: () @@ -400,7 +451,11 @@ private func mentionText(_ name: String) -> String { } func simplexLinkText(_ linkType: SimplexLinkType, _ smpHosts: [String]) -> String { - linkType.description + " " + "(via \(smpHosts.first ?? "?"))" + linkType.description + " " + viaHost(smpHosts) +} + +func viaHost(_ smpHosts: [String]) -> String { + "(via \(smpHosts.first ?? "?"))" } struct MsgContentView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 712a88114f..2f8d6f2acd 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -59,6 +59,7 @@ struct ChatView: View { @State private var ignoreLoadingRequests: Int64? = nil @State private var animatedScrollingInProgress: Bool = false @State private var showUserSupportChatSheet = false + @State private var showCommandsMenu = false @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @@ -109,6 +110,9 @@ struct ChatView: View { if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty { GroupMentionsView(im: im, groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible) } + if !chat.chatInfo.menuCommands.isEmpty { + CommandsMenuView(chat: chat, composeState: $composeState, selectedRange: $selectedRange, showCommandsMenu: $showCommandsMenu) + } FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: { mergedItems.boxedValue = MergedItems.create(im, revealedItems) scrollView.updateItems(mergedItems.boxedValue.items) @@ -135,6 +139,7 @@ struct ChatView: View { chat: chat, im: im, composeState: $composeState, + showCommandsMenu: $showCommandsMenu, keyboardVisible: $keyboardVisible, keyboardHiddenDate: $keyboardHiddenDate, selectedRange: $selectedRange, @@ -919,10 +924,12 @@ struct ChatView: View { case .inv: "Tap Connect to chat" case .con: - "Tap Connect to send request" + contact.isBot ? "Tap Connect to use bot" : "Tap Connect to send request" } } else if contact.nextAcceptContactRequest { "Accept contact request" + } else if case .bot = contact.profile.peerType { + "Bot" } else { "Your contact" } @@ -956,9 +963,7 @@ struct ChatView: View { switch (chat.chatInfo) { case let .direct(contact): if !contact.sndReady && contact.active && !contact.sendMsgToConnect && !contact.nextAcceptContactRequest { - contact.preparedContact?.uiConnLinkType == .con - ? "contact should accept…" - : contact.contactGroupMemberId != nil + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil ? "contact should accept…" : "connecting…" } else { diff --git a/apps/ios/Shared/Views/Chat/CommandsMenuView.swift b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift new file mode 100644 index 0000000000..525bf5725c --- /dev/null +++ b/apps/ios/Shared/Views/Chat/CommandsMenuView.swift @@ -0,0 +1,187 @@ +// +// CommandsMenuView.swift +// SimpleX (iOS) +// +// Created by EP on 03/08/2025. +// Copyright © 2025 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +let COMMAND_ROW_SIZE: CGFloat = 48 +let MAX_VISIBLE_COMMAND_ROWS: CGFloat = 5.8 + +struct CommandsMenuView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat + @Binding var composeState: ComposeState + @Binding var selectedRange: NSRange + @Binding var showCommandsMenu: Bool + + @State private var currentCommands: [ChatBotCommand] = [] + @State private var menuTreeBackPath: [(label: String, commands: [ChatBotCommand])] = [] + @State private var keywordWidth: CGFloat = 0 + + var body: some View { + ZStack(alignment: .bottom) { + if !currentCommands.isEmpty { + Color.white.opacity(0.01) + .edgesIgnoringSafeArea(.all) + .onTapGesture { + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + VStack(spacing: 0) { + Spacer() + let cmdsCount = currentCommands.count + (menuTreeBackPath.isEmpty ? 0 : 1) + let scroll = ScrollView { + VStack(spacing: 0) { + if let prev = menuTreeBackPath.last { + Divider() + menuLabelRow(prev) + } + ForEach(currentCommands, id: \.self, content: commandRow) + } + } + .frame(maxWidth: .infinity, maxHeight: COMMAND_ROW_SIZE * min(MAX_VISIBLE_COMMAND_ROWS, CGFloat(cmdsCount))) + .background(theme.colors.background) + + if #available(iOS 16.0, *) { + scroll.scrollDismissesKeyboard(.never) + } else { + scroll + } + } + .onPreferenceChange(DetermineWidth.Key.self) { keywordWidth = $0 } + } + } + .onChange(of: composeState.message) { message in + let msg = message.trimmingCharacters(in: .whitespaces) + if msg == "/" { + currentCommands = chat.chatInfo.menuCommands + } else if msg.first == "/" { + currentCommands = filterShownCommands(chat.chatInfo.menuCommands, msg.dropFirst()) + } else { + showCommandsMenu = false + currentCommands = [] + } + menuTreeBackPath = [] + } + .onChange(of: showCommandsMenu) { show in + currentCommands = show ? chat.chatInfo.menuCommands : [] + menuTreeBackPath = [] + } + } + + private func menuLabelRow(_ prev: (label: String, commands: [ChatBotCommand])) -> some View { + HStack { + Image(systemName: "chevron.left") + .foregroundColor(theme.colors.secondary) + Text(prev.label) + .fontWeight(.medium) + .frame(maxWidth: .infinity) + } + .padding(.horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if !menuTreeBackPath.isEmpty { + currentCommands = menuTreeBackPath.removeLast().commands + } + } + } + + @ViewBuilder + private func commandRow(_ command: ChatBotCommand) -> some View { + Divider() + switch command { + case let .command(keyword, label, params): + HStack { + Text(label) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + Text("/" + keyword) + .font(.subheadline) + .lineLimit(1) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: keywordWidth, alignment: .trailing) + .overlay(DetermineWidth()) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + if let params { + composeState.message = "/\(keyword) \(params)" + selectedRange = NSRange(location: composeState.message.count, length: 0) + } else { + composeState.message = "" + sendCommandMsg(chat, "/\(keyword)") + } + showCommandsMenu = false + currentCommands = [] + menuTreeBackPath = [] + } + case let .menu(label, cmds): + HStack { + Text(label) + .fontWeight(.medium) + .lineLimit(1) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(theme.colors.secondary) + } + .padding(.horizontal) + .frame(height: COMMAND_ROW_SIZE, alignment: .center) + .contentShape(Rectangle()) + .onTapGesture { + menuTreeBackPath.append((label: label, commands: currentCommands)) + currentCommands = cmds + } + } + } + + private func filterShownCommands(_ commands: [ChatBotCommand], _ msg: String.SubSequence) -> [ChatBotCommand] { + var cmds: [ChatBotCommand] = [] + for command in commands { + switch command { + case let .command(keyword, _, _): + if keyword.starts(with: msg) { + cmds.append(command) + } + case let .menu(_, innerCmds): + cmds.append(contentsOf: filterShownCommands(innerCmds, msg)) + } + } + return cmds + } +} + +func sendCommandMsg(_ chat: Chat, _ cmd: String) { + if chat.chatInfo.sndReady { + Task { + if let chatItems = await apiSendMessages( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + scope: chat.chatInfo.groupChatScope(), + composedMessages: [ComposedMessage(msgContent: .text(cmd))] + ) { + await MainActor.run { + for ci in chatItems { + ChatModel.shared.addChatItem(chat.chatInfo, ci) + } + } + } + } + } else { + showAlert( + NSLocalizedString("You can't send messages!", comment: "alert title"), + message: NSLocalizedString("To send commands you must be connected.", comment: "alert message"), + actions: { [okAlertAction] } + ) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index e629a984df..878ebd9cbf 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -49,7 +49,7 @@ struct ComposeLinkView: View { VStack(alignment: .center, spacing: 4) { Text(linkPreview.title) .lineLimit(1) - Text(linkPreview.uri.absoluteString) + Text(linkPreview.uri) .font(.caption) .lineLimit(1) .foregroundColor(theme.colors.secondary) @@ -63,7 +63,7 @@ struct ComposeLinkView: View { struct SmallLinkPreview_Previews: PreviewProvider { static var previews: some View { let preview = LinkPreview( - uri: URL(string: "http://DuckDuckGo.com")!, + uri: "http://DuckDuckGo.com", title: "Privacy, simplified.", description: "", image: "data:image/jpg;base64,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" diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 876761a588..619f0b95f3 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -323,15 +323,16 @@ struct ComposeView: View { @ObservedObject var chat: Chat @ObservedObject var im: ItemsModel @Binding var composeState: ComposeState + @Binding var showCommandsMenu: Bool @Binding var keyboardVisible: Bool @Binding var keyboardHiddenDate: Date @Binding var selectedRange: NSRange var disabledText: LocalizedStringKey? = nil - @State var linkUrl: URL? = nil + @State var linkUrl: String? = nil @State var hasSimplexLink: Bool = false - @State var prevLinkUrl: URL? = nil - @State var pendingLinkUrl: URL? = nil + @State var prevLinkUrl: String? = nil + @State var pendingLinkUrl: String? = nil @State var cancelledLinks: Set = [] @Environment(\.colorScheme) private var colorScheme @@ -352,6 +353,8 @@ struct ComposeView: View { @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true + @State private var updatingCompose = false var body: some View { VStack(spacing: 0) { @@ -410,18 +413,7 @@ struct ComposeView: View { if chat.chatInfo.groupInfo?.nextConnectPrepared == true { if chat.chatInfo.groupInfo?.businessChat == nil { - Button(action: connectPreparedGroup) { - ZStack(alignment: .trailing) { - Label("Join group", systemImage: "person.2.fill") - .frame(maxWidth: .infinity) - if composeState.progressByTimeout { - ProgressView() - .padding() - } - } - } - .frame(height: 60) - .disabled(composeState.inProgress) + connectButtonView("Join group", icon: "person.2.fill", connect: connectPreparedGroup) } else { sendContactRequestView(disableSendButton, icon: "briefcase.fill", sendRequest: connectPreparedGroup) } @@ -429,27 +421,22 @@ struct ComposeView: View { contextSendMessageToConnect("Send direct message to connect") Divider() HStack (alignment: .center) { - attachmentButton().disabled(true) + attachmentAndCommandsButtons().disabled(true) sendMessageView(disableSendButton, sendToConnect: sendMemberContactInvitation) } .padding(.horizontal, 12) - } else if contact?.nextConnectPrepared == true, let linkType = contact?.preparedContact?.uiConnLinkType { + } else if let contact, + contact.nextConnectPrepared == true, + let linkType = contact.preparedContact?.uiConnLinkType { switch linkType { case .inv: - Button(action: sendConnectPreparedContact) { - ZStack(alignment: .trailing) { - Label("Connect", systemImage: "person.fill.badge.plus") - .frame(maxWidth: .infinity) - if composeState.progressByTimeout { - ProgressView() - .padding() - } - } - } - .frame(height: 60) - .disabled(composeState.inProgress) + connectButtonView("Connect", icon: "person.fill.badge.plus", connect: sendConnectPreparedContact) case .con: - sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest) + if contact.isBot { + connectButtonView("Connect", icon: "bolt.fill", connect: sendConnectPreparedContact) + } else { + sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest) + } } } else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId { ContextContactRequestActionsView(contactRequestId: crId) @@ -457,7 +444,7 @@ struct ComposeView: View { ContextMemberContactActionsView(contact: ct, groupDirectInv: groupDirectInv) } else { HStack (alignment: .center) { - attachmentButton() + attachmentAndCommandsButtons() sendMessageView(disableSendButton) } .padding(.horizontal, 12) @@ -469,8 +456,26 @@ struct ComposeView: View { .ignoresSafeArea(.all, edges: .bottom) } .onChange(of: composeState.message) { msg in - let parsedMsg = parseSimpleXMarkdown(msg) - composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) + if updatingCompose { + updatingCompose = false + return + } + var parsedMsg = parseSimpleXMarkdown(msg) + if privacySanitizeLinks, let parsed = parsedMsg { + let r = sanitizeMessage(parsed) + if let sanitizedPos = r.sanitizedPos { + updatingCompose = true + composeState = composeState.copy(message: r.message, parsedMessage: r.parsedMsg) + if sanitizedPos < selectedRange.location { + selectedRange = NSRange(location: sanitizedPos, length: 0) + } + parsedMsg = r.parsedMsg + } else { + composeState = composeState.copy(parsedMessage: parsedMsg) + } + } else { + composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg)) + } if composeState.linkPreviewAllowed { if msg.count > 0 { showLinkPreview(parsedMsg) @@ -479,7 +484,7 @@ struct ComposeView: View { hasSimplexLink = false } } else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) { - (_, hasSimplexLink) = getSimplexLink(parsedMsg) + (_, hasSimplexLink) = getMessageLinks(parsedMsg) } else { hasSimplexLink = false } @@ -635,6 +640,21 @@ struct ComposeView: View { } } + private func connectButtonView(_ label: LocalizedStringKey, icon: String, connect: @escaping () -> Void) -> some View { + Button(action: connect) { + ZStack(alignment: .trailing) { + Label(label, systemImage: icon) + .frame(maxWidth: .infinity) + if composeState.progressByTimeout { + ProgressView() + .padding() + } + } + } + .frame(height: 60) + .disabled(composeState.inProgress) + } + private func sendContactRequestView(_ disableSendButton: Bool, icon: String, sendRequest: @escaping () -> Void) -> some View { HStack (alignment: .center) { sendMessageView( @@ -703,6 +723,35 @@ struct ComposeView: View { } } + @ViewBuilder private func attachmentAndCommandsButtons() -> some View { + let msg = composeState.message.trimmingCharacters(in: .whitespaces) + let showAttachment = chat.chatInfo.contact?.profile.peerType != .bot || chat.chatInfo.featureEnabled(.files) + let showCommands = chat.chatInfo.useCommands && (!showAttachment || msg.isEmpty || msg.starts(with: "/")) + if showCommands { + commandsButton() + } + if showAttachment { + attachmentButton() + .padding(.trailing, 3) + .if(showCommands) { v in v.padding(.leading, 3) } + } + } + + private func commandsButton() -> some View { + Button { + showCommandsMenu.toggle() + } label: { + Text(verbatim: "//") + .font(.title3) + .italic() + .contentShape(Rectangle()) + } + .disabled(!chat.chatInfo.sendMsgEnabled || chat.chatInfo.menuCommands.isEmpty) + .frame(width: 25, height: 25) + .tint(theme.colors.primary) + .padding(.bottom, 2) + } + @ViewBuilder private func attachmentButton() -> some View { let b = Button { showChooseSource = true @@ -714,12 +763,11 @@ struct ComposeView: View { .frame(width: 25, height: 25) .tint(theme.colors.primary) if im.secondaryIMFilter == nil, - case let .group(g, _) = chat.chatInfo, - !g.fullGroupPreferences.files.on(for: g.membership) { + !chat.chatInfo.featureEnabled(.files) { b.disabled(true).onTapGesture { AlertManager.shared.showAlertMsg( title: "Files and media prohibited!", - message: "Only group owners can enable files and media." + message: chat.chatInfo.groupInfo == nil ? nil : "Only group owners can enable files and media." ) } } else { @@ -750,7 +798,6 @@ struct ComposeView: View { } } - // TODO [short links] different messages for business private func sendConnectPreparedContactRequest() { hideKeyboard() let empty = composeState.whitespaceOnly @@ -818,7 +865,7 @@ struct ComposeView: View { switch (composeState.preview) { case let .linkPreview(linkPreview: linkPreview): if let parsedMsg = parseSimpleXMarkdown(msgText), - let url = getSimplexLink(parsedMsg).url, + let url = getMessageLinks(parsedMsg).url, let linkPreview = linkPreview, url == linkPreview.uri { return .link(text: msgText, preview: linkPreview) @@ -1421,7 +1468,7 @@ struct ComposeView: View { private func showLinkPreview(_ parsedMsg: [FormattedText]?) { prevLinkUrl = linkUrl - (linkUrl, hasSimplexLink) = getSimplexLink(parsedMsg) + (linkUrl, hasSimplexLink) = getMessageLinks(parsedMsg) if let url = linkUrl { if url != composeState.linkPreview?.uri && url != pendingLinkUrl { pendingLinkUrl = url @@ -1438,39 +1485,38 @@ struct ComposeView: View { } } - private func getSimplexLink(_ parsedMsg: [FormattedText]?) -> (url: URL?, hasSimplexLink: Bool) { + private func getMessageLinks(_ parsedMsg: [FormattedText]?) -> (url: String?, hasSimplexLink: Bool) { guard let parsedMsg else { return (nil, false) } - let url: URL? = if let uri = parsedMsg.first(where: { ft in - ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) - }) { - URL(string: uri.text) - } else { - nil - } let simplexLink = parsedMsgHasSimplexLink(parsedMsg) - return (url, simplexLink) + for ft in parsedMsg { + if let link = ft.linkUri, !cancelledLinks.contains(link) && !isSimplexLink(link) { + return (link, simplexLink) + } + } + return (nil, simplexLink) } private func isSimplexLink(_ link: String) -> Bool { - link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") + link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat") || link.starts(with: "simplex:/") } private func cancelLinkPreview() { - if let pendingLink = pendingLinkUrl?.absoluteString { + if let pendingLink = pendingLinkUrl { cancelledLinks.insert(pendingLink) } - if let uri = composeState.linkPreview?.uri.absoluteString { + if let uri = composeState.linkPreview?.uri { cancelledLinks.insert(uri) } pendingLinkUrl = nil composeState = composeState.copy(preview: .noPreview) } - private func loadLinkPreview(_ url: URL) { - if pendingLinkUrl == url { + private func loadLinkPreview(_ urlStr: String) { + if pendingLinkUrl == urlStr, let url = URL(string: urlStr) { composeState = composeState.copy(preview: .linkPreview(linkPreview: nil)) getLinkPreview(url: url) { linkPreview in - if let linkPreview, pendingLinkUrl == url { + if let linkPreview, pendingLinkUrl == urlStr { + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview)) } else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -1490,32 +1536,31 @@ struct ComposeView: View { } } -struct ComposeView_Previews: PreviewProvider { - static var previews: some View { - let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) - let im = ItemsModel.shared - @State var composeState = ComposeState(message: "hello") - @State var selectedRange = NSRange() - - return Group { - ComposeView( - chat: chat, - im: im, - composeState: $composeState, - keyboardVisible: Binding.constant(true), - keyboardHiddenDate: Binding.constant(Date.now), - selectedRange: $selectedRange - ) - .environmentObject(ChatModel()) - ComposeView( - chat: chat, - im: im, - composeState: $composeState, - keyboardVisible: Binding.constant(true), - keyboardHiddenDate: Binding.constant(Date.now), - selectedRange: $selectedRange - ) - .environmentObject(ChatModel()) +func sanitizeMessage(_ parsedMsg: [FormattedText]) -> (message: String, parsedMsg: [FormattedText], sanitizedPos: Int?) { + var pos: Int = 0 + var updatedMsg = "" + var sanitizedPos: Int? = nil + let updatedParsedMsg = parsedMsg.map { ft in + var updated = ft + switch ft.format { + case .uri: + if let sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized { + updated = FormattedText(text: sanitized, format: .uri) + pos += updated.text.count + sanitizedPos = pos + } + case let .hyperLink(text, uri): + if let sanitized = parseSanitizeUri(uri)?.uriInfo?.sanitized { + let updatedText = if let text { "[\(text)](\(sanitized))" } else { sanitized } + updated = FormattedText(text: updatedText, format: .hyperLink(showText: text, linkUri: sanitized)) + pos += updated.text.count + sanitizedPos = pos + } + default: + pos += ft.text.count } + updatedMsg += updated.text + return updated } + return (message: updatedMsg, parsedMsg: updatedParsedMsg, sanitizedPos: sanitizedPos) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift index 07cc7bd217..440ed5227d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift @@ -67,7 +67,7 @@ struct GroupMentionsView: View { } } .frame(maxHeight: MEMBER_ROW_SIZE * min(MAX_VISIBLE_MEMBER_ROWS, CGFloat(filtered.count))) - .background(Color(UIColor.systemBackground)) + .background(theme.colors.background) if #available(iOS 16.0, *) { scroll.scrollDismissesKeyboard(.never) diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 9f453f1aa3..0450bd439c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -629,7 +629,7 @@ struct ChatListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 79f72e539a..c56d947a5a 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -351,12 +351,14 @@ struct ChatPreviewView: View { if contact.isContactCard { Text("Tap to Connect") .foregroundColor(theme.colors.primary) + } else if contact.isBot && contact.nextConnectPrepared { + Text("Open to use bot") } else if contact.sendMsgToConnect { Text("Open to connect") } else if contact.nextAcceptContactRequest { Text("Open to accept") } else if !contact.sndReady && contact.activeConn != nil && contact.active { - contact.preparedContact?.uiConnLinkType == .con + (contact.preparedContact?.uiConnLinkType == .con && !contact.isBot) || contact.contactGroupMemberId != nil ? Text("contact should accept…") : Text("connecting…") } else { diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 4cc244cb24..9c2916880c 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -27,6 +27,7 @@ struct ProfileImage: View { Image(systemName: iconName) .resizable() .foregroundColor(c) + .scaledToFit() .frame(width: size, height: size) .background( Circle() diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index c009bf0d35..2e3119a8b8 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -381,7 +381,7 @@ struct ContactsListSearchBar: View { } else { if let link = strHasSingleSimplexLink(t.trimmingCharacters(in: .whitespaces)) { // if SimpleX link is pasted, show connection dialogue searchFocussed = false - if case let .simplexLink(linkType, _, smpHosts) = link.format { + if case let .simplexLink(_, linkType, _, smpHosts) = link.format { ignoreSearchTextChange = true searchText = simplexLinkText(linkType, smpHosts) } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 432422d77b..3de1fdb972 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -1039,7 +1039,11 @@ private func showPrepareContactAlert( profileImage: ProfileImage( imageStr: contactShortLinkData.profile.image, - iconName: contactShortLinkData.business ? "briefcase.circle.fill" : "person.crop.circle.fill", + iconName: contactShortLinkData.business + ? "briefcase.circle.fill" + : contactShortLinkData.profile.peerType == .bot + ? "cube.fill" + : "person.crop.circle.fill", size: alertProfileImageSize ), theme: theme, @@ -1112,7 +1116,7 @@ private func showOpenKnownContactAlert( profileImage: ProfileImage( imageStr: contact.profile.image, - iconName: "person.crop.circle.fill", + iconName: contact.chatIconName, size: alertProfileImageSize ), theme: theme, @@ -1138,7 +1142,7 @@ private func showOpenKnownGroupAlert( profileImage: ProfileImage( imageStr: groupInfo.groupProfile.image, - iconName: groupInfo.businessChat == nil ? "person.2.circle.fill" : "briefcase.circle.fill", + iconName: groupInfo.chatIconName, size: alertProfileImageSize ), theme: theme, diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 54454b7cef..6df2d5422e 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -14,6 +14,7 @@ struct DeveloperView: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false @State private var hintsUnchanged = hintDefaultsUnchanged() + @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @Environment(\.colorScheme) var colorScheme @@ -65,6 +66,21 @@ struct DeveloperView: View { Text("Developer options") } } + Section("Deprecated options") { + settingsRow("link", color: theme.colors.secondary) { + Picker("SimpleX links", selection: $simplexLinkMode) { + ForEach( + SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) + ) { mode in + Text(mode.text) + } + } + } + .frame(height: 36) + .onChange(of: simplexLinkMode) { mode in + privacySimplexLinkModeDefault.set(mode) + } + } } } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 06fe20a3fd..48c2efe0ac 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -14,11 +14,11 @@ struct PrivacySettings: View { @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true + @AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = true @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = 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 - @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -75,8 +75,12 @@ struct PrivacySettings: View { Toggle("Send link previews", isOn: $useLinkPreviews) .onChange(of: useLinkPreviews) { linkPreviews in privacyLinkPreviewsGroupDefault.set(linkPreviews) + privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5 } } + settingsRow("link", color: theme.colors.secondary) { + Toggle("Remove link tracking", isOn: $privacySanitizeLinks) + } settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) } @@ -89,19 +93,6 @@ struct PrivacySettings: View { m.draftChatId = nil } } - settingsRow("link", color: theme.colors.secondary) { - Picker("SimpleX links", selection: $simplexLinkMode) { - ForEach( - SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) - ) { mode in - Text(mode.text) - } - } - } - .frame(height: 36) - .onChange(of: simplexLinkMode) { mode in - privacySimplexLinkModeDefault.set(mode) - } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) 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 8bb83a9cec..2331fc9a32 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -1374,12 +1374,14 @@ خطأ في تغيير الإعدادات No comment provided by engineer. - + Error creating address + خطأ في إنشاء العنوان No comment provided by engineer. - + Error creating group + خطأ في إنشاء المجموعة No comment provided by engineer. @@ -5757,6 +5759,102 @@ This is your own one-time link! Encryption re-negotiation failed. فشل إعادة التفاوض على التشفير. + + Accept as member + اقبل كعضو + + + Accept as observer + اقبل كمراقب + + + Accept contact request + اقبل طلب الاتصال + + + Accept member + اقبل العضو + + + Add message + أضف رسالة + + + All servers + كل الخوادم + + + Bio + نبذة + + + Bio too large + النبذة كبيرة جدًا + + + Can't change profile + لا يمكن تغيير الحساب + + + Chat with admins + تحدث مع المدراء + + + Chat with member + تحدث مع العضو + + + Chat with members before they join. + تحدث مع الأعضاء قبل انضمامهم. + + + Chats with members + تحدث مع الأعضاء + + + Connect faster! 🚀 + اتصل بسرعة! 🚀 + + + Contact requests from groups + طلبات الاتصال من المجموعات + + + Create your address + أنشئ عنوانك + + + Delete chat with member? + حذف المحادثة مع العضو؟ + + + Description too large + الوصف كبير جدًا + + + Empty message! + رسالة فارغة! + + + Enable disappearing messages by default. + فعّل حذف الرسائل تلقائيا. + + + Error accepting member + خطأ في قبول العضو + + + Error adding short link + خطأ في إضافة الرابط القصير + + + Error changing chat profile + خطأ في تغيير حساب المحادثات + + + Error connecting to forwarding server %@. Please try later. + خطأ في الإتصال بخادم التحويل @%. حاول مجددا بعد حين. + 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 c4f75e1989..00a8ea67ae 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -855,6 +855,10 @@ swipe action Позволи понижаване 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 часа) @@ -939,6 +943,10 @@ swipe action Позволи на вашите контакти да изпращат изчезващи съобщения. 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. Позволи на вашите контакти да изпращат гласови съобщения. @@ -1287,6 +1295,10 @@ swipe action Размазване на медия No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. И вие, и вашият контакт можете да добавяте реакции към съобщението. @@ -1307,6 +1319,10 @@ swipe action И вие, и вашият контакт можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. И вие, и вашият контакт можете да изпращате гласови съобщения. @@ -2547,6 +2563,10 @@ swipe action Потвърждениe за доставка! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Описание @@ -3607,6 +3627,10 @@ snd error text Файлове и медия chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файловете и медията са забранени в тази група. @@ -4269,7 +4293,7 @@ More improvements are coming soon! Invalid link Невалиден линк - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5378,6 +5402,10 @@ Requires compatible VPN. Само вие можете да изпращате изчезващи съобщения. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Само вие можете да изпращате гласови съобщения. @@ -5403,6 +5431,10 @@ Requires compatible VPN. Само вашият контакт може да изпраща изчезващи съобщения. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Само вашият контакт може да изпраща гласови съобщения. @@ -5432,10 +5464,18 @@ Requires compatible VPN. Отвори конзолата authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Отвори група @@ -5470,6 +5510,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Приложението се отваря… @@ -6161,6 +6205,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Острани член @@ -7431,6 +7479,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7815,6 +7867,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана. @@ -8658,7 +8714,7 @@ Repeat join request? You can't send messages! Не може да изпращате съобщения! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 0ae8e0a70a..1f32d7010e 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -819,6 +819,10 @@ swipe action 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) Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) @@ -901,6 +905,10 @@ swipe action Umožněte svým kontaktům odesílat mizící zprávy. 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. Povolte svým kontaktům odesílání hlasových zpráv. @@ -1219,6 +1227,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Vy i váš kontakt můžete přidávat reakce na zprávy. @@ -1239,6 +1251,10 @@ swipe action Vy i váš kontakt můžete posílat mizící zprávy. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Hlasové zprávy můžete posílat vy i váš kontakt. @@ -2439,6 +2455,10 @@ swipe action Potvrzení o doručení! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Popis @@ -3469,6 +3489,10 @@ snd error text Soubory a média chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Soubory a média jsou zakázány v této skupině. @@ -4108,7 +4132,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5185,6 +5209,10 @@ Vyžaduje povolení sítě VPN. Mizící zprávy můžete odesílat pouze vy. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Hlasové zprávy můžete posílat pouze vy. @@ -5210,6 +5238,10 @@ Vyžaduje povolení sítě VPN. Zmizelé zprávy může odesílat pouze váš kontakt. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Hlasové zprávy může odesílat pouze váš kontakt. @@ -5239,10 +5271,18 @@ Vyžaduje povolení sítě VPN. Otevřete konzolu chatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5275,6 +5315,10 @@ Vyžaduje povolení sítě VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -5944,6 +5988,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Odstranit člena @@ -7190,6 +7238,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7564,6 +7616,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. @@ -8365,7 +8421,7 @@ Repeat join request? You can't send messages! Nemůžete posílat zprávy! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 cda7e1ead0..6f294d82d5 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -867,6 +867,10 @@ swipe action Herabstufung erlauben 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) Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden) @@ -952,6 +956,10 @@ swipe action Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. 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. Erlauben Sie Ihren Kontakten Sprachnachrichten zu senden. @@ -1312,6 +1320,10 @@ swipe action Medien verpixeln No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben. @@ -1332,6 +1344,10 @@ swipe action Ihr Kontakt und Sie können beide verschwindende Nachrichten senden. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden. @@ -1636,7 +1652,7 @@ set passcode view Chat with members before they join. - Chat mit Mitgliedern bevor sie beitreten. + Mit Mitgliedern chatten bevor sie beitreten. No comment provided by engineer. @@ -2087,6 +2103,7 @@ Das ist Ihr eigener Einmal-Link! Contact requests from groups + KONTAKTANFRAGEN VON GRUPPEN No comment provided by engineer. @@ -2663,6 +2680,10 @@ swipe action Empfangsbestätigungen! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Beschreibung @@ -3021,7 +3042,7 @@ chat item action Enable disappearing messages by default. - Verschwindende Nachrichten per Voreinstellung aktiviert. + Verschwindende Nachrichten sind per Voreinstellung aktiviert. No comment provided by engineer. @@ -3531,6 +3552,7 @@ chat item action Error setting auto-accept + Fehler bei der Einstellung des automatischen Akzeptierens No comment provided by engineer. @@ -3794,6 +3816,10 @@ snd error text Dateien und Medien chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. In dieser Gruppe sind Dateien und Medien nicht erlaubt. @@ -4495,7 +4521,7 @@ Weitere Verbesserungen sind bald verfügbar! Invalid link Ungültiger Link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4662,7 +4688,7 @@ Das ist Ihr Link für die Gruppe %@! Keep your chats clean - Halten Sie Ihre Chats übersichtlich + Ihre Chats übersichtlich halten No comment provided by engineer. @@ -4872,6 +4898,7 @@ Das ist Ihr Link für die Gruppe %@! Member is deleted - can't accept request + Mitglied ist gelöscht - Anfrage kann nicht angenommen werden No comment provided by engineer. @@ -5684,6 +5711,10 @@ Dies erfordert die Aktivierung eines VPNs. Nur Sie können verschwindende Nachrichten senden. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Nur Sie können Sprachnachrichten versenden. @@ -5709,6 +5740,10 @@ Dies erfordert die Aktivierung eines VPNs. Nur Ihr Kontakt kann verschwindende Nachrichten senden. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Nur Ihr Kontakt kann Sprachnachrichten versenden. @@ -5739,11 +5774,19 @@ Dies erfordert die Aktivierung eines VPNs. Chat-Konsole öffnen authentication reason + + Open clean link + alert action + Open conditions Nutzungsbedingungen öffnen No comment provided by engineer. + + Open full link + alert action + Open group Gruppe öffnen @@ -5784,6 +5827,10 @@ Dies erfordert die Aktivierung eines VPNs. Zum Beitreten öffnen No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… App wird geöffnet… @@ -6529,6 +6576,10 @@ swipe action Bild entfernen No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Mitglied entfernen @@ -7377,7 +7428,7 @@ chat item action Set profile bio and welcome message. - Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein. + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. No comment provided by engineer. @@ -7914,6 +7965,10 @@ report reason Verbinden tippen, um die Anfrage zu senden No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tippen Sie im Menü auf SimpleX-Adresse erstellen, um sie später zu erstellen. @@ -8238,6 +8293,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro This setting is for your current profile **%@**. + Diese Einstellung gilt für Ihr aktuelles Profil **%@**. No comment provided by engineer. @@ -8327,6 +8383,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Für das Senden No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden. @@ -9224,7 +9284,7 @@ Verbindungsanfrage wiederholen? You can't send messages! Sie können keine Nachrichten versenden! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -10315,10 +10375,12 @@ time to disappear requested connection + Angefragte Verbindung rcv group event chat item requested connection from group %@ + Angefragte Verbindung von Gruppe %@ rcv direct event chat item 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 947f0a2d6c..7a6efcfba8 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -867,6 +867,11 @@ swipe action Allow downgrade No comment provided by engineer. + + Allow files and media only if your contact allows them. + 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) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -952,6 +957,11 @@ swipe action Allow your contacts to send disappearing messages. No comment provided by engineer. + + Allow your contacts to send files and media. + Allow your contacts to send files and media. + No comment provided by engineer. + Allow your contacts to send voice messages. Allow your contacts to send voice messages. @@ -1312,6 +1322,11 @@ swipe action Blur media No comment provided by engineer. + + Bot + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1332,6 +1347,11 @@ swipe action Both you and your contact can send disappearing messages. No comment provided by engineer. + + Both you and your contact can send files and media. + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Both you and your contact can send voice messages. @@ -2664,6 +2684,11 @@ swipe action Delivery receipts! No comment provided by engineer. + + Deprecated options + Deprecated options + No comment provided by engineer. + Description Description @@ -3796,6 +3821,11 @@ snd error text Files and media chat feature + + Files and media are prohibited in this chat. + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Files and media are prohibited. @@ -4497,7 +4527,7 @@ More improvements are coming soon! Invalid link Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5687,6 +5717,11 @@ Requires compatible VPN. Only you can send disappearing messages. No comment provided by engineer. + + Only you can send files and media. + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Only you can send voice messages. @@ -5712,6 +5747,11 @@ Requires compatible VPN. Only your contact can send disappearing messages. No comment provided by engineer. + + Only your contact can send files and media. + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Only your contact can send voice messages. @@ -5742,11 +5782,21 @@ Requires compatible VPN. Open chat console authentication reason + + Open clean link + Open clean link + alert action + Open conditions Open conditions No comment provided by engineer. + + Open full link + Open full link + alert action + Open group Open group @@ -5787,6 +5837,11 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + Open to use bot + No comment provided by engineer. + Opening app… Opening app… @@ -6532,6 +6587,11 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + Remove link tracking + No comment provided by engineer. + Remove member Remove member @@ -7917,6 +7977,11 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tap Create SimpleX address in the menu to create it later. @@ -8331,6 +8396,11 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. To support instant push notifications the chat database has to be migrated. @@ -9228,7 +9298,7 @@ Repeat join request? You can't send messages! You can't send messages! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 b58f716f8e..71415bb02d 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -859,7 +859,7 @@ swipe action Allow disappearing messages only if your contact allows it to you. - Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí. + Se permiten los mensajes temporales pero sólo si tu contacto también los permite. No comment provided by engineer. @@ -867,9 +867,13 @@ swipe action Permitir versión anterior 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) - Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas) + 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. @@ -952,6 +956,10 @@ swipe action Permites a tus contactos enviar mensajes temporales. 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. Permites a tus contactos enviar mensajes de voz. @@ -1312,6 +1320,10 @@ swipe action Difuminar multimedia No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1332,6 +1344,10 @@ swipe action Tanto tú como tu contacto podéis enviar mensajes temporales. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Tanto tú como tu contacto podéis enviar mensajes de voz. @@ -1626,7 +1642,7 @@ set passcode view Chat with admins - Chatea con los administradores + Chatea con administradores chat toolbar @@ -1676,7 +1692,7 @@ set passcode view Choose file - Elije archivo + Elegir archivo No comment provided by engineer. @@ -2087,6 +2103,7 @@ This is your own one-time link! Contact requests from groups + Solicitudes de contacto en grupo No comment provided by engineer. @@ -2663,6 +2680,10 @@ swipe action ¡Confirmación de entrega! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Descripción @@ -3531,6 +3552,7 @@ chat item action Error setting auto-accept + Error al configurar auto aceptar No comment provided by engineer. @@ -3794,6 +3816,10 @@ snd error text Archivos y multimedia chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Los archivos y multimedia no están permitidos en este grupo. @@ -4495,7 +4521,7 @@ More improvements are coming soon! Invalid link Enlace no válido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4605,17 +4631,17 @@ More improvements are coming soon! Join - Unirte + Unirme swipe action Join as %@ - unirte como %@ + Unirme como %@ No comment provided by engineer. Join group - Unirte al grupo + Unirme al grupo new chat sheet title @@ -4625,7 +4651,7 @@ More improvements are coming soon! Join incognito - Unirte en modo incógnito + Unirme en modo incógnito No comment provided by engineer. @@ -4872,6 +4898,7 @@ This is your link for group %@! Member is deleted - can't accept request + Miembro eliminado, no puede aceptar solicitudes No comment provided by engineer. @@ -4881,7 +4908,7 @@ This is your link for group %@! Member role will be changed to "%@". All chat members will be notified. - El rol del miembro cambiará a "%@" y todos serán notificados. + El rol del miembro cambiará a "%@". Se notificará en el chat. No comment provided by engineer. @@ -5071,7 +5098,7 @@ This is your link for group %@! Messages from %@ will be shown! - ¡Los mensajes de %@ serán mostrados! + ¡Los mensajes nuevos de %@ serán mostrados! No comment provided by engineer. @@ -5391,7 +5418,7 @@ This is your link for group %@! No chats with members - Sin chats con miembros + Sin chats No comment provided by engineer. @@ -5684,6 +5711,10 @@ Requiere activación de la VPN. Sólo tú puedes enviar mensajes temporales. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Sólo tú puedes enviar mensajes de voz. @@ -5709,6 +5740,10 @@ Requiere activación de la VPN. Sólo tu contacto puede enviar mensajes temporales. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Sólo tu contacto puede enviar mensajes de voz. @@ -5739,11 +5774,19 @@ Requiere activación de la VPN. Abrir consola de Chat authentication reason + + Open clean link + alert action + Open conditions Abrir condiciones No comment provided by engineer. + + Open full link + alert action + Open group Grupo abierto @@ -5781,7 +5824,11 @@ Requiere activación de la VPN. Open to join - Abrir para unirte + Abre para unirte + No comment provided by engineer. + + + Open to use bot No comment provided by engineer. @@ -6529,6 +6576,10 @@ swipe action Eliminar imagen No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Expulsar miembro @@ -7598,7 +7649,7 @@ chat item action SimpleX address or 1-time link? - ¿Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un solo uso? No comment provided by engineer. @@ -7914,6 +7965,10 @@ report reason Pulsa Conectar para enviar solicitud No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Pulsa Crear dirección SimpleX en el menú para crearla más tarde. @@ -7921,7 +7976,7 @@ report reason Tap Join group - Pulsa Unirte al grupo + Pulsa Unirme al grupo No comment provided by engineer. @@ -7951,7 +8006,7 @@ report reason Tap to paste link - Pulsa para pegar el enlacePulsa para pegar enlace + Pulsa aquí para pegar el enlace No comment provided by engineer. @@ -8203,7 +8258,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This display name is invalid. Please choose another name. - Éste nombre mostrado no es válido. Por favor, elije otro nombre. + Éste nombre mostrado no es válido. Por favor, elige otro nombre. No comment provided by engineer. @@ -8238,6 +8293,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. This setting is for your current profile **%@**. + Esta configuración se aplica al perfil actual **%@**. No comment provided by engineer. @@ -8327,6 +8383,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para enviar No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar. @@ -8429,7 +8489,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Unblock member for all? - ¿Desbloquear el miembro para todos? + ¿Desbloquear al miembro para todos? No comment provided by engineer. @@ -9218,13 +9278,13 @@ Repeat join request? You can view your reports in Chat with admins. - Puedes ver tus informes en Chat con los administradores. + Puedes ver tus informes en Chat con administradores. alert message You can't send messages! ¡No puedes enviar mensajes! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -9340,12 +9400,12 @@ Repeat connection request? You will stop receiving messages from this chat. Chat history will be preserved. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. No comment provided by engineer. You will stop receiving messages from this group. Chat history will be preserved. - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. No comment provided by engineer. @@ -9796,12 +9856,12 @@ marked deleted chat item preview text contact not ready - el contacto no está listo + en espera de ser aceptado No comment provided by engineer. contact should accept… - el contacto debe aceptar… + el contacto debe aceptarte… No comment provided by engineer. @@ -10315,10 +10375,12 @@ time to disappear requested connection + conexión solicitada rcv group event chat item requested connection from group %@ + conexión solicitada desde el grupo %@ rcv direct event chat item 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 b5ba7bb864..de08d5f39e 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -792,6 +792,10 @@ swipe action 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) Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) @@ -874,6 +878,10 @@ swipe action Salli kontaktiesi lähettää katoavia viestejä. 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. Salli kontaktiesi lähettää ääniviestejä. @@ -1191,6 +1199,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -1211,6 +1223,10 @@ swipe action Sekä sinä että kontaktisi voitte lähettää katoavia viestejä. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sekä sinä että kontaktisi voitte lähettää ääniviestejä. @@ -2410,6 +2426,10 @@ swipe action Toimituskuittaukset! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Kuvaus @@ -3437,6 +3457,10 @@ snd error text Tiedostot ja media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Tiedostot ja media ovat tässä ryhmässä kiellettyjä. @@ -4076,7 +4100,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5152,6 +5176,10 @@ Edellyttää VPN:n sallimista. Vain sinä voit lähettää katoavia viestejä. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vain sinä voit lähettää ääniviestejä. @@ -5177,6 +5205,10 @@ Edellyttää VPN:n sallimista. Vain kontaktisi voi lähettää katoavia viestejä. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Vain kontaktisi voi lähettää ääniviestejä. @@ -5205,10 +5237,18 @@ Edellyttää VPN:n sallimista. Avaa keskustelukonsoli authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5241,6 +5281,10 @@ Edellyttää VPN:n sallimista. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -5910,6 +5954,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Poista jäsen @@ -7154,6 +7202,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7528,6 +7580,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. @@ -8328,7 +8384,7 @@ Repeat join request? You can't send messages! Et voi lähettää viestejä! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 8072b5784b..bfc09b29ec 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -861,6 +861,10 @@ swipe action Autoriser la rétrogradation 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) Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures) @@ -946,6 +950,10 @@ swipe action Autorise votre contact à envoyer des messages éphémères. 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. Autorise vos contacts à envoyer des messages vocaux. @@ -1303,6 +1311,10 @@ swipe action Flouter les médias No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1323,6 +1335,10 @@ swipe action Vous et votre contact êtes tous deux en mesure d'envoyer des messages éphémères. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux. @@ -2645,6 +2661,10 @@ swipe action Justificatifs de réception ! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Description @@ -3767,6 +3787,10 @@ snd error text Fichiers et médias chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Les fichiers et les médias sont interdits dans ce groupe. @@ -4454,7 +4478,7 @@ D'autres améliorations sont à venir ! Invalid link Lien invalide - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5611,6 +5635,10 @@ Nécessite l'activation d'un VPN. Seulement vous pouvez envoyer des messages éphémères. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Vous seul pouvez envoyer des messages vocaux. @@ -5636,6 +5664,10 @@ Nécessite l'activation d'un VPN. Seulement votre contact peut envoyer des messages éphémères. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Seul votre contact peut envoyer des messages vocaux. @@ -5666,11 +5698,19 @@ Nécessite l'activation d'un VPN. Ouvrir la console du chat authentication reason + + Open clean link + alert action + Open conditions Ouvrir les conditions No comment provided by engineer. + + Open full link + alert action + Open group Ouvrir le groupe @@ -5705,6 +5745,10 @@ Nécessite l'activation d'un VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Ouverture de l'app… @@ -6435,6 +6479,10 @@ swipe action Enlever l'image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Retirer le membre @@ -7779,6 +7827,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Appuyez sur Créer une adresse SimpleX dans le menu pour la créer ultérieurement. @@ -8184,6 +8236,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour envoyer No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée. @@ -9065,7 +9121,7 @@ Répéter la demande d'adhésion ? You can't send messages! Vous ne pouvez pas envoyer de messages ! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 1339f0ce74..a23021b67e 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -867,6 +867,10 @@ swipe action Visszafejlesztés engedélyezése 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) Az üzenetek végleges törlése csak abban az esetben van engedélyezve, ha a partnere is engedélyezi. (24 óra) @@ -952,6 +956,10 @@ swipe action Az eltűnő üzenetek küldésének engedélyezése a partnerei számára. 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. A hangüzenetek küldése engedélyezve van a partnerei számára. @@ -1312,6 +1320,10 @@ swipe action Médiatartalom elhomályosítása No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Mindkét fél hozzáadhat az üzenetekhez reakciókat. @@ -1332,6 +1344,10 @@ swipe action Mindkét fél küldhet eltűnő üzeneteket. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Mindkét fél küldhet hangüzeneteket. @@ -2087,6 +2103,7 @@ Ez a saját egyszer használható meghívója! Contact requests from groups + Partneri kapcsolatkérések a csoportokból No comment provided by engineer. @@ -2663,6 +2680,10 @@ swipe action Kézbesítési jelentések! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Leírás @@ -3531,6 +3552,7 @@ chat item action Error setting auto-accept + Hiba az automatikus elfogadás beállításakor No comment provided by engineer. @@ -3794,6 +3816,10 @@ snd error text Fájlok és médiatartalmak chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. A fájlok- és a médiatartalmak küldése le van tiltva. @@ -4495,7 +4521,7 @@ További fejlesztések hamarosan! Invalid link Érvénytelen hivatkozás - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4872,6 +4898,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz! Member is deleted - can't accept request + A tag törölve lett – nem lehet elfogadni a kérést No comment provided by engineer. @@ -5684,6 +5711,10 @@ VPN engedélyezése szükséges. Csak Ön tud eltűnő üzeneteket küldeni. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Csak Ön tud hangüzeneteket küldeni. @@ -5709,6 +5740,10 @@ VPN engedélyezése szükséges. Csak a partnere tud eltűnő üzeneteket küldeni. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Csak a partnere tud hangüzeneteket küldeni. @@ -5739,11 +5774,19 @@ VPN engedélyezése szükséges. Csevegési konzol megnyitása authentication reason + + Open clean link + alert action + Open conditions Feltételek megnyitása No comment provided by engineer. + + Open full link + alert action + Open group Csoport megnyitása @@ -5784,6 +5827,10 @@ VPN engedélyezése szükséges. Megnyitás a csatlakozáshoz No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Az alkalmazás megnyitása… @@ -6529,6 +6576,10 @@ swipe action Kép eltávolítása No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Eltávolítás @@ -7914,6 +7965,10 @@ report reason Koppintson a „Kapcsolódás” gombra a kérés elküldéséhez No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Koppintson a SimpleX-cím létrehozása menüpontra a későbbi létrehozáshoz. @@ -8238,6 +8293,7 @@ Ez valamilyen hiba vagy sérült kapcsolat esetén fordulhat elő. 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. @@ -8327,6 +8383,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll A küldéshez No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. @@ -9224,7 +9284,7 @@ Megismétli a csatlakozási kérést? You can't send messages! Nem lehet üzeneteket küldeni! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -10137,7 +10197,7 @@ pref value moderated by %@ - moderálva lett %@ által + %@ moderálta ezt az üzenetet marked deleted chat item preview text @@ -10315,10 +10375,12 @@ time to disappear requested connection + partneri kapcsolatot kért rcv group event chat item requested connection from group %@ + a(z) %@ nevű csoportból partneri kapcsolatot kért rcv direct event chat item diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 4b51e9dae3..7f789cb85c 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -867,6 +867,10 @@ swipe action Consenti 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) Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore) @@ -952,6 +956,10 @@ swipe action Permetti ai tuoi contatti di inviare messaggi a tempo. 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. Permetti ai tuoi contatti di inviare messaggi vocali. @@ -1312,6 +1320,10 @@ swipe action Sfocatura dei file multimediali No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1332,6 +1344,10 @@ swipe action Sia tu che il tuo contatto potete inviare messaggi a tempo. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sia tu che il tuo contatto potete inviare messaggi vocali. @@ -2087,6 +2103,7 @@ Questo è il tuo link una tantum! Contact requests from groups + Richieste di contatto dai gruppi No comment provided by engineer. @@ -2663,6 +2680,10 @@ swipe action Ricevute di consegna! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Descrizione @@ -3531,6 +3552,7 @@ chat item action Error setting auto-accept + Errore impostando l'accettazione automatica No comment provided by engineer. @@ -3794,6 +3816,10 @@ snd error text File e multimediali chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. File e contenuti multimediali sono vietati in questo gruppo. @@ -4495,7 +4521,7 @@ Altri miglioramenti sono in arrivo! Invalid link Link non valido - No comment provided by engineer. + alert title Invalid migration confirmation @@ -4872,6 +4898,7 @@ Questo è il tuo link per il gruppo %@! Member is deleted - can't accept request + Il membro è eliminato - impossibile accettare la richiesta No comment provided by engineer. @@ -5684,6 +5711,10 @@ Richiede l'attivazione della VPN. Solo tu puoi inviare messaggi a tempo. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Solo tu puoi inviare messaggi vocali. @@ -5709,6 +5740,10 @@ Richiede l'attivazione della VPN. Solo il tuo contatto può inviare messaggi a tempo. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Solo il tuo contatto può inviare messaggi vocali. @@ -5739,11 +5774,19 @@ Richiede l'attivazione della VPN. Apri la console della chat authentication reason + + Open clean link + alert action + Open conditions Apri le condizioni No comment provided by engineer. + + Open full link + alert action + Open group Apri gruppo @@ -5784,6 +5827,10 @@ Richiede l'attivazione della VPN. Apri per entrare No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Apertura dell'app… @@ -6529,6 +6576,10 @@ swipe action Rimuovi immagine No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Rimuovi membro @@ -7914,6 +7965,10 @@ report reason Tocca Connetti per inviare la richiesta No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tocca Crea indirizzo SimpleX nel menu per crearlo più tardi. @@ -8238,6 +8293,7 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa. This setting is for your current profile **%@**. + Questa impostazione è per il tuo profilo attuale **%@**. No comment provided by engineer. @@ -8327,6 +8383,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per inviare No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Per supportare le notifiche push istantanee, il database della chat deve essere migrato. @@ -9224,7 +9284,7 @@ Ripetere la richiesta di ingresso? You can't send messages! Non puoi inviare messaggi! - No comment provided by engineer. + alert title You could not be verified; please try again. @@ -10315,10 +10375,12 @@ time to disappear requested connection + connessione richiesta rcv group event chat item requested connection from group %@ + connessione richiesta dal gruppo %@ rcv direct event chat item 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 743c5392e6..2c34ae3499 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -192,6 +192,7 @@ %d seconds(s) + %d 秒 delete after time @@ -206,7 +207,6 @@ %lld - No comment provided by engineer. @@ -465,6 +465,7 @@ time interval 1 year + 1年 delete after time @@ -548,6 +549,7 @@ time interval About operators + オペレーターについて No comment provided by engineer. @@ -564,14 +566,17 @@ swipe action Accept as member + メンバーとして承認する alert action Accept as observer + オブザーバーとして承認する alert action Accept conditions + 条件に同意する No comment provided by engineer. @@ -581,6 +586,7 @@ swipe action Accept contact request + 連絡先リクエストを受け入れる alert title @@ -596,10 +602,12 @@ swipe action Accept member + メンバーを承認する alert title Accepted conditions + 承諾された条件 No comment provided by engineer. @@ -831,6 +839,10 @@ swipe action 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時間) @@ -915,6 +927,10 @@ swipe action 送信相手が消えるメッセージを送るのを許可する。 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. 送信相手からの音声メッセージを許可する。 @@ -1240,6 +1256,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1260,6 +1280,10 @@ swipe action あなたと連絡相手が消えるメッセージを送信できます。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. あなたと連絡相手が音声メッセージを送信できます。 @@ -2480,6 +2504,10 @@ swipe action 配信通知! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 説明 @@ -3510,6 +3538,10 @@ snd error text ファイルとメディア chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. このグループでは、ファイルとメディアは禁止されています。 @@ -4149,7 +4181,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5228,6 +5260,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなただけです。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 音声メッセージを送れるのはあなただけです。 @@ -5253,6 +5289,10 @@ VPN を有効にする必要があります。 消えるメッセージを送れるのはあなたの連絡相手だけです。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 音声メッセージを送れるのはあなたの連絡相手だけです。 @@ -5282,10 +5322,18 @@ VPN を有効にする必要があります。 チャットのコンソールを開く authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5318,6 +5366,10 @@ VPN を有効にする必要があります。 Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -5987,6 +6039,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member メンバーを除名する @@ -7225,6 +7281,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7598,6 +7658,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。 @@ -8399,7 +8463,7 @@ Repeat join request? You can't send messages! メッセージを送信できませんでした! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 6d65bd070f..4079a6cd9c 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -865,6 +865,10 @@ swipe action Downgraden toestaan 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) Sta het definitief verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) @@ -950,6 +954,10 @@ swipe action Sta toe dat uw contacten verdwijnende berichten verzenden. 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. Sta toe dat uw contacten spraak berichten verzenden. @@ -1308,6 +1316,10 @@ swipe action Vervaag media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zowel u als uw contact kunnen bericht reacties toevoegen. @@ -1328,6 +1340,10 @@ swipe action Zowel jij als je contact kunnen verdwijnende berichten sturen. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zowel jij als je contact kunnen spraak berichten verzenden. @@ -2654,6 +2670,10 @@ swipe action Ontvangstbewijzen! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Beschrijving @@ -3778,6 +3798,10 @@ snd error text Bestanden en media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Bestanden en media zijn niet toegestaan. @@ -4478,7 +4502,7 @@ Binnenkort meer verbeteringen! Invalid link Ongeldige link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5660,6 +5684,10 @@ Vereist het inschakelen van VPN. Alleen jij kunt verdwijnende berichten verzenden. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Alleen jij kunt spraak berichten verzenden. @@ -5685,6 +5713,10 @@ Vereist het inschakelen van VPN. Alleen uw contact kan verdwijnende berichten verzenden. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Alleen uw contact kan spraak berichten verzenden. @@ -5715,11 +5747,19 @@ Vereist het inschakelen van VPN. Chat console openen authentication reason + + Open clean link + alert action + Open conditions Open voorwaarden No comment provided by engineer. + + Open full link + alert action + Open group Open groep @@ -5755,6 +5795,10 @@ Vereist het inschakelen van VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… App openen… @@ -6498,6 +6542,10 @@ swipe action Verwijder afbeelding No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Lid verwijderen @@ -7866,6 +7914,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Tik op SimpleX-adres maken in het menu om het later te maken. @@ -8275,6 +8327,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Om te verzenden No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd. @@ -9163,7 +9219,7 @@ Deelnameverzoek herhalen? You can't send messages! Je kunt geen berichten versturen! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 5dd679dfff..a0a929ac63 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -861,6 +861,10 @@ swipe action Zezwól na obniżenie wersji 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) Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) @@ -946,6 +950,10 @@ swipe action Zezwól swoim kontaktom na wysyłanie znikających wiadomości. 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. Zezwól swoim kontaktom na wysyłanie wiadomości głosowych. @@ -1302,6 +1310,10 @@ swipe action Rozmycie mediów No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1322,6 +1334,10 @@ swipe action Zarówno Ty, jak i Twój kontakt możecie wysyłać znikające wiadomości. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. @@ -2614,6 +2630,10 @@ swipe action Potwierdzenia dostawy! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Opis @@ -3708,6 +3728,10 @@ snd error text Pliki i media chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Pliki i media są zabronione w tej grupie. @@ -4386,7 +4410,7 @@ More improvements are coming soon! Invalid link Nieprawidłowy link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5525,6 +5549,10 @@ Wymaga włączenia VPN. Tylko Ty możesz wysyłać znikające wiadomości. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Tylko Ty możesz wysyłać wiadomości głosowe. @@ -5550,6 +5578,10 @@ Wymaga włączenia VPN. Tylko Twój kontakt może wysyłać znikające wiadomości. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Tylko Twój kontakt może wysyłać wiadomości głosowe. @@ -5579,10 +5611,18 @@ Wymaga włączenia VPN. Otwórz konsolę czatu authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group Grupa otwarta @@ -5617,6 +5657,10 @@ Wymaga włączenia VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Otwieranie aplikacji… @@ -6341,6 +6385,10 @@ swipe action Usuń obraz No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Usuń członka @@ -7669,6 +7717,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -8064,6 +8116,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. @@ -8933,7 +8989,7 @@ Powtórzyć prośbę dołączenia? You can't send messages! Nie możesz wysyłać wiadomości! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 eeed89c00e..01b8f7e244 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -39,7 +39,7 @@ Available in v5.1 (can be copied) - .(pode ser copiado) + (pode ser copiado) 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 156c341f8a..d4f37a9b35 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -867,6 +867,10 @@ swipe action Разрешить прямую доставку 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 часа) @@ -952,6 +956,10 @@ swipe action Разрешить Вашим контактам отправлять исчезающие сообщения. 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. Разрешить Вашим контактам отправлять голосовые сообщения. @@ -1312,6 +1320,10 @@ swipe action Размытие изображений No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1332,6 +1344,10 @@ swipe action Вы и Ваш контакт можете отправлять исчезающие сообщения. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Вы и Ваш контакт можете отправлять голосовые сообщения. @@ -2664,6 +2680,10 @@ swipe action Отчёты о доставке! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Описание @@ -3796,6 +3816,10 @@ snd error text Файлы и медиа chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файлы и медиа запрещены в этой группе. @@ -4496,7 +4520,7 @@ More improvements are coming soon! Invalid link Ошибка ссылки - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5686,6 +5710,10 @@ Requires compatible VPN. Только Вы можете отправлять исчезающие сообщения. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Только Вы можете отправлять голосовые сообщения. @@ -5711,6 +5739,10 @@ Requires compatible VPN. Только Ваш контакт может отправлять исчезающие сообщения. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Только Ваш контакт может отправлять голосовые сообщения. @@ -5741,11 +5773,19 @@ Requires compatible VPN. Открыть консоль authentication reason + + Open clean link + alert action + Open conditions Открыть условия No comment provided by engineer. + + Open full link + alert action + Open group Открыть группу @@ -5786,6 +5826,10 @@ Requires compatible VPN. Откройте чтобы вступить No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Приложение отрывается… @@ -6531,6 +6575,10 @@ swipe action Удалить изображение No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Удалить члена группы @@ -7916,6 +7964,10 @@ report reason Нажмите Соединиться, чтобы отправить запрос No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Нажмите Создать адрес SimpleX в меню, чтобы создать его позже. @@ -8330,6 +8382,10 @@ You will be prompted to complete authentication before this feature is enabled.< Для оправки No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. @@ -9227,7 +9283,7 @@ Repeat join request? You can't send messages! Вы не можете отправлять сообщения! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 b7fbf073bd..d2f4b979d8 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -784,6 +784,10 @@ swipe action 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) อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น @@ -866,6 +870,10 @@ swipe action อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (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. อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง @@ -1183,6 +1191,10 @@ swipe action Blur media No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -1203,6 +1215,10 @@ swipe action ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้ No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้ @@ -2398,6 +2414,10 @@ swipe action ใบตอบรับการจัดส่ง! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description คำอธิบาย @@ -3422,6 +3442,10 @@ snd error text ไฟล์และสื่อ chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้ @@ -4060,7 +4084,7 @@ More improvements are coming soon! Invalid link - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5131,6 +5155,10 @@ Requires compatible VPN. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -5156,6 +5184,10 @@ Requires compatible VPN. เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้ No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้ @@ -5184,10 +5216,18 @@ Requires compatible VPN. เปิดคอนโซลการแชท authentication reason + + Open clean link + alert action + Open conditions No comment provided by engineer. + + Open full link + alert action + Open group new chat action @@ -5220,6 +5260,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… No comment provided by engineer. @@ -5887,6 +5931,10 @@ swipe action Remove image No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member ลบสมาชิกออก @@ -7127,6 +7175,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -7500,6 +7552,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย @@ -8298,7 +8354,7 @@ Repeat join request? You can't send messages! คุณไม่สามารถส่งข้อความได้! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 cd07746337..0e758dc1d7 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -867,6 +867,10 @@ swipe action Sürüm düşürmeye izin ver 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) Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde) @@ -952,6 +956,10 @@ swipe action Kişilerinizin kaybolan mesajlar göndermesine izin verin. 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. Kişilerinizin sesli mesajlar göndermesine izin verin. @@ -1312,6 +1320,10 @@ swipe action Medyayı bulanıklaştır No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1332,6 +1344,10 @@ swipe action Sen ve konuştuğun kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Sen ve konuştuğun kişi sesli mesaj gönderebilir. @@ -2087,6 +2103,7 @@ Bu senin kendi tek kullanımlık bağlantın! Contact requests from groups + Gruplardan gelen iletişim talepleri No comment provided by engineer. @@ -2206,6 +2223,7 @@ Bu senin kendi tek kullanımlık bağlantın! Create your address + Adresinizi oluşturun No comment provided by engineer. @@ -2662,6 +2680,10 @@ swipe action Mesaj gönderildi bilgisi! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Açıklama @@ -3020,6 +3042,7 @@ chat item action Enable disappearing messages by default. + Varsayılan olarak kaybolan mesajları etkinleştirin. No comment provided by engineer. @@ -3792,6 +3815,10 @@ snd error text Dosyalar ve medya chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Dosyalar ve medya bu grupta yasaklandı. @@ -4493,7 +4520,7 @@ Daha fazla iyileştirme yakında geliyor! Invalid link Geçersiz bağlantı - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5681,6 +5708,10 @@ VPN'nin etkinleştirilmesi gerekir. Sadece sen kaybolan mesajlar gönderebilirsin. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Sadece sen sesli mesajlar gönderebilirsin. @@ -5706,6 +5737,10 @@ VPN'nin etkinleştirilmesi gerekir. Sadece karşıdaki kişi kaybolan mesajlar gönderebilir. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Sadece karşıdaki kişi sesli mesajlar gönderebilir. @@ -5736,11 +5771,19 @@ VPN'nin etkinleştirilmesi gerekir. Sohbet konsolunu aç authentication reason + + Open clean link + alert action + Open conditions Açık koşullar No comment provided by engineer. + + Open full link + alert action + Open group Grubu aç @@ -5781,6 +5824,10 @@ VPN'nin etkinleştirilmesi gerekir. Katılmak için aç No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Uygulama açılıyor… @@ -6526,6 +6573,10 @@ swipe action Resmi kaldır No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Kişiyi sil @@ -7906,6 +7957,10 @@ report reason Bağlan'a dokunarak isteği gönderin No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Daha sonra oluşturmak için menüden BasitX adresi oluştur'a dokunun. @@ -8316,6 +8371,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Göndermek için No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir. @@ -9205,7 +9264,7 @@ Katılma isteği tekrarlansın mı? You can't send messages! Mesajlar gönderemezsiniz! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 9b6ff96324..555baafd69 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -867,6 +867,10 @@ swipe action Дозволити пониження версії 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 години) @@ -952,6 +956,10 @@ swipe action Дозвольте своїм контактам надсилати зникаючі повідомлення. 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. Дозвольте своїм контактам надсилати голосові повідомлення. @@ -1312,6 +1320,10 @@ swipe action Розмиття медіа No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1332,6 +1344,10 @@ swipe action Ви і ваш контакт можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. Надсилати голосові повідомлення можете як ви, так і ваш контакт. @@ -2663,6 +2679,10 @@ swipe action Квитанції про доставку! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description Опис @@ -3794,6 +3814,10 @@ snd error text Файли і медіа chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. Файли та медіа в цій групі заборонені. @@ -4495,7 +4519,7 @@ More improvements are coming soon! Invalid link Невірне посилання - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5684,6 +5708,10 @@ Requires compatible VPN. Тільки ви можете надсилати зникаючі повідомлення. No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. Тільки ви можете надсилати голосові повідомлення. @@ -5709,6 +5737,10 @@ Requires compatible VPN. Тільки ваш контакт може надсилати зникаючі повідомлення. No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. Тільки ваш контакт може надсилати голосові повідомлення. @@ -5739,11 +5771,19 @@ Requires compatible VPN. Відкрийте консоль чату authentication reason + + Open clean link + alert action + Open conditions Відкриті умови No comment provided by engineer. + + Open full link + alert action + Open group Відкрита група @@ -5784,6 +5824,10 @@ Requires compatible VPN. Відкрито для приєднання No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… Відкриваємо програму… @@ -6529,6 +6573,10 @@ swipe action Видалити зображення No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member Видалити учасника @@ -7914,6 +7962,10 @@ report reason Натисніть Підключитися, щоб відправити запит No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. Натисніть «Створити адресу SimpleX» у меню, щоб створити її пізніше. @@ -8327,6 +8379,10 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб відправити No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату. @@ -9224,7 +9280,7 @@ Repeat join request? You can't send messages! Ви не можете надсилати повідомлення! - No comment provided by engineer. + alert title You could not be verified; please try again. 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 fce4207e84..562b623217 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 @@ -862,6 +862,10 @@ swipe action 允许降级 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) 仅有您的联系人许可后才允许不可撤回消息移除 @@ -947,6 +951,10 @@ swipe action 允许您的联系人发送限时消息。 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. 允许您的联系人发送语音消息。 @@ -1305,6 +1313,10 @@ swipe action 模糊媒体 No comment provided by engineer. + + Bot + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -1325,6 +1337,10 @@ swipe action 您和您的联系人都可以发送限时消息。 No comment provided by engineer. + + Both you and your contact can send files and media. + No comment provided by engineer. + Both you and your contact can send voice messages. 您和您的联系人都可以发送语音消息。 @@ -2646,6 +2662,10 @@ swipe action 送达回执! No comment provided by engineer. + + Deprecated options + No comment provided by engineer. + Description 描述 @@ -3767,6 +3787,10 @@ snd error text 文件和媒体 chat feature + + Files and media are prohibited in this chat. + No comment provided by engineer. + Files and media are prohibited. 此群组中禁止文件和媒体。 @@ -4467,7 +4491,7 @@ More improvements are coming soon! Invalid link 无效链接 - No comment provided by engineer. + alert title Invalid migration confirmation @@ -5645,6 +5669,10 @@ Requires compatible VPN. 只有您可以发送限时消息。 No comment provided by engineer. + + Only you can send files and media. + No comment provided by engineer. + Only you can send voice messages. 只有您可以发送语音消息。 @@ -5670,6 +5698,10 @@ Requires compatible VPN. 只有您的联系人才可以发送限时消息。 No comment provided by engineer. + + Only your contact can send files and media. + No comment provided by engineer. + Only your contact can send voice messages. 只有您的联系人可以发送语音消息。 @@ -5700,11 +5732,19 @@ Requires compatible VPN. 打开聊天控制台 authentication reason + + Open clean link + alert action + Open conditions 打开条款 No comment provided by engineer. + + Open full link + alert action + Open group 打开群 @@ -5739,6 +5779,10 @@ Requires compatible VPN. Open to join No comment provided by engineer. + + Open to use bot + No comment provided by engineer. + Opening app… 正在打开应用程序… @@ -6465,6 +6509,10 @@ swipe action 移除图片 No comment provided by engineer. + + Remove link tracking + No comment provided by engineer. + Remove member 删除成员 @@ -7794,6 +7842,10 @@ report reason Tap Connect to send request No comment provided by engineer. + + Tap Connect to use bot + No comment provided by engineer. + Tap Create SimpleX address in the menu to create it later. No comment provided by engineer. @@ -8186,6 +8238,10 @@ You will be prompted to complete authentication before this feature is enabled.< To send No comment provided by engineer. + + To send commands you must be connected. + alert message + To support instant push notifications the chat database has to be migrated. 为了支持即时推送通知,聊天数据库必须被迁移。 @@ -9053,7 +9109,7 @@ Repeat join request? You can't send messages! 您无法发送消息! - No comment provided by engineer. + alert title You could not be verified; please try again. diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 12a775f85c..5080cf2040 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -390,7 +390,7 @@ enum SharedContent { switch self { case let .image(preview, _): .image(text: comment, image: preview) case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) - case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) + case let .url(preview): .link(text: preview.uri + (comment == "" ? "" : "\n" + comment), preview: preview) case .text: .text(comment) case .data: .file(comment) } @@ -464,12 +464,13 @@ fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result ParsedUri? { + var c = s.cString(using: .utf8)! + if let cjson = chat_parse_uri(&c) { + if let d = dataFromCString(cjson) { + do { + return try jsonDecoder.decode(ParsedUri.self, from: d) + } catch { + logger.error("parseSanitizeUri jsonDecoder.decode error: \(error.localizedDescription)") + } + } + } + return nil +} + +public struct ParsedUri: Decodable { + public var uriInfo: UriInfo? + public var parseError: String +} + +public struct UriInfo: Decodable { + public var scheme: String + public var sanitized: String? +} + @inline(__always) public func fromCString(_ c: UnsafeMutablePointer) -> String { let s = String.init(cString: c) diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 5eed01a2a2..47932397fc 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -27,6 +27,8 @@ let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled" public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" // replaces DEFAULT_PRIVACY_LINK_PREVIEWS let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" +public let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "privacyLinkPreviewsShowAlert" +public let GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS = "privacySanitizeLinks" // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used @@ -95,6 +97,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT: true, + GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, @@ -222,6 +226,8 @@ public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) +public let privacyLinkPreviewsShowAlertGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT) + // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index db370efdc1..7a70c6b664 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -115,7 +115,8 @@ public struct Profile: Codable, NamedChat, Hashable { shortDescr: String? = nil, image: String? = nil, contactLink: String? = nil, - preferences: Preferences? = nil + preferences: Preferences? = nil, + peerType: ChatPeerType? = nil ) { self.displayName = displayName self.fullName = fullName @@ -131,6 +132,7 @@ public struct Profile: Codable, NamedChat, Hashable { public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String { get { "" } } var profileViewName: String { @@ -152,6 +154,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { image: String? = nil, contactLink: String? = nil, preferences: Preferences? = nil, + peerType: ChatPeerType? = nil, localAlias: String ) { self.profileId = profileId @@ -161,6 +164,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { self.image = image self.contactLink = contactLink self.preferences = preferences + self.peerType = peerType self.localAlias = localAlias } @@ -171,6 +175,7 @@ public struct LocalProfile: Codable, NamedChat, Hashable { public var image: String? public var contactLink: String? public var preferences: Preferences? + public var peerType: ChatPeerType? public var localAlias: String var profileViewName: String { @@ -188,6 +193,11 @@ public struct LocalProfile: Codable, NamedChat, Hashable { ) } +public enum ChatPeerType: String, Codable { + case human + case bot +} + public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias: String) -> LocalProfile { LocalProfile( profileId: profileId, @@ -197,6 +207,7 @@ public func toLocalProfile (_ profileId: Int64, _ profile: Profile, _ localAlias image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences, + peerType: profile.peerType, localAlias: localAlias ) } @@ -208,7 +219,8 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { shortDescr: profile.shortDescr, image: profile.image, contactLink: profile.contactLink, - preferences: profile.preferences + preferences: profile.preferences, + peerType: profile.peerType ) } @@ -249,20 +261,26 @@ public struct FullPreferences: Decodable, Equatable, Hashable { public var fullDelete: SimplePreference public var reactions: SimplePreference public var voice: SimplePreference + public var files: SimplePreference public var calls: SimplePreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesPreference, fullDelete: SimplePreference, reactions: SimplePreference, voice: SimplePreference, - calls: SimplePreference + files: SimplePreference, + calls: SimplePreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = FullPreferences( @@ -270,7 +288,9 @@ public struct FullPreferences: Decodable, Equatable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: [] ) } @@ -279,20 +299,26 @@ public struct Preferences: Codable, Hashable { public var fullDelete: SimplePreference? public var reactions: SimplePreference? public var voice: SimplePreference? + public var files: SimplePreference? public var calls: SimplePreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesPreference?, fullDelete: SimplePreference?, reactions: SimplePreference?, voice: SimplePreference?, - calls: SimplePreference? + files: SimplePreference?, + calls: SimplePreference?, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } func copy( @@ -300,14 +326,18 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference? = nil, reactions: SimplePreference? = nil, voice: SimplePreference? = nil, - calls: SimplePreference? = nil + files: SimplePreference? = nil, + calls: SimplePreference? = nil, + commands: [ChatBotCommand]? = nil ) -> Preferences { Preferences( timedMessages: timedMessages ?? self.timedMessages, fullDelete: fullDelete ?? self.fullDelete, reactions: reactions ?? self.reactions, voice: voice ?? self.voice, - calls: calls ?? self.calls + files: files ?? self.files, + calls: calls ?? self.calls, + commands: commands ?? self.commands ) } @@ -317,6 +347,7 @@ public struct Preferences: Codable, Hashable { case .fullDelete: return copy(fullDelete: SimplePreference(allow: allowed)) case .reactions: return copy(reactions: SimplePreference(allow: allowed)) case .voice: return copy(voice: SimplePreference(allow: allowed)) + case .files: return copy(voice: SimplePreference(allow: allowed)) case .calls: return copy(calls: SimplePreference(allow: allowed)) } } @@ -326,17 +357,72 @@ public struct Preferences: Codable, Hashable { fullDelete: SimplePreference(allow: .no), reactions: SimplePreference(allow: .yes), voice: SimplePreference(allow: .yes), - calls: SimplePreference(allow: .yes) + files: SimplePreference(allow: .always), + calls: SimplePreference(allow: .yes), + commands: nil ) } +public indirect enum ChatBotCommand: Hashable { + case command(keyword: String, label: String, params: String?) + case menu(label: String, commands: [ChatBotCommand]) + + enum CodingKeys: String, CodingKey { + case type + case keyword + case label + case params + case hidden + case commands + } +} + +extension ChatBotCommand: Decodable { + public init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + let type = try c.decode(String.self, forKey: CodingKeys.type) + switch type { + case "command": + let keyword = try c.decode(String.self, forKey: CodingKeys.keyword) + let label = try c.decode(String.self, forKey: CodingKeys.label) + let params = c.contains(CodingKeys.params) ? try c.decode((String?).self, forKey: CodingKeys.params) : nil + self = .command(keyword: keyword, label: label, params: params) + case "menu": + let label = try c.decode(String.self, forKey: CodingKeys.label) + let commands = try c.decode(([ChatBotCommand]).self, forKey: CodingKeys.commands) + self = .menu(label: label, commands: commands) + default: + throw DecodingError.dataCorruptedError(forKey: CodingKeys.type, in: c, debugDescription: "Unsupported command type: \(type)") + } + } +} + +extension ChatBotCommand: Encodable { + public func encode(to encoder: Encoder) throws { + var c = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .command(keyword, label, params): + try c.encode("command", forKey: .type) + try c.encode(keyword, forKey: .keyword) + try c.encode(label, forKey: .label) + if let params { try c.encode(params, forKey: .params) } + case let .menu(label, commands): + try c.encode("menu", forKey: .type) + try c.encode(label, forKey: .label) + try c.encode(commands, forKey: .commands) + } + } +} + public func fullPreferencesToPreferences(_ fullPreferences: FullPreferences) -> Preferences { Preferences( timedMessages: fullPreferences.timedMessages, fullDelete: fullPreferences.fullDelete, reactions: fullPreferences.reactions, voice: fullPreferences.voice, - calls: fullPreferences.calls + files: fullPreferences.files, + calls: fullPreferences.calls, + commands: fullPreferences.commands ) } @@ -346,7 +432,9 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac fullDelete: contactUserPreferences.fullDelete.userPreference.preference, reactions: contactUserPreferences.reactions.userPreference.preference, voice: contactUserPreferences.voice.userPreference.preference, - calls: contactUserPreferences.calls.userPreference.preference + files: contactUserPreferences.files.userPreference.preference, + calls: contactUserPreferences.calls.userPreference.preference, + commands: contactUserPreferences.commands ) } @@ -484,20 +572,26 @@ public struct ContactUserPreferences: Decodable, Hashable { public var fullDelete: ContactUserPreference public var reactions: ContactUserPreference public var voice: ContactUserPreference + public var files: ContactUserPreference public var calls: ContactUserPreference + public var commands: [ChatBotCommand]? public init( timedMessages: ContactUserPreference, fullDelete: ContactUserPreference, reactions: ContactUserPreference, voice: ContactUserPreference, - calls: ContactUserPreference + files: ContactUserPreference, + calls: ContactUserPreference, + commands: [ChatBotCommand]? ) { self.timedMessages = timedMessages self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactUserPreferences( @@ -521,11 +615,17 @@ public struct ContactUserPreferences: Decodable, Hashable { userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) ), + files: ContactUserPreference( + enabled: FeatureEnabled(forUser: true, forContact: true), + userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), + contactPreference: SimplePreference(allow: .yes) + ), calls: ContactUserPreference( enabled: FeatureEnabled(forUser: true, forContact: true), userPreference: ContactUserPref.user(preference: SimplePreference(allow: .yes)), contactPreference: SimplePreference(allow: .yes) - ) + ), + commands: nil ) } @@ -598,6 +698,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case fullDelete case reactions case voice + case files case calls public var id: Self { self } @@ -624,6 +725,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return NSLocalizedString("Delete for everyone", comment: "chat feature") case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature") case .voice: return NSLocalizedString("Voice messages", comment: "chat feature") + case .files: return NSLocalizedString("Files and media", comment: "chat feature") case .calls: return NSLocalizedString("Audio/video calls", comment: "chat feature") } } @@ -634,6 +736,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash" case .reactions: return "face.smiling" case .voice: return "mic" + case .files: return "doc" case .calls: return "phone" } } @@ -644,6 +747,7 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .fullDelete: return "trash.slash.fill" case .reactions: return "face.smiling.fill" case .voice: return "mic.fill" + case .files: return "doc.fill" case .calls: return "phone.fill" } } @@ -681,6 +785,12 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { case .yes: return "Allow voice messages only if your contact allows them." case .no: return "Prohibit sending voice messages." } + case .files: + switch allowed { + case .always: return "Allow your contacts to send files and media." + case .yes: return "Allow files and media only if your contact allows them." + case .no: return "Prohibit sending files and media." + } case .calls: switch allowed { case .always: return "Allow your contacts to call you." @@ -724,6 +834,14 @@ public enum ChatFeature: String, Decodable, Feature, Hashable { : enabled.forContact ? "Only your contact can send voice messages." : "Voice messages are prohibited in this chat." + case .files: + return enabled.forUser && enabled.forContact + ? "Both you and your contact can send files and media." + : enabled.forUser + ? "Only you can send files and media." + : enabled.forContact + ? "Only your contact can send files and media." + : "Files and media are prohibited in this chat." case .calls: return enabled.forUser && enabled.forContact ? "Both you and your contact can make calls." @@ -957,7 +1075,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { public var fullDelete: ContactFeatureAllowed public var reactions: ContactFeatureAllowed public var voice: ContactFeatureAllowed + public var files: ContactFeatureAllowed public var calls: ContactFeatureAllowed + public var commands: [ChatBotCommand]? public init( timedMessagesAllowed: Bool, @@ -965,14 +1085,18 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed, reactions: ContactFeatureAllowed, voice: ContactFeatureAllowed, - calls: ContactFeatureAllowed + files: ContactFeatureAllowed, + calls: ContactFeatureAllowed, + commands: [ChatBotCommand]? ) { self.timedMessagesAllowed = timedMessagesAllowed self.timedMessagesTTL = timedMessagesTTL self.fullDelete = fullDelete self.reactions = reactions self.voice = voice + self.files = files self.calls = calls + self.commands = commands } public static let sampleData = ContactFeaturesAllowed( @@ -981,7 +1105,9 @@ public struct ContactFeaturesAllowed: Equatable, Hashable { fullDelete: ContactFeatureAllowed.userDefault(.no), reactions: ContactFeatureAllowed.userDefault(.yes), voice: ContactFeatureAllowed.userDefault(.yes), - calls: ContactFeatureAllowed.userDefault(.yes) + files: ContactFeatureAllowed.userDefault(.always), + calls: ContactFeatureAllowed.userDefault(.yes), + commands: nil ) } @@ -994,7 +1120,9 @@ public func contactUserPrefsToFeaturesAllowed(_ contactUserPreferences: ContactU fullDelete: contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions: contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice: contactUserPrefToFeatureAllowed(contactUserPreferences.voice), - calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls) + files: contactUserPrefToFeatureAllowed(contactUserPreferences.files), + calls: contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands: contactUserPreferences.commands ) } @@ -1016,7 +1144,9 @@ public func contactFeaturesAllowedToPrefs(_ contactFeaturesAllowed: ContactFeatu fullDelete: contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions: contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice: contactFeatureAllowedToPref(contactFeaturesAllowed.voice), - calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls) + files: contactFeatureAllowedToPref(contactFeaturesAllowed.files), + calls: contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands: contactFeaturesAllowed.commands ) } @@ -1057,6 +1187,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var simplexLinks: RoleGroupPreference public var reports: GroupPreference public var history: GroupPreference + public var commands: [ChatBotCommand] public init( timedMessages: TimedMessagesGroupPreference, @@ -1067,7 +1198,8 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { files: RoleGroupPreference, simplexLinks: RoleGroupPreference, reports: GroupPreference, - history: GroupPreference + history: GroupPreference, + commands: [ChatBotCommand] ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1078,6 +1210,7 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.commands = commands } public static let sampleData = FullGroupPreferences( @@ -1089,7 +1222,8 @@ public struct FullGroupPreferences: Decodable, Equatable, Hashable { files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), - history: GroupPreference(enable: .on) + history: GroupPreference(enable: .on), + commands: [] ) } @@ -1103,6 +1237,7 @@ public struct GroupPreferences: Codable, Hashable { public var simplexLinks: RoleGroupPreference? public var reports: GroupPreference? public var history: GroupPreference? + public var commands: [ChatBotCommand]? public init( timedMessages: TimedMessagesGroupPreference? = nil, @@ -1113,7 +1248,8 @@ public struct GroupPreferences: Codable, Hashable { files: RoleGroupPreference? = nil, simplexLinks: RoleGroupPreference? = nil, reports: GroupPreference? = nil, - history: GroupPreference? = nil + history: GroupPreference? = nil, + commands: [ChatBotCommand]? = nil ) { self.timedMessages = timedMessages self.directMessages = directMessages @@ -1124,6 +1260,7 @@ public struct GroupPreferences: Codable, Hashable { self.simplexLinks = simplexLinks self.reports = reports self.history = history + self.commands = commands } public static let sampleData = GroupPreferences( @@ -1135,7 +1272,8 @@ public struct GroupPreferences: Codable, Hashable { files: RoleGroupPreference(enable: .on, role: nil), simplexLinks: RoleGroupPreference(enable: .on, role: nil), reports: GroupPreference(enable: .on), - history: GroupPreference(enable: .on) + history: GroupPreference(enable: .on), + commands: nil ) } @@ -1149,7 +1287,8 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group files: fullPreferences.files, simplexLinks: fullPreferences.simplexLinks, reports: fullPreferences.reports, - history: fullPreferences.history + history: fullPreferences.history, + commands: fullPreferences.commands ) } @@ -1368,6 +1507,19 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var sndReady: Bool { + switch self { + case let .direct(contact): contact.sndReady + case let .group(groupInfo, groupScope): + groupInfo.membership.memberActive + && (groupScope != nil || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != .observer)) + case .local: true + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } + public var chatDeleted: Bool { get { switch self { @@ -1502,6 +1654,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return cups.fullDelete.enabled.forUser case .reactions: return cups.reactions.enabled.forUser case .voice: return cups.voice.enabled.forUser + case .files: return cups.files.enabled.forUser case .calls: return cups.calls.enabled.forUser } case let .group(groupInfo, _): @@ -1511,12 +1664,17 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return prefs.fullDelete.on case .reactions: return prefs.reactions.on case .voice: return prefs.voice.on(for: groupInfo.membership) + case .files: return prefs.files.on(for: groupInfo.membership) case .calls: return false } case .local: switch feature { + case .timedMessages: return false + case .fullDelete: return false + case .reactions: return false case .voice: return true - default: return false + case .files: return true + case .calls: return false } default: return false } @@ -1619,6 +1777,22 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { if case .group = self { true } else { false } } + public var useCommands: Bool { + switch self { + case let .direct(c): c.isBot + case let .group(g, _): (g.groupProfile.groupPreferences?.commands?.count ?? 0) > 0 + default: false + } + } + + public var menuCommands: [ChatBotCommand] { + switch self { + case let .direct(c): c.isBot ? c.profile.preferences?.commands ?? [] : [] + case let .group(g, _): g.groupProfile.groupPreferences?.commands ?? [] + default: [] + } + } + public var chatTags: [Int64]? { switch self { case let .direct(contact): return contact.chatTags @@ -1803,16 +1977,26 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { (activeConn == nil || activeConn?.connStatus == .prepared) && profile.contactLink != nil && active && preparedContact == nil && contactRequestId == nil } + @inline(__always) + public var isBot: Bool { + profile.peerType == .bot + } + public var contactConnIncognito: Bool { activeConn?.customUserProfileId != nil } + public var chatIconName: String { + isBot ? "cube.fill" : "person.crop.circle.fill" + } + public func allowsFeature(_ feature: ChatFeature) -> Bool { switch feature { case .timedMessages: return mergedPreferences.timedMessages.contactPreference.allow != .no case .fullDelete: return mergedPreferences.fullDelete.contactPreference.allow != .no case .reactions: return mergedPreferences.reactions.contactPreference.allow != .no case .voice: return mergedPreferences.voice.contactPreference.allow != .no + case .files: return mergedPreferences.files.contactPreference.allow != .no case .calls: return mergedPreferences.calls.contactPreference.allow != .no } } @@ -1823,6 +2007,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { case .fullDelete: return mergedPreferences.fullDelete.userPreference.preference.allow != .no case .reactions: return mergedPreferences.reactions.userPreference.preference.allow != .no case .voice: return mergedPreferences.voice.userPreference.preference.allow != .no + case .files: return mergedPreferences.files.userPreference.preference.allow != .no case .calls: return mergedPreferences.calls.userPreference.preference.allow != .no } } @@ -4426,6 +4611,11 @@ public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? + public init(text: String, format: Format? = nil) { + self.text = text + self.format = format + } + public static func plain(_ text: String) -> [FormattedText] { text.isEmpty ? [] @@ -4435,6 +4625,14 @@ public struct FormattedText: Decodable, Hashable { public var isSecret: Bool { if case .secret = format { true } else { false } } + + public var linkUri: String? { + switch format { + case .uri: text + case let .hyperLink(_, linkUri): linkUri + default: nil + } + } } public enum Format: Decodable, Equatable, Hashable { @@ -4445,7 +4643,9 @@ public enum Format: Decodable, Equatable, Hashable { case secret case colored(color: FormatColor) case uri - case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case hyperLink(showText: String?, linkUri: String) + case simplexLink(showText: String?, linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) + case command(commandStr: String) case mention(memberName: String) case email case phone @@ -4562,14 +4762,14 @@ extension ReportReason: Decodable { // Struct to use with simplex API public struct LinkPreview: Codable, Equatable, Hashable { - public init(uri: URL, title: String, description: String = "", image: String) { + public init(uri: String, title: String, description: String = "", image: String) { self.uri = uri self.title = title self.description = description self.image = image } - public var uri: URL + public var uri: String public var title: String // TODO remove once optional in haskell public var description: String = "" diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 2016094958..98ee9cd5d4 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -93,7 +93,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { public func chatIconName(_ cInfo: ChatInfo) -> String { switch cInfo { - case .direct: "person.crop.circle.fill" + case let .direct(contact): contact.chatIconName case let .group(groupInfo, _): groupInfo.chatIconName case .local: "folder.circle.fill" case .contactRequest: "person.crop.circle.fill" diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index be43158bc1..c70ca5edd8 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -446,7 +446,7 @@ public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { let resized = resizeImageToStrSizeSync(image, maxDataSize: 14000), let title = metadata.title, let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) + linkPreview = LinkPreview(uri: uri.absoluteString, title: title, image: resized) } } cb(linkPreview) diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 8a443017e1..66f570f1b6 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -24,6 +24,7 @@ extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); +extern char *chat_parse_uri(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); extern int chat_json_length(char *str); diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index a5fa71198b..5b5646c470 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -4301,7 +4301,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Можете да видите отново линкът за покана в подробностите за връзката."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Не може да изпращате съобщения!"; /* chat item text */ diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index cd90568881..47bfd2aa12 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -3363,7 +3363,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "K formátování zpráv můžete použít markdown:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nemůžete posílat zprávy!"; /* chat item text */ diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 350729e01c..f00d0a7378 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1087,7 +1087,7 @@ set passcode view */ "Chat with member" = "Chat mit einem Mitglied"; /* No comment provided by engineer. */ -"Chat with members before they join." = "Chat mit Mitgliedern bevor sie beitreten."; +"Chat with members before they join." = "Mit Mitgliedern chatten bevor sie beitreten."; /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "KONTAKTANFRAGEN VON GRUPPEN"; + /* No comment provided by engineer. */ "contact should accept…" = "Kontakt sollte akzeptieren…"; @@ -2011,7 +2014,7 @@ chat item action */ "Enable camera access" = "Kamera-Zugriff aktivieren"; /* No comment provided by engineer. */ -"Enable disappearing messages by default." = "Verschwindende Nachrichten per Voreinstellung aktiviert."; +"Enable disappearing messages by default." = "Verschwindende Nachrichten sind per Voreinstellung aktiviert."; /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Für einen besseren Metadatenschutz Flux in den Netzwerk- und Servereinstellungen aktivieren."; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Fehler beim Senden der Nachricht"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Fehler bei der Einstellung des automatischen Akzeptierens"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; @@ -3105,7 +3111,7 @@ snd error text */ "Keep unused invitation?" = "Nicht genutzte Einladung behalten?"; /* No comment provided by engineer. */ -"Keep your chats clean" = "Halten Sie Ihre Chats übersichtlich"; +"Keep your chats clean" = "Ihre Chats übersichtlich halten"; /* No comment provided by engineer. */ "Keep your connections" = "Ihre Verbindungen beibehalten"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Mitglied inaktiv"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Mitglied ist gelöscht - Anfrage kann nicht angenommen werden"; + /* chat feature */ "Member reports" = "Mitglieder-Meldungen"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "Beitrittsanfrage abgelehnt"; +/* rcv group event chat item */ +"requested connection" = "Angefragte Verbindung"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "Angefragte Verbindung von Gruppe %@"; + /* chat list item title */ "requested to connect" = "Zur Verbindung aufgefordert"; @@ -4898,7 +4913,7 @@ chat item action */ "Set passphrase to export" = "Passwort für den Export festlegen"; /* No comment provided by engineer. */ -"Set profile bio and welcome message." = "Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein."; +"Set profile bio and welcome message." = "Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben."; /* No comment provided by engineer. */ "Set the message shown to new members!" = "Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**."; +/* 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. */ "Time to disappear is set only for new contacts." = "Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Sie können Ihre Meldungen im Chat mit den Administratoren sehen."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Sie können keine Nachrichten versenden!"; /* chat item text */ diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 80c3d7cc93..b9fda78da4 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -552,13 +552,13 @@ swipe action */ "Allow calls?" = "¿Permitir llamadas?"; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí."; +"Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite."; /* No comment provided by engineer. */ "Allow downgrade" = "Permitir versión anterior"; /* No comment provided by engineer. */ -"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 la permite para tí. (24 horas)"; +"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 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."; @@ -1081,7 +1081,7 @@ set passcode view */ "Chat will be deleted for you - this cannot be undone!" = "El chat será eliminado para tí. ¡No puede deshacerse!"; /* chat toolbar */ -"Chat with admins" = "Chatea con los administradores"; +"Chat with admins" = "Chatea con administradores"; /* No comment provided by engineer. */ "Chat with member" = "Chat con miembro"; @@ -1111,7 +1111,7 @@ set passcode view */ "Choose _Migrate from another device_ on the new device and scan QR code." = "En el nuevo dispositivo selecciona _Migrar desde otro dispositivo_ y escanéa el código QR."; /* No comment provided by engineer. */ -"Choose file" = "Elije archivo"; +"Choose file" = "Elegir archivo"; /* No comment provided by engineer. */ "Choose from library" = "Elige de la biblioteca"; @@ -1405,13 +1405,16 @@ set passcode view */ "Contact name" = "Contacto"; /* No comment provided by engineer. */ -"contact not ready" = "el contacto no está listo"; +"contact not ready" = "en espera de ser aceptado"; /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; /* No comment provided by engineer. */ -"contact should accept…" = "el contacto debe aceptar…"; +"Contact requests from groups" = "Solicitudes de contacto en grupo"; + +/* No comment provided by engineer. */ +"contact should accept…" = "el contacto debe aceptarte…"; /* No comment provided by engineer. */ "Contact will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No puede deshacerse!"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Error al enviar mensaje"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Error al configurar auto aceptar"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "¡Error al configurar confirmaciones de entrega!"; @@ -3072,19 +3078,19 @@ snd error text */ "Japanese interface" = "Interfáz en japonés"; /* swipe action */ -"Join" = "Unirte"; +"Join" = "Unirme"; /* No comment provided by engineer. */ -"Join as %@" = "unirte como %@"; +"Join as %@" = "Unirme como %@"; /* new chat sheet title */ -"Join group" = "Unirte al grupo"; +"Join group" = "Unirme al grupo"; /* No comment provided by engineer. */ "Join group conversations" = "Unirse a la conversación del grupo"; /* No comment provided by engineer. */ -"Join incognito" = "Unirte en modo incógnito"; +"Join incognito" = "Unirme en modo incógnito"; /* new chat action */ "Join your group?\nThis is your link for group %@!" = "¿Unirse a tu grupo?\n¡Este es tu enlace para el grupo %@!"; @@ -3248,11 +3254,14 @@ snd error text */ /* item status text */ "Member inactive" = "Miembro inactivo"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Miembro eliminado, no puede aceptar solicitudes"; + /* 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 \"%@\" y todos serán notificados."; +"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."; @@ -3372,7 +3381,7 @@ snd error text */ "Messages are protected by **end-to-end encryption**." = "Los mensajes están protegidos mediante **cifrado de extremo a extremo**."; /* No comment provided by engineer. */ -"Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; +"Messages from %@ will be shown!" = "¡Los mensajes nuevos de %@ serán mostrados!"; /* alert message */ "Messages in this chat will never be deleted." = "Los mensajes de esta conversación nunca se eliminan."; @@ -3591,7 +3600,7 @@ snd error text */ "No chats in list %@" = "Sin chats en la lista %@"; /* No comment provided by engineer. */ -"No chats with members" = "Sin chats con miembros"; +"No chats with members" = "Sin chats"; /* No comment provided by engineer. */ "No contacts selected" = "Ningún contacto seleccionado"; @@ -3848,7 +3857,7 @@ new chat action */ "Open to connect" = "Abrir para conectar"; /* No comment provided by engineer. */ -"Open to join" = "Abrir para unirte"; +"Open to join" = "Abre para unirte"; /* No comment provided by engineer. */ "Opening app…" = "Iniciando aplicación…"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "petición para unirse rechazada"; +/* rcv group event chat item */ +"requested connection" = "conexión solicitada"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "conexión solicitada desde el grupo %@"; + /* chat list item title */ "requested to connect" = "solicitado para conectar"; @@ -5013,7 +5028,7 @@ chat item action */ "SimpleX address and 1-time links are safe to share via any messenger." = "Compartir enlaces de un solo uso y direcciones SimpleX es seguro a través de cualquier medio."; /* No comment provided by engineer. */ -"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un uso?"; +"SimpleX address or 1-time link?" = "¿Dirección SimpleX o enlace de un solo uso?"; /* alert title */ "SimpleX address settings" = "Auto aceptar configuración"; @@ -5221,7 +5236,7 @@ report reason */ "Tap Create SimpleX address in the menu to create it later." = "Pulsa Crear dirección SimpleX en el menú para crearla más tarde."; /* No comment provided by engineer. */ -"Tap Join group" = "Pulsa Unirte al grupo"; +"Tap Join group" = "Pulsa Unirme al grupo"; /* No comment provided by engineer. */ "Tap to activate profile." = "Pulsa sobre un perfil para activarlo."; @@ -5236,7 +5251,7 @@ report reason */ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; +"Tap to paste link" = "Pulsa aquí para pegar el enlace"; /* No comment provided by engineer. */ "Tap to scan" = "Pulsa para escanear"; @@ -5410,7 +5425,7 @@ report reason */ "This device name" = "Nombre del dispositivo"; /* No comment provided by engineer. */ -"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elije otro nombre."; +"This display name is invalid. Please choose another name." = "Éste nombre mostrado no es válido. Por favor, elige otro nombre."; /* No comment provided by engineer. */ "This group has over %lld members, delivery receipts are not sent." = "Este grupo tiene más de %lld miembros, no se enviarán confirmaciones de entrega."; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Esta configuración se aplica a los mensajes del perfil actual **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Esta configuración se aplica al perfil actual **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Mensajes temporales activados sólo para los contactos nuevos."; @@ -5542,7 +5560,7 @@ report reason */ "Unblock member" = "Desbloquear miembro"; /* No comment provided by engineer. */ -"Unblock member for all?" = "¿Desbloquear el miembro para todos?"; +"Unblock member for all?" = "¿Desbloquear al miembro para todos?"; /* No comment provided by engineer. */ "Unblock member?" = "¿Desbloquear miembro?"; @@ -6088,9 +6106,9 @@ report reason */ "You can view invitation link again in connection details." = "Puedes ver el enlace de invitación de nuevo en los detalles de la conexión."; /* alert message */ -"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con los administradores."; +"You can view your reports in Chat with admins." = "Puedes ver tus informes en Chat con administradores."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "¡No puedes enviar mensajes!"; /* chat item text */ @@ -6187,10 +6205,10 @@ report reason */ "You will still receive calls and notifications from muted profiles when they are active." = "Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos."; /* No comment provided by engineer. */ -"You will stop receiving messages from this chat. Chat history will be preserved." = "Dejarás de recibir mensajes de este chat. El historial del chat se conserva."; +"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 conserva."; /* No comment provided by engineer. */ -"You will stop receiving messages from this group. Chat history will be preserved." = "Dejarás de recibir mensajes de este grupo. El historial del chat se conservará."; +"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á."; /* No comment provided by engineer. */ "You won't lose your contacts if you later delete your address." = "Si lo eliminas más tarde tus contactos no se perderán."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index b1a21bfc69..670607daa9 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -3258,7 +3258,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Voit käyttää markdownia viestien muotoiluun:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Et voi lähettää viestejä!"; /* chat item text */ diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 4502e4a292..be6af95358 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -5540,7 +5540,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Vous ne pouvez pas envoyer de messages !"; /* chat item text */ diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 6b902b5850..a09637a77d 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Partnerbeállítások"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Partneri kapcsolatkérések a csoportokból"; + /* No comment provided by engineer. */ "contact should accept…" = "a partnernek el kell fogadnia…"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Hiba történt az üzenet elküldésekor"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Hiba az automatikus elfogadás beállításakor"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Hiba történt a kézbesítési jelentések beállításakor!"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Inaktív tag"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "A tag törölve lett – nem lehet elfogadni a kérést"; + /* chat feature */ "Member reports" = "Tagok jelentései"; @@ -3447,7 +3456,7 @@ snd error text */ "Moderated at: %@" = "Moderálva: %@"; /* marked deleted chat item preview text */ -"moderated by %@" = "moderálva lett %@ által"; +"moderated by %@" = "%@ moderálta ezt az üzenetet"; /* member role */ "moderator" = "moderátor"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "csatlakozási kérés elutasítva"; +/* rcv group event chat item */ +"requested connection" = "partneri kapcsolatot kért"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "a(z) %@ nevű csoportból partneri kapcsolatot kért"; + /* chat list item title */ "requested to connect" = "függőben lévő kapcsolat"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás csak az Ön jelenlegi **%@** nevű csevegési profiljában lévő üzenetekre vonatkozik."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Ez a beállítás csak a jelenlegi **%@** nevű csevegési profiljára vonatkozik."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Az üzeneteltűnési idő csak az új partnerekre vonatkozik."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nem lehet üzeneteket küldeni!"; /* chat item text */ diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 7165a63668..0fe22cf12f 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Richieste di contatto dai gruppi"; + /* No comment provided by engineer. */ "contact should accept…" = "il contatto dovrebbe accettare…"; @@ -2367,6 +2370,9 @@ chat item action */ /* No comment provided by engineer. */ "Error sending message" = "Errore nell'invio del messaggio"; +/* No comment provided by engineer. */ +"Error setting auto-accept" = "Errore impostando l'accettazione automatica"; + /* No comment provided by engineer. */ "Error setting delivery receipts!" = "Errore nell'impostazione delle ricevute di consegna!"; @@ -3248,6 +3254,9 @@ snd error text */ /* item status text */ "Member inactive" = "Membro inattivo"; +/* No comment provided by engineer. */ +"Member is deleted - can't accept request" = "Il membro è eliminato - impossibile accettare la richiesta"; + /* chat feature */ "Member reports" = "Segnalazioni dei membri"; @@ -4422,6 +4431,12 @@ swipe action */ /* No comment provided by engineer. */ "request to join rejected" = "richiesta di entrare rifiutata"; +/* rcv group event chat item */ +"requested connection" = "connessione richiesta"; + +/* rcv direct event chat item */ +"requested connection from group %@" = "connessione richiesta dal gruppo %@"; + /* chat list item title */ "requested to connect" = "richiesto di connettersi"; @@ -5430,6 +5445,9 @@ report reason */ /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"This setting is for your current profile **%@**." = "Questa impostazione è per il tuo profilo attuale **%@**."; + /* No comment provided by engineer. */ "Time to disappear is set only for new contacts." = "Il tempo di scomparsa è impostato solo per i contatti nuovi."; @@ -6090,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Puoi vedere le tue segnalazioni nella chat con gli amministratori."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Non puoi inviare messaggi!"; /* chat item text */ diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 2c93438a8f..818b2eb1ce 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -178,15 +178,15 @@ /* time interval */ "%d sec" = "%d 秒"; +/* delete after time */ +"%d seconds(s)" = "%d 秒"; + /* integrity error chat item */ "%d skipped message(s)" = "%d 件のスキップされたメッセージ"; /* time interval */ "%d weeks" = "%d 週"; -/* No comment provided by engineer. */ -"%lld" = ""; - /* No comment provided by engineer. */ "%lld %@" = "%lld %@"; @@ -283,6 +283,9 @@ time interval */ time interval */ "1 week" = "1週間"; +/* delete after time */ +"1 year" = "1年"; + /* No comment provided by engineer. */ "1-time link" = "使い捨てリンク"; @@ -322,6 +325,9 @@ time interval */ /* No comment provided by engineer. */ "Abort changing address?" = "アドレス変更を中止しますか?"; +/* No comment provided by engineer. */ +"About operators" = "オペレーターについて"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "SimpleX Chat について"; @@ -334,9 +340,21 @@ alert action swipe action */ "Accept" = "承諾"; +/* alert action */ +"Accept as member" = "メンバーとして承認する"; + +/* alert action */ +"Accept as observer" = "オブザーバーとして承認する"; + +/* No comment provided by engineer. */ +"Accept conditions" = "条件に同意する"; + /* No comment provided by engineer. */ "Accept connection request?" = "接続要求を承認?"; +/* alert title */ +"Accept contact request" = "連絡先リクエストを受け入れる"; + /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; @@ -344,9 +362,15 @@ swipe action */ swipe action */ "Accept incognito" = "シークレットモードで承諾"; +/* alert title */ +"Accept member" = "メンバーを承認する"; + /* call status */ "accepted call" = "受けた通話"; +/* No comment provided by engineer. */ +"Accepted conditions" = "承諾された条件"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。"; @@ -3459,7 +3483,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "メッセージの書式にmarkdownを使用することができます:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "メッセージを送信できませんでした!"; /* chat item text */ diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index c5dedbd0d4..92959a3bfe 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -5898,7 +5898,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "U kunt uw rapporten bekijken in Chat met beheerders."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Je kunt geen berichten versturen!"; /* chat item text */ diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 4e8a26d05c..b45d753b3c 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -5156,7 +5156,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Nie możesz wysyłać wiadomości!"; /* chat item text */ diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index d797ebfbbd..93c3bedd4f 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -6108,7 +6108,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Вы можете найти Ваши жалобы в Чате с админами."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Вы не можете отправлять сообщения!"; /* chat item text */ diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 03c1bdfed1..ef5d68b0fb 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -3168,7 +3168,7 @@ chat item action */ /* No comment provided by engineer. */ "You can use markdown to format messages:" = "คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "คุณไม่สามารถส่งข้อความได้!"; /* chat item text */ diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e93e824921..0dc37ccb57 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1410,6 +1410,9 @@ set passcode view */ /* No comment provided by engineer. */ "Contact preferences" = "Kişi tercihleri"; +/* No comment provided by engineer. */ +"Contact requests from groups" = "Gruplardan gelen iletişim talepleri"; + /* No comment provided by engineer. */ "contact should accept…" = "kişi kabul etmeli…"; @@ -1482,6 +1485,9 @@ set passcode view */ /* No comment provided by engineer. */ "Create SimpleX address" = "SimpleX adresi oluştur"; +/* No comment provided by engineer. */ +"Create your address" = "Adresinizi oluşturun"; + /* No comment provided by engineer. */ "Create your profile" = "Profilini oluştur"; @@ -2007,6 +2013,9 @@ chat item action */ /* No comment provided by engineer. */ "Enable camera access" = "Kamera erişimini etkinleştir"; +/* No comment provided by engineer. */ +"Enable disappearing messages by default." = "Varsayılan olarak kaybolan mesajları etkinleştirin."; + /* No comment provided by engineer. */ "Enable Flux in Network & servers settings for better metadata privacy." = "Daha iyi meta veri gizliliği için Ağ & sunucu ayarlarında Flux'u etkinleştirin."; @@ -6033,7 +6042,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Raporlarınızı Yöneticilerle Sohbet bölümünde görüntüleyebilirsiniz."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Mesajlar gönderemezsiniz!"; /* chat item text */ diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 6c4cdf1510..daa954b2fb 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -6090,7 +6090,7 @@ report reason */ /* alert message */ "You can view your reports in Chat with admins." = "Ви можете переглянути свої звіти у чаті з адміністраторами."; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "Ви не можете надсилати повідомлення!"; /* chat item text */ diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 5ba26b8363..933286ccd5 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -5507,7 +5507,7 @@ chat item action */ /* alert message */ "You can view invitation link again in connection details." = "您可以在连接详情中再次查看邀请链接。"; -/* No comment provided by engineer. */ +/* alert title */ "You can't send messages!" = "您无法发送消息!"; /* chat item text */ diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index d8fa2c65a7..cfbed65c76 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -64,6 +64,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -146,6 +147,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused j return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str) { + const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) { const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 2614b1a561..076e323ca6 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -37,6 +37,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); +extern char *chat_parse_uri(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -156,6 +157,14 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass cla return res; } +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + jstring res = decode_to_utf8_string(env, chat_parse_uri(_str)); + (*env)->ReleaseStringUTFChars(env, str, _str); + return res; +} + JNIEXPORT jstring JNICALL Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { const char *_pwd = encode_to_utf8_chars(env, pwd); 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 d61e44f528..1d00d7cdb0 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 @@ -1633,6 +1633,18 @@ sealed class ChatInfo: SomeChat, NamedChat { val sendMsgEnabled get() = userCantSendReason == null + val sndReady: Boolean get() = + when(this) { + is Direct -> contact.sndReady + is Group -> + groupInfo.membership.memberActive + && (groupChatScope != null || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != GroupMemberRole.Observer)) + is Local -> true + is ContactRequest -> false + is ContactConnection -> false + is InvalidJSON -> false + } + fun groupChatScope(): GroupChatScope? = when (this) { is Group -> groupChatScope?.toChatScope() else -> null @@ -1676,6 +1688,20 @@ sealed class ChatInfo: SomeChat, NamedChat { val hasMentions: Boolean get() = this is Group + val useCommands: Boolean get() = when(this) { + is Direct -> contact.isBot + is Group -> groupInfo.groupProfile.groupPreferences?.commands?.isNotEmpty() ?: false + else -> false + } + + val menuCommands: List get() = when(this) { + is Direct -> + if (contact.isBot) contact.profile.preferences?.commands ?: emptyList() + else emptyList() + is Group -> groupInfo.groupProfile.groupPreferences?.commands ?: emptyList() + else -> emptyList() + } + val contactCard: Boolean get() = when (this) { is Direct -> contact.isContactCard @@ -1759,6 +1785,7 @@ data class Contact( ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser ChatFeature.Reactions -> mergedPreferences.reactions.enabled.forUser ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser + ChatFeature.Files -> mergedPreferences.files.enabled.forUser ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser } override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null } @@ -1775,7 +1802,6 @@ data class Contact( return profile.chatViewName.lowercase().contains(s) || profile.displayName.lowercase().contains(s) || profile.fullName.lowercase().contains(s) } - val directOrUsed: Boolean get() = if (activeConn != null) { (activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed @@ -1783,16 +1809,22 @@ data class Contact( true } - val isContactCard: Boolean = + val isContactCard: Boolean get() = (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) && profile.contactLink != null && active && preparedContact == null && contactRequestId == null - val contactConnIncognito = + val isBot: Boolean get() = profile.peerType == ChatPeerType.Bot + + val contactConnIncognito: Boolean get() = activeConn?.customUserProfileId != null + val chatIconName: ImageResource get() = + if (isBot) MR.images.ic_cube else MR.images.ic_account_circle_filled + fun allowsFeature(feature: ChatFeature): Boolean = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.contactPreference.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.contactPreference.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO } @@ -1802,6 +1834,7 @@ data class Contact( ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Reactions -> mergedPreferences.reactions.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO + ChatFeature.Files -> mergedPreferences.files.userPreference.pref.allow != FeatureAllowed.NO ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO } @@ -1931,14 +1964,15 @@ data class Profile( override val image: String? = null, override val localAlias : String = "", val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { 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) + fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = Profile( @@ -1958,11 +1992,12 @@ data class LocalProfile( override val image: String? = null, override val localAlias: String, val contactLink: String? = null, - val preferences: ChatPreferences? = null + val preferences: ChatPreferences? = null, + val peerType: ChatPeerType? = null ): NamedChat { val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" } - fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences) + fun toProfile(): Profile = Profile(displayName, fullName, shortDescr, image, localAlias, contactLink, preferences, peerType) companion object { val sampleData = LocalProfile( @@ -1976,6 +2011,12 @@ data class LocalProfile( } } +@Serializable +enum class ChatPeerType { + @SerialName("human") Human, + @SerialName("bot") Bot +} + @Serializable data class UserProfileUpdateSummary( val updateSuccesses: Int, @@ -2030,6 +2071,7 @@ data class GroupInfo ( ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on ChatFeature.Reactions -> fullGroupPreferences.reactions.on ChatFeature.Voice -> fullGroupPreferences.voice.on(membership) + ChatFeature.Files -> fullGroupPreferences.files.on(membership) ChatFeature.Calls -> false } override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null } @@ -2484,7 +2526,14 @@ class NoteFolder( override val nextConnectPrepared get() = false override val profileChangeProhibited get() = false override val incognito get() = false - override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice + override fun featureEnabled(feature: ChatFeature) = when (feature) { + ChatFeature.TimedMessages -> false + ChatFeature.FullDelete -> false + ChatFeature.Reactions -> false + ChatFeature.Voice -> true + ChatFeature.Files -> true + ChatFeature.Calls -> false + } override val timedMessagesTTL: Int? get() = null override val displayName get() = generalGetString(MR.strings.note_folder_local_display_name) override val fullName get() = "" @@ -4320,19 +4369,12 @@ sealed class MsgChatLink { @Serializable class FormattedText(val text: String, val format: Format? = null) { - fun link(mode: SimplexLinkMode): String? = when (format) { - is Format.Uri -> if (text.startsWith("http://", ignoreCase = true) || text.startsWith("https://", ignoreCase = true)) text else "https://$text" - is Format.SimplexLink -> if (mode == SimplexLinkMode.BROWSER) text else format.simplexUri - is Format.Email -> "mailto:$text" - is Format.Phone -> "tel:$text" - else -> null - } - - fun viewText(mode: SimplexLinkMode): String = - if (format is Format.SimplexLink && mode == SimplexLinkMode.DESCRIPTION) simplexLinkText(format.linkType, format.smpHosts) else text - - fun simplexLinkText(linkType: SimplexLinkType, smpHosts: List): String = - "${linkType.description} (${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + val linkUri: String? get() = + when (format) { + is Format.Uri -> text + is Format.HyperLink -> format.linkUri + else -> null + } companion object { fun plain(text: String): List = if (text.isEmpty()) emptyList() else listOf(FormattedText(text)) @@ -4348,7 +4390,14 @@ sealed class Format { @Serializable @SerialName("secret") class Secret: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() - @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() + @Serializable @SerialName("hyperLink") class HyperLink(val showText: String?, val linkUri: String): Format() + @Serializable @SerialName("simplexLink") class SimplexLink(val showText: String?, val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() { + val simplexLinkText: String get() = + "${linkType.description} $viaHosts" + val viaHosts: String get() = + "(${String.format(generalGetString(MR.strings.simplex_link_connection), smpHosts.firstOrNull() ?: "?")})" + } + @Serializable @SerialName("command") class Command(val commandStr: String): Format() @Serializable @SerialName("mention") class Mention(val memberName: String): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() @@ -4362,7 +4411,9 @@ sealed class Format { is Secret -> SpanStyle(color = Color.Transparent, background = SecretColor) is Colored -> SpanStyle(color = this.color.uiColor) is Uri -> linkStyle + is HyperLink -> linkStyle is SimplexLink -> linkStyle + is Command -> SpanStyle(color = MaterialTheme.colors.primary, fontFamily = FontFamily.Monospace) is Mention -> SpanStyle(fontWeight = FontWeight.Medium) is Email -> linkStyle is Phone -> linkStyle 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 9e10d249c0..3e56ef3a2e 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.GroupFeature.Files import chat.simplex.common.model.MsgContent.MCUnknown import chat.simplex.common.model.SMPProxyFallback.AllowProtected import chat.simplex.common.model.SMPProxyMode.Always @@ -103,6 +104,9 @@ 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 privacyLinkPreviewsShowAlert = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS_SHOW_ALERT, true) + val privacySanitizeLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SANITIZE_LINKS, true) + // TODO remove val privacyChatListOpenLinks = mkEnumPreference(SHARED_PREFS_PRIVACY_CHAT_LIST_OPEN_LINKS, PrivacyChatListOpenLinksMode.ASK) { PrivacyChatListOpenLinksMode.values().firstOrNull { it.name == this } } val simplexLinkMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default) val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) @@ -368,7 +372,9 @@ 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_CHAT_LIST_OPEN_LINKS = "ChatListOpenLinks" + 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_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" @@ -2643,8 +2649,6 @@ object ChatController { if (cItem.isActiveReport) { chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id) } - } - withContext(Dispatchers.Main) { chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem) } } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { @@ -4630,6 +4634,19 @@ data class ParsedServerAddress ( var parseError: String ) +fun parseSanitizeUri(s: String): ParsedUri? { + val parsed = chatParseUri(s) + return runCatching { json.decodeFromString(ParsedUri.serializer(), parsed) } + .onFailure { Log.d(TAG, "parseSanitizeUri decode error: $it") } + .getOrNull() +} + +@Serializable +data class ParsedUri(val uriInfo: UriInfo?, val parseError: String) + +@Serializable +data class UriInfo(val scheme: String, val sanitized: String?) + @Serializable data class NetCfg( val socksProxy: String?, @@ -4881,14 +4898,18 @@ data class FullChatPreferences( val fullDelete: SimpleChatPreference, val reactions: SimpleChatPreference, val voice: SimpleChatPreference, + val files: SimpleChatPreference, val calls: SimpleChatPreference, + val commands: List ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages, fullDelete = fullDelete, reactions = reactions, voice = voice, - calls = calls + files = files, + calls = calls, + commands = commands, ) companion object { @@ -4897,7 +4918,9 @@ data class FullChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = listOf(), ) } } @@ -4908,7 +4931,9 @@ data class ChatPreferences( val fullDelete: SimpleChatPreference?, val reactions: SimpleChatPreference?, val voice: SimpleChatPreference?, + val files: SimpleChatPreference?, val calls: SimpleChatPreference?, + val commands: List?, ) { fun setAllowed(feature: ChatFeature, allowed: FeatureAllowed = FeatureAllowed.YES, param: Int? = null): ChatPreferences = when (feature) { @@ -4916,6 +4941,7 @@ data class ChatPreferences( ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed)) ChatFeature.Reactions -> this.copy(reactions = SimpleChatPreference(allow = allowed)) ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed)) + ChatFeature.Files -> this.copy(files = SimpleChatPreference(allow = allowed)) ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed)) } @@ -4925,7 +4951,9 @@ data class ChatPreferences( fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO), reactions = SimpleChatPreference(allow = FeatureAllowed.YES), voice = SimpleChatPreference(allow = FeatureAllowed.YES), + files = SimpleChatPreference(allow = FeatureAllowed.YES), calls = SimpleChatPreference(allow = FeatureAllowed.YES), + commands = null, ) } } @@ -4953,6 +4981,12 @@ data class TimedMessagesPreference( } } +@Serializable +sealed class ChatBotCommand { + @Serializable @SerialName("command") class Command(val keyword: String, val label: String, val params: String?): ChatBotCommand() + @Serializable @SerialName("menu") class Menu(val label: String, val commands: List): ChatBotCommand() +} + @Serializable data class PresentedServersSummary( val statsStartedAt: Instant, @@ -5203,14 +5237,18 @@ data class ContactUserPreferences( val fullDelete: ContactUserPreference, val reactions: ContactUserPreference, val voice: ContactUserPreference, + val files: ContactUserPreference, val calls: ContactUserPreference, + val commands: List?, ) { fun toPreferences(): ChatPreferences = ChatPreferences( timedMessages = timedMessages.userPreference.pref, fullDelete = fullDelete.userPreference.pref, reactions = reactions.userPreference.pref, voice = voice.userPreference.pref, - calls = calls.userPreference.pref + files = files.userPreference.pref, + calls = calls.userPreference.pref, + commands = commands, ) companion object { @@ -5235,11 +5273,17 @@ data class ContactUserPreferences( userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + files = ContactUserPreference( + enabled = FeatureEnabled(forUser = true, forContact = true), + userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), + contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) + ), calls = ContactUserPreference( enabled = FeatureEnabled(forUser = true, forContact = true), userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)), contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES) ), + commands = null, ) } } @@ -5329,6 +5373,7 @@ enum class ChatFeature: Feature { @SerialName("fullDelete") FullDelete, @SerialName("reactions") Reactions, @SerialName("voice") Voice, + @SerialName("files") Files, @SerialName("calls") Calls; val asymmetric: Boolean get() = when (this) { @@ -5348,6 +5393,7 @@ enum class ChatFeature: Feature { FullDelete -> generalGetString(MR.strings.full_deletion) Reactions -> generalGetString(MR.strings.message_reactions) Voice -> generalGetString(MR.strings.voice_messages) + Files -> generalGetString(MR.strings.files_and_media) Calls -> generalGetString(MR.strings.audio_video_calls) } @@ -5357,6 +5403,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever) Reactions -> painterResource(MR.images.ic_add_reaction) Voice -> painterResource(MR.images.ic_keyboard_voice) + Files -> painterResource(MR.images.ic_draft) Calls -> painterResource(MR.images.ic_call) } @@ -5366,6 +5413,7 @@ enum class ChatFeature: Feature { FullDelete -> painterResource(MR.images.ic_delete_forever_filled) Reactions -> painterResource(MR.images.ic_add_reaction_filled) Voice -> painterResource(MR.images.ic_keyboard_voice_filled) + Files -> painterResource(MR.images.ic_draft_filled) Calls -> painterResource(MR.images.ic_call_filled) } @@ -5386,11 +5434,16 @@ enum class ChatFeature: Feature { FeatureAllowed.YES -> generalGetString(MR.strings.allow_message_reactions_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_message_reactions) } - Voice -> when (allowed) { + Voice -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_voice_messages) FeatureAllowed.YES -> generalGetString(MR.strings.allow_voice_messages_only_if) FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_voice_messages) } + Files -> when (allowed) { + FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_send_files_and_media) + FeatureAllowed.YES -> generalGetString(MR.strings.allow_files_and_media_only_if) + FeatureAllowed.NO -> generalGetString(MR.strings.prohibit_sending_files_and_media) + } Calls -> when (allowed) { FeatureAllowed.ALWAYS -> generalGetString(MR.strings.allow_your_contacts_to_call) FeatureAllowed.YES -> generalGetString(MR.strings.allow_calls_only_if) @@ -5424,6 +5477,12 @@ enum class ChatFeature: Feature { enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_voice) else -> generalGetString(MR.strings.voice_prohibited_in_this_chat) } + Files -> when { + enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_send_files) + enabled.forUser -> generalGetString(MR.strings.only_you_can_send_files) + enabled.forContact -> generalGetString(MR.strings.only_your_contact_can_send_files) + else -> generalGetString(MR.strings.files_prohibited_in_this_chat) + } Calls -> when { enabled.forUser && enabled.forContact -> generalGetString(MR.strings.both_you_and_your_contact_can_make_calls) enabled.forUser -> generalGetString(MR.strings.only_you_can_make_calls) @@ -5618,7 +5677,9 @@ data class ContactFeaturesAllowed( val fullDelete: ContactFeatureAllowed, val reactions: ContactFeatureAllowed, val voice: ContactFeatureAllowed, + val files: ContactFeatureAllowed, val calls: ContactFeatureAllowed, + val commands: List?, ) { companion object { val sampleData = ContactFeaturesAllowed( @@ -5627,7 +5688,9 @@ data class ContactFeaturesAllowed( fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO), reactions = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + files = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES), + commands = null, ) } } @@ -5641,7 +5704,9 @@ fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPrefere fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete), reactions = contactUserPrefToFeatureAllowed(contactUserPreferences.reactions), voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice), + files = contactUserPrefToFeatureAllowed(contactUserPreferences.files), calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls), + commands = contactUserPreferences.commands, ) } @@ -5661,7 +5726,9 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete), reactions = contactFeatureAllowedToPref(contactFeaturesAllowed.reactions), voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice), + files = contactFeatureAllowedToPref(contactFeaturesAllowed.files), calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls), + commands = contactFeaturesAllowed.commands, ) fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): SimpleChatPreference? = @@ -5697,6 +5764,7 @@ data class FullGroupPreferences( val simplexLinks: RoleGroupPreference, val reports: GroupPreference, val history: GroupPreference, + val commands: List, ) { fun toGroupPreferences(): GroupPreferences = GroupPreferences( @@ -5709,6 +5777,7 @@ data class FullGroupPreferences( simplexLinks = simplexLinks, reports = reports, history = history, + commands = commands, ) companion object { @@ -5722,6 +5791,7 @@ data class FullGroupPreferences( simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), + commands = listOf() ) } } @@ -5737,6 +5807,7 @@ data class GroupPreferences( val simplexLinks: RoleGroupPreference? = null, val reports: GroupPreference? = null, val history: GroupPreference? = null, + val commands: List? = null ) { companion object { val sampleData = GroupPreferences( @@ -5749,6 +5820,7 @@ data class GroupPreferences( simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null), reports = GroupPreference(GroupFeatureEnabled.ON), history = GroupPreference(GroupFeatureEnabled.ON), + commands = null, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 9d8a699775..35194ba1e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -28,6 +28,7 @@ external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String +external fun chatParseUri(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String external fun chatJsonLength(str: String): Int 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 d19c19b83a..a3365b2087 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 @@ -102,19 +102,21 @@ fun ChatView( val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } - val activeChatInfo = remember { derivedStateOf { - var chatInfo = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo + val activeChat = remember { derivedStateOf { + var chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value } + val chatInfo = chat?.chatInfo if ( chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext + && chat != null && chatInfo is ChatInfo.Group ) { val scopeInfo = chatsCtx.secondaryContextFilter.groupScopeInfo - chatInfo = chatInfo.copy(groupChatScope = scopeInfo) + chat = chat.copy(chatInfo = chatInfo.copy(groupChatScope = scopeInfo)) } - chatInfo + chat } } val user = chatModel.currentUser.value - val chatInfo = activeChatInfo.value + val chatInfo = activeChat.value?.chatInfo if (chat == null || chatInfo == null || user == null) { LaunchedEffect(Unit) { chatModel.chatId.value = null @@ -138,6 +140,7 @@ fun ChatView( val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val scope = rememberCoroutineScope() val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } + val showCommandsMenu = rememberSaveable { mutableStateOf(false) } if (appPlatform.isAndroid) { DisposableEffect(Unit) { onDispose { @@ -186,7 +189,6 @@ fun ChatView( chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } - val reportsCount = reportsCount(chatInfo.id) val clipboard = LocalClipboardManager.current CompositionLocalProvider( LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false), @@ -214,7 +216,7 @@ fun ChatView( ChatLayout( chatsCtx = chatsCtx, remoteHostId = remoteHostId, - chatInfo = activeChatInfo, + chat = activeChat, unreadCount, composeState, composeView = { focusRequester -> @@ -237,7 +239,7 @@ fun ChatView( ) } ComposeView( - rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, + rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, showCommandsMenu, attachmentOption, showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }, focusRequester = focusRequester ) @@ -347,7 +349,7 @@ fun ChatView( ModalManager.end.showCustomModal { close -> val appBar = remember { mutableStateOf(null as @Composable (BoxScope.() -> Unit)?) } ModalView(close, appBar = appBar.value) { - val chatInfo = remember { activeChatInfo }.value + val chatInfo = remember { activeChat }.value?.chatInfo if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } var code: String? by remember { mutableStateOf(preloadedCode) } @@ -377,7 +379,7 @@ fun ChatView( } } LaunchedEffect(Unit) { - snapshotFlow { activeChatInfo.value?.id } + snapshotFlow { activeChat.value?.id } .drop(1) .collect { appBar.value = null @@ -389,37 +391,37 @@ fun ChatView( } }, showReports = { - val info = activeChatInfo.value ?: return@ChatLayout + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) scope.launch { - showGroupReportsView(staleChatId, scrollToItemId, info) + showGroupReportsView(staleChatId, scrollToItemId, cInfo) } }, showSupportChats = { - val info = activeChatInfo.value ?: return@ChatLayout + val cInfo = activeChat.value?.chatInfo ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) scope.launch { - if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { + if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) { ModalManager.end.showCustomModal { close -> MemberSupportView( chatRh, chat, - info.groupInfo, + cInfo.groupInfo, scrollToItemId, close ) } - } else if (info is ChatInfo.Group) { + } else if (cInfo is ChatInfo.Group) { val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) - val supportChatInfo = ChatInfo.Group(info.groupInfo, groupChatScope = scopeInfo) + val supportChatInfo = ChatInfo.Group(cInfo.groupInfo, groupChatScope = scopeInfo) scope.launch { showMemberSupportChatView( chatModel.chatId, @@ -723,7 +725,8 @@ fun ChatView( onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), - showSearch = showSearch + showSearch = showSearch, + showCommandsMenu = showCommandsMenu ) } } @@ -768,9 +771,7 @@ private fun connectingText(chatInfo: ChatInfo): String? { && !chatInfo.contact.sendMsgToConnect && !chatInfo.contact.nextAcceptContactRequest ) { - if (chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { - generalGetString(MR.strings.contact_should_accept) - } else if (chatInfo.contact.contactGroupMemberId != null) { + if ((chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !chatInfo.contact.isBot) || chatInfo.contact.contactGroupMemberId != null) { generalGetString(MR.strings.contact_should_accept) } else { generalGetString(MR.strings.contact_connection_pending) @@ -807,7 +808,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) fun ChatLayout( chatsCtx: ChatModel.ChatsContext, remoteHostId: State, - chatInfo: State, + chat: State, unreadCount: State, composeState: MutableState, composeView: (@Composable (FocusRequester?) -> Unit), @@ -854,8 +855,10 @@ fun ChatLayout( onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, showViaProxy: Boolean, - showSearch: MutableState + showSearch: MutableState, + showCommandsMenu: MutableState ) { + val chatInfo = remember { derivedStateOf { chat.value?.chatInfo } } val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } Box( @@ -886,19 +889,20 @@ fun ChatLayout( val composeViewHeight = remember { mutableStateOf(0.dp) } Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.secondaryContextFilter == null)) { val remoteHostId = remember { remoteHostId }.value - val chatInfo = remember { chatInfo }.value + val chat = remember { chat }.value + val chatInfo = chat?.chatInfo val oneHandUI = remember { appPrefs.oneHandUI.state } val chatBottomBar = remember { appPrefs.chatBottomBar.state } val composeViewFocusRequester = remember { if (appPlatform.isDesktop) FocusRequester() else null } AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { - if (chatInfo != null) { + if (chat != null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) { // disables scrolling to top of chat item on click inside the bubble CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec { override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f }) { ChatItemsList( - chatsCtx, remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + chatsCtx, remoteHostId, chat, unreadCount, composeState, composeViewHeight, searchValue, useLinkPreviews, linkMode, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, archiveReports, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, @@ -920,6 +924,11 @@ fun ChatLayout( ) } } + if (chatInfo != null && chatInfo.menuCommands.isNotEmpty()) { + Column(Modifier.align(Alignment.BottomStart).padding(bottom = composeViewHeight.value)) { + CommandsMenuView(chatsCtx, chat, composeState, showCommandsMenu) + } + } } } if (chatsCtx.contentTag == MsgContentTag.Report) { @@ -1362,7 +1371,7 @@ private var reportsListState: LazyListState? = null fun BoxScope.ChatItemsList( chatsCtx: ChatModel.ChatsContext, remoteHostId: Long?, - chatInfo: ChatInfo, + chat: Chat, unreadCount: State, composeState: MutableState, composeViewHeight: State, @@ -1399,6 +1408,7 @@ fun BoxScope.ChatItemsList( developerTools: Boolean, showViaProxy: Boolean ) { + val chatInfo = chat.chatInfo val loadingTopItems = remember { mutableStateOf(false) } val loadingBottomItems = remember { mutableStateOf(false) } // just for changing local var here based on request @@ -1561,7 +1571,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(chatsCtx, remoteHostId, chat, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1769,10 +1779,12 @@ fun BoxScope.ChatItemsList( if (contact.nextConnectPrepared && preparedLinkType != null) { when (preparedLinkType) { ConnectionMode.Inv -> generalGetString(MR.strings.chat_banner_connect_to_chat) - ConnectionMode.Con -> generalGetString(MR.strings.chat_banner_send_request_to_connect) + ConnectionMode.Con -> generalGetString(if (contact.isBot) MR.strings.chat_banner_connect_to_use_bot else MR.strings.chat_banner_send_request_to_connect) } } else if (contact.nextAcceptContactRequest) { generalGetString(MR.strings.chat_banner_accept_contact_request) + } else if (contact.isBot) { + generalGetString(MR.strings.chat_banner_bot) } else { generalGetString(MR.strings.chat_banner_your_contact) } @@ -3304,7 +3316,7 @@ fun PreviewChatLayout() { ChatLayout( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = { _ -> }, @@ -3351,7 +3363,8 @@ fun PreviewChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } @@ -3383,7 +3396,7 @@ fun PreviewGroupChatLayout() { ChatLayout( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), remoteHostId = remember { mutableStateOf(null) }, - chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, + chat = remember { mutableStateOf(Chat.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = { _ -> }, @@ -3430,7 +3443,8 @@ fun PreviewGroupChatLayout() { onComposed = {}, developerTools = false, showViaProxy = false, - showSearch = remember { mutableStateOf(false) } + showSearch = remember { mutableStateOf(false) }, + showCommandsMenu = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt new file mode 100644 index 0000000000..26b1fce741 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/CommandsMenuView.kt @@ -0,0 +1,244 @@ +package chat.simplex.common.views.chat + +import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +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.layout.layout +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +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.views.chat.group.* +import chat.simplex.common.views.chat.item.sendCommandMsg +import chat.simplex.common.views.helpers.commandMenuAnimSpec +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import kotlinx.coroutines.launch + +private val COMMAND_MENU_ROW_SIZE = 48.dp +private val MAX_COMMAND_MENU_HEIGHT = COMMAND_MENU_ROW_SIZE * 6 - 8.dp + +@Composable +fun CommandsMenuView( + chatsCtx: ChatModel.ChatsContext, + chat: Chat, + composeState: MutableState, + showCommandsMenu: MutableState +) { + val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() } + val offsetY = remember { Animatable(maxHeightInPx) } + val scope = rememberCoroutineScope() + + val currentCommands = remember { mutableStateOf>(emptyList()) } + val menuTreeBackPath = remember { mutableStateOf>>>(emptyList()) } + + fun filterShownCommands(commands: List, msg: CharSequence): List { + val cmds = mutableListOf() + for (cmd in commands) { + when (cmd) { + is ChatBotCommand.Command -> + if (cmd.keyword.startsWith(msg)) { + cmds.add(cmd) + } + is ChatBotCommand.Menu -> + cmds.addAll(filterShownCommands(cmd.commands, msg)) + } + } + return cmds + } + + suspend fun closeCommandsMenu() { + showCommandsMenu.value = false + currentCommands.value = emptyList() + menuTreeBackPath.value = emptyList() + if (offsetY.value != 0f) { + return + } + offsetY.animateTo( + targetValue = maxHeightInPx, + animationSpec = commandMenuAnimSpec() + ) + } + + fun messageChanged(message: String) { + val msg = message.trim() + menuTreeBackPath.value = emptyList() + if (msg == "/") { + currentCommands.value = chat.chatInfo.menuCommands + } else if (msg.startsWith("/")) { + currentCommands.value = filterShownCommands(chat.chatInfo.menuCommands, msg.drop(1)) + } else { + scope.launch { closeCommandsMenu() } + } + } + + LaunchedEffect(currentCommands.value.isNotEmpty()) { + if (currentCommands.value.isNotEmpty()) { + offsetY.animateTo( + targetValue = 0f, + animationSpec = commandMenuAnimSpec() + ) + } + } + + LaunchedEffect(composeState.value.message) { + messageChanged(composeState.value.message.text) + } + + LaunchedEffect(showCommandsMenu.value) { + if (showCommandsMenu.value) { + currentCommands.value = chat.chatInfo.menuCommands + menuTreeBackPath.value = emptyList() + } else { + closeCommandsMenu() + } + } + + @Composable + fun MenuLabelRow(prev: Pair>) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (menuTreeBackPath.value.isNotEmpty()) { + currentCommands.value = menuTreeBackPath.value.last().second + menuTreeBackPath.value = menuTreeBackPath.value.dropLast(1) + } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(MR.images.ic_arrow_back_ios_new), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = prev.first, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + } + } + } + + @Composable + fun CommandRow(cmd: ChatBotCommand) { + when (cmd) { + is ChatBotCommand.Command -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + if (cmd.params != null) { + val msg = "/${cmd.keyword} ${cmd.params}" + composeState.value = ComposeState(message = ComposeMessage(msg, TextRange(msg.length)), useLinkPreviews = true) + } else { + composeState.value = ComposeState(message = ComposeMessage(), useLinkPreviews = true) + sendCommandMsg(chatsCtx, chat,"/${cmd.keyword}") + } + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + maxLines = 1, + modifier = Modifier.weight(1f), + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Text( + text = "/${cmd.keyword}", + style = MaterialTheme.typography.body2, + maxLines = 1, + color = MaterialTheme.colors.secondary + ) + } + } + } + is ChatBotCommand.Menu -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(COMMAND_MENU_ROW_SIZE) + .clickable { + menuTreeBackPath.value += Pair(cmd.label, currentCommands.value) + currentCommands.value = cmd.commands + }, + contentAlignment = Alignment.Center + ) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text( + text = cmd.label, + style = MaterialTheme.typography.body1, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.weight(1f), + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + Icon( + painterResource(MR.images.ic_chevron_right), + contentDescription = null, + tint = MaterialTheme.colors.secondary + ) + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, offsetY.value.toInt()) } + .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) { + scope.launch { closeCommandsMenu() } + }, + contentAlignment = Alignment.BottomStart + ) { + LazyColumnWithScrollBarNoAppBar( + Modifier + .heightIn(max = MAX_COMMAND_MENU_HEIGHT) + .background(MaterialTheme.colors.surface), + maxHeight = remember { mutableStateOf(MAX_COMMAND_MENU_HEIGHT) }, + containerAlignment = Alignment.BottomEnd + ) { + itemsIndexed(currentCommands.value, key = { i, cmd -> "$i ${cmd.hashCode()}" }) { i, command -> + if (i == 0) { + val prev = menuTreeBackPath.value.lastOrNull() + if (prev != null) { + Divider() + MenuLabelRow(prev) + } + } + Divider() + Box(Modifier.fillMaxWidth()) { CommandRow(command) } + } + } + } +} 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 ce55c62ae2..fdc6a3fed5 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 @@ -17,7 +17,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextDecoration import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp @@ -345,6 +347,7 @@ fun ComposeView( chatsCtx: ChatModel.ChatsContext, chat: Chat, composeState: MutableState, + showCommandsMenu: MutableState, attachmentOption: MutableState, showChooseAttachment: () -> Unit, focusRequester: FocusRequester?, @@ -353,16 +356,21 @@ fun ComposeView( fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) - fun getSimplexLink(parsedMsg: List?): Pair { + fun getMessageLinks(parsedMsg: List?): Pair { if (parsedMsg == null) return null to false - val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink } - return link?.text to simplexLink + for (ft in parsedMsg) { + val link = ft.linkUri + if (link != null && !cancelledLinks.contains(link) && !isSimplexLink(link)) { + return link to simplexLink + } + } + return null to simplexLink } val linkUrl = rememberSaveable { mutableStateOf(null) } // default value parsed because of draft - val hasSimplexLink = rememberSaveable { mutableStateOf(getSimplexLink(parseToMarkdown(composeState.value.message.text)).second) } + val hasSimplexLink = rememberSaveable { mutableStateOf(getMessageLinks(parseToMarkdown(composeState.value.message.text)).second) } val prevLinkUrl = rememberSaveable { mutableStateOf(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() @@ -379,6 +387,7 @@ fun ComposeView( if (wait != null) delay(wait) val lp = getLinkPreview(url) if (lp != null && pendingLinkUrl.value == url) { + chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) // to avoid showing alert to current users, show alert in v6.5 composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp)) pendingLinkUrl.value = null } else if (pendingLinkUrl.value == url) { @@ -391,7 +400,7 @@ fun ComposeView( fun showLinkPreview(parsedMessage: List?) { prevLinkUrl.value = linkUrl.value - val linkParsed = getSimplexLink(parsedMessage) + val linkParsed = getMessageLinks(parsedMessage) linkUrl.value = linkParsed.first hasSimplexLink.value = linkParsed.second val url = linkUrl.value @@ -483,7 +492,7 @@ fun ComposeView( if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> withContext(Dispatchers.Main) { - chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -498,7 +507,7 @@ fun ComposeView( return when (val composePreview = composeState.value.preview) { is ComposePreview.CLinkPreview -> { val parsedMsg = parseToMarkdown(msgText) - val url = getSimplexLink(parsedMsg).first + val url = getMessageLinks(parsedMsg).first val lp = composePreview.linkPreview if (lp != null && url == lp.uri) { MsgContent.MCLink(msgText, preview = lp) @@ -593,7 +602,6 @@ fun ComposeView( } suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List? { - val cInfo = chat.chatInfo val cs = composeState.value var sent: List? var lastMessageFailedToSend: ComposeState? = null @@ -859,9 +867,53 @@ fun ComposeView( } } + fun sanitizeMessage(parsedMsg: List): Triple, Int?> { + var pos = 0 + var updatedMsg = "" + var sanitizedPos: Int? = null + val updatedParsedMsg = parsedMsg.map { ft -> + var updated = ft + when(ft.format) { + is Format.Uri -> { + val sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized + if (sanitized != null) { + updated = FormattedText(text = sanitized, format = Format.Uri()) + pos += updated.text.count() + sanitizedPos = pos + } + } + is Format.HyperLink -> { + val sanitized = parseSanitizeUri(ft.format.linkUri)?.uriInfo?.sanitized + if (sanitized != null) { + val updatedText = if (ft.format.showText == null) sanitized else "[${ft.format.showText}]($sanitized)" + updated = FormattedText(text = updatedText, format = Format.HyperLink(showText = ft.format.showText, linkUri = sanitized)) + pos += updated.text.count() + sanitizedPos = pos + } + } + else -> + pos += ft.text.count() + } + updatedMsg += updated.text + updated + } + return Triple(updatedMsg, updatedParsedMsg, sanitizedPos) + } + fun onMessageChange(s: ComposeMessage) { - val parsedMessage = parseToMarkdown(s.text) - composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + var parsedMessage = parseToMarkdown(s.text) + if (chatModel.controller.appPrefs.privacySanitizeLinks.state.value && parsedMessage != null) { + val (updatedMsg, updatedParsedMsg, sanitizedPos) = sanitizeMessage(parsedMessage) + if (sanitizedPos == null) { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage) + } else { + val message = if (sanitizedPos < s.selection.start) s.copy(text = updatedMsg) else ComposeMessage(updatedMsg, TextRange(sanitizedPos, sanitizedPos)) + composeState.value = composeState.value.copy(message = message, parsedMessage = updatedParsedMsg) + parsedMessage = updatedParsedMsg + } + } else { + composeState.value = composeState.value.copy(message = s, parsedMessage = parsedMessage ?: FormattedText.plain(s.text)) + } if (isShortEmoji(s.text)) { textStyle.value = if (s.text.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont } else { @@ -874,7 +926,7 @@ fun ComposeView( hasSimplexLink.value = false } } else if (s.text.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) { - hasSimplexLink.value = getSimplexLink(parsedMessage).second + hasSimplexLink.value = getMessageLinks(parsedMessage).second } else { hasSimplexLink.value = false } @@ -1064,6 +1116,22 @@ fun ComposeView( val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) + @Composable + fun CommandsButton() { + val commandsEnabled = chat.chatInfo.sendMsgEnabled && chat.chatInfo.menuCommands.isNotEmpty() + IconButton( + onClick = { showCommandsMenu.value = !showCommandsMenu.value }, + enabled = commandsEnabled + ) { + Box( + modifier = Modifier.size(28.dp).clip(CircleShape), + contentAlignment = Alignment.Center + ) { + Text("//", style = MaterialTheme.typography.h3.copy(fontStyle = FontStyle.Italic, color = if (commandsEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)) + } + } + } + @Composable fun AttachmentButton() { val isGroupAndProhibitedFiles = @@ -1087,7 +1155,6 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), enabled = attachmentEnabled ) { Icon( @@ -1101,6 +1168,24 @@ fun ComposeView( } } + @Composable + fun AttachmentAndCommandsButtons() { + val cInfo = chat.chatInfo + Row( + Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), + horizontalArrangement = Arrangement.spacedBy((-8).dp) + ) { + val msg = composeState.value.message.text.trim() + val showAttachment = cInfo !is ChatInfo.Direct || cInfo.contact.profile.peerType != ChatPeerType.Bot || cInfo.featureEnabled(ChatFeature.Files) + if (cInfo.useCommands && (!showAttachment || msg.isEmpty() || msg.startsWith("/"))) { + CommandsButton() + } + if (showAttachment) { + AttachmentButton() + } + } + } + val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } LaunchedEffect(allowedVoiceByPrefs) { if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { @@ -1280,7 +1365,7 @@ fun ComposeView( ) Text( text, - style = MaterialTheme.typography.caption, + style = MaterialTheme.typography.body2, color = if (composeState.value.inProgress) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) } @@ -1433,7 +1518,7 @@ fun ComposeView( ContextSendMessageToConnect(generalGetString(MR.strings.compose_send_direct_message_to_connect)) Divider() Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentButton() + AttachmentAndCommandsButtons() SendMsgView_( disableSendButton = disableSendButton, sendToConnect = { withApi { sendMemberContactInvitation() } } @@ -1453,11 +1538,19 @@ fun ComposeView( connect = { withApi { sendConnectPreparedContact() } } ) ConnectionMode.Con -> - SendContactRequestView( - disableSendButton = disableSendButton, - icon = MR.images.ic_person_add_filled, - sendRequest = { showSendConnectPreparedContactAlert() } - ) + if (chat.chatInfo.contact.isBot) { + ConnectButtonView( + text = stringResource(MR.strings.compose_view_connect), + icon = MR.images.ic_bolt_filled, + connect = { withApi { sendConnectPreparedContact() } } + ) + } else { + SendContactRequestView( + disableSendButton = disableSendButton, + icon = MR.images.ic_person_add_filled, + sendRequest = { showSendConnectPreparedContactAlert() } + ) + } } } else if ( chat.chatInfo is ChatInfo.Direct @@ -1480,7 +1573,7 @@ fun ComposeView( ) } else { Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { - AttachmentButton() + AttachmentAndCommandsButtons() SendMsgView_(disableSendButton = disableSendButton) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index ac722783a3..03d0b99854 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -77,7 +77,7 @@ fun SelectedItemsButtonsToolbar( val forwardCountProhibited = remember { mutableStateOf(false) } Box { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty - ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(false) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) Row( Modifier .matchParentSize() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt index 62f1a4337c..81e7d80da3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt @@ -44,7 +44,7 @@ fun SelectedItemsMembersToolbar( ) { // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty Box(Modifier.alpha(0f)) { - ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) + ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(false) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() }) } Row( Modifier 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 0f9b3151fe..6f873035f1 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 @@ -65,7 +65,7 @@ data class ChatItemReactionMenuItem ( fun ChatItemView( chatsCtx: ChatModel.ChatsContext, rhId: Long?, - cInfo: ChatInfo, + chat: Chat, cItem: ChatItem, composeState: MutableState, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -109,6 +109,7 @@ fun ChatItemView( itemSeparation: ItemSeparation, preview: Boolean = false, ) { + val cInfo = chat.chatInfo val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart @@ -327,7 +328,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(chatsCtx, cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem) + FramedItemView(chatsCtx, chat, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem) } fun deleteMessageQuestionText(): String { @@ -1449,7 +1450,7 @@ fun PreviewChatItemView( ChatItemView( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.sampleData, chatItem, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, @@ -1500,7 +1501,7 @@ fun PreviewChatItemViewDeletedContent() { ChatItemView( chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null), rhId = null, - ChatInfo.Direct.sampleData, + Chat.sampleData, ChatItem.getDeletedContentSampleData(), useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, 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 ad02b8d812..f36da6c908 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 @@ -21,14 +21,17 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.math.ceil @Composable fun FramedItemView( chatsCtx: ChatModel.ChatsContext, - chatInfo: ChatInfo, + chat: Chat, ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, @@ -43,6 +46,7 @@ fun FramedItemView( scrollToItemId: MutableState, scrollToQuotedItemFromItem: (Long) -> Unit = {}, ) { + val chatInfo = chat.chatInfo val sent = ci.chatDir.sent val chatTTL = chatInfo.timedMessagesTTL @@ -182,7 +186,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } @@ -295,7 +299,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVideo -> { @@ -303,26 +307,26 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } is MsgContent.MCReport -> { @@ -331,9 +335,9 @@ fun FramedItemView( append(if (mc.text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") } } - CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) + CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp, prefix = prefix) } - else -> CIMarkdownText(ci, chatInfo, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) + else -> CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp) } } } @@ -353,8 +357,9 @@ fun FramedItemView( @Composable fun CIMarkdownText( + chatsCtx: ChatModel.ChatsContext, ci: ChatItem, - chatInfo: ChatInfo, + chat: Chat, chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, @@ -364,9 +369,11 @@ fun CIMarkdownText( prefix: AnnotatedString? = null ) { Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) { + val chatInfo = chat.chatInfo val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, + sendCommandMsg = if (chatInfo.useCommands && chat.chatInfo.sndReady) { { msg -> sendCommandMsg(chatsCtx, chat, msg) } } else null, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, mentions = ci.mentions, userMemberId = when { chatInfo is ChatInfo.Group -> chatInfo.groupInfo.membership.memberId @@ -377,6 +384,32 @@ fun CIMarkdownText( } } +fun sendCommandMsg(chatsCtx: ChatModel.ChatsContext, chat: Chat, msg: String) { + if (chat.chatInfo.sndReady) { + withLongRunningApi(slow = 60_000) { + val cInfo = chat.chatInfo + val chatItems = + chatModel.controller.apiSendMessages( + rh = chat.remoteHostId, + type = cInfo.chatType, + id = cInfo.apiId, + scope = cInfo.groupChatScope(), + composedMessages = listOf(ComposedMessage(fileSource = null, quotedItemId = null, msgContent = MsgContent.MCText(msg), mentions = emptyMap())) + ) + if (!chatItems.isNullOrEmpty()) { + chatItems.forEach { aChatItem -> + withContext(Dispatchers.Main) { + chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) + } + } + } + } + } else { + AlertManager.shared.showAlertMsg(MR.strings.cant_send_message_alert_title, MR.strings.cant_send_commands_alert_text) + } +} + + const val CHAT_IMAGE_LAYOUT_ID = "chatImage" const val CHAT_BUBBLE_LAYOUT_ID = "chatBubble" const val CHAT_COMPOSE_LAYOUT_ID = "chatCompose" 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 ad11eb4897..6a6485bc42 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 @@ -1,5 +1,8 @@ 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.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme @@ -11,15 +14,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.AnnotatedString.Range +import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.LayoutDirection 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.views.helpers.* +import chat.simplex.res.* import kotlinx.coroutines.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) @@ -61,6 +65,7 @@ fun MarkdownText ( mentions: Map? = null, userMemberId: String? = null, toggleSecrets: Boolean, + sendCommandMsg: ((String) -> Unit)? = null, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, @@ -134,51 +139,111 @@ fun MarkdownText ( } Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent?.second ?: mapOf()) } else { - var hasAnnotations = false + var hasLinks = false + var hasSecrets = false + var hasCommands = false val annotatedText = buildAnnotatedString { inlineContent?.first?.invoke(this) appendSender(this, sender, senderBold) if (prefix != null) append(prefix) for ((i, ft) in formattedText.withIndex()) { if (ft.format == null) append(ft.text) - else if (toggleSecrets && ft.format is Format.Secret) { - val ftStyle = ft.format.style - hasAnnotations = true - val key = i.toString() - withAnnotation(tag = "SECRET", annotation = key) { - if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } - } - } else if (ft.format is Format.Mention) { - val mention = mentions?.get(ft.format.memberName) - - if (mention != null) { - if (mention.memberRef != null) { - val displayName = mention.memberRef.displayName - val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { - displayName - } else { - "${mention.memberRef.localAlias} ($displayName)" - } - val mentionStyle = if (mention.memberId == userMemberId) ft.format.style.copy(color = MaterialTheme.colors.primary) else ft.format.style - - withStyle(mentionStyle) { append(mentionText(name)) } - } else { - withStyle( ft.format.style) { append(mentionText(ft.format.memberName)) } - } - } else { - append(ft.text) - } - } else { - val link = ft.link(linkMode) - if (link != null) { - hasAnnotations = true + else when(ft.format) { + is Format.Bold -> withStyle(ft.format.style) { append(ft.text) } + is Format.Italic -> withStyle(ft.format.style) { append(ft.text) } + is Format.StrikeThrough -> withStyle(ft.format.style) { append(ft.text) } + is Format.Snippet -> withStyle(ft.format.style) { append(ft.text) } + is Format.Colored -> withStyle(ft.format.style) { append(ft.text) } + is Format.Secret -> { val ftStyle = ft.format.style - withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { - withStyle(ftStyle) { append(ft.viewText(linkMode)) } + if (toggleSecrets) { + hasSecrets = true + val key = i.toString() + withAnnotation(tag = "SECRET", annotation = key) { + if (showSecrets[key] == true) append(ft.text) else withStyle(ftStyle) { append(ft.text) } + } + } else { + withStyle(ftStyle) { append(ft.text) } } - } else { - withStyle(ft.format.style) { append(ft.text) } } + is Format.Mention -> { + val mention = mentions?.get(ft.format.memberName) + if (mention != null) { + val ftStyle = ft.format.style + if (mention.memberRef != null) { + val displayName = mention.memberRef.displayName + val name = if (mention.memberRef.localAlias.isNullOrEmpty()) { + displayName + } else { + "${mention.memberRef.localAlias} ($displayName)" + } + val mentionStyle = if (mention.memberId == userMemberId) ftStyle.copy(color = MaterialTheme.colors.primary) else ftStyle + withStyle(mentionStyle) { append(mentionText(name)) } + } else { + withStyle(ftStyle) { append(mentionText(ft.format.memberName)) } + } + } else { + append(ft.text) + } + } + is Format.Command -> + if (sendCommandMsg == null) { + append(ft.text) + } else { + hasCommands = true + val ftStyle = ft.format.style + val cmd = ft.format.commandStr + withAnnotation(tag = "COMMAND", annotation = cmd) { + withStyle(ftStyle) { append("/$cmd") } + } + } + is Format.Uri -> { + hasLinks = true + val ftStyle = Format.linkStyle + val s = ft.text + val link = if (s.startsWith("http://") || s.startsWith("https://")) s else "https://$s" + withAnnotation(tag = "WEB_URL", annotation = link) { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.HyperLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "WEB_URL", annotation = ft.format.linkUri) { + withStyle(ftStyle) { append(ft.format.showText ?: ft.text) } + } + } + is Format.SimplexLink -> { + hasLinks = true + val ftStyle = Format.linkStyle + val link = + if (linkMode == SimplexLinkMode.BROWSER && ft.format.showText == null && !ft.text.startsWith("[")) ft.text + else ft.format.simplexUri + val t = ft.format.showText ?: if (linkMode == SimplexLinkMode.DESCRIPTION) ft.format.linkType.description else null + withAnnotation(tag = "SIMPLEX_URL", annotation = link) { + if (t == null) { + withStyle(ftStyle) { append(ft.text) } + } else { + withStyle(ftStyle) { append("$t ") } + withStyle(ftStyle.copy(fontStyle = FontStyle.Italic)) { append(ft.format.viaHosts) } + } + } + } + is Format.Email -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "mailto:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Phone -> { + hasLinks = true + val ftStyle = Format.linkStyle + withAnnotation(tag = "OTHER_URL", annotation = "tel:${ft.text}") { + withStyle(ftStyle) { append(ft.text) } + } + } + is Format.Unknown -> append(ft.text) } } if (meta?.isLive == true) { @@ -189,51 +254,51 @@ fun MarkdownText ( withStyle(reserveTimestampStyle) { append("\n" + metaText) } else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } - if (hasAnnotations && uriHandler != null) { + if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) { val icon = remember { mutableStateOf(PointerIcon.Default) } ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } + if (hasLinks) { + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + withAnnotation("WEB_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("SIMPLEX_URL") { a -> onLinkLongClick(a.item) } + withAnnotation("OTHER_URL") { a -> onLinkLongClick(a.item) } + } }, onClick = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - try { - uriHandler.openUri(annotation.item) - } catch (e: Exception) { - // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch - // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) - Log.e(TAG, "Open url: ${e.stackTraceToString()}") - } - } - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - uriHandler.openVerifiedSimplexUri(annotation.item) - } - annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { annotation -> - val key = annotation.item + val withAnnotation: (String, (Range) -> Unit) -> Unit = { tag, f -> + annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f) + } + if (hasLinks && uriHandler != null) { + withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) } + withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) } + withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) } + } + if (hasSecrets) { + withAnnotation("SECRET") { a -> + val key = a.item showSecrets[key] = !(showSecrets[key] ?: false) } + } + if (hasCommands && sendCommandMsg != null) { + withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") } + } }, onHover = { offset -> - icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) - .firstOrNull()?.let { + 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("SECRET") || hasAnnotation("COMMAND")) { PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: annotatedText.getStringAnnotations(tag = "SECRET", start = offset, end = offset) - .firstOrNull()?.let { - PointerIcon.Hand - } ?: PointerIcon.Default + } else { + PointerIcon.Default + } }, shouldConsumeEvent = { offset -> - annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any() - annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any() + 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) } ) } else { @@ -302,6 +367,74 @@ fun ClickableText( ) } +fun openBrowserAlert(uri: String, uriHandler: UriHandler) { + val (res, err) = sanitizeUri(uri) + if (res == null) { + showInvalidLinkAlert(uri, err) + } else { + val message = if (uri.count() > 160) uri.substring(0, 159) + "…" else uri + val sanitizedUri = res.second + if (sanitizedUri == null) { + AlertManager.shared.showAlertDialog( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + confirmText = generalGetString(MR.strings.open_verb), + onConfirm = { safeOpenUri(uri, uriHandler) } + ) + } else { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.privacy_chat_list_open_web_link_question), + message, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(uri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_full_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + SectionItemView({ + AlertManager.shared.hideAlert() + safeOpenUri(sanitizedUri, uriHandler) + }) { + Text(generalGetString(MR.strings.privacy_chat_list_open_clean_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }) + } + } +} + +fun safeOpenUri(uri: String, uriHandler: UriHandler) { + try { + uriHandler.openUri(uri) + } catch (e: Exception) { + // It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch + // `tel:` scheme in url installed on a device (no phone app or contacts, maybe) + Log.e(TAG, "Open url: ${e.stackTraceToString()}") + showInvalidLinkAlert(uri, error = e.message) + } +} + +fun showInvalidLinkAlert(uri: String, error: String? = null) { + val message = if (error.isNullOrEmpty()) { uri } else { error + "\n" + uri } + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), message) +} + +fun sanitizeUri(s: String): Pair?, String?> { + val parsed = parseSanitizeUri(s) + return if (parsed?.uriInfo != null) { + (true to parsed.uriInfo.sanitized) to null + } else { + null to parsed?.parseError + } +} + private fun isRtl(s: CharSequence): Boolean { for (element in s) { val d = Character.getDirectionality(element) 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 5e9bac515d..7b5be0c323 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 @@ -659,7 +659,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = link.simplexLinkText(link.format.linkType, link.format.smpHosts) + val linkText = link.format.simplexLinkText searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true 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 e6c74b7558..d5d6facafe 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 @@ -176,12 +176,14 @@ fun ChatPreviewView( is ChatInfo.Direct -> if (cInfo.contact.isContactCard) { stringResource(MR.strings.contact_tap_to_connect) to MaterialTheme.colors.primary + } else if (cInfo.contact.isBot && cInfo.contact.nextConnectPrepared) { + stringResource(MR.strings.open_to_use_bot) to Color.Unspecified } else if (cInfo.contact.sendMsgToConnect) { stringResource(MR.strings.open_to_connect) to Color.Unspecified } else if (cInfo.contact.nextAcceptContactRequest) { stringResource(MR.strings.open_to_accept) to Color.Unspecified } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null && cInfo.contact.active) { - if (cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) { + if ((cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con && !cInfo.contact.isBot) || cInfo.contact.contactGroupMemberId != null) { stringResource(MR.strings.contact_should_accept) to Color.Unspecified } else { stringResource(MR.strings.contact_connection_pending) to Color.Unspecified @@ -296,37 +298,9 @@ fun ChatPreviewView( val uriHandler = LocalUriHandler.current when (mc) { is MsgContent.MCLink -> SmallContentPreview { - val linkClicksEnabled = remember { appPrefs.privacyChatListOpenLinks.state }.value != PrivacyChatListOpenLinksMode.NO - IconButton({ - when (appPrefs.privacyChatListOpenLinks.get()) { - PrivacyChatListOpenLinksMode.YES -> uriHandler.openUriCatching(mc.preview.uri) - PrivacyChatListOpenLinksMode.NO -> defaultClickAction() - PrivacyChatListOpenLinksMode.ASK -> AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.privacy_chat_list_open_web_link_question), - text = mc.preview.uri, - buttons = { - Column { - if (chatModel.chatId.value != chat.id) { - SectionItemView({ - AlertManager.shared.hideAlert() - defaultClickAction() - }) { - Text(stringResource(MR.strings.open_chat), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - SectionItemView({ - AlertManager.shared.hideAlert() - uriHandler.openUriCatching(mc.preview.uri) - } - ) { - Text(stringResource(MR.strings.privacy_chat_list_open_web_link), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - ) - } - }, - if (linkClicksEnabled) Modifier.desktopPointerHoverIconHand() else Modifier, + IconButton( + { openBrowserAlert(mc.preview.uri, uriHandler) }, + Modifier.desktopPointerHoverIconHand(), ) { Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt index 75f61dda04..a55b1a0d56 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -2,14 +2,14 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.core.* -fun chatListAnimationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) - -fun newChatSheetAnimSpec() = tween(256, 0, LinearEasing) +fun chatListAnimationSpec() = tween(durationMillis = 350, easing = FastOutSlowInEasing) fun audioProgressBarAnimationSpec() = tween(durationMillis = 30, easing = LinearEasing) -fun userPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun userPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) -fun mentionPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun mentionPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) -fun contextUserPickerAnimSpec() = tween(256, 0, FastOutSlowInEasing) +fun commandMenuAnimSpec() = tween(350, 0, FastOutSlowInEasing) + +fun contextUserPickerAnimSpec() = tween(350, 0, FastOutSlowInEasing) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index 72fab4990b..72ef8c623d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -33,6 +33,7 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme when (chatInfo) { is ChatInfo.Group -> chatInfo.groupInfo.chatIconName is ChatInfo.Local -> MR.images.ic_folder_filled + is ChatInfo.Direct -> chatInfo.contact.chatIconName else -> MR.images.ic_account_circle_filled } ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) 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 786e3483c5..434cb6ce27 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 @@ -63,7 +63,7 @@ private suspend fun planAndConnectTask( val (connectionLink, connectionPlan) = result val link = strHasSingleSimplexLink(shortOrFullLink.trim()) val linkText = if (link?.format is Format.SimplexLink) - "

${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}" + "

${link.format.simplexLinkText}" else "" when (connectionPlan) { @@ -390,7 +390,7 @@ private fun showOpenKnownContactAlert(chatModel: ChatModel, rhId: Long?, close: ProfileImage( size = alertProfileImageSize, image = contact.profile.image, - icon = MR.images.ic_account_circle_filled + icon = contact.chatIconName ) }, confirmText = generalGetString(if (contact.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat), @@ -474,7 +474,7 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: (( ProfileImage( size = alertProfileImageSize, image = groupInfo.groupProfile.image, - icon = if (groupInfo.businessChat == null) MR.images.ic_supervised_user_circle_filled else MR.images.ic_work_filled_padded + icon = groupInfo.chatIconName ) }, confirmText = generalGetString( @@ -515,7 +515,10 @@ fun showPrepareContactAlert( ProfileImage( size = alertProfileImageSize, image = contactShortLinkData.profile.image, - icon = if (contactShortLinkData.business) MR.images.ic_work_filled_padded else MR.images.ic_account_circle_filled + icon = + if (contactShortLinkData.business) MR.images.ic_work_filled_padded + else if (contactShortLinkData.profile.peerType == ChatPeerType.Bot) MR.images.ic_cube + else MR.images.ic_account_circle_filled ) }, confirmText = generalGetString(MR.strings.connect_plan_open_new_chat), 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 1cb1903a14..ef6e426141 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 @@ -519,10 +519,8 @@ private fun ContactsSearchBar( // if SimpleX link is pasted, show connection dialogue hideKeyboard(view) if (link.format is Format.SimplexLink) { - val linkText = - link.simplexLinkText(link.format.linkType, link.format.smpHosts) - searchText.value = - searchText.value.copy(linkText, selection = TextRange.Zero) + val linkText = link.format.simplexLinkText + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) } searchShowingSimplexLink.value = true searchChatFilteredBySimplexLink.value = null 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 c5a4ae5f70..dcb71a552d 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 @@ -2,18 +2,10 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced -import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler -import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource @@ -67,7 +59,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) } } - SectionBottomSpacer() + SectionDividerSpaced(maxTopPadding = true) + SectionView(stringResource(MR.strings.deprecated_options_section).uppercase()) { + val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode + SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { + simplexLinkMode.set(it) + chatModel.simplexLinkMode.value = it + }) + SectionBottomSpacer() + } } } 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 915119fa64..2771b5ac62 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 @@ -56,16 +56,22 @@ fun PrivacySettingsView( setPerformLA: (Boolean) -> Unit ) { ColumnWithScrollBar { - val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) PrivacyDeviceSection(showSettingsModal, setPerformLA) SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) - ChatListLinksOptions(appPrefs.privacyChatListOpenLinks.state, onSelected = { - appPrefs.privacyChatListOpenLinks.set(it) - }) + SettingsPreferenceItem( + painterResource(MR.images.ic_travel_explore), + stringResource(MR.strings.send_link_previews), + chatModel.controller.appPrefs.privacyLinkPreviews, + onChange = { _ -> chatModel.controller.appPrefs.privacyLinkPreviewsShowAlert.set(false) } // to avoid showing alert to current users, show alert in v6.5 + ) + SettingsPreferenceItem( + painterResource(MR.images.ic_link), + stringResource(MR.strings.sanitize_links_toggle), + chatModel.controller.appPrefs.privacySanitizeLinks + ) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -84,10 +90,6 @@ fun PrivacySettingsView( chatModel.draftChatId.value = null } }) - SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { - simplexLinkMode.set(it) - chatModel.simplexLinkMode.value = it - }) } SectionDividerSpaced() @@ -218,27 +220,7 @@ fun PrivacySettingsView( } @Composable -private fun ChatListLinksOptions(state: State, onSelected: (PrivacyChatListOpenLinksMode) -> Unit) { - val values = remember { - PrivacyChatListOpenLinksMode.entries.map { - when (it) { - PrivacyChatListOpenLinksMode.YES -> it to generalGetString(MR.strings.privacy_chat_list_open_links_yes) - PrivacyChatListOpenLinksMode.NO -> it to generalGetString(MR.strings.privacy_chat_list_open_links_no) - PrivacyChatListOpenLinksMode.ASK -> it to generalGetString(MR.strings.privacy_chat_list_open_links_ask) - } - } - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.privacy_chat_list_open_links), - values, - state, - icon = painterResource(MR.images.ic_open_in_new), - onSelected = onSelected - ) -} - -@Composable -private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { +fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value) val values = remember { 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 e962c3a646..0b53b8d577 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -449,6 +449,7 @@ No chats found Tap to Connect Open to connect + Open to use bot Open to accept contact should accept… Connect with %1$s? @@ -489,8 +490,10 @@ Tap Connect to chat Tap Connect to send request + Tap Connect to use bot Accept contact request Your contact + Bot Tap Join group Your group Group @@ -562,6 +565,7 @@ you are observer reviewed by admins member has old version + To send commands you must be connected. Image @@ -1068,6 +1072,7 @@ Enable logs Database IDs and Transport isolation option. Developer options + Deprecated options Show internal errors Show slow API calls Shutdown? @@ -1364,6 +1369,7 @@ The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). Without Tor or VPN, your IP address will be visible to file servers. Send link previews + Remove link tracking Show last messages Message draft App data backup @@ -1430,6 +1436,8 @@ Ask Open web link? Open link + Open full link + Open clean link YOU @@ -2157,6 +2165,9 @@ Allow your contacts to send voice messages. Allow voice messages only if your contact allows them. Prohibit sending voice messages. + Allow your contacts to send files and media. + Allow files and media only if your contact allows them. + Prohibit sending files and media. Allow your contacts adding message reactions. Allow message reactions only if your contact allows them. Prohibit message reactions. @@ -2175,6 +2186,10 @@ Only you can send voice messages. Only your contact can send voice messages. Voice messages are prohibited in this chat. + Both you and your contact can send files and media. + Only you can send files and media. + Only your contact can send files and media. + Files and media are prohibited in this chat. Both you and your contact can add message reactions. Only you can add message reactions. Only your contact can add message reactions. 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 ebd4b05e71..384a885e3b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -1201,8 +1201,8 @@ Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно! Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно! Няма да загубите контактите си, ако по-късно изтриете адреса си. - Вашите настройки - Вашият адрес в SimpleX + Настройки + SimpleX адрес Използвай за нови връзки Вашите XFTP сървъри Използвай сървърите на SimpleX Chat\? @@ -1260,7 +1260,7 @@ Вие контролирате своя чат! Грешна парола! Гласови съобщения - Вашите настройки + Настройки Какво е новото Вашите контакти могат да позволят пълното изтриване на съобщението. Актуализирай паролата на базата данни @@ -1376,7 +1376,7 @@ Грешка при създаване на контакт с член Изпрати лично съобщение за свързване изпрати за свързване - свързан директно + заявка за връзка Разшири Блокиране на членове на групата Изпрати отново заявката за свързване? @@ -2171,7 +2171,7 @@ Вижте актуализираните условия Xiaomi устройства : моля, активирайте Autostart в системните настройки, за да работят известията.]]> Няма съобщение - Или да се сподели лично + Или сподели лично криптирани от край до край, с постквантова сигурност в директните съобщения.]]> Без фонова услуга Проверявай за съобщения на всеки 10 минути @@ -2504,4 +2504,8 @@ Сподели стар линк Линкът ще бъде кратък и профилът на групата ще бъде споделен чрез него. Обнови групов линк + ЗАЯВКИ ЗА КОНТАКТ ОТ ГРУПИ + Членът е изтрит - не може да се приеме заявката + заявка за връзка от група %1$s + Тази настройка е за текущия профил diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml index 57891501f8..f106e8085b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ca/strings.xml @@ -524,7 +524,7 @@ Confirmeu el nou mot de pas… Voleu canviar la frase de pas per a la base de dades? connectat - connectat directament + connexió sol·licitada canviant d\'adreça per %s… connectat complet @@ -2477,4 +2477,8 @@ Compartir l\'enllaç antic L\'enllaç serà curt i el perfil del grup es compartirà a través d\'ell. Actualitzar l\'enllaç del grup + SOL·LICITUDS DE CONTACTE DE GRUPS + Membre eliminat(da); no es pot acceptar la sol·licitud. + connexió sol·licitada del grup %1$s + Aquesta configuració és per al perfil actual 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 334deb0490..c369ab6b8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -2432,4 +2432,22 @@ Otevřít chat Otevřít nový chat Otevřít novou skupinu + Připojit + Připojte se rychleji! 🚀 + POŽADAVKY NA PŘIPOJENÍ ZE SKUPIN + kontakt by měl přijmout… + Vytvořit vaši adresu + Popis příliš dlouhý + Povolení mizících zpráv ve výchozím nastavení. + Chyba změny profilu + Chyba otevření chatu + Chyba otevření skupiny + Chyba odmítnutí žádosti o kontakt + Skupina + Připojit ke skupině + Udržujte chat čistý + Méně provozu na mobilních sítích. + Načítání profilu… + Člen je smazán - nemůže přijmout žádost + Nová skupinová role: Moderátor 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 2421337ce8..d8d710dd4d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -2543,7 +2543,7 @@ Änderung des Profils nicht möglich Wenn Sie nach dem Verbindungsversuch ein anderes Profil verwenden möchten, löschen Sie den Chat und verwenden Sie den Link erneut. Chat mit Administratoren - Chat mit Mitgliedern bevor sie beitreten. + Mit Mitgliedern chatten bevor sie beitreten. Schneller miteinander verbinden! 🚀 Weniger Datenverkehr in mobilen Netzen. Sobald Sie auf Verbinden tippen, erhalten Sie sofort eine Nachricht. @@ -2573,9 +2573,9 @@ 4 neue Sprachen für die Bedienoberfläche Katalanisch, Indonesisch, Rumänisch und Vietnamesisch - Dank unserer Nutzer! Ihre Adresse erstellen - Verschwindende Nachrichten per Voreinstellung aktiviert. - Halten Sie Ihre Chats übersichtlich - Geben Sie eine Profil-Biografie und eine Begrüßungsmeldung ein. + Verschwindende Nachrichten sind per Voreinstellung aktiviert. + Ihre Chats übersichtlich halten + Sie können eine Profil-Biografie und eine Begrüßungsmeldung eingeben. Ihre Adresse teilen Verkürzte SimpleX-Adresse Die Zeit bis zum Verschwinden wird nur für neue Kontakte eingestellt. 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 18458e8ebc..4c92d8e7da 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -14,7 +14,7 @@ Añadir servidores mediante el escaneo de códigos QR. Añadir servidores predefinidos Todos los miembros del grupo permanecerán conectados. - Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite para tí. (24 horas) + Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también lo permite. (24 horas) Android Keystore se usará para almacenar la frase de contraseña de forma segura después de cambiarla o reiniciar la aplicación - permitirá recibir notificaciones. Permites a tus contactos enviar mensajes temporales Permites a tus contactos enviar mensajes de voz. @@ -304,7 +304,7 @@ Imagen guardada en la Galería El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Enlace de invitación de un uso - Pegar el enlace recibido + Pega el enlace recibido Error al guardar perfil de grupo Salir sin guardar Archivo guardado @@ -443,8 +443,8 @@ Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes. Cómo afecta a la batería Instantánea - Unirte - Unirte en modo incógnito + Unirme + Unirme en modo incógnito indirecta (%1$s) Claro Activado @@ -495,7 +495,7 @@ Error en la entrega del mensaje Lo más probable es que este contacto haya eliminado la conexión contigo. Moderar - Unirte como %s + Unirme como %s Sólo se pueden enviar 10 imágenes al mismo tiempo ¡Archivo grande! Silenciar @@ -518,7 +518,7 @@ OK (sólo almacenado por miembros del grupo) Ayuda sintaxis markdown - Servidores y Red + Servidores y Redes Se usarán hosts .onion si están disponibles. cursiva Llamada audio entrante @@ -715,7 +715,7 @@ envío no autorizado Escribe un nombre para el contacto Error desconocido - El rol cambiará a %s. Todos serán notificados. + El rol cambiará a %s. Se notificará en el grupo. La seguridad de SimpleX Chat ha sido auditada por Trail of Bits. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. Mensajes de chat SimpleX @@ -924,7 +924,7 @@ No tienes chats El contacto ha enviado un archivo mayor al máximo admitido (%1$s ). %1$d mensaje(s) omitido(s) - Dejarás de recibir mensajes de este grupo. El historial del chat se conservará. + Dejarás de recibir mensajes del grupo. El historial del chat se conservará. Mostrar código de seguridad Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos. ¡Mensajes de voz no permitidos! @@ -1424,7 +1424,7 @@ Grupo abierto Conexión finalizada (este dispositivo v%s)]]> - ¡Los mensajes de %s serán mostrados! + ¡Los mensajes nuevos de %s serán mostrados! Nombre de este dispositivo… Error Conectar con ordenador @@ -1534,7 +1534,7 @@ Añadir contacto Pulsa para escanear Guardar - Pulsa para pegar el enlace + Pulsa aquí para pegar el enlace Buscar o pegar enlace SimpleX Con uso reducido de batería. bloqueado por administrador @@ -1552,7 +1552,7 @@ ¿Bloqear miembro para todos? Creado: %s Bloquear para todos - ¿Desbloquear el miembro para todos? + ¿Desbloquear al miembro para todos? Desbloquear para todos bloqueado bloqueado por administrador @@ -1577,7 +1577,7 @@ El ordenador tiene un código de invitación incorrecto El ordenador ha sido desconectado estado desconocido - Migración de la base de datos en curso. \nPodría tardar varios minutos. + Migrando base de datos.\nPuede tardar varios minutos. El ordenador tiene una versión sin soporte. Por favor, asegúrate de usar la misma versión en ambos dispositivos el contacto %1$s ha cambiado a %2$s perfil actualizado @@ -2122,7 +2122,7 @@ Ajustes de dirección Crear enlace de un solo uso Para redes sociales - ¿Dirección SimpleX o enlace de un uso? + ¿Dirección SimpleX o enlace de un solo uso? Operadores de servidores Operadores de red Las condiciones de los operadores habilitados serán aceptadas después de 30 días. @@ -2143,7 +2143,7 @@ El operador del servidor ha cambiado. El protocolo del servidor ha cambiado. Barras de herramientas - Difuminar + Difuminado Navegación en el chat mejorada Descentralización de la red - El chat abre en el primer mensaje no leído.\n- Desplazamiento hasta los mensajes citados. @@ -2222,8 +2222,8 @@ El chat será eliminado para tí. ¡No puede deshacerse! Sólo los propietarios del chat pueden cambiar las preferencias. El miembro será eliminado del chat. ¡No puede deshacerse! - El rol cambiará a %s. Todos serán notificados. - Dejarás de recibir mensajes de este chat. El historial del chat se conserva. + El rol cambiará a %s. Se notificará en el chat. + Dejarás de recibir mensajes del chat. El historial del chat se conserva. Cómo ayuda a la privacidad Cuando está habilitado más de un operador, ninguno dispone de los metadatos para conocer quién se comunica con quién. Tu perfil de chat será enviado a los miembros de chat @@ -2359,7 +2359,7 @@ rechazado rechazado ¿Expulsar miembros? - ¡Los mensajes de estos miembros serán mostrados! + ¡Los mensajes nuevos de estos miembros serán mostrados! ¿Desbloquear los miembros para todos? ¡Todos los mensajes nuevos de estos miembros estarán ocultos! ¿Bloquear miembros para todos? @@ -2394,14 +2394,14 @@ has aceptado al miembro pendiente de revisión por revisar - Chat con los administradores + Chat con administradores Chat con miembro en revisión por los administradores %1$s aceptado desactivado Admisión de miembros el miembro usa una versión antigua - Sin chats con miembros + Sin chats Error al eliminar el chat con el miembro %d chats con miembros %d mensajes @@ -2411,19 +2411,19 @@ no se pueden enviar mensajes contacto eliminado contacto desactivado - el contacto no está listo + en espera de ser aceptado el grupo ha sido eliminado no sincronizado expulsado del grupo petición para unirse rechazada ¡No puedes enviar mensajes! - Puedes ver tus informes en Chat con los administradores + Puedes ver tus informes en Chat con administradores has salido te ha aceptado Un miembro nuevo desea unirse al grupo. todos Chat con miembros - Chat con los administradores + Chat con administradores Eliminar chat ¿Eliminar chat con el miembro? Rechazar @@ -2442,12 +2442,12 @@ Chatea con el miembro antes de unirse. Conectar ¡Conéctate más rápido! 🚀 - el contacto debe aceptar… + el contacto debe aceptarte… Error al cambiar el perfil Error al abrir el chat Error al abrir el grupo Error al rechazar la solicitud del contacto - Unirte al grupo + Unirme al grupo Menos tráfico en redes móviles. Tras pulsar Contactar, mensajea ya. Nuevo rol de grupo: Moderador @@ -2457,7 +2457,7 @@ Abrir grupo nuevo Abrir para aceptar Abrir para conectar - Abrir para unirte + Abre para unirte Timeout enrutamiento privado La dirección pasará a ser corta y tu perfil será compartido mediante la dirección. Timeout protocolo en segundo plano @@ -2491,7 +2491,7 @@ Grupo Pulsa Conectar para chatear Pulsa Conectar para enviar solicitud - Pulsa Unirte al grupo + Pulsa Unirme al grupo Mensajes temporales activados sólo para los contactos nuevos. Usar perfil incógnito Mi contacto empresarial 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 ebd2936312..4703958190 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -11,12 +11,12 @@ لغو تغییر نشانی تغییر نشانی را لغو می‌کنید؟ درباره سیمپل‌اکس(SimpleX) - به وسیله نشانی مخاطب متصل می‌شوید؟ - به وسیله لینک یک بار مصرف متصل می‌شوید؟ - از پروفایل ناشناس جدید استفاده کن + با نشانی مخاطب ارتباط برقرار شود؟ + با لینک یک بار مصرف ارتباط برقرار شود؟ + استفاده از پروفایل ناشناس جدید در حال باز کردن پایگاه داده… پروفایل شما به مخاطبی که این لینک را از او دریافت کردید، فرستاده خواهد شد. - متصل شدن به صورت ناشناس + اتصال ناشناس شما یک مسیر نامعتبر فایل به اشتراک گذاشتید. موضوع را به توسعه‌دهندگان برنامه گزارش دهید. هنوز از دریافت فایل پشتیبانی نمی‌شود قالب پیام نامعتبر @@ -30,7 +30,7 @@ در حال اتصال k به گروه می‌پیوندید؟ - از پروفایل کنونی استفاده کن + استفاده از پروفایل کنونی به تمام اعضای گروه متصل خواهید شد. متصل شدن مسیر نامعتبر فایل @@ -1001,14 +1001,14 @@ عضو پیشین %1$s بسط دادن انتخاب نقش عبارت عبور رمزنگاری پایگاه داده به‌روز خواهد شد. - مستقیما متصل شد + درخواست اتصال کرد %s متصل شد پیوستن به گروه به‌روزرسانی تایید عبارت عبور جدید… پایگاه داده رمزنگاری شود؟ نسخه پایگاه داده ناسازگار - پروفایل گروه به‌روز شد + پروفایل گروه به‌روز شده شما نشانی را برای %s تغییر دادید شما نشانی را تغییر دادید تجدید مذاکره رمزنگاری مجاز است @@ -2320,7 +2320,7 @@ پایگاه داده چت تکراری‌ها تغییر - Flux را در تنظیمات شبکه و سرورها فعال کنید تا حریم خصوصی متاداده بهتر شود. + Flux را در تنظیمات شبکه و سرورها فعال کنید تا حریم خصوصی فراداده بهتر شود. فعال کردن لاگ‌ها مذاکره مجدد رمزنگاری در حال انجام است. خطا @@ -2352,7 +2352,7 @@ اتصال اصلاح شود؟ اندازه فونت برای همه مدیران - برای بهبود حریم خصوصی متاداده. + برای بهبود حریم خصوصی فراداده. به عنوان مثال، اگر مخاطب شما پیام‌ها را از طریق یک سرور چت SimpleX دریافت کند، برنامه شما آن‌ها را از طریق یک سرور Flux تحویل خواهد داد. برای من برای مسیریابی خصوصی @@ -2508,4 +2508,8 @@ آدرس خود را به اشتراک بگذارید ۴ زبان جدید رابط کاربری کاتالان، اندونزیایی، رومانیایی و ویتنامی - با تشکر از کاربران ما! + عضو حذف شده است - نمی‌توان درخواست را قبول کرد + این تنظیمات برای پروفایل فعلی شماست + درخواست‌های تماس از گروه‌ها + درخواست اتصال از گروه %1$s 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 c6f89dd3c0..36b69f2f31 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -193,7 +193,7 @@ Kapcsolódás Közvetlenül kapcsolódik? Kapcsolódás - partnerkapcsolatot kért + partneri kapcsolatot kért kapcsolat %1$d a partner e2e titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. @@ -846,7 +846,7 @@ ÜZENETEK ÉS FÁJLOK tag Privát kapcsolat létrehozása - moderálva lett %s által + %s moderálta ezt az üzenetet Győződjön meg arról, hogy a fájl helyes YAML-szintaxist tartalmaz. Exportálja a témát, hogy legyen egy példa a témafájl szerkezetére. dőlt Érvénytelen a fájl elérési útvonala @@ -2481,6 +2481,6 @@ Régi hivatkozás megosztása PARTNERI KAPCSOLATKÉRÉSEK A CSOPORTOKBÓL A tag törölve lett – nem lehet elfogadni a kérést - a(z) %1$s nevű csoportból partnerkapcsolatot kért + a(z) %1$s nevű csoportból partneri kapcsolatot kért Ez a beállítás a jelenlegi profiljára vonatkozik diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg new file mode 100644 index 0000000000..252afc1acc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 77d38c0824..5785f32de5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -1380,7 +1380,7 @@ Errore di creazione del contatto Invia messaggio diretto per connetterti invia per connettere - si è connesso/a direttamente + connessione richiesta Espandi Ripetere la richiesta di connessione? contatto eliminato @@ -2515,4 +2515,8 @@ Condividi il link vecchio Il link sarà breve e il profilo del gruppo verrà condiviso attraverso il link. Aggiorna il link del gruppo + RICHIESTE DI CONTATTO DAI GRUPPI + Il membro è eliminato - impossibile accettare la richiesta + connessione richiesta dal gruppo %1$s + Questa impostazione è per il tuo profilo attuale 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 dc15648e64..efc1810bac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -2018,4 +2018,28 @@ 承諾 サーバオペレータは、プライベートチャット・グループ・連絡先にはアクセスできません。 SimpleX Chat を利用することで、以下の事項に同意したものと見なされます:\n- パブリックグループでは合法なコンテンツのみを送信すること。\n- 他のユーザを尊重すること、またスパムメッセージを送信しないこと。 + チャットを開く + 新しいチャットを開始 + 新しいグループを開始 + 無効なリンク + SimpleXのリンクが正しいか確認してください + あなたとモデレーターのみが見ることができます + 送信者とモデレーターのみが見ることができます + アーカイブされたレポート + %sによってアーカイブされたレポート + E2E暗号化.]]> + 接続を要求されました + 招待を受け入れました + SimpleXチャンネルのリンク + スパム + 不適切なコンテンツ + コミュニティガイドライン違反 + 不適切なプロフィール + 他の理由 + サーバーの保存中にエラーが発生しました + メッセージサーバーがありません + メッセージを受信するサーバーがありません + プライベートメッセージルーティング用のサーバーがありません。 + メディアおよびファイルサーバーは存在しません。 + ファイルを送信するサーバーがありません。 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 5d113ecb46..5f12e762aa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -268,7 +268,7 @@ O contacto permite Preferências de conversa SimpleX - m + mil Conectar através do endereço de contacto? Conectar via link de convite? Conectar através da ligação do grupo\? @@ -971,8 +971,9 @@ Todos os perfis Já conectando! Já entrando no grupo! - %1$d arquivo(s) falharam o download. - %1$d erro(s) no arquivo:\n%2$s - %1$d arquivo(s) estão sendo baixados. - %1$d arquivo(s) foram deletados. + Não foi possível descarregar %1$d ficheiros(s). + %1$d erro(s) nos ficheiros:\n%2$s + Ainda a descarregar %1$d ficheiros(s). + %1$d ficheiros(s) eliminado(s). + Não foi possível descarregar %1$d ficheiro(s). 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 c67e843d07..56e8faf690 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -22,7 +22,7 @@ %1$s MEMBRI 1 zi 1 minut - Link de unică folosință + Link unic 5 minute Despre SimpleX Despre adresa SimpleX @@ -135,7 +135,7 @@ Aplică Creează profil Creează profil - Acceptă automat cererile de contactare + Acceptare automată a cererilor de contact personalizat În prezent dimensiunea maximă pentru fișiere este %1$s. Creează adresă SimpleX @@ -186,8 +186,8 @@ Restaurați Reîmprospătează Revocați fișierul? - Revocați - Renegociați criptarea? + Revocă + Renegociezi criptarea? Resetare Respinge Salvați fraza de acces în setări @@ -205,16 +205,16 @@ Salvați parola profilului Salvați fraza de acces și deschideți chatul %s și %s - Renegociați criptarea - Salvați și notificați contactul - Salvați și notificați contactele - Salvați și notificați membrii grupului + Renegociază criptarea + Salvează și notifică contactul + Salvează și notifică contactele + Salvează și notifică membrii grupului Rulează când aplicația este deschisă Răspunde - Revocați fișierul + Revocă fișierul Salvează Salvați setările? - Salvați preferințele? + Salvezi preferințele? Repornire Restaurați copia de rezervă a bazei de date Restaurați copia de rezervă a bazei de date? @@ -232,7 +232,7 @@ Salvați fraza de acces în Keystore Salvați profilul grupului Repetă - Trimiteți previzualizări de linkuri + Trimite previzualizări de linkuri Setați frază de acces Partajează adresa Trimis la @@ -297,7 +297,7 @@ setați o nouă poză de profil Trimis la: %s SERVERE - Trimiteți mesaj live + Trimite mesaj în direct %s descărcat Partajați adresa cu contactele? Afișează previzualizare @@ -321,7 +321,7 @@ SimpleX nu poate rula în fundal. Vei primi notificări doar atunci când aplicația rulează. Serviciul SimpleX Chat Adresă SimpleX - Oprește + Oprire Linkuri SimpleX Linkurile SimpleX sunt interzise. Securitatea chatului SimpleX a fost auditată de Trail of Bits. @@ -359,7 +359,7 @@ Folosește mereu releu Aplică pentru Începe o nouă conversație - Stea pe GitHub + Acordă o stea pe GitHub criptare standard end-to-end Pornește periodic Mereu @@ -421,7 +421,7 @@ Contactul nu este conectat încă! Conectare prin link Puteți vedea din nou linkul de invitație în detaliile conexiunii. - Profilurile tale de chat + Profilurile tale de conversație Creează profil de chat apel încheiat %1$s Creează @@ -520,7 +520,7 @@ Te conectezi la tine? Verifică conexiunea la internet și încearcă din nou Culori conversație - Temă pentru chat + Tema conversației Bun pentru baterie. Aplicația verifică mesajele la fiecare 10 minute. Pot fi pierdute apeluri sau mesaje urgente.]]> Negru Blocați membrul pentru toți? @@ -545,7 +545,7 @@ Versiunea aplicației: %s pentru fiecare profil de conversație pe care le aveți în aplicație]]> Permiteți downgrade-ul - pentru fiecare contact și membru de grup \nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua.]]> + pentru fiecare contact și membru al grupului.\nRețineți: dacă aveți multe conexiuni, consumul bateriei și al traficului de date poate crește considerabil, iar unele conexiuni pot eșua.]]> Conexiune terminată Se conectează la desktop S-a solicitat primirea imaginii @@ -554,7 +554,7 @@ conectat Confirmați codul de access Confirmare actualizare bază de date - "schimbat rolul lui %s la %s" + a schimbat rolul lui %s în %s conectat se conectează (introdus) conectat @@ -586,13 +586,13 @@ Șterge conversația Conectare Consolă conversație - Confirmați parola + Confirmă parola colorat Toate contactele, conversațiile și fișierele dvs. vor fi criptate securizat și încărcate în fragmente către releele XFTP configurate. S-a solicitat primirea videoclipului Comparați fișierul Vă rugăm să rețineți: nu veți putea recupera sau schimba parola dacă o veți pierde.]]> - schimbat rolul dumneavoastră la %s + rolul tău a fost schimbat în %s conectat se conectează conectat @@ -600,7 +600,7 @@ Buton de închidere Ștergeți verificarea Configurare servere ICE - conectat direct + conexiune solicitată se conectează (anunțat) Cel mai bun pentru baterie. Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]> Se conectează apelul @@ -634,7 +634,7 @@ Eroare de decriptare Șterge imaginea Șterge după - Ștergeți pentru toată lumea + Șterge pentru toată lumea Descriere Șterge fișierul Șterge coada @@ -645,12 +645,12 @@ %d zile %d zi Șterge adresa - Ștergeți mesajele + Șterge mesajele Ștergeți %d mesaje? Ștergi adresa? Șterge toate fișierele Șterge linkul - Ștergeți linkul? + Ștergi linkul? zile Șterge Ștergeți mesajul? @@ -659,7 +659,7 @@ Ștergeți serverul Șterge baza de date contact șters - Ștergeți grupul? + Ștergi grupul? Șterge baza de date de pe acest dispozitiv Șterge profil de conversație Șterge fișierele pentru toate profilurile de chat @@ -685,7 +685,7 @@ Verifică pentru actualizări Creează Estompează media - BAZĂ DE DATE DISCUȚIE + BAZĂ DE DATE CONVERSAȚIE Conectează-te cu prietenii mai ușor. încercări Completat @@ -717,13 +717,13 @@ Se conectează la contact, vă rugăm să așteptați sau să verificați mai târziu! Setări avansate Setări - Setările tale + Setări apel audio criptat e2e apel video criptat e2e DISPOZITIV EXPERIMENTAL Criptează - erori la descifrare + erori de decriptare TU nicio criptare e2e criptat e2e @@ -747,7 +747,7 @@ Prin browser Spam Conținut inadecvat - Încălcă normelor comunitare + Încălcarea liniilor directoare ale comunității Profil inadecvat Alt motiv Eroare la salvarea serverelor SMP @@ -761,7 +761,7 @@ Eroare de renegociere a criptării invitat să se conecteze incognito prin linkul adresei de contact - printr-un link de unică folosință + prin link de unică folosință 1 raport 1 an redirecționat @@ -819,9 +819,9 @@ Importați baza de date de chat? Ștergeți raportul Redirecționați mesajele… - Ștergeți doar conversația + Șterge doar conversația Păstrezi invitația nefolosită? - Introduceți numele dumneavoastră: + Introdu numele tău: Acordați permisiunea (permisiunile) de a efectua apeluri Notificări și baterie Deschide @@ -948,7 +948,7 @@ Editează Info Redirecționat de la - Șterge %d mesaje de la membri? + Ștergi cele %d mesaje ale membrilor? Rapoarte de membri Fișierele și conținutul media sunt interzise! Fișierul nu a fost găsit @@ -1037,13 +1037,13 @@ Server nou Asigurați -vă că adresele Serverului WebRTC ICE sunt în format corect, separate pe linii și nu sunt duplicate. Rutarea mesajelor - Editați imaginea + Editează imaginea Deschide setările Acordare în setări Instant Cum afectează bateria Activare (păstrați suprascrierile) - Activați codul de autodistrugere + Activează codul de autodistrugere MEMBRU Operator de rețea Desktop-uri conectate @@ -1077,7 +1077,7 @@ Acordare permisiuni Cască Închide - Criptați fișierele locale + Criptează fișierele locale Blochează după Mod de blocare Cod de acces nou @@ -1091,7 +1091,7 @@ indirect (%1$s) Remedierea nu este acceptată de membrul grupului Operator - Faceți profilul privat! + Fă profilul privat! Deschideți contiții Nu mai afișa Permite să aibă multe conexiuni anonime fără date partajate între ele într -un singur profil de chat. @@ -1114,7 +1114,7 @@ %d conversații cu membri activat Conexiunea a atins limita de mesaje nelivrate, este posibil ca persoana de contact să fie offline. - Dezactivați SimpleX Lock + Dezactivează blocarea SimpleX Eroare la salvarea setărilor Deconectezi desktopul? Nu activați @@ -1131,7 +1131,7 @@ Eroare la ștergerea conexiunii de contact în așteptare criptare agreată pentru %s Introduceți mesajul de bun venit… (opțional) - Criptați fișierele stocate și media + Criptează fișierele și conținutul media stocat Eroare la acceptarea condițiilor Eroare la activarea confirmărilor de livrare! Eroare fișier @@ -1169,7 +1169,7 @@ Invitați la conversație Nou rol de membru Invitați membrii - Editați profilul de grup + Editează profilul grupului Grupul va fi șters pentru dvs. - această acțiune nu poate fi anulată! Membrul va fi eliminat din grup - acest lucru nu poate fi anulat! Membrii vor fi eliminați din chat - acest lucru nu poate fi anulat! @@ -1288,7 +1288,7 @@ Vă protejează adresa și conexiunile IP. Mod luminos Sursa mesajelor rămâne privată. - Mesaje live + Mesaje în direct Asigurați-vă că configurația proxy este corectă. Cel mai probabil acest contact a șters conexiunea cu dvs. Avertizare de livrare a mesajelor @@ -1313,7 +1313,7 @@ profilul grupului a fost actualizat proprietar Șterge conversația - ieșit + părăsit Remedierea nu este suportată de contact Erori Deschide grupul @@ -1333,7 +1333,7 @@ Află mai multe Invită Eroare la inițializarea WebView. Asigurați-vă că aveți WebView instalat și că arhitectura sa suportată este arm64.\nEroare: %s - Faceți o conexiune privată + Stabilește o conexiune privată Deschide SimpleX Chat pentru a accepta apelul Activați blocarea Se poate întâmpla atunci când tu sau conexiunea ta ați folosit backupul vechi al bazei de date. @@ -1416,7 +1416,7 @@ Reacții la mesaje expirat Descărcați %s (%s) - Instalați update + Instalează actualizarea Nu crea adresă De exemplu, dacă persoana de contact primește mesaje prin intermediul unui server SimpleX Chat, aplicația le va livra prin intermediul unui server Flux. Deschide Setările Safari / Site-uri web / Microfon, apoi selectează Permite pentru localhost. @@ -1452,7 +1452,7 @@ Descoperă și alătură-te grupurilor Chiar și atunci când este dezactivat în conversație. - livrare mai stabilă a mesajelor.\n- grupuri mai bune.\n- și multe altele! - Face ca un mesaj să dispară + Fă un mesaj să dispară Migrați către un alt dispozitiv prin codul QR. Ștergeți până la 20 de mesaje odată. Vrei să te alături grupului tău? @@ -1460,7 +1460,7 @@ Notificări instant! Deschide setările aplicației %d secunde - Activați SimpleX Lock + Activează blocarea SimpleX Eroare la afișarea notificării, contactați dezvoltatorii. Eroare de livrare a mesajelor Eroare server destinație: %1$s @@ -1487,12 +1487,12 @@ Notificări Doar proprietarii grupului pot activa mesajele vocale. Editați - Mesaj live! + Mesaj în direct! Fără sunet toate OK Previzualizare imagine din link Conversație nouă - Link invitație unică + Link de invitație unic Servere mesaje Alte servere SMP Introduceți serverul manual @@ -1513,14 +1513,14 @@ niciodată Niciun fișier primit sau trimis Frază de acces nouă… - Criptați baza de date? + Criptezi baza de date? Introduceți fraza de acces corectă. Vrei să te alături grupului? Migrații: %s Alătură-te Părăsiți Părăsiți conversația? - ieșit + a părăsit invitat prin linkul dvs. de grup Un membru nou vrea să se alăture grupului. criptare agreată @@ -1580,7 +1580,7 @@ Exportă tema Importați tema Eroare la importul temei - Eroare la acceptarea solicitării de contact + Eroare la acceptarea cererii de contact Descărcați fișierul Eroare la ștergerea contactului Eroare la crearea adresei @@ -1628,7 +1628,7 @@ Servere presetate Vă rugăm să o rețineți sau să o păstrați în siguranță - nu există nicio modalitate de a recupera o parolă pierdută! Confidențialitate și securitate - Protejați ecranul aplicației + Protejează ecranul aplicației Imagini de profil Vă rugăm să stocați în siguranță fraza de acces, NU veți putea accesa chatul dacă o pierdeți. Interziceți trimiterea de mesaje vocale. @@ -1739,7 +1739,7 @@ Resetare Rapoarte informații despre coada serverului: %1$s\n\nultimul mesaj primit: %2$s - Salvați și reconectați + Salvează și reconectează-te Raportați altceva: doar moderatorii grupului îl vor vedea. caută Setați numele chatului… @@ -1752,7 +1752,7 @@ Examinați condițiile Raport Adresa de primire va fi schimbată la un alt server. Schimbarea adresei se va finaliza după ce expeditorul se conectează online. - Salvați setările pentru admitere? + Salvezi setările de acces? Revizuiți mai târziu Trimiterea confirmărilor este dezactivată pentru %d grupuri recenzie @@ -1802,7 +1802,7 @@ Selectează profilul de chat Salvați setările de acceptare automată Salvați lista - TRIMITEȚI CONFIRMĂRI DE LIVRARE LA + TRIMITE CONFIRMĂRI DE LIVRARE LA Parola de autodistrugere a fost schimbată! Înregistrare actualizată la Selectează operatorii de rețea de utilizat. @@ -1851,7 +1851,7 @@ Cheie greșită sau adresă necunoscută a fragmentului de fișier - cel mai probabil fișierul este șters. Prea multe videoclipuri! Videoclip trimis - Opriți partajarea + Oprește partajarea Servere ICE WebRTC Actualizați și deschideți chatul Fișiere încărcate @@ -1904,7 +1904,7 @@ Folosește acreditări proxy diferite pentru fiecare conexiune. Adresă SimpleX sau link unic? Pentru a vă proteja confidențialitatea, SimpleX folosește ID-uri separate pentru fiecare dintre contactele dvs. - Ești invitat(ă) în grup + Ai fost invitat în grup Pentru a primi Utilizare pentru fișiere Pentru a trimite @@ -1933,7 +1933,7 @@ necunoscut Poți ascunde sau dezactiva notificările unui profil de utilizator – ține apăsat pentru meniu. Ați permis - Partajează link cu utilizare unică cu un prieten + Partajează linkul unic cu un prieten Mesaje vocale Mesaj vocal… Setări proxy SOCKS @@ -1946,11 +1946,11 @@ Mesajele vocale sunt interzise! Ați acceptat conexiunea Partajează adresa public - Partajează acest link de invitație cu utilizare unică + Partajează acest link de invitație unic Pentru a verifica criptarea end-to-end cu contactul dvs., comparați (sau scanați) codul de pe dispozitivele dvs. Utilizare pentru conexiuni noi Utilizați proxy SOCKS - Opriți partajarea adresei? + Oprești partajarea adresei? Când aplicația rulează Aplicația vă protejează confidențialitatea utilizând operatori diferiți în fiecare conversație. Vizualizați condițiile @@ -1997,7 +1997,7 @@ Fără Tor sau VPN, adresa ta IP va fi vizibilă pentru aceste retransmiteri XFTP:\n%1$s. Actualizați parola bazei de date Opriți trimiterea fișierului? - lovitură + tăiere Viitorul mesageriei Pentru a efectua apeluri, permiteți utilizarea microfonului. Încheiați apelul și încercați să sunați din nou. Când sunt activați mai mulți operatori, niciunul dintre ei nu are metadate pentru a afla cine comunică cu cine. @@ -2093,13 +2093,13 @@ Deconectați desktopul? Se așteaptă desktopul… Prea multe imagini! - Încărcați fișierul + Încarcă fișier Atinge butonul Atingeți pentru a scana (pentru a partaja cu persoana de contact) Pentru a începe o nouă conversație Video - Partajează link cu utilizare unică + Partajează link unic Acest link nu este un link de conectare valid! Acest cod QR nu este un link! Link scurt @@ -2161,7 +2161,7 @@ Raportul va fi arhivat pentru dvs. Atingeți Creează o adresă SimpleX în meniu pentru a o crea mai târziu. Acest text este disponibil în setări - ești invitat în grup + Ești invitat în grup Se așteaptă imaginea Se așteaptă imaginea Se așteaptă videoclipul @@ -2174,7 +2174,7 @@ Partajează adresa SimpleX pe rețelele de socializare. Pentru a se conecta, contactul dvs. poate scana codul QR sau poate folosi linkul din aplicație. Puteți seta numele conexiunii, pentru a vă aminti cu cine a fost partajat linkul. - Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice serviciu de mesagerie. + Adresa SimpleX și linkurile unice pot fi partajate în siguranță prin orice aplicație de mesagerie. Codul scanat nu este un cod QR de tip link SimpleX. Server de testare Servere de testare @@ -2235,7 +2235,7 @@ Bucăți șterse Serverele tale ICE ai eliminat %1$s - Preferințele dumneavoastră + Preferințele tale Adaugă listă Toate Puteți vizualiza în continuare conversația cu %1$s în lista de conversații. @@ -2245,7 +2245,7 @@ Conexiune blocată Conexiunea este blocată de operatorul serverului:\n%1$s. criptate end-to-end, cu securitate post-cuantică în mesajele directe.]]> - Bare de instrumente ale aplicației + Bare de instrumente Securitate îmbunătățită ✅ Toate conversațiile vor fi eliminate din lista %s, iar lista va fi ștearsă Conexiunea dvs. a fost mutată la %s, dar a apărut o eroare neașteptată la redirecționarea către profil. @@ -2257,20 +2257,20 @@ Întreabă %s.]]> Schimbați ștergerea automată a mesajelor? - Adresă sau link de unică folosință? + Adresă sau link unic? Sesiune de aplicație - Contactele dvs. - Este posibil ca datele dumneavoastră de autentificare să fie trimise necriptate. + Contactele tale + Datele tale de conectare pot fi trimise necriptat. Adresă de afaceri Adăugați membrii echipei dvs. la conversații. Verifică mesajele la fiecare 10 minute %s, acceptați condițiile de utilizare.]]> - Confirmați autentificarea dvs + Confirmă-ți datele de autentificare Schimbă profilurile de chat Contacte Conexiunea necesită renegocierea criptării. Adresa ta SimpleX - Creează link de unică folosință + Creează link unic Profilul tău actual Aplicația rulează întotdeauna în fundal acceptat %1$s @@ -2282,8 +2282,8 @@ Arhivează contactele pentru a discuta mai târziu. %1$s.]]> Ai partajat o cale de fișier nevalidă. Raportează problema dezvoltatorilor aplicației. - Deschideți în aplicația mobilă, apoi atingeți Conectați-vă în aplicație.]]> - Adaugă link scurt + Deschide în aplicația mobilă, apoi atinge Conectare în aplicație.]]> + Actualizează adresa Configurați operatorii serverului Condițiile vor fi acceptate pentru operatorii activați după 30 de zile. Faceți clic pe butonul de informații de lângă câmpul de adresă pentru a permite utilizarea microfonului. @@ -2295,10 +2295,10 @@ Poți partaja un link sau un cod QR - oricine se va putea alătura grupului. Nu vei pierde membrii grupului dacă îl ștergi ulterior. Blocați membrii pentru toți? Chat - %s.]]> + %s.]]> Condiții acceptate pe: %s. Condițiile vor fi acceptate automat pentru operatorii activați pe: %s. - %s.]]> + %s.]]> %s.]]> %s.]]> Condiții de utilizare @@ -2320,7 +2320,7 @@ Adaugă prieteni Adaugă membri echipei Chatul există deja! - Deschideți în aplicația mobilă.]]> + Deschide în aplicația mobilă.]]> Continuă %s a fost deconectat]]> Despre operatori @@ -2338,7 +2338,7 @@ invitație acceptată Puteți să vă împărtășiți adresa ca link sau cod QR - oricine se poate conecta la dvs. Acceptă - Confidențialitatea dumneavoastră + Confidențialitatea ta Acceptă condițiile Zoom %1$s.]]> @@ -2369,10 +2369,10 @@ %s cu motivul: %s]]> %s a fost deconectat]]> (nou)]]> - Serverul dumneavoastră + Serverul tău Trebuie să îi permiți contactului tău să te sune pentru a-l putea suna. Adresa serverului dvs. - Serverele dumneavoastră + Serverele tale Vei fi conectat la grup atunci când dispozitivul gazdei grupului va fi online, te rugăm să aștepți sau să verifici mai târziu! Tu decizi cine se poate conecta. Vei fi conectat când cererea ta de conectare va fi acceptată, te rugăm să aștepți sau să verifici mai târziu! @@ -2380,7 +2380,7 @@ Experiență de utilizare îmbunătățită 1 chat cu un membru Toate rapoartele vor fi arhivate pentru dvs. - repozitoriul nostru GitHub.]]> + depozitul nostru GitHub.]]> Permiteți în următoarea fereastră de dialog să primească notificări instantaneu.]]> Setări adresă Arhivare rapoarte @@ -2388,10 +2388,10 @@ toți Afaceri Prin utilizarea SimpleX Chat sunteți de acord să:\n- trimiteți doar conținut legal în grupurile publice.\n- respectați ceilalți utilizatori – fără spam. - cu un singur contact - partajați personal sau prin orice mesagerie.]]> + doar cu un singur contact - partajează-l personal sau prin orice altă aplicație de mesagerie.]]> Nu trebuie să utilizați aceeași bază de date pe două dispozitive. arătați codul QR în apelul video sau distribuiți linkul.]]> - Utilizează de pe desktop în aplicația mobilă și scanați codul QR.]]> + Utilizare de pe desktop în aplicația mobilă și scanează codul QR.]]> SimpleX rulează în fundal în loc să utilizeze notificări push.]]> Utilizarea bateriei aplicației / Nerestricționat în setările aplicației.]]> Bucăți descărcate @@ -2428,4 +2428,26 @@ Deschide pentru conectare Deschide pentru a te alătura Timp de așteptare depășit pentru rutarea privată + Te poți conecta la conversație și poți trimite mesaje imediat ce apeși pe Conectare. + Rol nou în grup: Moderator + Descriere scurtă: + Trimis contactului tău după conectare. + Actualizezi la o adresă permanentă? + Mesaj de bun venit + SOLICITĂRI DE CONTACT DE LA GRUPURI + Această setare este pentru profilul tău actual + Partajează adresa veche + Partajează linkul vechi + Se încarcă profilul… + cerere de conectare din grupul %1$s + Eroare la deschiderea conversației + Eroare la deschiderea grupului + Mai puțin trafic pe rețelele mobile. + Adresa va fi scurtă, iar profilul tău va fi partajat prin intermediul acestei adrese. + Actualizează + Adresă SimpleX scurtă + Atinge Conectare pentru a începe conversația + Creează-ți adresa + Acceptă cererea de contact + doar după ce cererea ta este acceptată.]]> 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 a13106edaf..8fe730fab1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -372,7 +372,7 @@ Проверьте адрес сервера и попробуйте снова. Удалить сервер SimpleX Chat для терминала - Поставить звёздочку в GitHub + Поставить звёздочку на GitHub Внести свой вклад Оценить приложение Использовать серверы предосталенные SimpleX Chat? @@ -1463,7 +1463,7 @@ Послать прямое сообщение контакту Ошибка отправки приглашения Отправьте сообщение чтобы соединиться - соединен напрямую + запрос на соединение Раскрыть Блокируйте членов группы Повторить запрос на соединение? 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 436f80a8fe..43e67bb51b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2502,4 +2502,5 @@ Biyografi çok uzun Açıklama çok büyük Kaybolma süresi yalnızca yeni kişiler için ayarlanır. + Gizli profil kullan 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 0922ed5d55..ee0c785e9f 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 @@ -2075,4 +2075,54 @@ 更好的訊息日期。 為了更好的元資料隱私。 你可以再試一次。 + 使用匿名個人檔案 + 開啟聊天 + 連接 + 升級 + 群組 + 可見的紀錄 + 慢速函數 + 有線以太網 + 桌布背景 + 桌布輔色 + 訂閱已忽略 + 訂閱錯誤 + 代理身份驗證 + 已靜音 + 未送達的訊息 + 沒有訊息 + 預設伺服器 + 審閱使用條款 + %s 個伺服器 + 檢視使用條款 + 接收 + 開啟使用條款 + 開啟變更 + 網路去中心化 + 離開聊天? + 離開聊天 + 啟用日誌 + 沒有聊天 + 成員舉報 + 僅自己 + 舉報:%s + 待批准 + 已更新使用條款 + 預設伺服器 + 加入群組 + 增加訊息 + 傳送請求 + 你的個人資料 + 歡迎訊息 + 升級地址? + 正在載入個人資料… + 你的簡介: + 簡短描述: + 你的聯絡人 + 你的群組 + 商業連接 + 顯示最近的訊息 + 簡化的匿名模式 + 點擊以連接 + 從桌面使用 diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 8107b664c4..d9f091a13a 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -11,10 +11,11 @@ import Data.Text (Text) import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, + botDisplayName :: Text, publishers :: [KnownContact], welcomeMessage :: Text, prohibitedMessage :: Text @@ -29,6 +30,12 @@ defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts broadcastBotOpts appDir defaultDbName = do coreOptions <- coreChatOptsP appDir defaultDbName + botDisplayName <- + strOption + ( long "display-name" + <> metavar "DISPLAY_NAME" + <> help "The display name of the broadcast bot" + ) publishers <- option parseKnownContacts @@ -55,6 +62,7 @@ broadcastBotOpts appDir defaultDbName = do pure BroadcastBotOpts { coreOptions, + botDisplayName, publishers, welcomeMessage = fromMaybe (defaultWelcomeMessage publishers) welcomeMessage_, prohibitedMessage = fromMaybe (defaultProhibitedMessage publishers) prohibitedMessage_ @@ -72,7 +80,7 @@ getBroadcastBotOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: BroadcastBotOpts -> ChatOpts -mkChatOpts BroadcastBotOpts {coreOptions} = +mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = ChatOpts { coreOptions, chatCmd = "", @@ -86,5 +94,6 @@ mkChatOpts BroadcastBotOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}, maintenance = False } diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 2c26905e79..2eba633609 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -16,7 +16,7 @@ import qualified Data.Text as T import Options.Applicative import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) -import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, coreChatOptsP) +import Simplex.Chat.Options (ChatCmdLog (..), ChatOpts (..), CoreChatOpts, CreateBotOpts (..), coreChatOptsP) data DirectoryOpts = DirectoryOpts { coreOptions :: CoreChatOpts, @@ -119,7 +119,7 @@ directoryOpts appDir defaultDbName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" <> value "SimpleX-Directory" ) - runCLI <- + runCLI <- switch ( long "run-cli" <> help "Run directory service as CLI" @@ -155,7 +155,7 @@ getDirectoryOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: DirectoryOpts -> ChatOpts -mkChatOpts DirectoryOpts {coreOptions} = +mkChatOpts DirectoryOpts {coreOptions, serviceName} = ChatOpts { coreOptions, chatCmd = "", @@ -169,5 +169,6 @@ mkChatOpts DirectoryOpts {coreOptions} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}, maintenance = False } diff --git a/bots/README.md b/bots/README.md index 0b1943b7a7..9449e9d847 100644 --- a/bots/README.md +++ b/bots/README.md @@ -1,6 +1,8 @@ # SimpleX Chat bot API - [Why create a bot](#why-create-a-bot) +- [What is SimpleX bot](#what-is-simplex-bot) +- [How to configure bot profile](#how-to-configure-bot-profile) - [How to create a bot](#how-to-create-a-bot) - [Sending commands](#sending-commands) - [Processing events](#processing-events) @@ -12,8 +14,9 @@ ## Why create a bot You can implement SimpleX Chat for these and many other scenarios: -- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address]() feature), +- customer support - both as a single- and a multi-agent support chat (using SimpleX Chat [business address](https://simplex.chat/docs/business.html) feature), - information search and retrieval bots, with or without LLM integration, +- moderation bots, to moderate your group and communities. - broadcast bot, when messages from your trusted users are forwarded to all connected contacts - e.g., see our SimpleX Status bot in the app ([source code](../apps/simplex-broadcast-bot/)), - feedback bot, when messages from connected contacts are forwarded to a preset list of your trusted users, - P2P trading bots, connecting buyers and sellers, @@ -22,6 +25,65 @@ You can implement SimpleX Chat for these and many other scenarios: We will share all useful bots you create in the bottom of this page - please submit a PR to add it. +## What is SimpleX bot + +SimpleX bot is a participant of SimpleX network. Theoretically, bot can do everything that a usual SimpleX Chat user can do – send and receive messages and files, connect to addresses and join groups, etc. But to be useful, a bot should distinguish itself as a bot, and to provide an interface for the users to interact with it. + +## How to configure bot profile + +Starting from v6.4.3, SimpleX Chat apps support bot configuration to distinguish bots, to highlight commands in messages, and to show command menus. + +### Set up bot profile + +To distinguish SimpleX user profile as a bot, set its `peerType` property to `"bot"`. It can be done in one of these ways: +- using CLI options `--create-bot-display-name` and `create-bot-allow-files` when first starting CLI to create bot profile, +- using command `/create bot [files=on] [ ]` (if name contains spaces, it must be in single quotes), when creating additional bot profiles in the same database, +- by configuring bot commands that the users will see in the UI when they type `/` character or tap `//` button with `/set bot commands ...` CLI command (see syntax below), +- by using [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) bot command to set `peerType` and configure bot commands at the same time. + +### Configure bot commands + +Bot commands are messages that start from `/` character. Normally, they would consist of lowercase latin letters, but commands can use any letters, digits and underscores. Commands can have parameters. + +All commands in messages will be highlighted in the chats with the bot, and when users tap them, they will be instantly sent. If the message has a single line and starts from `/` character, the whole message will be highlighted. Otherwise, if command is included as part of the message, it will be highlighted until the first space after `/` character: e.g., `/list` command in Directory service shows user's groups. + +*Please note*: commands in messages will be highlighted based purely on `/` character, regardless of whether they are supported by the bot or included in bot configuration. It allows bots to have "hidden" commands that bot would support, but that won't be shown in the menu. But it may also lead to mistakes if bot sends incorrect commands in the instructions to the users. + +Bots can also send highlighted commands with parameters. To do that, bots should surround both command and its parameters in single quotes: e.g., `/'role 2'`. Quotes won't show in the apps UI, and if the user taps this command, it will be sent as `/role 2`. + +Configured bot commands will be be offered to the users as a menu, and for quick lookup as the user types. + +Bot commands configuration is a property in `preferences` object in bot profile received by the user. These preferences can be configured both on the bot user profile level, to offer the same commands to all connected users, and as overrides for specific contacts, to offer different commands to different bot contacts. + +Configuring commands in bot user can be done either with [APIUpdateProfile](./api/COMMANDS.md#apiupdateprofile) or with `/set bot commands` CLI command: + +``` +/set bot commands +``` + +where: + +``` +commands = [,...] +commandOrMenu = command | menu +command = '