Files
simplex-chat/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift
T
Evgeny @ SimpleX Chat 17c9ad1c61 ios, android, desktop: previews render the same in every snapshot run (#7938)
* ios, android, desktop: previews draw fixed sample data instead of calling the core

* desktop: database error preview passes the keychain setting

* ios: snapshot fixtures use fixed parsed markdown instead of the core

* ios: snapshot fixtures: raw string delimiters past the # in markdown JSON

* desktop: snapshot test sets the app bar height as the app does

* docs: plan to replace preview-only parameters with test measures

* ios, desktop: previews run master's code; badges and version views split

Preview-only parameters are removed. BadgesCheckOrderView, BadgesHowLongView and VersionView are split into a loader and a layout, which their previews draw. Snapshot tests set a fixed QR picture: the environment value fixedQRCode on iOS, LocalFixedQRCode on desktop.

* ios: snapshot fixtures build parsed markdown with initializers

The table held the JSON of the Linux core, which the iOS app does not decode. The values are those the core returns for each text.

* ci: references of fixed QR codes and version; ios: call overlay preview as on master

iOS references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40944118370, desktop references from https://github.com/evgeny-simplex/simplex-chat/actions/runs/40932566864. ActiveCallOverlay_Previews, its scene and its references are restored as on master.

* ios, desktop: snapshot tests render every preview, no exclusion lists

BadgesChooseBadgeView, ServersSummaryView and TerminalView are split into a loader and a layout. AddChannelView, AddGroupView, GroupProfileView and UserProfileView layouts take the name and description validators, so desktop previews render without the core library. NewChatView's preview opens the Connect tab, UserProfilesView's preview has its own users. AppearanceSettings, MigrateFromDevice and the duplicate UserProfileLayoutEditOn previews are deleted.

* ios, desktop: previews show short links; tests draw the fixed QR code with each app's generator

The snapshot tests draw the fixed QR picture for a short address link with generateImage on iOS and qrCodeBitmap on desktop, in place of committed images. Previews of group links, addresses, a pending connection and QR codes pass short links, as the apps show them. The fixture markdown table equals the output of master's parser.

* ios, desktop: excluded previews and their views as on master

* ci: references of short-link QR codes
2026-10-30 06:41:40 +00:00

258 lines
10 KiB
Swift

//
// GroupLinkView.swift
// SimpleX (iOS)
//
// Created by JRoberts on 15.10.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
struct GroupLinkView: View {
@EnvironmentObject var theme: AppTheme
var groupId: Int64
@Binding var groupLink: GroupLink?
@Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false
var creatingGroup: Bool = false
var isChannel: Bool = false
var groupInfo: GroupInfo? = nil
var composeState: Binding<ComposeState>? = nil
var linkCreatedCb: (() -> Void)? = nil
@State private var showSharePicker = false
@State private var showShortLink = true
@State private var creatingLink = false
@State private var alert: GroupLinkAlert?
@State private var shouldCreate = true
private enum GroupLinkAlert: Identifiable {
case deleteLink
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
var id: String {
switch self {
case .deleteLink: return "deleteLink"
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
ZStack {
if creatingGroup {
groupLinkView()
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button ("Continue") { linkCreatedCb?() }
}
}
} else {
groupLinkView()
}
if creatingLink {
ProgressView()
.scaleEffect(2)
.frame(maxWidth: .infinity)
}
}
}
private func groupLinkView() -> some View {
List {
Group {
if showTitle {
Text(isChannel ? "Channel link" : "Group link")
.font(.largeTitle)
.bold()
.fixedSize(horizontal: false, vertical: true)
}
if isChannel {
Text("You can share a link or a QR code - anybody will be able to join the channel.")
} else {
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
}
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
Section {
if let groupLink = groupLink {
if !isChannel {
Picker("Initial role", selection: $groupLinkMemberRole) {
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
Text(role.text(isChannel: isChannel))
}
}
.frame(height: 36)
}
SimpleXCreatedLinkQRCode(link: groupLink.connLinkContact, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink.connLinkContact.simplexChatUri(short: showShortLink))")
if !isChannel && groupLink.shouldBeUpgraded {
Button {
upgradeAndShareLinkAlert()
} label: {
Label("Upgrade link", systemImage: "arrow.up")
}
}
Button {
if !isChannel && groupLink.shouldBeUpgraded {
upgradeAndShareLinkAlert(groupLink: groupLink)
} else {
groupLink.shareAddress(short: showShortLink)
}
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
if groupInfo?.groupProfile.publicGroup != nil {
Button { showSharePicker = true } label: {
Label("Share via chat", systemImage: "arrowshape.turn.up.forward")
}
}
if !creatingGroup && !isChannel {
Button(role: .destructive) { alert = .deleteLink } label: {
Label("Delete link", systemImage: "trash")
}
}
} else {
Button(action: createGroupLink) {
Label("Create link", systemImage: "link.badge.plus")
}
.disabled(creatingLink)
}
} header: {
if !isChannel, let groupLink, groupLink.connLinkContact.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink.connLinkContact, short: $showShortLink)
}
}
.alert(item: $alert) { alert in
switch alert {
case .deleteLink:
return Alert(
title: Text("Delete link?"),
message: Text("All group members will remain connected."),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
try await apiDeleteGroupLink(groupId)
await MainActor.run { groupLink = nil }
} catch let error {
logger.error("GroupLinkView apiDeleteGroupLink: \(responseError(error))")
}
}
}, secondaryButton: .cancel()
)
case let .error(title, error):
return mkAlert(title: title, message: error)
}
}
.onChange(of: groupLinkMemberRole) { _ in
Task {
do {
groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
} catch let error {
await MainActor.run {
showErrorAlert(error, NSLocalizedString("Error updating group link", comment: ""))
}
}
}
}
.onAppear {
if groupLink == nil && !creatingLink && shouldCreate {
createGroupLink()
}
shouldCreate = false
}
}
.modifier(ThemedBackground(grouped: true))
.sheet(isPresented: $showSharePicker) {
if let gInfo = groupInfo {
shareChannelPicker(groupInfo: gInfo, composeState: composeState)
}
}
}
private func createGroupLink() {
Task {
do {
creatingLink = true
let gLink = try await apiCreateGroupLink(groupId)
await MainActor.run {
creatingLink = false
groupLink = gLink
}
} catch let error {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
await MainActor.run {
creatingLink = false
showErrorAlert(error, NSLocalizedString("Error creating group link", comment: ""))
}
}
}
}
private func upgradeAndShareLinkAlert(groupLink: GroupLink? = nil) {
showAlert(
NSLocalizedString("Upgrade group link?", comment: "alert message"),
message: NSLocalizedString("The link will be short, and group profile will be shared via the link.", comment: "alert message"),
actions: {
var actions = [UIAlertAction(title: NSLocalizedString("Upgrade", comment: "alert button"), style: .default) { _ in
addShortLink(shareOnCompletion: groupLink != nil)
}]
if let groupLink {
actions.append(UIAlertAction(title: NSLocalizedString("Share old link", comment: "alert button"), style: .default) { _ in
groupLink.shareAddress(short: showShortLink)
})
}
actions.append(cancelAlertAction)
return actions
}
)
}
private func addShortLink(shareOnCompletion: Bool = false) {
Task {
do {
creatingLink = true
let gLink = try await apiAddGroupShortLink(groupId)
await MainActor.run {
creatingLink = false
groupLink = gLink
if shareOnCompletion, let gLink {
gLink.shareAddress(short: showShortLink)
}
}
} catch let error {
logger.error("apiAddGroupShortLink: \(responseError(error))")
await MainActor.run {
creatingLink = false
showErrorAlert(error, NSLocalizedString("Error adding short link", comment: ""))
}
}
}
}
}
struct GroupLinkView_Previews: PreviewProvider {
static var previews: some View {
@State var groupLink: GroupLink? = GroupLink(
userContactLinkId: 1,
connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: "https://smp6.simplex.im/g#cL3iklYlt_Tws1B_1aGylbT_3HowPMQhuCFoBiCjZ1M"),
shortLinkDataSet: true,
shortLinkLargeDataSet: true,
groupLinkId: "abc",
acceptMemberRole: .member
)
@State var noGroupLink: GroupLink? = nil
return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
GroupLinkView(groupId: 1, groupLink: $noGroupLink, groupLinkMemberRole: Binding.constant(.member))
}
}
}