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>
This commit is contained in:
Evgeny 2024-09-10 09:31:53 +01:00 committed by GitHub
parent 388609563d
commit fb4475027d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 335 additions and 286 deletions

1
.gitignore vendored
View file

@ -61,6 +61,7 @@ website/package/generated*
# Ignore build tool output, e.g. code coverage # Ignore build tool output, e.g. code coverage
website/.nyc_output/ website/.nyc_output/
website/coverage/ website/coverage/
result
# Ignore API documentation # Ignore API documentation
website/api-docs/ website/api-docs/

View file

@ -38,6 +38,7 @@ struct IncomingCallView: View {
} }
HStack { HStack {
ProfilePreview(profileOf: invitation.contact, color: .white) ProfilePreview(profileOf: invitation.contact, color: .white)
.padding(.vertical, 6)
Spacer() Spacer()
callButton("Reject", "phone.down.fill", .red) { callButton("Reject", "phone.down.fill", .red) {

View file

@ -9,6 +9,17 @@
import SwiftUI import SwiftUI
import SimpleXChat import SimpleXChat
enum UserPickerSheet: Identifiable {
case address
case chatPreferences
case chatProfiles
case currentProfile
case useFromDesktop
case settings
var id: Self { self }
}
struct ChatListView: View { struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@ -18,9 +29,9 @@ struct ChatListView: View {
@State private var searchText = "" @State private var searchText = ""
@State private var searchShowingSimplexLink = false @State private var searchShowingSimplexLink = false
@State private var searchChatFilteredBySimplexLink: String? = nil @State private var searchChatFilteredBySimplexLink: String? = nil
@State private var userPickerVisible = false
@State private var showConnectDesktop = false
@State private var scrollToSearchBar = false @State private var scrollToSearchBar = false
@State private var activeUserPickerSheet: UserPickerSheet? = nil
@State private var userPickerShown: Bool = false
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
@ -46,21 +57,44 @@ struct ChatListView: View {
), ),
destination: chatView destination: chatView
) { chatListView } ) { chatListView }
if userPickerVisible { }
Rectangle().fill(.white.opacity(0.001)).onTapGesture { .sheet(isPresented: $userPickerShown) {
withAnimation { UserPicker(activeSheet: $activeUserPickerSheet)
userPickerVisible.toggle() .sheet(item: $activeUserPickerSheet) { sheet in
if let currentUser = chatModel.currentUser {
switch sheet {
case .address:
NavigationView {
UserAddressView(shareViaProfile: currentUser.addressShared)
.navigationTitle("Public address")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
case .chatProfiles:
NavigationView {
UserProfilesView()
}
case .currentProfile:
NavigationView {
UserProfile()
.navigationTitle("Your current profile")
.modifier(ThemedBackground())
}
case .chatPreferences:
NavigationView {
PreferencesView(profile: currentUser.profile, preferences: currentUser.fullPreferences, currentPreferences: currentUser.fullPreferences)
.navigationTitle("Your preferences")
.navigationBarTitleDisplayMode(.large)
.modifier(ThemedBackground(grouped: true))
}
case .useFromDesktop:
ConnectDesktopView(viaSettings: false)
case .settings:
SettingsView(showSettings: $showSettings)
.navigationBarTitleDisplayMode(.large)
}
} }
} }
}
UserPicker(
showSettings: $showSettings,
showConnectDesktop: $showConnectDesktop,
userPickerVisible: $userPickerVisible
)
}
.sheet(isPresented: $showConnectDesktop) {
ConnectDesktopView()
} }
} }
@ -73,7 +107,7 @@ struct ChatListView: View {
.navigationBarHidden(searchMode || oneHandUI) .navigationBarHidden(searchMode || oneHandUI)
} }
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
.onDisappear() { withAnimation { userPickerVisible = false } } .onDisappear() { activeUserPickerSheet = nil }
.refreshable { .refreshable {
AlertManager.shared.showAlert(Alert( AlertManager.shared.showAlert(Alert(
title: Text("Reconnect servers?"), title: Text("Reconnect servers?"),
@ -164,7 +198,7 @@ struct ChatListView: View {
let user = chatModel.currentUser ?? User.sampleData let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel))
.padding(.trailing, 4) .padding([.top, .trailing], 3)
let allRead = chatModel.users let allRead = chatModel.users
.filter { u in !u.user.activeUser && !u.user.hidden } .filter { u in !u.user.activeUser && !u.user.hidden }
.allSatisfy { u in u.unreadCount == 0 } .allSatisfy { u in u.unreadCount == 0 }
@ -173,13 +207,7 @@ struct ChatListView: View {
} }
} }
.onTapGesture { .onTapGesture {
if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { userPickerShown = true
withAnimation {
userPickerVisible.toggle()
}
} else {
showSettings = true
}
} }
} }
@ -269,7 +297,7 @@ struct ChatListView: View {
} }
} }
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { private func unreadBadge(size: CGFloat = 18) -> some View {
Circle() Circle()
.frame(width: size, height: size) .frame(width: size, height: size)
.foregroundColor(theme.colors.primary) .foregroundColor(theme.colors.primary)

View file

@ -8,179 +8,228 @@ import SimpleXChat
struct UserPicker: View { struct UserPicker: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.scenePhase) var scenePhase
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Binding var showSettings: Bool @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
@Binding var showConnectDesktop: Bool @Environment(\.scenePhase) private var scenePhase: ScenePhase
@Binding var userPickerVisible: Bool @Environment(\.colorScheme) private var colorScheme: ColorScheme
@State var scrollViewContentSize: CGSize = .zero @Environment(\.dismiss) private var dismiss: DismissAction
@State var disableScrolling: Bool = true @Binding var activeSheet: UserPickerSheet?
private let menuButtonHeight: CGFloat = 68 @State private var switchingProfile = false
@State var chatViewNameWidth: CGFloat = 0
var body: some View { var body: some View {
VStack { if #available(iOS 16.0, *) {
Spacer().frame(height: 1) let v = viewBody.presentationDetents([.height(420)])
VStack(spacing: 0) { if #available(iOS 16.4, *) {
ScrollView { v.scrollBounceBehavior(.basedOnSize)
ScrollViewReader { sp in } else {
let users = m.users v
.filter({ u in u.user.activeUser || !u.user.hidden }) }
.sorted { u, _ in u.user.activeUser } } else {
VStack(spacing: 0) { viewBody
ForEach(users) { u in }
userView(u) }
Divider()
if u.user.activeUser { Divider() } private var viewBody: some View {
} let otherUsers = m.users.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId }
} return List {
.overlay { Section(header: Text("You").foregroundColor(theme.colors.secondary)) {
GeometryReader { geo -> Color in if let user = m.currentUser {
DispatchQueue.main.async { openSheetOnTap(label: {
scrollViewContentSize = geo.size ZStack {
let scenes = UIApplication.shared.connectedScenes let v = ProfilePreview(profileOf: user)
if let windowScene = scenes.first as? UIWindowScene { .foregroundColor(.primary)
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame .padding(.leading, -8)
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height if #available(iOS 16.0, *) {
} v
} } else {
return Color.clear v.padding(.vertical, 4)
}
}
.onChange(of: userPickerVisible) { visible in
if visible, let u = users.first {
sp.scrollTo(u.id)
} }
} }
}) {
activeSheet = .currentProfile
} }
}
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
menuButton("Use from desktop", icon: "desktopcomputer") { openSheetOnTap(title: m.userAddress == nil ? "Create public address" : "Your public address", icon: "qrcode") {
showConnectDesktop = true activeSheet = .address
withAnimation { }
userPickerVisible.toggle()
openSheetOnTap(title: "Chat preferences", icon: "switch.2") {
activeSheet = .chatPreferences
} }
} }
Divider() }
menuButton("Settings", icon: "gearshape") {
showSettings = true Section {
withAnimation { if otherUsers.isEmpty {
userPickerVisible.toggle() openSheetOnTap(title: "Your chat profiles", icon: "person.crop.rectangle.stack") {
activeSheet = .chatProfiles
}
} else {
let v = userPickerRow(otherUsers, size: 44)
.padding(.leading, -11)
if #available(iOS 16.0, *) {
v
} else {
v.padding(.vertical, 4)
}
}
openSheetOnTap(title: "Use from desktop", icon: "desktopcomputer") {
activeSheet = .useFromDesktop
}
ZStack(alignment: .trailing) {
openSheetOnTap(title: "Settings", icon: "gearshape") {
activeSheet = .settings
}
Label {} icon: {
Image(systemName: colorScheme == .light ? "sun.max" : "moon.fill")
.resizable()
.symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary)
.frame(maxWidth: 20, maxHeight: 20)
}
.onTapGesture {
if (colorScheme == .light) {
ThemeManager.applyTheme(systemDarkThemeDefault.get())
} else {
ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName)
}
}
.onLongPressGesture {
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
} }
} }
} }
} }
.clipShape(RoundedRectangle(cornerRadius: 16)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(
Rectangle()
.fill(theme.colors.surface)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
.padding(8)
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear { .onAppear {
// This check prevents the call of listUsers after the app is suspended, and the database is closed. // This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase { if case .active = scenePhase {
Task {
do {
let users = try await listUsersAsync()
await MainActor.run { m.users = users }
} catch {
logger.error("Error loading users \(responseError(error))")
}
}
}
}
}
private func userView(_ u: UserInfo) -> some View {
let user = u.user
return Button(action: {
if user.activeUser {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
} else {
Task { Task {
do { do {
try await changeActiveUserAsync_(user.userId, viewPwd: nil) let users = try await listUsersAsync()
await MainActor.run { userPickerVisible = false } await MainActor.run { m.users = users }
} catch { } catch {
await MainActor.run { logger.error("Error loading users \(responseError(error))")
AlertManager.shared.showAlertMsg(
title: "Error switching profile!",
message: "Error: \(responseError(error))"
)
}
} }
} }
} }
}, label: { }
HStack(spacing: 0) { .modifier(ThemedBackground(grouped: true))
ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill)) .disabled(switchingProfile)
.padding(.trailing, 12) }
Text(user.chatViewName)
.fontWeight(user.activeUser ? .medium : .regular) private func userPickerRow(_ users: [UserInfo], size: CGFloat) -> some View {
.foregroundColor(theme.colors.onBackground) HStack(spacing: 6) {
.overlay(DetermineWidth()) let s = ScrollView(.horizontal) {
Spacer() HStack(spacing: 27) {
if user.activeUser { ForEach(users) { u in
Image(systemName: "checkmark") if !u.user.hidden && u.user.userId != m.currentUser?.userId {
} else if u.unreadCount > 0 { userView(u, size: size)
unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary) }
} else if !user.showNtfs { }
Image(systemName: "speaker.slash") }
.padding(.leading, 4)
.padding(.trailing, 22)
}
ZStack(alignment: .trailing) {
if #available(iOS 16.0, *) {
s.scrollIndicators(.hidden)
} else {
s
}
LinearGradient(
colors: [.clear, .black],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: size, height: size + 3)
.blendMode(.destinationOut)
.allowsHitTesting(false)
}
.compositingGroup()
.padding(.top, -3) // to fit unread badge
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(theme.colors.secondary)
.padding(.trailing, 4)
.onTapGesture {
activeSheet = .chatProfiles
}
}
}
private func userView(_ u: UserInfo, size: CGFloat) -> some View {
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground))
.padding([.top, .trailing], 3)
if (u.unreadCount > 0) {
unreadBadge(u)
}
}
.frame(width: size)
.onTapGesture {
switchingProfile = true
Task {
do {
try await changeActiveUserAsync_(u.user.userId, viewPwd: nil)
await MainActor.run {
switchingProfile = false
dismiss()
}
} catch {
await MainActor.run {
switchingProfile = false
AlertManager.shared.showAlertMsg(
title: "Error switching profile!",
message: "Error: \(responseError(error))"
)
}
} }
} }
.padding(.trailing) }
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill)))
} }
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { private func openSheetOnTap(title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) { openSheetOnTap(label: {
HStack(spacing: 0) { ZStack(alignment: .leading) {
Text(title) Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
.symbolRenderingMode(.monochrome) .symbolRenderingMode(.monochrome)
.foregroundColor(theme.colors.secondary) .foregroundColor(theme.colors.secondary)
Text(title)
.foregroundColor(.primary)
.padding(.leading, 36)
} }
.padding(.horizontal) }, action: action)
.padding(.vertical, 22) }
.frame(height: menuButtonHeight)
} private func openSheetOnTap<V: View>(label: () -> V, action: @escaping () -> Void) -> some View {
.buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill))) Button(action: action, label: label)
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
private func unreadBadge(_ u: UserInfo) -> some View {
let size = dynamicSize(userFont).chatInfoSize
return unreadCountText(u.unreadCount)
.font(userFont <= .xxxLarge ? .caption : .caption2)
.foregroundColor(.white)
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
.frame(minWidth: size, minHeight: size)
.background(u.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
.cornerRadius(dynamicSize(userFont).unreadCorner)
} }
}
private func unreadCounter(_ unread: Int, color: Color) -> some View {
unreadCountText(unread)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(color)
.cornerRadius(10)
} }
struct UserPicker_Previews: PreviewProvider { struct UserPicker_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var activeSheet: UserPickerSheet?
let m = ChatModel() let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData] m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker( return UserPicker(
showSettings: Binding.constant(false), activeSheet: $activeSheet
showConnectDesktop: Binding.constant(false),
userPickerVisible: Binding.constant(true)
) )
.environmentObject(m) .environmentObject(m)
} }

