mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00

* ios: new user picker (#4770) * current user picker progress * one hand picker * thin bullet icon * more user picker buttons * button clickable areas * divider padding * extra space after sun * send current user option to address view * add unread count badge * with anim for apperance close * edit current profile from picker * remove you section from settings * remove help and support * simplify * move settings and sun to same row * remove redundant vstack * long press on sun/moon switches to system setting * remove back button from migrate device * smooth profile transitions * close user picker on list profiles * fix dismiss on migrate from device * fix dismiss when deleting last visible user while having hidden users * picker visibility toggle tweaks * remove strange square from profile switcher click * dirty way to save auto accept settings on dismiss * Revert "dirty way to save auto accept settings on dismiss" This reverts commite7b19ee8aa
. * consistent animation on user picker toggle * change space between profiles * remove result * ignore result * unread badge * move to sheet * half sheet on one hand ui * fix dismiss on device migration * fix desktop connect * sun to meet other action icons * fill bullet list button * fix tap in settings to take full width * icon sizings and paddings * open settings in same sheet * apply same trick as other buttons for ligth toggle * layout * open profiles sheet large when +3 users * layout * layout * paddings * paddings * remove show progress * always small user picker * fixed height * open all actions as sheets * type, color * simpler and more effective way of avoid moving around on user select * dismiss user profiles sheet on user change * connect desktop back button remove * remove back buttons from user address view * remove porgress * header inside list * alert on auto accept unsaved changes * Cancel -> Discard * revert * fix connect to desktop * remove extra space * fix share inside multi sheet * user picker and options as separate sheet * revert showShareSheet * fix current profile and all profiles selection * change alert * update * cleanup user address * remove func * alert on unsaved changes in chat prefs * fix layout * cleanup --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com> * ios: fix switching profiles (#4822) * ios: different user picker layout (#4826) * ios: different user picker layout * remove section * layout, color * color * remove activeUser * fix gradient * recursive sheets * gradient padding * share sheet * layout * dismiss sheets --------- Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com> * ios: use the same way to share from all sheets (#4829) * ios: close user picker before opening other sheets * Revert "share sheet" This reverts commit0064155825
. * dismiss/show via callback * Revert "ios: close user picker before opening other sheets" This reverts commit19110398f8
. * ios: show alerts from sheets (#4839) * padding * remove gradient * cleanup * simplify settings * padding --------- Co-authored-by: Diogo <diogofncunha@gmail.com> Co-authored-by: Levitating Pineapple <noreply@levitatingpineapple.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
450 lines
16 KiB
Swift
450 lines
16 KiB
Swift
//
|
|
// UserAddressView.swift
|
|
// SimpleX (iOS)
|
|
//
|
|
// Created by spaced4ndy on 26.04.2023.
|
|
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import MessageUI
|
|
import SimpleXChat
|
|
|
|
struct UserAddressView: View {
|
|
@Environment(\.dismiss) var dismiss: DismissAction
|
|
@EnvironmentObject private var chatModel: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@State var shareViaProfile = false
|
|
@State private var aas = AutoAcceptState()
|
|
@State private var savedAAS = AutoAcceptState()
|
|
@State private var ignoreShareViaProfileChange = false
|
|
@State private var showMailView = false
|
|
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
|
|
@State private var alert: UserAddressAlert?
|
|
@State private var progressIndicator = false
|
|
@FocusState private var keyboardVisible: Bool
|
|
|
|
private enum UserAddressAlert: Identifiable {
|
|
case deleteAddress
|
|
case profileAddress(on: Bool)
|
|
case shareOnCreate
|
|
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
|
|
|
var id: String {
|
|
switch self {
|
|
case .deleteAddress: return "deleteAddress"
|
|
case let .profileAddress(on): return "profileAddress \(on)"
|
|
case .shareOnCreate: return "shareOnCreate"
|
|
case let .error(title, _): return "error \(title)"
|
|
}
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
userAddressScrollView()
|
|
.onDisappear {
|
|
if savedAAS != aas {
|
|
showAlert(
|
|
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
|
|
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
|
|
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
|
buttonAction: saveAAS,
|
|
cancelButton: true
|
|
)
|
|
}
|
|
}
|
|
|
|
if progressIndicator {
|
|
ZStack {
|
|
if chatModel.userAddress != nil {
|
|
Circle()
|
|
.fill(.white)
|
|
.opacity(0.7)
|
|
.frame(width: 56, height: 56)
|
|
}
|
|
ProgressView().scaleEffect(2)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Namespace private var bottomID
|
|
|
|
private func userAddressScrollView() -> some View {
|
|
ScrollViewReader { proxy in
|
|
userAddressView()
|
|
.onChange(of: keyboardVisible) { _ in
|
|
if keyboardVisible {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
withAnimation {
|
|
proxy.scrollTo(bottomID, anchor: .top)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func userAddressView() -> some View {
|
|
List {
|
|
if let userAddress = chatModel.userAddress {
|
|
existingAddressView(userAddress)
|
|
.onAppear {
|
|
aas = AutoAcceptState(userAddress: userAddress)
|
|
savedAAS = aas
|
|
}
|
|
.onChange(of: aas.enable) { _ in
|
|
if !aas.enable { aas = AutoAcceptState() }
|
|
}
|
|
} else {
|
|
Section {
|
|
createAddressButton()
|
|
} footer: {
|
|
Text("Create an address to let people connect with you.")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
Section {
|
|
learnMoreButton()
|
|
}
|
|
}
|
|
}
|
|
.alert(item: $alert) { alert in
|
|
switch alert {
|
|
case .deleteAddress:
|
|
return Alert(
|
|
title: Text("Delete address?"),
|
|
message:
|
|
shareViaProfile
|
|
? Text("All your contacts will remain connected. Profile update will be sent to your contacts.")
|
|
: Text("All your contacts will remain connected."),
|
|
primaryButton: .destructive(Text("Delete")) {
|
|
progressIndicator = true
|
|
Task {
|
|
do {
|
|
if let u = try await apiDeleteUserAddress() {
|
|
DispatchQueue.main.async {
|
|
chatModel.userAddress = nil
|
|
chatModel.updateUser(u)
|
|
if shareViaProfile {
|
|
ignoreShareViaProfileChange = true
|
|
shareViaProfile = false
|
|
}
|
|
}
|
|
}
|
|
await MainActor.run { progressIndicator = false }
|
|
} catch let error {
|
|
logger.error("UserAddressView apiDeleteUserAddress: \(responseError(error))")
|
|
await MainActor.run { progressIndicator = false }
|
|
}
|
|
}
|
|
}, secondaryButton: .cancel()
|
|
)
|
|
case let .profileAddress(on):
|
|
if on {
|
|
return Alert(
|
|
title: Text("Share address with contacts?"),
|
|
message: Text("Profile update will be sent to your contacts."),
|
|
primaryButton: .default(Text("Share")) {
|
|
setProfileAddress(on)
|
|
}, secondaryButton: .cancel() {
|
|
ignoreShareViaProfileChange = true
|
|
shareViaProfile = !on
|
|
}
|
|
)
|
|
} else {
|
|
return Alert(
|
|
title: Text("Stop sharing address?"),
|
|
message: Text("Profile update will be sent to your contacts."),
|
|
primaryButton: .default(Text("Stop sharing")) {
|
|
setProfileAddress(on)
|
|
}, secondaryButton: .cancel() {
|
|
ignoreShareViaProfileChange = true
|
|
shareViaProfile = !on
|
|
}
|
|
)
|
|
}
|
|
case .shareOnCreate:
|
|
return Alert(
|
|
title: Text("Share address with contacts?"),
|
|
message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."),
|
|
primaryButton: .default(Text("Share")) {
|
|
setProfileAddress(true)
|
|
ignoreShareViaProfileChange = true
|
|
shareViaProfile = true
|
|
}, secondaryButton: .cancel()
|
|
)
|
|
case let .error(title, error):
|
|
return mkAlert(title: title, message: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
|
|
Section {
|
|
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
|
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
|
|
shareQRCodeButton(userAddress)
|
|
if MFMailComposeViewController.canSendMail() {
|
|
shareViaEmailButton(userAddress)
|
|
}
|
|
shareWithContactsButton()
|
|
autoAcceptToggle()
|
|
learnMoreButton()
|
|
} header: {
|
|
Text("Address")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
|
|
if aas.enable {
|
|
autoAcceptSection()
|
|
}
|
|
|
|
Section {
|
|
deleteAddressButton()
|
|
} footer: {
|
|
Text("Your contacts will remain connected.")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
.id(bottomID)
|
|
}
|
|
|
|
private func createAddressButton() -> some View {
|
|
Button {
|
|
progressIndicator = true
|
|
Task {
|
|
do {
|
|
let connReqContact = try await apiCreateUserAddress()
|
|
DispatchQueue.main.async {
|
|
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
|
|
alert = .shareOnCreate
|
|
progressIndicator = false
|
|
}
|
|
} catch let error {
|
|
logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))")
|
|
let a = getErrorAlert(error, "Error creating address")
|
|
alert = .error(title: a.title, error: a.message)
|
|
await MainActor.run { progressIndicator = false }
|
|
}
|
|
}
|
|
} label: {
|
|
Label("Create public address", systemImage: "qrcode")
|
|
}
|
|
}
|
|
|
|
private func deleteAddressButton() -> some View {
|
|
Button(role: .destructive) {
|
|
alert = .deleteAddress
|
|
} label: {
|
|
Label("Delete address", systemImage: "trash")
|
|
.foregroundColor(Color.red)
|
|
}
|
|
}
|
|
|
|
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
|
|
Button {
|
|
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)])
|
|
} label: {
|
|
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
|
Text("Share address")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func shareViaEmailButton(_ userAddress: UserContactLink) -> some View {
|
|
Button {
|
|
showMailView = true
|
|
} label: {
|
|
settingsRow("envelope", color: theme.colors.secondary) {
|
|
Text("Invite friends")
|
|
}
|
|
}
|
|
.sheet(isPresented: $showMailView) {
|
|
SendAddressMailView(
|
|
showMailView: $showMailView,
|
|
mailViewResult: $mailViewResult,
|
|
userAddress: userAddress
|
|
)
|
|
.edgesIgnoringSafeArea(.bottom)
|
|
}
|
|
.onChange(of: mailViewResult == nil) { _ in
|
|
if let r = mailViewResult {
|
|
switch r {
|
|
case .success: ()
|
|
case let .failure(error):
|
|
logger.error("UserAddressView share via email: \(responseError(error))")
|
|
let a = getErrorAlert(error, "Error sending email")
|
|
alert = .error(title: a.title, error: a.message)
|
|
}
|
|
mailViewResult = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private func autoAcceptToggle() -> some View {
|
|
settingsRow("checkmark", color: theme.colors.secondary) {
|
|
Toggle("Auto-accept", isOn: $aas.enable)
|
|
.onChange(of: aas.enable) { _ in
|
|
saveAAS()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func learnMoreButton() -> some View {
|
|
NavigationLink {
|
|
UserAddressLearnMore()
|
|
.navigationTitle("SimpleX address")
|
|
.modifier(ThemedBackground(grouped: true))
|
|
.navigationBarTitleDisplayMode(.large)
|
|
} label: {
|
|
settingsRow("info.circle", color: theme.colors.secondary) {
|
|
Text("About SimpleX address")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func shareWithContactsButton() -> some View {
|
|
settingsRow("person", color: theme.colors.secondary) {
|
|
Toggle("Share with contacts", isOn: $shareViaProfile)
|
|
.onChange(of: shareViaProfile) { on in
|
|
if ignoreShareViaProfileChange {
|
|
ignoreShareViaProfileChange = false
|
|
} else {
|
|
alert = .profileAddress(on: on)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setProfileAddress(_ on: Bool) {
|
|
progressIndicator = true
|
|
Task {
|
|
do {
|
|
if let u = try await apiSetProfileAddress(on: on) {
|
|
DispatchQueue.main.async {
|
|
chatModel.updateUser(u)
|
|
}
|
|
}
|
|
await MainActor.run { progressIndicator = false }
|
|
} catch let error {
|
|
logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))")
|
|
await MainActor.run { progressIndicator = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct AutoAcceptState: Equatable {
|
|
var enable = false
|
|
var incognito = false
|
|
var welcomeText = ""
|
|
|
|
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") {
|
|
self.enable = enable
|
|
self.incognito = incognito
|
|
self.welcomeText = welcomeText
|
|
}
|
|
|
|
init(userAddress: UserContactLink) {
|
|
if let aa = userAddress.autoAccept {
|
|
enable = true
|
|
incognito = aa.acceptIncognito
|
|
if let msg = aa.autoReply {
|
|
welcomeText = msg.text
|
|
} else {
|
|
welcomeText = ""
|
|
}
|
|
} else {
|
|
enable = false
|
|
incognito = false
|
|
welcomeText = ""
|
|
}
|
|
}
|
|
|
|
var autoAccept: AutoAccept? {
|
|
if enable {
|
|
var autoReply: MsgContent? = nil
|
|
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if s != "" { autoReply = .text(s) }
|
|
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@ViewBuilder private func autoAcceptSection() -> some View {
|
|
Section {
|
|
acceptIncognitoToggle()
|
|
welcomeMessageEditor()
|
|
saveAASButton()
|
|
.disabled(aas == savedAAS)
|
|
} header: {
|
|
Text("Auto-accept")
|
|
.foregroundColor(theme.colors.secondary)
|
|
}
|
|
}
|
|
|
|
private func acceptIncognitoToggle() -> some View {
|
|
settingsRow(
|
|
aas.incognito ? "theatermasks.fill" : "theatermasks",
|
|
color: aas.incognito ? .indigo : theme.colors.secondary
|
|
) {
|
|
Toggle("Accept incognito", isOn: $aas.incognito)
|
|
}
|
|
}
|
|
|
|
private func welcomeMessageEditor() -> some View {
|
|
ZStack {
|
|
Group {
|
|
if aas.welcomeText.isEmpty {
|
|
TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder")))
|
|
.foregroundColor(theme.colors.secondary)
|
|
.disabled(true)
|
|
}
|
|
TextEditor(text: $aas.welcomeText)
|
|
.focused($keyboardVisible)
|
|
}
|
|
.padding(.horizontal, -5)
|
|
.padding(.top, -8)
|
|
.frame(height: 90, alignment: .topLeading)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
private func saveAASButton() -> some View {
|
|
Button {
|
|
keyboardVisible = false
|
|
saveAAS()
|
|
} label: {
|
|
Text("Save")
|
|
}
|
|
}
|
|
|
|
private func saveAAS() {
|
|
Task {
|
|
do {
|
|
if let address = try await userAddressAutoAccept(aas.autoAccept) {
|
|
chatModel.userAddress = address
|
|
savedAAS = aas
|
|
}
|
|
} catch let error {
|
|
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UserAddressView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let chatModel = ChatModel()
|
|
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
|
|
|
|
|
|
return Group {
|
|
UserAddressView()
|
|
.environmentObject(chatModel)
|
|
UserAddressView()
|
|
.environmentObject(ChatModel())
|
|
}
|
|
}
|
|
}
|