ios: view conditions as markdown (#5248)

* ios: view conditions as markdown

* changes

* removed Down

* refactor

* unused

* react on theme change
This commit is contained in:
Stanislav Dmitrenko
2024-11-26 20:00:39 +07:00
committed by GitHub
parent 345e0acdec
commit 25893177d0
5 changed files with 164 additions and 13 deletions

View File

@@ -0,0 +1,83 @@
//
// ConditionsWebView.swift
// SimpleX (iOS)
//
// Created by Stanislav Dmitrenko on 26.11.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import WebKit
struct ConditionsWebView: UIViewRepresentable {
@State var html: String
@EnvironmentObject var theme: AppTheme
@State var pageLoaded = false
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.backgroundColor = .clear
view.isOpaque = false
view.navigationDelegate = context.coordinator
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// just to make sure that even if updateUIView will not be called for any reason, the page
// will be rendered anyway
if !pageLoaded {
loadPage(view)
}
}
return view
}
func updateUIView(_ view: WKWebView, context: Context) {
loadPage(view)
}
private func loadPage(_ webView: WKWebView) {
let styles = """
<style>
body {
color: \(theme.colors.onBackground.toHTMLHex());
font-family: Helvetica;
}
a {
color: \(theme.colors.primary.toHTMLHex());
}
code, pre {
font-family: Menlo;
background: \(theme.colors.secondary.opacity(theme.colors.isLight ? 0.2 : 0.3).toHTMLHex());
}
</style>
"""
let head = "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no'>\(styles)</head>"
webView.loadHTMLString(head + html, baseURL: nil)
DispatchQueue.main.async {
pageLoaded = true
}
}
func makeCoordinator() -> Cordinator {
Cordinator()
}
class Cordinator: NSObject, WKNavigationDelegate {
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else { return decisionHandler(.allow) }
switch navigationAction.navigationType {
case .linkActivated:
decisionHandler(.cancel)
if url.absoluteString.starts(with: "https://simplex.chat/contact#") {
ChatModel.shared.appOpenUrl = url
} else {
UIApplication.shared.open(url)
}
default:
decisionHandler(.allow)
}
}
}
}

View File