View file

@ -8,15 +8,22 @@
import SwiftUI import SwiftUI
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { func getTopViewController() -> UIViewController? {
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first, if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
let rootViewController = keyWindow.rootViewController { let rootViewController = keyWindow.rootViewController {
// Find the top-most presented view controller // Find the top-most presented view controller
var topController = rootViewController var topController = rootViewController
while let presentedViewController = topController.presentedViewController { while let presentedViewController = topController.presentedViewController {
topController = presentedViewController topController = presentedViewController
} }
return topController
}
return nil
}
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
if let topController = getTopViewController() {
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
if let completed = completed { if let completed = completed {
activityViewController.completionWithItemsHandler = { _, _, _, _ in activityViewController.completionWithItemsHandler = { _, _, _, _ in
@ -26,3 +33,22 @@ func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
topController.present(activityViewController, animated: true) topController.present(activityViewController, animated: true)
} }
} }
func showAlert(
title: String,
message: String? = nil,
buttonTitle: String,
buttonAction: @escaping () -> Void,
cancelButton: Bool
) -> Void {
if let topController = getTopViewController() {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: buttonTitle, style: .default) { _ in
buttonAction()
})
if cancelButton {
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel))
}
topController.present(alert, animated: true)
}
}

View file

@ -56,8 +56,6 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
struct MigrateFromDevice: View { struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool @Binding var showProgressOnSettings: Bool
@State private var migrationState: MigrationFromState = .chatStopInProgress @State private var migrationState: MigrationFromState = .chatStopInProgress
@State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var useKeychain = storeDBPassphraseGroupDefault.get()
@ -106,9 +104,6 @@ struct MigrateFromDevice: View {
finishedView(chatDeletion) finishedView(chatDeletion)
} }
} }
.modifier(BackButton(label: "Back", disabled: $backDisabled) {
dismiss()
})
.onChange(of: migrationState) { state in .onChange(of: migrationState) { state in
backDisabled = switch migrationState { backDisabled = switch migrationState {
case .chatStopInProgress, .archiving, .linkShown, .finished: true case .chatStopInProgress, .archiving, .linkShown, .finished: true
@ -590,7 +585,7 @@ struct MigrateFromDevice: View {
} catch let error { } catch let error {
fatalError("Error starting chat \(responseError(error))") fatalError("Error starting chat \(responseError(error))")
} }
showSettings = false dismissAllSheets(animated: true)
} }
} catch let error { } catch let error {
alert = .error(title: "Error deleting database", error: responseError(error)) alert = .error(title: "Error deleting database", error: responseError(error))
@ -613,9 +608,7 @@ struct MigrateFromDevice: View {
} }
// Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered // Hide settings anyway if chatDbStatus is not ok, probably passphrase needs to be entered
if dismiss || m.chatDbStatus != .ok { if dismiss || m.chatDbStatus != .ok {
await MainActor.run { dismissAllSheets(animated: true)
showSettings = false
}
} }
} }
@ -767,6 +760,6 @@ private class MigrationChatReceiver {
struct MigrateFromDevice_Previews: PreviewProvider { struct MigrateFromDevice_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false)) MigrateFromDevice(showProgressOnSettings: Binding.constant(false))
} }
} }

