core, ui: markdown for hyperlinks, warn on unsanitized links, option to sanitize sent links (#6160)

* core: markdown for "hidden" links

* update, test

* api docs

* chatParseUri FFI function

* ios: hyperlinks, offer to open sanitized links, an option to send sanitized links (enabled by default)

* update markdown

* android, desktop: ditto

* ios: export localizations

* core: rename constructor, change Maybe semantics for web links

* rename
This commit is contained in:
Evgeny
2025-08-09 10:52:35 +01:00
committed by GitHub
parent b4293e361b
commit ef60ceea12
55 changed files with 1004 additions and 288 deletions
File diff suppressed because one or more lines are too long
@@ -187,23 +187,25 @@ private func handleTextTaps(
}
}
}
if let index, let (url, browser) = attributedStringLink(s, for: index) {
if let index, let (uri, browser) = attributedStringLink(s, for: index) {
if browser {
openBrowserAlert(uri: url)
} else {
openBrowserAlert(uri: uri)
} else if let url = URL(string: uri) {
UIApplication.shared.open(url)
} else {
showInvalidLinkAlert(uri)
}
}
})
}
func attributedStringLink(_ s: NSAttributedString, for index: CFIndex) -> (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) {
@@ -356,22 +358,32 @@ 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)
@@ -403,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: ()
@@ -439,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 {
File diff suppressed because one or more lines are too long
@@ -329,10 +329,10 @@ struct ComposeView: View {
@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<String> = []
@Environment(\.colorScheme) private var colorScheme
@@ -353,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) {
@@ -454,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)
@@ -464,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
}
@@ -845,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)
@@ -1448,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
@@ -1465,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) {
@@ -1516,3 +1535,32 @@ struct ComposeView: View {
cancelledLinks = []
}
}
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)
}
@@ -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)
}
@@ -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)
}
@@ -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)
}
}
}
}
}
@@ -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)
@@ -2563,6 +2563,10 @@ swipe action</note>
<target>Потвърждениe за доставка!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Описание</target>
@@ -4289,7 +4293,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Невалиден линк</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5460,10 +5464,18 @@ Requires compatible VPN.</source>
<target>Отвори конзолата</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Отвори група</target>
@@ -6193,6 +6205,10 @@ swipe action</note>
<source>Remove image</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Острани член</target>
@@ -2455,6 +2455,10 @@ swipe action</note>
<target>Potvrzení o doručení!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Popis</target>
@@ -4128,7 +4132,7 @@ More improvements are coming soon!</source>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5267,10 +5271,18 @@ Vyžaduje povolení sítě VPN.</target>
<target>Otevřete konzolu chatu</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>new chat action</note>
@@ -5976,6 +5988,10 @@ swipe action</note>
<source>Remove image</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Odstranit člena</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>Empfangsbestätigungen!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Beschreibung</target>
@@ -4517,7 +4521,7 @@ Weitere Verbesserungen sind bald verfügbar!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Ungültiger Link</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5770,11 +5774,19 @@ Dies erfordert die Aktivierung eines VPNs.</target>
<target>Chat-Konsole öffnen</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Nutzungsbedingungen öffnen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Gruppe öffnen</target>
@@ -6564,6 +6576,10 @@ swipe action</note>
<target>Bild entfernen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Mitglied entfernen</target>
@@ -2684,6 +2684,11 @@ swipe action</note>
<target>Delivery receipts!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<target>Deprecated options</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Description</target>
@@ -4522,7 +4527,7 @@ More improvements are coming soon!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Invalid link</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5777,11 +5782,21 @@ Requires compatible VPN.</target>
<target>Open chat console</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<target>Open clean link</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Open conditions</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<target>Open full link</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Open group</target>
@@ -6572,6 +6587,11 @@ swipe action</note>
<target>Remove image</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<target>Remove link tracking</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Remove member</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>¡Confirmación de entrega!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Descripción</target>
@@ -4517,7 +4521,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Enlace no válido</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5770,11 +5774,19 @@ Requiere activación de la VPN.</target>
<target>Abrir consola de Chat</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Abrir condiciones</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Grupo abierto</target>
@@ -6564,6 +6576,10 @@ swipe action</note>
<target>Eliminar imagen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Expulsar miembro</target>
@@ -2426,6 +2426,10 @@ swipe action</note>
<target>Toimituskuittaukset!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Kuvaus</target>
@@ -4096,7 +4100,7 @@ More improvements are coming soon!</source>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5233,10 +5237,18 @@ Edellyttää VPN:n sallimista.</target>
<target>Avaa keskustelukonsoli</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>new chat action</note>
@@ -5942,6 +5954,10 @@ swipe action</note>
<source>Remove image</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Poista jäsen</target>
@@ -2661,6 +2661,10 @@ swipe action</note>
<target>Justificatifs de réception!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Description</target>
@@ -4474,7 +4478,7 @@ D'autres améliorations sont à venir!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Lien invalide</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5694,11 +5698,19 @@ Nécessite l'activation d'un VPN.</target>
<target>Ouvrir la console du chat</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Ouvrir les conditions</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Ouvrir le groupe</target>
@@ -6467,6 +6479,10 @@ swipe action</note>
<target>Enlever l'image</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Retirer le membre</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>Kézbesítési jelentések!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Leírás</target>
@@ -4517,7 +4521,7 @@ További fejlesztések hamarosan!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Érvénytelen hivatkozás</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5770,11 +5774,19 @@ VPN engedélyezése szükséges.</target>
<target>Csevegési konzol megnyitása</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Feltételek megnyitása</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Csoport megnyitása</target>
@@ -6564,6 +6576,10 @@ swipe action</note>
<target>Kép eltávolítása</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Eltávolítás</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>Ricevute di consegna!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Descrizione</target>
@@ -4517,7 +4521,7 @@ Altri miglioramenti sono in arrivo!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Link non valido</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5770,11 +5774,19 @@ Richiede l'attivazione della VPN.</target>
<target>Apri la console della chat</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Apri le condizioni</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Apri gruppo</target>
@@ -6564,6 +6576,10 @@ swipe action</note>
<target>Rimuovi immagine</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Rimuovi membro</target>
@@ -2504,6 +2504,10 @@ swipe action</note>
<target>配信通知!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>説明</target>
@@ -4177,7 +4181,7 @@ More improvements are coming soon!</source>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5318,10 +5322,18 @@ VPN を有効にする必要があります。</target>
<target>チャットのコンソールを開く</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>new chat action</note>
@@ -6027,6 +6039,10 @@ swipe action</note>
<source>Remove image</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>メンバーを除名する</target>
@@ -2670,6 +2670,10 @@ swipe action</note>
<target>Ontvangstbewijzen!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Beschrijving</target>
@@ -4498,7 +4502,7 @@ Binnenkort meer verbeteringen!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Ongeldige link</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5743,11 +5747,19 @@ Vereist het inschakelen van VPN.</target>
<target>Chat console openen</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Open voorwaarden</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Open groep</target>
@@ -6530,6 +6542,10 @@ swipe action</note>
<target>Verwijder afbeelding</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Lid verwijderen</target>
@@ -2630,6 +2630,10 @@ swipe action</note>
<target>Potwierdzenia dostawy!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Opis</target>
@@ -4406,7 +4410,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Nieprawidłowy link</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5607,10 +5611,18 @@ Wymaga włączenia VPN.</target>
<target>Otwórz konsolę czatu</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Grupa otwarta</target>
@@ -6373,6 +6385,10 @@ swipe action</note>
<target>Usuń obraz</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Usuń członka</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>Отчёты о доставке!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Описание</target>
@@ -4516,7 +4520,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Ошибка ссылки</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5769,11 +5773,19 @@ Requires compatible VPN.</source>
<target>Открыть консоль</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Открыть условия</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Открыть группу</target>
@@ -6563,6 +6575,10 @@ swipe action</note>
<target>Удалить изображение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Удалить члена группы</target>
@@ -2414,6 +2414,10 @@ swipe action</note>
<target>ใบตอบรับการจัดส่ง!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>คำอธิบาย</target>
@@ -4080,7 +4084,7 @@ More improvements are coming soon!</source>
</trans-unit>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5212,10 +5216,18 @@ Requires compatible VPN.</source>
<target>เปิดคอนโซลการแชท</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<note>new chat action</note>
@@ -5919,6 +5931,10 @@ swipe action</note>
<source>Remove image</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>ลบสมาชิกออก</target>
@@ -2680,6 +2680,10 @@ swipe action</note>
<target>Mesaj gönderildi bilgisi!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Açıklama</target>
@@ -4516,7 +4520,7 @@ Daha fazla iyileştirme yakında geliyor!</target>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Geçersiz bağlantı</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5767,11 +5771,19 @@ VPN'nin etkinleştirilmesi gerekir.</target>
<target>Sohbet konsolunu aç</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Açık koşullar</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Grubu aç</target>
@@ -6561,6 +6573,10 @@ swipe action</note>
<target>Resmi kaldır</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Kişiyi sil</target>
@@ -2679,6 +2679,10 @@ swipe action</note>
<target>Квитанції про доставку!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>Опис</target>
@@ -4515,7 +4519,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>Невірне посилання</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5767,11 +5771,19 @@ Requires compatible VPN.</source>
<target>Відкрийте консоль чату</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>Відкриті умови</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>Відкрита група</target>
@@ -6561,6 +6573,10 @@ swipe action</note>
<target>Видалити зображення</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>Видалити учасника</target>
@@ -2662,6 +2662,10 @@ swipe action</note>
<target>送达回执!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Deprecated options" xml:space="preserve">
<source>Deprecated options</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Description" xml:space="preserve">
<source>Description</source>
<target>描述</target>
@@ -4487,7 +4491,7 @@ More improvements are coming soon!</source>
<trans-unit id="Invalid link" xml:space="preserve">
<source>Invalid link</source>
<target>无效链接</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Invalid migration confirmation" xml:space="preserve">
<source>Invalid migration confirmation</source>
@@ -5728,11 +5732,19 @@ Requires compatible VPN.</source>
<target>打开聊天控制台</target>
<note>authentication reason</note>
</trans-unit>
<trans-unit id="Open clean link" xml:space="preserve">
<source>Open clean link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open conditions" xml:space="preserve">
<source>Open conditions</source>
<target>打开条款</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Open full link" xml:space="preserve">
<source>Open full link</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Open group" xml:space="preserve">
<source>Open group</source>
<target>打开群</target>
@@ -6497,6 +6509,10 @@ swipe action</note>
<target>移除图片</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove link tracking" xml:space="preserve">
<source>Remove link tracking</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Remove member" xml:space="preserve">
<source>Remove member</source>
<target>删除成员</target>
+8 -7
View File
@@ -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<SharedCo
// Prepare Link message
case .url:
if let url = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? URL {
let content: SharedContent =
if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) {
.url(preview: linkPreview)
} else {
.text(string: url.absoluteString)
}
let content: SharedContent
if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) {
privacyLinkPreviewsShowAlertGroupDefault.set(false) // to avoid showing alert to current users, show alert in v6.5
content = .url(preview: linkPreview)
} else {
content = .text(string: url.absoluteString)
}
return .success(content)
} else { return .failure(ErrorAlert("Error preparing message")) }
+1 -1
View File
@@ -172,7 +172,7 @@ struct ShareView: View {
VStack(alignment: .center, spacing: 4) {
Text(linkPreview.title)
.lineLimit(1)
Text(linkPreview.uri.absoluteString)
Text(linkPreview.uri)
.font(.caption)
.lineLimit(1)
.foregroundColor(.secondary)
+8 -8
View File
@@ -178,8 +178,8 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
@@ -545,8 +545,8 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -708,8 +708,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -795,8 +795,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-B5zq2t60gbA45EwEeb0rfE.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.2.1-K3DJnRKS60C6Ik3NluxepC.a */,
);
path = Libraries;
sourceTree = "<group>";
+24
View File
@@ -186,6 +186,30 @@ struct ParsedServerAddress: Decodable {
var parseError: String
}
public func parseSanitizeUri(_ s: String) -> 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<CChar>) -> String {
let s = String.init(cString: c)
+6
View File
@@ -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)
+17 -3
View File
@@ -4611,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
? []
@@ -4620,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 {
@@ -4630,7 +4643,8 @@ 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
@@ -4748,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 = ""
+1 -1
View File
@@ -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)
+1
View File
@@ -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);
@@ -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);
@@ -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);
@@ -4369,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>): 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<FormattedText> = if (text.isEmpty()) emptyList() else listOf(FormattedText(text))
@@ -4397,7 +4390,13 @@ 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<String>): 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<String>): 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()
@@ -4412,6 +4411,7 @@ 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)
@@ -104,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<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default)
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
@@ -369,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"
@@ -4629,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?,
@@ -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
@@ -356,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<FormattedText>?): Pair<String?, Boolean> {
fun getMessageLinks(parsedMsg: List<FormattedText>?): Pair<String?, Boolean> {
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<String?>(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<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
@@ -382,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) {
@@ -394,7 +400,7 @@ fun ComposeView(
fun showLinkPreview(parsedMessage: List<FormattedText>?) {
prevLinkUrl.value = linkUrl.value
val linkParsed = getSimplexLink(parsedMessage)
val linkParsed = getMessageLinks(parsedMessage)
linkUrl.value = linkParsed.first
hasSimplexLink.value = linkParsed.second
val url = linkUrl.value
@@ -501,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)
@@ -861,9 +867,53 @@ fun ComposeView(
}
}
fun sanitizeMessage(parsedMsg: List<FormattedText>): Triple<String, List<FormattedText>, 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 {
@@ -876,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
}
@@ -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
@@ -12,15 +15,15 @@ import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.AnnotatedString.Range
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.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)
@@ -145,55 +148,102 @@ fun MarkdownText (
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
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 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)"
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
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) }
}
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 if (ft.format 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") }
withStyle(ftStyle) { append(ft.text) }
}
}
} else {
val link = ft.link(linkMode)
if (link != null) {
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 = ft.format.style
withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) {
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
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) }
}
} else {
withStyle(ft.format.style) { 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) {
@@ -209,10 +259,12 @@ fun MarkdownText (
ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow,
onLongClick = { offset ->
if (hasLinks) {
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) }
val withAnnotation: (String, (Range<String>) -> 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 ->
@@ -220,37 +272,33 @@ fun MarkdownText (
annotatedText.getStringAnnotations(tag, start = offset, end = offset).firstOrNull()?.let(f)
}
if (hasLinks && uriHandler != null) {
withAnnotation("URL") { a ->
try {
uriHandler.openUri(a.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()}")
}
}
withAnnotation("WEB_URL") { a -> openBrowserAlert(a.item, uriHandler) }
withAnnotation("OTHER_URL") { a -> safeOpenUri(a.item, uriHandler) }
withAnnotation("SIMPLEX_URL") { a -> uriHandler.openVerifiedSimplexUri(a.item) }
} else if (hasSecrets) {
}
if (hasSecrets) {
withAnnotation("SECRET") { a ->
val key = a.item
showSecrets[key] = !(showSecrets[key] ?: false)
}
} else if (hasCommands && sendCommandMsg != null) {
}
if (hasCommands && sendCommandMsg != null) {
withAnnotation("COMMAND") { a -> sendCommandMsg("/${a.item}") }
}
},
onHover = { offset ->
val hasAnnotation: (String) -> Boolean = { tag -> annotatedText.hasStringAnnotations(tag, start = offset, end = offset) }
icon.value =
if (hasAnnotation("URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
PointerIcon.Hand
} else {
PointerIcon.Default
}
},
shouldConsumeEvent = { offset ->
annotatedText.hasStringAnnotations(tag = "URL", start = offset, end = offset)
annotatedText.hasStringAnnotations(tag = "WEB_URL", start = offset, end = offset)
|| annotatedText.hasStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset)
|| annotatedText.hasStringAnnotations(tag = "OTHER_URL", start = offset, end = offset)
}
)
} else {
@@ -319,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<Pair<Boolean, String?>?, 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)
@@ -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
@@ -298,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)
}
@@ -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)
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
"<br><br><u>${link.format.simplexLinkText}</u>"
else
""
when (connectionPlan) {
@@ -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
@@ -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()
}
}
}
@@ -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<PrivacyChatListOpenLinksMode>, 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<SimplexLinkMode>, onSelected: (SimplexLinkMode) -> Unit) {
fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, 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 {
@@ -1072,6 +1072,7 @@
<string name="debug_logs">Enable logs</string>
<string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="deprecated_options_section">Deprecated options</string>
<string name="show_internal_errors">Show internal errors</string>
<string name="show_slow_api_calls">Show slow API calls</string>
<string name="shutdown_alert_question">Shutdown?</string>
@@ -1368,6 +1369,7 @@
<string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string>
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string>
<string name="send_link_previews">Send link previews</string>
<string name="sanitize_links_toggle">Remove link tracking</string>
<string name="privacy_show_last_messages">Show last messages</string>
<string name="privacy_message_draft">Message draft</string>
<string name="full_backup">App data backup</string>
@@ -1434,6 +1436,8 @@
<string name="privacy_chat_list_open_links_ask">Ask</string>
<string name="privacy_chat_list_open_web_link_question">Open web link?</string>
<string name="privacy_chat_list_open_web_link">Open link</string>
<string name="privacy_chat_list_open_full_web_link">Open full link</string>
<string name="privacy_chat_list_open_clean_web_link">Open clean link</string>
<!-- Settings sections -->
<string name="settings_section_title_you">YOU</string>