@@ -8,6 +8,7 @@
import SwiftUI
import SimpleXChat
import Ink
struct OperatorView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@@ -342,6 +343,7 @@ struct OperatorInfoView: View {
struct ConditionsTextView: View {
@State private var conditionsData: (UsageConditions, String?, UsageConditions?)?
@State private var failedToLoad: Bool = false
@State private var conditionsHTML: String? = nil
let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
@@ -350,7 +352,18 @@ struct ConditionsTextView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.task {
do {
conditionsData = try await getUsageConditions()
let conditions = try await getUsageConditions()
let conditionsText = conditions.1
let parentLink = "https://github.com/simplex-chat/simplex-chat/blob/\(conditions.0.conditionsCommit)"
let preparedText: String?
if let conditionsText {
let prepared = prepareMarkdown(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines), parentLink)
conditionsHTML = MarkdownParser().html(from: prepared)
preparedText = prepared
} else {
preparedText = nil
}
conditionsData = (conditions.0, preparedText, conditions.2)
} catch let error {
logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))")
failedToLoad = true
@@ -358,18 +371,16 @@ struct ConditionsTextView: View {
}
}
// TODO Markdown & diff rendering
// TODO Diff rendering
@ViewBuilder private func viewBody() -> some View {
if let (usageConditions, conditionsText, acceptedConditions) = conditionsData {
if let conditionsText = conditionsText {
ScrollView {
Text(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines))
.padding()
}
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
if let (usageConditions, _, _) = conditionsData {
if let conditionsHTML {
ConditionsWebView(html: conditionsHTML)
.padding(6)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(uiColor: .secondarySystemGroupedBackground))
)
} else {
let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md"
conditionsLinkView(conditionsLink)
@@ -391,6 +402,16 @@ struct ConditionsTextView: View {
}
}
}
private func prepareMarkdown(_ text: String, _ parentLink: String) -> String {
let localLinkRegex = try! NSRegularExpression(pattern: "\\[([^\\(]*)\\]\\(#.*\\)")
let h1Regex = try! NSRegularExpression(pattern: "^# ")
var text = localLinkRegex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "$1")
text = h1Regex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "")
return text
.replacingOccurrences(of: "](/", with: "](\(parentLink)/")
.replacingOccurrences(of: "](./", with: "](\(parentLink)/")
}
}
struct SingleOperatorUsageConditionsView: View {

View File

@@ -199,6 +199,8 @@
8C8118722C220B5B00E6FC94 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 8C8118712C220B5B00E6FC94 /* Yams */; };
8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; };
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; };
8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB3476B2CF5CFFA006787A5 /* Ink */; };
8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */; };
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; };
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
@@ -547,6 +549,7 @@
8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = "<group>"; };
8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionsWebView.swift; sourceTree = "<group>"; };
8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = "<group>"; };
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
@@ -636,6 +639,7 @@
files = (
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */,
8C8118722C220B5B00E6FC94 /* Yams in Frameworks */,
8CB3476C2CF5CFFA006787A5 /* Ink in Frameworks */,
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */,
D7197A1829AE89660055C05A /* WebRTC in Frameworks */,
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */,
@@ -1072,6 +1076,7 @@
643B3B4D2CCFD6400083A2CF /* OperatorView.swift */,
5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */,
5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */,
8CB3476D2CF5F58B006787A5 /* ConditionsWebView.swift */,
);
path = NetworkAndServers;
sourceTree = "<group>";
@@ -1183,6 +1188,7 @@
D7F0E33829964E7E0068AF69 /* LZString */,
D7197A1729AE89660055C05A /* WebRTC */,
8C8118712C220B5B00E6FC94 /* Yams */,
8CB3476B2CF5CFFA006787A5 /* Ink */,
);
productName = "SimpleX (iOS)";
productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */;
@@ -1326,6 +1332,7 @@
D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */,
D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */,
8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */,
8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */,
);
productRefGroup = 5CA059CB279559F40002BEB4 /* Products */;
projectDirPath = "";
@@ -1516,6 +1523,7 @@
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */,
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */,
5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */,
8CB3476E2CF5F58B006787A5 /* ConditionsWebView.swift in Sources */,
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */,
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
@@ -2375,6 +2383,14 @@
version = 5.1.2;
};
};
8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/johnsundell/ink";
requirement = {
kind = exactVersion;
version = 0.6.0;
};
};
D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/simplex-chat/WebRTC.git";
@@ -2412,6 +2428,11 @@
package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */;
productName = Yams;
};
8CB3476B2CF5CFFA006787A5 /* Ink */ = {
isa = XCSwiftPackageProductDependency;
package = 8CB3476A2CF5CFFA006787A5 /* XCRemoteSwiftPackageReference "ink" */;
productName = Ink;
};
CE38A29B2C3FCD72005ED185 /* SwiftyGif */ = {
isa = XCSwiftPackageProductDependency;
package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115",
"originHash" : "33afc44be5f4225325b3cb940ed71b6cbf3ef97290d348d7b6803697bcd0637d",
"pins" : [
{
"identity" : "codescanner",
@@ -10,6 +10,15 @@
"version" : "2.5.0"
}
},
{
"identity" : "ink",
"kind" : "remoteSourceControl",
"location" : "https://github.com/johnsundell/ink",
"state" : {
"revision" : "bcc9f219900a62c4210e6db726035d7f03ae757b",
"version" : "0.6.0"
}
},
{
"identity" : "lzstring-swift",
"kind" : "remoteSourceControl",

View File

@@ -63,6 +63,23 @@ extension Color {
)
}
public func toHTMLHex() -> String {
let uiColor: UIColor = .init(self)
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
uiColor.getRed(&r, green: &g, blue: &b, alpha: &a)
// Can be negative values and more than 1. Extended color range, making it normal
r = min(1, max(0, r))
g = min(1, max(0, g))
b = min(1, max(0, b))
a = min(1, max(0, a))
return String(format: "#%02x%02x%02x%02x",
Int((r * 255).rounded()),
Int((g * 255).rounded()),
Int((b * 255).rounded()),
Int((a * 255).rounded())
)
}
public func darker(_ factor: CGFloat = 0.1) -> Color {
var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a)