View file

@ -59,13 +59,6 @@ struct ConnectDesktopView: View {
var body: some View { var body: some View {
if viaSettings { if viaSettings {
viewBody viewBody
.modifier(BackButton(label: "Back", disabled: Binding.constant(false)) {
if m.activeRemoteCtrl {
alert = .disconnectDesktop(action: .back)
} else {
dismiss()
}
})
} else { } else {
NavigationView { NavigationView {
viewBody viewBody

View file

@ -32,6 +32,17 @@ struct PreferencesView: View {
.disabled(currentPreferences == preferences) .disabled(currentPreferences == preferences)
} }
} }
.onDisappear {
if currentPreferences != preferences {
showAlert(
title: NSLocalizedString("Your chat preferences", comment: "alert title"),
message: NSLocalizedString("Chat preferences were changed.", comment: "alert message"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: savePreferences,
cancelButton: true
)
}
}
} }
private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View { private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {

View file

@ -262,7 +262,9 @@ struct SettingsView: View {
var body: some View { var body: some View {
ZStack { ZStack {
settingsView() NavigationView {
settingsView()
}
if showProgress { if showProgress {
progressView() progressView()
} }
@ -274,63 +276,7 @@ struct SettingsView: View {
@ViewBuilder func settingsView() -> some View { @ViewBuilder func settingsView() -> some View {
let user = chatModel.currentUser let user = chatModel.currentUser
NavigationView {
List { List {
Section(header: Text("You").foregroundColor(theme.colors.secondary)) {
if let user = user {
NavigationLink {
UserProfile()
.navigationTitle("Your current profile")
.modifier(ThemedBackground())
} label: {
ProfilePreview(profileOf: user)
.padding(.leading, -8)
}
}
NavigationLink {
UserProfilesView(showSettings: $showSettings)
} label: {
settingsRow("person.crop.rectangle.stack", color: theme.colors.secondary) { Text("Your chat profiles") }
}
if let user = user {
NavigationLink {
UserAddressView(shareViaProfile: user.addressShared)
.navigationTitle("SimpleX address")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("qrcode", color: theme.colors.secondary) { Text("Your SimpleX address") }
}
NavigationLink {
PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences)
.navigationTitle("Your preferences")
.modifier(ThemedBackground(grouped: true))
} label: {
settingsRow("switch.2", color: theme.colors.secondary) { Text("Chat preferences") }
}
}
NavigationLink {
ConnectDesktopView(viaSettings: true)
} label: {
settingsRow("desktopcomputer", color: theme.colors.secondary) { Text("Use from desktop") }
}
NavigationLink {
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
}
}
.disabled(chatModel.chatRunning != true)
Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) {
NavigationLink { NavigationLink {
NotificationsView() NotificationsView()
@ -381,10 +327,20 @@ struct SettingsView: View {
} }
.disabled(chatModel.chatRunning != true) .disabled(chatModel.chatRunning != true)
} }
chatDatabaseRow()
} }
Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) {
chatDatabaseRow()
NavigationLink {
MigrateFromDevice(showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.modifier(ThemedBackground(grouped: true))
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") }
}
}
Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { Section(header: Text("Help").foregroundColor(theme.colors.secondary)) {
if let user = user { if let user = user {
NavigationLink { NavigationLink {
@ -462,11 +418,10 @@ struct SettingsView: View {
} }
.navigationTitle("Your settings") .navigationTitle("Your settings")
.modifier(ThemedBackground(grouped: true)) .modifier(ThemedBackground(grouped: true))
} .onDisappear {
.onDisappear { chatModel.showingTerminal = false
chatModel.showingTerminal = false chatModel.terminalItems = []
chatModel.terminalItems = [] }
}
} }
private func chatDatabaseRow() -> some View { private func chatDatabaseRow() -> some View {
@ -549,17 +504,18 @@ struct ProfilePreview: View {
HStack { HStack {
ProfileImage(imageStr: profileOf.image, size: 44, color: color) ProfileImage(imageStr: profileOf.image, size: 44, color: color)
.padding(.trailing, 6) .padding(.trailing, 6)
.padding(.vertical, 6) profileName().lineLimit(1)
VStack(alignment: .leading) {
Text(profileOf.displayName)
.fontWeight(.bold)
.font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
Text(profileOf.fullName)
}
}
} }
} }
private func profileName() -> Text {
var t = Text(profileOf.displayName).fontWeight(.semibold).font(.title2)
if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName {
t = t + Text(" (" + profileOf.fullName + ")")
// .font(.callout)
}
return t
}
} }
struct SettingsView_Previews: PreviewProvider { struct SettingsView_Previews: PreviewProvider {

View file

@ -14,7 +14,6 @@ struct UserAddressView: View {
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var chatModel: ChatModel @EnvironmentObject private var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@State var viaCreateLinkView = false
@State var shareViaProfile = false @State var shareViaProfile = false
@State private var aas = AutoAcceptState() @State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState() @State private var savedAAS = AutoAcceptState()
@ -22,7 +21,6 @@ struct UserAddressView: View {
@State private var showMailView = false @State private var showMailView = false
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil @State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
@State private var alert: UserAddressAlert? @State private var alert: UserAddressAlert?
@State private var showSaveDialogue = false
@State private var progressIndicator = false @State private var progressIndicator = false
@FocusState private var keyboardVisible: Bool @FocusState private var keyboardVisible: Bool
@ -44,26 +42,19 @@ struct UserAddressView: View {
var body: some View { var body: some View {
ZStack { ZStack {
if viaCreateLinkView { userAddressScrollView()
userAddressScrollView() .onDisappear {
} else { if savedAAS != aas {
userAddressScrollView() showAlert(
.modifier(BackButton(disabled: Binding.constant(false)) { title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
if savedAAS == aas { message: NSLocalizedString("Settings were changed.", comment: "alert message"),
dismiss() buttonTitle: NSLocalizedString("Save", comment: "alert button"),
} else { buttonAction: saveAAS,
keyboardVisible = false cancelButton: true
showSaveDialogue = true )
}
})
.confirmationDialog("Save settings?", isPresented: $showSaveDialogue) {
Button("Save auto-accept settings") {
saveAAS()
dismiss()
}
Button("Exit without saving") { dismiss() }
} }
} }
if progressIndicator { if progressIndicator {
ZStack { ZStack {
if chatModel.userAddress != nil { if chatModel.userAddress != nil {
@ -238,7 +229,7 @@ struct UserAddressView: View {
} }
} }
} label: { } label: {
Label("Create SimpleX address", systemImage: "qrcode") Label("Create public address", systemImage: "qrcode")
} }
} }
@ -342,7 +333,7 @@ struct UserAddressView: View {
} }
} }
} }
private struct AutoAcceptState: Equatable { private struct AutoAcceptState: Equatable {
var enable = false var enable = false
var incognito = false var incognito = false
@ -447,6 +438,8 @@ struct UserAddressView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let chatModel = ChatModel() 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") 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 { return Group {
UserAddressView() UserAddressView()
.environmentObject(chatModel) .environmentObject(chatModel)

View file

@ -9,7 +9,6 @@ import SimpleXChat
struct UserProfilesView: View { struct UserProfilesView: View {
@EnvironmentObject private var m: ChatModel @EnvironmentObject private var m: ChatModel
@EnvironmentObject private var theme: AppTheme @EnvironmentObject private var theme: AppTheme
@Binding var showSettings: Bool
@Environment(\.editMode) private var editMode @Environment(\.editMode) private var editMode
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true @AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true @AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@ -96,8 +95,7 @@ struct UserProfilesView: View {
} label: { } label: {
Label("Add profile", systemImage: "plus") Label("Add profile", systemImage: "plus")
} }
.frame(height: 44) .frame(height: 38)
.padding(.vertical, 4)
} }
} footer: { } footer: {
Text("Tap to activate profile.") Text("Tap to activate profile.")
@ -285,7 +283,7 @@ struct UserProfilesView: View {
await MainActor.run { await MainActor.run {
onboardingStageDefault.set(.step1_SimpleXInfo) onboardingStageDefault.set(.step1_SimpleXInfo)
m.onboardingStage = .step1_SimpleXInfo m.onboardingStage = .step1_SimpleXInfo
showSettings = false dismissAllSheets()
} }
} }
} else { } else {
@ -308,14 +306,14 @@ struct UserProfilesView: View {
Task { Task {
do { do {
try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user)) try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user))
dismissAllSheets()
} catch { } catch {
await MainActor.run { alert = .activateUserError(error: responseError(error)) } await MainActor.run { alert = .activateUserError(error: responseError(error)) }
} }
} }
} label: { } label: {
HStack { HStack {
ProfileImage(imageStr: user.image, size: 44) ProfileImage(imageStr: user.image, size: 38)
.padding(.vertical, 4)
.padding(.trailing, 12) .padding(.trailing, 12)
Text(user.chatViewName) Text(user.chatViewName)
Spacer() Spacer()
@ -415,6 +413,6 @@ public func correctPassword(_ user: User, _ pwd: String) -> Bool {
struct UserProfilesView_Previews: PreviewProvider { struct UserProfilesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UserProfilesView(showSettings: Binding.constant(true)) UserProfilesView()
} }
} }