mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 09:11:47 +00:00
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:
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>
|
||||
|
||||
@@ -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")) }
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>";
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
+14
-14
@@ -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)
|
||||
|
||||
+19
-1
@@ -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
|
||||
|
||||
+59
-9
@@ -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
|
||||
}
|
||||
|
||||
+178
-62
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-31
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+2
-4
@@ -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
|
||||
|
||||
+9
-9
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-30
@@ -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>
|
||||
|
||||
@@ -1979,8 +1979,14 @@ Colored:
|
||||
Uri:
|
||||
- type: "uri"
|
||||
|
||||
HyperLink:
|
||||
- type: "hyperLink"
|
||||
- showText: string?
|
||||
- linkUri: string
|
||||
|
||||
SimplexLink:
|
||||
- type: "simplexLink"
|
||||
- showText: string?
|
||||
- linkType: [SimplexLinkType](#simplexlinktype)
|
||||
- simplexUri: string
|
||||
- smpHosts: [string]
|
||||
|
||||
@@ -382,6 +382,7 @@
|
||||
"chat_migrate_init"
|
||||
"chat_parse_markdown"
|
||||
"chat_parse_server"
|
||||
"chat_parse_uri"
|
||||
"chat_password_hash"
|
||||
"chat_read_file"
|
||||
"chat_recv_msg"
|
||||
@@ -489,6 +490,7 @@
|
||||
"chat_migrate_init"
|
||||
"chat_parse_markdown"
|
||||
"chat_parse_server"
|
||||
"chat_parse_uri"
|
||||
"chat_password_hash"
|
||||
"chat_read_file"
|
||||
"chat_recv_msg"
|
||||
|
||||
@@ -12,6 +12,7 @@ EXPORTS
|
||||
chat_recv_msg_wait
|
||||
chat_parse_markdown
|
||||
chat_parse_server
|
||||
chat_parse_uri
|
||||
chat_password_hash
|
||||
chat_valid_name
|
||||
chat_json_length
|
||||
|
||||
@@ -298,6 +298,7 @@ library
|
||||
, tls >=1.9.0 && <1.10
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, uri-bytestring >=0.3.3.1 && <0.4
|
||||
, uuid ==1.3.*
|
||||
, zip ==2.0.*
|
||||
, zstd ==0.1.3.*
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
@@ -12,11 +14,13 @@
|
||||
module Simplex.Chat.Markdown where
|
||||
|
||||
import Control.Applicative (optional, (<|>))
|
||||
import Control.Monad
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Attoparsec.Text (Parser)
|
||||
import qualified Data.Attoparsec.Text as A
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isAlpha, isAscii, isDigit, isPunctuation, isSpace)
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
@@ -34,9 +38,10 @@ import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (.
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..))
|
||||
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8)
|
||||
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8, tshow)
|
||||
import System.Console.ANSI.Types
|
||||
import qualified Text.Email.Validate as Email
|
||||
import qualified URI.ByteString as U
|
||||
|
||||
data Markdown = Markdown (Maybe Format) Text | Markdown :|: Markdown
|
||||
deriving (Eq, Show)
|
||||
@@ -49,7 +54,9 @@ data Format
|
||||
| Secret
|
||||
| Colored {color :: FormatColor}
|
||||
| Uri
|
||||
| SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text}
|
||||
-- showText is Nothing for the usual Uri without text
|
||||
| HyperLink {showText :: Maybe Text, linkUri :: Text}
|
||||
| SimplexLink {showText :: Maybe Text, linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text}
|
||||
| Command {commandStr :: Text}
|
||||
| Mention {memberName :: Text}
|
||||
| Email
|
||||
@@ -187,6 +194,7 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
'!' -> coloredP <|> wordP
|
||||
'@' -> mentionP <|> wordP
|
||||
'/' -> commandP <|> wordP
|
||||
'[' -> sowLinkP <|> wordP
|
||||
_
|
||||
| isDigit c -> phoneP <|> wordP
|
||||
| otherwise -> wordP
|
||||
@@ -224,6 +232,20 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
let origStr = if c == '\'' then '\'' `T.cons` str `T.snoc` '\'' else str
|
||||
res = markdown (format str) (pfx `T.cons` origStr)
|
||||
pure $ if T.null punct then res else res :|: unmarked punct
|
||||
sowLinkP = do
|
||||
t <- '[' `inParens` ']'
|
||||
l <- '(' `inParens` ')'
|
||||
let hasPunct = T.any (\c -> isPunctuation c && c /= '-' && c /= '_') t
|
||||
when (hasPunct && t /= l && ("https://" <> t) /= l) $ fail "punctuation in hyperlink text"
|
||||
f <- case strDecode $ encodeUtf8 l of
|
||||
Right lnk@(ACL _ cLink) -> case cLink of
|
||||
CLShort _ -> pure $ simplexUriFormat (Just t) lnk
|
||||
CLFull _ -> fail "full SimpleX link in hyperlink"
|
||||
Left _ -> case parseUri $ encodeUtf8 l of
|
||||
Right _ -> pure $ HyperLink (Just t) l
|
||||
Left e -> fail $ "not uri: " <> T.unpack e
|
||||
pure $ markdown f $ T.concat ["[", t, "](", l, ")"]
|
||||
inParens open close = A.char open *> A.takeWhile1 (/= close) <* A.char close
|
||||
colorP =
|
||||
A.anyChar >>= \case
|
||||
'r' -> optional "ed" $> Red
|
||||
@@ -253,7 +275,11 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
wordMD :: Text -> Markdown
|
||||
wordMD s
|
||||
| T.null s = unmarked s
|
||||
| isUri s' = res $ uriMarkdown s'
|
||||
| isUri s' = case strDecode $ encodeUtf8 s of
|
||||
Right cLink -> res $ markdown (simplexUriFormat Nothing cLink) s'
|
||||
Left _ -> case parseUri $ encodeUtf8 s' of
|
||||
Right _ -> res $ markdown Uri s'
|
||||
Left _ -> unmarked s
|
||||
| isDomain s' = res $ markdown Uri s'
|
||||
| isEmail s' = res $ markdown Email s'
|
||||
| otherwise = unmarked s
|
||||
@@ -265,9 +291,6 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
'/' -> False
|
||||
')' -> False
|
||||
c -> isPunctuation c
|
||||
uriMarkdown s = case strDecode $ encodeUtf8 s of
|
||||
Right cLink -> markdown (simplexUriFormat cLink) s
|
||||
_ -> markdown Uri s
|
||||
isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"]
|
||||
-- matches what is likely to be a domain, not all valid domain names
|
||||
isDomain s = case T.splitOn "." s of
|
||||
@@ -281,11 +304,11 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
&& (let p c = isAscii c && isAlpha c in T.all p name && T.all p tld)
|
||||
isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s)
|
||||
noFormat = pure . unmarked
|
||||
simplexUriFormat :: AConnectionLink -> Format
|
||||
simplexUriFormat = \case
|
||||
simplexUriFormat :: Maybe Text -> AConnectionLink -> Format
|
||||
simplexUriFormat showText = \case
|
||||
ACL m (CLFull cReq) -> case cReq of
|
||||
CRContactUri crData -> SimplexLink (linkType' crData) cLink $ uriHosts crData
|
||||
CRInvitationUri crData _ -> SimplexLink XLInvitation cLink $ uriHosts crData
|
||||
CRContactUri crData -> SimplexLink showText (linkType' crData) cLink $ uriHosts crData
|
||||
CRInvitationUri crData _ -> SimplexLink showText XLInvitation cLink $ uriHosts crData
|
||||
where
|
||||
cLink = ACL m $ CLFull $ simplexConnReqUri cReq
|
||||
uriHosts ConnReqUriData {crSmpQueues} = L.map strEncodeText $ sconcat $ L.map (host . qServer) crSmpQueues
|
||||
@@ -293,8 +316,8 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
Just (CRDataGroup _) -> XLGroup
|
||||
Nothing -> XLContact
|
||||
ACL m (CLShort sLnk) -> case sLnk of
|
||||
CSLContact _ ct srv _ -> SimplexLink (linkType' ct) cLink $ uriHosts srv
|
||||
CSLInvitation _ srv _ _ -> SimplexLink XLInvitation cLink $ uriHosts srv
|
||||
CSLContact _ ct srv _ -> SimplexLink showText (linkType' ct) cLink $ uriHosts srv
|
||||
CSLInvitation _ srv _ _ -> SimplexLink showText XLInvitation cLink $ uriHosts srv
|
||||
where
|
||||
cLink = ACL m $ CLShort $ simplexShortLink sLnk
|
||||
uriHosts srv = L.map strEncodeText $ host srv
|
||||
@@ -305,6 +328,24 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
strEncodeText :: StrEncoding a => a -> Text
|
||||
strEncodeText = safeDecodeUtf8 . strEncode
|
||||
|
||||
parseUri :: B.ByteString -> Either Text U.URI
|
||||
parseUri s = case U.parseURI U.laxURIParserOptions s of
|
||||
Left e -> Left $ "Invalid URI: " <> tshow e
|
||||
Right uri@U.URI {uriScheme = U.Scheme sch, uriAuthority}
|
||||
| sch /= "http" && sch /= "https" -> Left $ "Unsupported URI scheme: " <> safeDecodeUtf8 sch
|
||||
| otherwise -> case uriAuthority of
|
||||
Nothing -> Left "No URI host"
|
||||
Just U.Authority {authorityHost = U.Host h}
|
||||
| '.' `B.notElem` h -> Left $ "Invalid URI host: " <> safeDecodeUtf8 h
|
||||
| otherwise -> Right uri
|
||||
|
||||
sanitizeUri :: U.URI -> Maybe U.URI
|
||||
sanitizeUri uri@U.URI {uriQuery = U.Query originalQS} =
|
||||
let sanitizedQS = filter (\(p, _) -> p == "q" || p == "search") originalQS
|
||||
in if length sanitizedQS == length originalQS
|
||||
then Nothing
|
||||
else Just $ uri {U.uriQuery = U.Query sanitizedQS}
|
||||
|
||||
markdownText :: FormattedText -> Text
|
||||
markdownText (FormattedText f_ t) = case f_ of
|
||||
Nothing -> t
|
||||
@@ -316,6 +357,7 @@ markdownText (FormattedText f_ t) = case f_ of
|
||||
Secret -> around '#'
|
||||
Colored (FormatColor c) -> color c
|
||||
Uri -> t
|
||||
HyperLink {} -> t
|
||||
SimplexLink {} -> t
|
||||
Mention _ -> t
|
||||
Command _ -> t
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
@@ -25,6 +26,7 @@ import Data.Functor (($>))
|
||||
import Data.List (find)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import Data.Word (Word8)
|
||||
import Foreign.C.String
|
||||
import Foreign.C.Types (CInt (..))
|
||||
@@ -35,7 +37,7 @@ import GHC.IO.Encoding (setFileSystemEncoding, setForeignEncoding, setLocaleEnco
|
||||
import Simplex.Chat
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList)
|
||||
import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList, parseUri, sanitizeUri)
|
||||
import Simplex.Chat.Mobile.File
|
||||
import Simplex.Chat.Mobile.Shared
|
||||
import Simplex.Chat.Mobile.WebRTC
|
||||
@@ -56,6 +58,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..)
|
||||
import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8)
|
||||
import System.IO (utf8)
|
||||
import System.Timeout (timeout)
|
||||
import qualified URI.ByteString as U
|
||||
#if !defined(dbPostgres)
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import Database.SQLite.Simple (SQLError (..))
|
||||
@@ -81,6 +84,20 @@ eitherToResult :: Maybe RemoteHostId -> Either ChatError r -> APIResult r
|
||||
eitherToResult rhId = either (APIError rhId) (APIResult rhId)
|
||||
{-# INLINE eitherToResult #-}
|
||||
|
||||
data ParsedUri = ParsedUri
|
||||
{ uriInfo :: Maybe UriInfo,
|
||||
parseError :: Text
|
||||
}
|
||||
|
||||
data UriInfo = UriInfo
|
||||
{ scheme :: Text,
|
||||
sanitized :: Maybe Text
|
||||
}
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''UriInfo)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''ParsedUri)
|
||||
|
||||
$(pure [])
|
||||
|
||||
instance ToJSON r => ToJSON (APIResult r) where
|
||||
@@ -111,6 +128,8 @@ foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO C
|
||||
|
||||
foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_parse_uri" cChatParseUri :: CString -> IO CJSONString
|
||||
|
||||
foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString
|
||||
|
||||
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
|
||||
@@ -200,6 +219,10 @@ cChatParseMarkdown s = newCStringFromLazyBS . chatParseMarkdown =<< B.packCStrin
|
||||
cChatParseServer :: CString -> IO CJSONString
|
||||
cChatParseServer s = newCStringFromLazyBS . chatParseServer =<< B.packCString s
|
||||
|
||||
-- | parse web URI - returns ParsedUri JSON
|
||||
cChatParseUri :: CString -> IO CJSONString
|
||||
cChatParseUri s = newCStringFromLazyBS . chatParseUri =<< B.packCString s
|
||||
|
||||
cChatPasswordHash :: CString -> CString -> IO CString
|
||||
cChatPasswordHash cPwd cSalt = do
|
||||
pwd <- B.packCString cPwd
|
||||
@@ -293,6 +316,7 @@ chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do
|
||||
DB.ErrorNotADatabase -> Left $ DBMErrorNotADatabase errDbStr
|
||||
_ -> dbError e
|
||||
#endif
|
||||
dbError :: Show e => e -> Either DBMigrationResult DBStore
|
||||
dbError e = Left . DBMErrorSQL errDbStr $ show e
|
||||
|
||||
chatCloseStore :: ChatController -> IO String
|
||||
@@ -342,6 +366,14 @@ chatParseServer = J.encode . toServerAddress . strDecode
|
||||
enc :: StrEncoding a => a -> String
|
||||
enc = B.unpack . strEncode
|
||||
|
||||
chatParseUri :: ByteString -> JSONByteString
|
||||
chatParseUri s = J.encode $ case parseUri s of
|
||||
Left e -> ParsedUri Nothing e
|
||||
Right uri@U.URI {uriScheme = U.Scheme sch} ->
|
||||
let sanitized = safeDecodeUtf8 . U.serializeURIRef' <$> sanitizeUri uri
|
||||
uriInfo = UriInfo {scheme = safeDecodeUtf8 sch, sanitized}
|
||||
in ParsedUri (Just uriInfo) ""
|
||||
|
||||
chatPasswordHash :: ByteString -> ByteString -> ByteString
|
||||
chatPasswordHash pwd salt = either (const "") passwordHash salt'
|
||||
where
|
||||
|
||||
+28
-5
@@ -20,6 +20,7 @@ markdownTests = do
|
||||
secretText
|
||||
textColor
|
||||
textWithUri
|
||||
textWithHyperlink
|
||||
textWithEmail
|
||||
textWithPhone
|
||||
textWithMentions
|
||||
@@ -172,11 +173,11 @@ uri :: Text -> Markdown
|
||||
uri = Markdown $ Just Uri
|
||||
|
||||
simplexLink :: SimplexLinkType -> Text -> NonEmpty Text -> Text -> Markdown
|
||||
simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts) t
|
||||
simplexLink linkType uriText smpHosts t = Markdown (simplexLinkFormat linkType uriText smpHosts Nothing) t
|
||||
|
||||
simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Format
|
||||
simplexLinkFormat linkType uriText smpHosts = case strDecode $ encodeUtf8 uriText of
|
||||
Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts}
|
||||
simplexLinkFormat :: SimplexLinkType -> Text -> NonEmpty Text -> Maybe Text -> Maybe Format
|
||||
simplexLinkFormat linkType uriText smpHosts showText = case strDecode $ encodeUtf8 uriText of
|
||||
Right simplexUri -> Just SimplexLink {linkType, simplexUri, smpHosts, showText}
|
||||
Left e -> error e
|
||||
|
||||
textWithUri :: Spec
|
||||
@@ -210,6 +211,7 @@ textWithUri = describe "text with Uri" do
|
||||
"www." <==> "www."
|
||||
".com" <==> ".com"
|
||||
"example.academytoolong" <==> "example.academytoolong"
|
||||
"simplex:/example" <==> "simplex:/example"
|
||||
it "SimpleX links" do
|
||||
let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||
("https://simplex.chat" <> inv) <==> simplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] ("https://simplex.chat" <> inv)
|
||||
@@ -222,6 +224,27 @@ textWithUri = describe "text with Uri" do
|
||||
("https://simplex.chat" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("https://simplex.chat" <> gr)
|
||||
("simplex:" <> gr) <==> simplexLink XLGroup ("simplex:" <> gr) ["smp4.simplex.im", "o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"] ("simplex:" <> gr)
|
||||
|
||||
web :: Text -> Text -> Text -> Markdown
|
||||
web t u = Markdown $ Just HyperLink {showText = Just t, linkUri = u}
|
||||
|
||||
textWithHyperlink :: Spec
|
||||
textWithHyperlink = describe "text with HyperLink without link text" do
|
||||
let addr = "https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw"
|
||||
addr' = "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im"
|
||||
it "correct markdown" do
|
||||
"[click here](https://example.com)" <==> web "click here" "https://example.com" "[click here](https://example.com)"
|
||||
"For details [click here](https://example.com)" <==> "For details " <> web "click here" "https://example.com" "[click here](https://example.com)"
|
||||
"[example.com](https://example.com)" <==> web "example.com" "https://example.com" "[example.com](https://example.com)"
|
||||
"[example.com/page](https://example.com/page)" <==> web "example.com/page" "https://example.com/page" "[example.com/page](https://example.com/page)"
|
||||
("[Connect to me](" <> addr <> ")") <==> Markdown (simplexLinkFormat XLContact addr' ["smp6.simplex.im"] (Just "Connect to me")) ("[Connect to me](" <> addr <> ")")
|
||||
it "potentially spoofed link" do
|
||||
"[https://example.com](https://another.com)" <==> "[https://example.com](https://another.com)"
|
||||
"[example.com/page](https://another.com/page)" <==> "[example.com/page](https://another.com/page)"
|
||||
("[Connect.to.me](" <> addr <> ")") <==> Markdown Nothing ("[Connect.to.me](" <> addr <> ")")
|
||||
it "ignored as markdown" do
|
||||
"[click here](example.com)" <==> "[click here](example.com)"
|
||||
"[click here](https://example.com )" <==> "[click here](https://example.com )"
|
||||
|
||||
email :: Text -> Markdown
|
||||
email = Markdown $ Just Email
|
||||
|
||||
@@ -330,7 +353,7 @@ multilineMarkdownList = describe "multiline markdown" do
|
||||
it "multiline with simplex link" do
|
||||
("https://simplex.chat" <> inv <> "\ntext")
|
||||
<<==>>
|
||||
[ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv),
|
||||
[ FormattedText (simplexLinkFormat XLInvitation ("simplex:" <> inv) ["smp.simplex.im"] Nothing) ("https://simplex.chat" <> inv),
|
||||
"\ntext"
|
||||
]
|
||||
it "command markdown" do
|
||||
|
||||
@@ -75,6 +75,9 @@ mobileTests = do
|
||||
it "should convert invalid name to a valid name" testValidNameCApi
|
||||
describe "JSON length" $ do
|
||||
it "should compute length of JSON encoded string" testChatJsonLengthCApi
|
||||
describe "Parsers" $ do
|
||||
it "should parse server address" testChatParseServer
|
||||
it "should parse and sanitize URI" testChatParseUri
|
||||
|
||||
noActiveUser :: LB.ByteString
|
||||
noActiveUser =
|
||||
@@ -318,6 +321,14 @@ testChatJsonLengthCApi _ = do
|
||||
cInt2 <- cChatJsonLength =<< newCString "こんにちは!"
|
||||
cInt2 `shouldBe` 18
|
||||
|
||||
testChatParseServer :: TestParams -> IO ()
|
||||
testChatParseServer _ = do
|
||||
pure ()
|
||||
|
||||
testChatParseUri :: TestParams -> IO ()
|
||||
testChatParseUri _ = do
|
||||
pure ()
|
||||
|
||||
jDecode :: FromJSON a => String -> IO (Maybe a)
|
||||
jDecode = pure . J.decode . LB.pack
|
||||
|
||||
|
||||
Reference in New Issue
Block a user