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

* core: markdown for "hidden" links

* update, test

* api docs

* chatParseUri FFI function

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

* update markdown

* android, desktop: ditto

* ios: export localizations

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

* rename
This commit is contained in:
Evgeny
2025-08-09 10:52:35 +01:00
committed by GitHub
parent b4293e361b
commit ef60ceea12
55 changed files with 1004 additions and 288 deletions
+24
View File
@@ -186,6 +186,30 @@ struct ParsedServerAddress: Decodable {
var parseError: String
}
public func parseSanitizeUri(_ s: String) -> ParsedUri? {
var c = s.cString(using: .utf8)!
if let cjson = chat_parse_uri(&c) {
if let d = dataFromCString(cjson) {
do {
return try jsonDecoder.decode(ParsedUri.self, from: d)
} catch {
logger.error("parseSanitizeUri jsonDecoder.decode error: \(error.localizedDescription)")
}
}
}
return nil
}
public struct ParsedUri: Decodable {
public var uriInfo: UriInfo?
public var parseError: String
}
public struct UriInfo: Decodable {
public var scheme: String
public var sanitized: String?
}
@inline(__always)
public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
let s = String.init(cString: c)
+6
View File
@@ -27,6 +27,8 @@ let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled"
public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension"
// replaces DEFAULT_PRIVACY_LINK_PREVIEWS
let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews"
public let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT = "privacyLinkPreviewsShowAlert"
public let GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS = "privacySanitizeLinks"
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
@@ -95,6 +97,8 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true,
GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false,
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: true,
GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT: true,
GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS: true,
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
@@ -222,6 +226,8 @@ public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults
public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS)
public let privacyLinkPreviewsShowAlertGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS_SHOW_ALERT)
// This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
+17 -3
View File
@@ -4611,6 +4611,11 @@ public struct FormattedText: Decodable, Hashable {
public var text: String
public var format: Format?
public init(text: String, format: Format? = nil) {
self.text = text
self.format = format
}
public static func plain(_ text: String) -> [FormattedText] {
text.isEmpty
? []
@@ -4620,6 +4625,14 @@ public struct FormattedText: Decodable, Hashable {
public var isSecret: Bool {
if case .secret = format { true } else { false }
}
public var linkUri: String? {
switch format {
case .uri: text
case let .hyperLink(_, linkUri): linkUri
default: nil
}
}
}
public enum Format: Decodable, Equatable, Hashable {
@@ -4630,7 +4643,8 @@ public enum Format: Decodable, Equatable, Hashable {
case secret
case colored(color: FormatColor)
case uri
case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String])
case hyperLink(showText: String?, linkUri: String)
case simplexLink(showText: String?, linkType: SimplexLinkType, simplexUri: String, smpHosts: [String])
case command(commandStr: String)
case mention(memberName: String)
case email
@@ -4748,14 +4762,14 @@ extension ReportReason: Decodable {
// Struct to use with simplex API
public struct LinkPreview: Codable, Equatable, Hashable {
public init(uri: URL, title: String, description: String = "", image: String) {
public init(uri: String, title: String, description: String = "", image: String) {
self.uri = uri
self.title = title
self.description = description
self.image = image
}
public var uri: URL
public var uri: String
public var title: String
// TODO remove once optional in haskell
public var description: String = ""
+1 -1
View File
@@ -446,7 +446,7 @@ public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
let resized = resizeImageToStrSizeSync(image, maxDataSize: 14000),
let title = metadata.title,
let uri = metadata.originalURL {
linkPreview = LinkPreview(uri: uri, title: title, image: resized)
linkPreview = LinkPreview(uri: uri.absoluteString, title: title, image: resized)
}
}
cb(linkPreview)
+1
View File
@@ -24,6 +24,7 @@ extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum);
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str);
extern char *chat_parse_uri(char *str);
extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);