Files
simplex-chat/apps/ios/Shared/Views/Chat/Group/ChannelWebAccessView.swift
T
shGitHubEvgeny PoberezkinEvgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
9bd38c4aec android, desktop, ios: connect via SimpleX name (#7068)
* android, desktop, ios: connect via SimpleX name

* android, desktop, ios: open known contact on name lookup; surface prepared contact

Name search opens the contact (not list-filter); resolved/prepared contacts and groups are added to the chat list so they're visible and openable. Kotlin compile-verified; iOS edits pattern-matched, pending Xcode build.

* feat(names): UI names role + agent NAME error

Parity with the core names rework (#7045):

- Add `names` to ServerRoles (Android + iOS) and a per-operator
  "To resolve names" toggle under the SMP section (xftp has no names
  role; the shared ServerRoles field stays false there).
- Mirror the new agent error: NameErrorType + a NAME case on both
  AgentErrorType and ProtocolErrorType (the SMP ErrorType mirror), so
  the new SMP/agent NAME errors decode instead of crashing the decoder.
- Remove ChatErrorType.SimplexNameResolverUnavailable (deleted in core)
  and repoint its "name resolution unavailable" alert to the agent
  NAME NO_SERVERS error, reusing the existing strings.

Android (multiplatform) compiles clean; iOS mirrors the same changes
(builds in Xcode).

* feat(names): UI warning when no server resolves names

Mirror core USWNoNamesServers: add the NoNamesServers variant to
UserServersWarning (Kotlin sealed class + Swift enum) and its
globalWarning / globalServersWarning branch, rendered by the existing
ServersWarningFooter / ServersWarningView. Matches the noChatRelays
warning exactly.

* fix(servers): show all validation errors and warnings, not just the first

globalServersError/Warning returned only the first entry, so a second
warning (e.g. no names servers behind no chat relays) or a second error
(e.g. no XFTP servers behind no SMP servers) was never displayed. Make
them return all entries (globalServersErrors/Warnings) and render one
footer row each, across the three combined-footer views. Per-protocol
SMP/XFTP footers are unchanged.

* docs(names): add SimpleX name UI plan

* feat(names): add name model fields + SimplexName helpers

* feat(names): verify + set-name API & responses

* docs(names): bump core sync to 5008b4e62

* feat(names): show name + verification on chat info

* feat(names): add Verify SimpleX names privacy toggle

* feat(names): add set-name screens (user + channel)

* update ui

* fix kotlin

* fix codable

* fix ios

* fix errors

* api in UI

* send name as string in protocol

* update simplexmq, capitalize

* verify that name is in profile for own and known contacts and channels as condition of name resolution

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
2026-06-30 11:26:04 +01:00

170 lines
6.3 KiB
Swift

//
// ChannelWebAccessView.swift
// SimpleX (iOS)
//
// Created by simplex.chat on 31/05/2026.
// Copyright © 2026 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ChannelWebAccessView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var groupInfo: GroupInfo
@State private var webPage: String
@State private var allowEmbedding: Bool
@State private var saving = false
@State private var groupRelays: [GroupRelay] = []
init(groupInfo: Binding<GroupInfo>) {
_groupInfo = groupInfo
let access = groupInfo.wrappedValue.groupProfile.publicGroup?.publicGroupAccess
_webPage = State(initialValue: access?.groupWebPage ?? "")
_allowEmbedding = State(initialValue: access?.allowEmbedding ?? false)
}
var body: some View {
List {
if let code = embedCode {
webpageInfo("Create a webpage to show your channel preview to visitors before they subscribe. Host it yourself or use any static hosting.")
Section {
ScrollView {
Text(code)
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
}
.frame(maxHeight: 88)
Button {
UIPasteboard.general.string = code
} label: {
Label("Copy code", systemImage: "doc.on.doc")
}
} header: {
Text("Webpage code")
} footer: {
Text("Add this code to your webpage. It will display the preview of your channel / group.")
}
} else {
webpageInfo("Used chat relays do not support webpages.")
}
Section {
TextField("https://", text: $webPage)
.keyboardType(.URL)
.autocapitalization(.none)
.disableAutocorrection(true)
} header: {
Text("Enter webpage URL")
} footer: {
Text("It will be shown to subscribers and used to allow loading the preview.")
}
Section {
Toggle("Allow anyone to embed", isOn: $allowEmbedding)
} footer: {
Text(allowEmbedding ? "Any webpage can show the preview." : "Only your page above can show the preview.")
}
Section {
Button {
saveAccess()
} label: {
HStack {
Text(groupInfo.isChannel ? "Save and notify subscribers" : "Save and notify members")
if saving { Spacer(); ProgressView() }
}
}
.disabled(!hasChanges || saving)
}
}
.modifier(ThemedBackground(grouped: true))
.onAppear {
Task {
let relays = await apiGetGroupRelays(groupInfo.groupId)
await MainActor.run { groupRelays = relays }
}
}
.onDisappear {
if hasChanges {
showAlert(
title: NSLocalizedString("Save webpage settings?", comment: "alert title"),
message: NSLocalizedString("Webpage settings were changed. If you save, the updated settings will be sent to subscribers.", comment: "alert message"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: saveAccess,
cancelButton: true
)
}
}
}
private func webpageInfo(_ text: LocalizedStringKey) -> some View {
Section {
Text(text).foregroundColor(theme.colors.secondary)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 0, trailing: 16))
}
private var hasChanges: Bool {
let access = groupInfo.groupProfile.publicGroup?.publicGroupAccess
let currentWebPage = access?.groupWebPage ?? ""
let currentEmbedding = access?.allowEmbedding ?? false
return webPage != currentWebPage || allowEmbedding != currentEmbedding
}
private var relayDomains: [String] {
groupRelays.compactMap { $0.relayCap.webDomain }
}
private var embedCode: String? {
if let pg = groupInfo.groupProfile.publicGroup,
!relayDomains.isEmpty {
"""
<div data-simplex-channel-preview
data-channel-link="\(pg.groupLink)"
data-channel-id="\(pg.publicGroupId)"
data-relay-domains="\(relayDomains.joined(separator: ","))"
data-app-download-buttons="on"
data-color-scheme="light"
></div>
<script src="https://simplex.chat/js/channel-preview.js"></script>
"""
} else {
nil
}
}
private func saveAccess() {
saving = true
Task {
do {
var gp = groupInfo.groupProfile
if var pg = gp.publicGroup {
let trimmedPage = webPage.trimmingCharacters(in: .whitespacesAndNewlines)
let existingAccess = pg.publicGroupAccess
pg.publicGroupAccess = PublicGroupAccess(
groupWebPage: trimmedPage.isEmpty ? nil : trimmedPage,
simplexName: existingAccess?.simplexName,
domainWebPage: existingAccess?.domainWebPage ?? false,
allowEmbedding: allowEmbedding
)
gp.publicGroup = pg
}
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
await MainActor.run {
groupInfo = gInfo
ChatModel.shared.updateGroup(gInfo)
saving = false
}
} catch {
logger.error("ChannelWebAccessView apiUpdateGroup error: \(responseError(error))")
await MainActor.run { saving = false }
}
}
}
}