simplex-chat/apps/ios/Shared/Views/UserSettings/UserAddressView.swift
Evgeny fb4475027d
ios: new user picker (#4821)
* 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 commit e7b19ee8aa.

* 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 commit 0064155825.

* dismiss/show via callback

* Revert "ios: close user picker before opening other sheets"

This reverts commit 19110398f8.

* 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>
2024-09-10 09:31:53 +01:00

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())
}
}
}