ui: open external link alerts (#6860)

* ui: open external link alerts

* update

* update

* update

* update

* update

* change link, add link to alert, close modals when opening chat

* refactor

* add string

* fix link in terms

* open simplex chat links from privacy policy in app

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
spaced4ndy
2026-04-25 14:59:42 +00:00
committed by GitHub
parent 0ff297b3b7
commit ea6a09b66e
23 changed files with 109 additions and 60 deletions

View File

@@ -86,6 +86,45 @@ func showSheet(
}
}
func openExternalLink(_ url: URL) {
let s = url.absoluteString
if s.starts(with: "https://simplex.chat/contact#") || (s.starts(with: "https://smp") && s.contains(".simplex.im/a#")) {
ChatModel.shared.appOpenUrl = url
} else {
showAlert(
title: NSLocalizedString("Open external link?", comment: "alert title"),
message: s,
buttonTitle: NSLocalizedString("Open", comment: "alert button"),
buttonAction: { UIApplication.shared.open(url) },
cancelButton: true
)
}
}
struct ExternalLink<Label: View>: View {
let destination: URL
let label: Label
init(destination: URL, @ViewBuilder label: () -> Label) {
self.destination = destination
self.label = label()
}
init(_ titleKey: LocalizedStringKey, destination: URL) where Label == Text {
self.destination = destination
self.label = Text(titleKey)
}
init<S: StringProtocol>(_ title: S, destination: URL) where Label == Text {
self.destination = destination
self.label = Text(title)
}
var body: some View {
Button { openExternalLink(destination) } label: { label }
}
}
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)

View File

@@ -26,7 +26,7 @@ struct AddContactLearnMore: View {
VStack(alignment: .leading, spacing: 18) {
Text("To connect, your contact can scan QR code or use the link in the app.")
Text("If you can't meet in person, show QR code in a video call, or share the link.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).")
ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/readme.html#connect-to-friends")!)
}
.frame(maxWidth: .infinity, alignment: .leading)
.listRowBackground(Color.clear)

View File

@@ -28,7 +28,7 @@ struct HowItWorks: View {
Text("Only client devices store user profiles, contacts, groups, and messages.")
Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.")
if !onboarding {
Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
ExternalLink("Read more in our GitHub repository.", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#readme")!)
}
}
.padding(.bottom)

View File

@@ -791,7 +791,7 @@ struct WhatsNewView: View {
}
}
if let post = v.post {
Link(destination: post) {
ExternalLink(destination: post) {
HStack {
Text("Read more")
Image(systemName: "arrow.up.right.circle")

View File

@@ -22,14 +22,16 @@ struct DeveloperView: View {
VStack {
List {
Section {
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
.colorMultiply(theme.colors.secondary)
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, 36)
ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) {
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
.colorMultiply(theme.colors.secondary)
Text("Install SimpleX Chat for terminal")
.padding(.leading, 36)
}
}
NavigationLink {
TerminalView()

View File

@@ -23,7 +23,7 @@ struct IncognitoHelp: View {
Text("Incognito mode protects your privacy by using a new random profile for each contact.")
Text("It allows having many anonymous connections without any shared data between them in a single chat profile.")
Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode).")
ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode")!)
}
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))

View File

@@ -71,11 +71,7 @@ struct ConditionsWebView: UIViewRepresentable {
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)
}
openExternalLink(url)
default:
decisionHandler(.allow)
}

View File

@@ -332,7 +332,7 @@ struct UsageConditionsView: View {
@ViewBuilder private func conditionsDiffButton(_ font: Font? = nil) -> some View {
let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
Link(destination: commitUrl) {
ExternalLink(destination: commitUrl) {
HStack {
Text("Open changes")
Image(systemName: "arrow.up.right.circle")

View File

@@ -364,11 +364,11 @@ struct OperatorInfoView: View {
Text(d)
}
}
Link(serverOperator.info.website.absoluteString, destination: serverOperator.info.website)
ExternalLink(serverOperator.info.website.absoluteString, destination: serverOperator.info.website)
}
if let selfhost = serverOperator.info.selfhost {
Section {
Link(selfhost.text, destination: selfhost.link)
ExternalLink(selfhost.text, destination: selfhost.link)
}
}
}
@@ -432,7 +432,7 @@ struct ConditionsTextView: View {
private func conditionsLinkView(_ conditionsLink: String) -> some View {
VStack(alignment: .leading, spacing: 20) {
Text("Current conditions text couldn't be loaded, you can review conditions via this link:")
Link(destination: URL(string: conditionsLink)!) {
ExternalLink(destination: URL(string: conditionsLink)!) {
Text(conditionsLink)
.multilineTextAlignment(.leading)
}
@@ -591,11 +591,11 @@ func conditionsLinkButton() -> some View {
let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
let mdUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/\(commit)/PRIVACY.md") ?? conditionsURL
return Menu {
Link(destination: mdUrl) {
ExternalLink(destination: mdUrl) {
Label("Open conditions", systemImage: "doc")
}
if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
Link(destination: commitUrl) {
ExternalLink(destination: commitUrl) {
Label("Open changes", systemImage: "ellipsis")
}
}

View File

@@ -223,9 +223,7 @@ struct YourServersView: View {
func howToButton() -> some View {
Button {
DispatchQueue.main.async {
UIApplication.shared.open(howToUrl)
}
openExternalLink(howToUrl)
} label: {
HStack {
Text("How to use your servers")

View File

@@ -139,9 +139,7 @@ struct RTCServers: View {
func howToButton() -> some View {
Button {
DispatchQueue.main.async {
UIApplication.shared.open(howToUrl)
}
openExternalLink(howToUrl)
} label: {
HStack{
Text("How to")

View File

@@ -11,7 +11,7 @@ import SwiftUI
import StoreKit
import SimpleXChat
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
let simplexTeamURL = URL(string: "simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im")!
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
@@ -399,7 +399,9 @@ struct SettingsView: View {
}
Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) {
settingsRow("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
settingsRow("keyboard", color: theme.colors.secondary) {
ExternalLink("Contribute", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#contribute")!)
}
settingsRow("star", color: theme.colors.secondary) {
Button("Rate the app") {
if let scene = sceneDelegate.windowScene {
@@ -407,14 +409,16 @@ struct SettingsView: View {
}
}
}
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
.colorMultiply(theme.colors.secondary)
Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)")
.padding(.leading, indent)
ExternalLink(destination: URL(string: "https://github.com/simplex-chat/simplex-chat")!) {
ZStack(alignment: .leading) {
Image(colorScheme == .dark ? "github_light" : "github")
.resizable()
.frame(width: 24, height: 24)
.opacity(0.5)
.colorMultiply(theme.colors.secondary)
Text("Star on GitHub")
.padding(.leading, indent)
}
}
}

View File

@@ -31,7 +31,7 @@ struct UserAddressLearnMore: View {
.padding(.top)
Text("SimpleX address and 1-time links are safe to share via any messenger.")
Text("To protect against your link being replaced, you can compare contact security codes.")
Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).")
ExternalLink("Read more in User Guide.", destination: URL(string: "https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses")!)
.padding(.top)
}

View File

@@ -537,6 +537,20 @@ fun UriHandler.openUriCatching(uri: String) {
}
}
fun UriHandler.openExternalLink(uri: String) {
val uriHandler = this
if (uri.startsWith("https://simplex.chat/contact#") || (uri.startsWith("https://smp") && ".simplex.im/a#" in uri)) {
uriHandler.openVerifiedSimplexUri(uri)
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.open_external_link_title),
text = uri,
confirmText = generalGetString(MR.strings.open_verb),
onConfirm = { uriHandler.openUriCatching(uri) }
)
}
}
fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver(
save = { it.width to it.height },
restore = { IntSize(it.first, it.second) }

View File

@@ -604,6 +604,7 @@ fun showPrepareContactAlert(
confirmText = generalGetString(MR.strings.connect_plan_open_new_chat),
onConfirm = {
AlertManager.privacySensitive.hideAlert()
ModalManager.closeAllModalsEverywhere()
withBGApi {
val chat = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData)
if (chat != null) {

View File

@@ -67,7 +67,7 @@ fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: T
newStyles
}
val uriHandler = LocalUriHandler.current
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp)
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openExternalLink(link) }, textAlign = textAlign, lineHeight = 22.sp)
}
@Composable

View File

@@ -59,7 +59,7 @@ fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Bool
Icon(
painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier
.clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }
.clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openExternalLink(link) }
)
}
@@ -229,7 +229,7 @@ fun ReadMoreButton(url: String) {
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
uriHandler.openUriCatching(url)
uriHandler.openExternalLink(url)
}
)
Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary)

View File

@@ -198,7 +198,7 @@ private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { uriHandler.openUriCatching("https://simplex.chat/docs/webrtc.html#configure-mobile-apps") }
modifier = Modifier.clickable { uriHandler.openExternalLink("https://simplex.chat/docs/webrtc.html#configure-mobile-apps") }
) {
Text(stringResource(MR.strings.how_to), color = MaterialTheme.colors.primary)
Icon(

View File

@@ -75,7 +75,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: (
}
val simplexTeamUri =
"simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
"simplex:/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw?h=smp6.simplex.im"
@Composable
fun SettingsLayout(
@@ -207,7 +207,7 @@ fun ChatLockItem(
}
@Composable private fun ContributeItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) {
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) {
Icon(
painterResource(MR.images.ic_keyboard),
contentDescription = "GitHub",
@@ -235,7 +235,7 @@ fun ChatLockItem(
}
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(MR.images.ic_github),
contentDescription = "GitHub",
@@ -268,7 +268,7 @@ fun ChatLockItem(
}
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(MR.images.ic_github),
contentDescription = "GitHub",

View File

@@ -769,7 +769,7 @@ fun UsageConditionsView(
.clip(shape = CircleShape)
.clickable {
val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit"
uriHandler.openUriCatching(commitUrl)
uriHandler.openExternalLink(commitUrl)
}
.padding(horizontal = 6.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,

View File

@@ -500,7 +500,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) {
Text(d)
}
val website = serverOperator.info.website
Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(website) })
Text(website, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(website) })
}
}
}
@@ -511,7 +511,7 @@ fun OperatorInfoView(serverOperator: ServerOperator) {
SectionView {
SectionItemView {
val (text, link) = selfhost
Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(link) })
Text(text, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(link) })
}
}
}
@@ -787,7 +787,7 @@ private fun ConditionsLinkView(conditionsLink: String) {
SectionItemView {
val uriHandler = LocalUriHandler.current
Text(stringResource(MR.strings.operator_conditions_failed_to_load), color = MaterialTheme.colors.onBackground)
Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openUriCatching(conditionsLink) })
Text(conditionsLink, color = MaterialTheme.colors.primary, modifier = Modifier.clickable { uriHandler.openExternalLink(conditionsLink) })
}
}
@@ -821,13 +821,13 @@ fun ConditionsLinkButton() {
val commit = chatModel.conditions.value.currentConditions.conditionsCommit
ItemAction(stringResource(MR.strings.operator_open_conditions), painterResource(MR.images.ic_draft), onClick = {
val mdUrl = "https://github.com/simplex-chat/simplex-chat/blob/$commit/PRIVACY.md"
uriHandler.openUriCatching(mdUrl)
showMenu.value = false
uriHandler.openExternalLink(mdUrl)
})
ItemAction(stringResource(MR.strings.operator_open_changes), painterResource(MR.images.ic_more_horiz), onClick = {
val commitUrl = "https://github.com/simplex-chat/simplex-chat/commit/$commit"
uriHandler.openUriCatching(commitUrl)
showMenu.value = false
uriHandler.openExternalLink(commitUrl)
})
}
IconButton({ showMenu.value = true }) {
@@ -838,11 +838,7 @@ fun ConditionsLinkButton() {
private fun internalUriHandler(parentUriHandler: UriHandler): UriHandler = object: UriHandler {
override fun openUri(uri: String) {
if (uri.startsWith("https://simplex.chat/contact#")) {
openVerifiedSimplexUri(uri)
} else {
parentUriHandler.openUriCatching(uri)
}
parentUriHandler.openExternalLink(uri)
}
}

View File

@@ -335,7 +335,7 @@ private fun HowToButton() {
SettingsActionItem(
painterResource(MR.images.ic_open_in_new),
stringResource(MR.strings.how_to_use_your_servers),
{ uriHandler.openUriCatching("https://simplex.chat/docs/server.html") },
{ uriHandler.openExternalLink("https://simplex.chat/docs/server.html") },
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary
)

View File

@@ -1376,6 +1376,7 @@
<string name="open_simplex_chat_to_accept_call">Open SimpleX Chat to accept call</string>
<string name="allow_accepting_calls_from_lock_screen">Enable calls from lock screen via Settings.</string>
<string name="open_verb">Open</string>
<string name="open_external_link_title">Open external link?</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string>