diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 4960281d72..5479a9fada 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -38,6 +38,7 @@ struct IncomingCallView: View { } HStack { ProfilePreview(profileOf: invitation.contact, color: .white) + .padding(.vertical, 6) Spacer() callButton("Reject", "phone.down.fill", .red) { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 674b7ab75c..da41fda635 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -16,7 +16,6 @@ enum UserPickerSheet: Identifiable { case currentProfile case useFromDesktop case settings - case userPicker var id: Self { self } } @@ -32,7 +31,8 @@ struct ChatListView: View { @State private var searchChatFilteredBySimplexLink: String? = nil @State private var scrollToSearchBar = false @State private var activeUserPickerSheet: UserPickerSheet? = nil - + @State private var isUserPickerPresented: Bool = false + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false @@ -58,44 +58,43 @@ struct ChatListView: View { destination: chatView ) { chatListView } } - .sheet(item: $activeUserPickerSheet) { sheet in - if let currentUser = chatModel.currentUser { - switch sheet { - case .address: - NavigationView { - UserAddressView(shareViaProfile: currentUser.addressShared) - .navigationTitle("Public address") + .sheet(isPresented: $isUserPickerPresented) { + UserPicker(activeSheet: $activeUserPickerSheet) + .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) - .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) - case .userPicker: - UserPicker( - activeSheet: $activeUserPickerSheet - ) } - } + } } } @@ -208,7 +207,7 @@ struct ChatListView: View { } } .onTapGesture { - activeUserPickerSheet = .userPicker + isUserPickerPresented = true } } @@ -298,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() .frame(width: size, height: size) .foregroundColor(theme.colors.primary) diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index d32d4acf73..0d5f0249fa 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -9,102 +9,77 @@ import SimpleXChat struct UserPicker: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme - @Environment(\.scenePhase) var scenePhase - @Environment(\.colorScheme) var colorScheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @Environment(\.scenePhase) private var scenePhase: ScenePhase + @Environment(\.colorScheme) private var colorScheme: ColorScheme + @Environment(\.dismiss) private var dismiss: DismissAction @Binding var activeSheet: UserPickerSheet? - @State private var activeUser: User? = nil - - var body: some View { - let v = List { - VStack(alignment: .leading, spacing: 6) { - if let currentUser = activeUser ?? m.currentUser { - HStack(alignment: .top) { - ProfileImage(imageStr: currentUser.image, size: 52) - .onTapGesture { - activeSheet = .currentProfile - } - Spacer() - let usersToPreview = m.users.filter({ u in !u.user.hidden && u.user.userId != currentUser.userId }) - ZStack(alignment: .leading) { - ZStack(alignment: .trailing) { - let ps = HStack(spacing: 20) { - Color.clear.frame(width: 48, height: 32) - ForEach(usersToPreview) { u in - userView(u) - } - Color.clear.frame(width: 32, height: 32) - } - - if usersToPreview.count > 3 { - let s = ScrollView(.horizontal) { ps }.frame(width: 284) - if #available(iOS 16.0, *) { - s.scrollIndicators(.hidden) - } else { - s - } - } else { - ps - } - HStack(spacing: 0) { - LinearGradient( - colors: [.clear, theme.colors.background.asGroupedBackground(theme.base.mode)], - startPoint: .leading, - endPoint: .trailing - ) - .frame(width: 32, height: 35) - Button { - activeSheet = .chatProfiles - } label: { - Image(systemName: "ellipsis.circle.fill") - .resizable() - .scaledToFit() - .frame(width: 31, height: 31) - .padding(.top, 4) - .foregroundColor(Color(uiColor: .quaternaryLabel)) - .modifier(ThemedBackground(grouped: true)) - } - } - } - .padding(.top, 10) - - LinearGradient( - colors: [.clear, theme.colors.background.asGroupedBackground(theme.base.mode)], - startPoint: .trailing, - endPoint: .leading - ) - .frame(width: 32, height: 35) - } - } - - Text(currentUser.displayName) - .fontWeight(.bold) - .font(.headline) - } - } - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(Color.clear) - .listRowSeparator(.hidden) - .padding(.horizontal, 12) + @State private var switchingProfile = false - Section { - if (m.currentUser != nil) { - openSheetOnTap(title: m.userAddress == nil ? "Create public address" : "Your public address", image: "qrcode") { + var body: some View { + if #available(iOS 16.0, *) { + let v = viewBody.presentationDetents([.height(420)]) + if #available(iOS 16.4, *) { + v.scrollBounceBehavior(.basedOnSize) + } else { + v + } + } else { + viewBody + } + } + + private var viewBody: some View { + let otherUsers = m.users.filter { u in !u.user.hidden && u.user.userId != m.currentUser?.userId } + return List { + Section(header: Text("You").foregroundColor(theme.colors.secondary)) { + if let user = m.currentUser { + openSheetOnTap(label: { + ZStack { + let v = ProfilePreview(profileOf: user) + .foregroundColor(.primary) + .padding(.leading, -8) + if #available(iOS 16.0, *) { + v + } else { + v.padding(.vertical, 4) + } + } + }) { + activeSheet = .currentProfile + } + + openSheetOnTap(title: m.userAddress == nil ? "Create public address" : "Your public address", icon: "qrcode") { activeSheet = .address } - openSheetOnTap(title: "Chat preferences", image: "switch.2") { + openSheetOnTap(title: "Chat preferences", icon: "switch.2") { activeSheet = .chatPreferences } - - openSheetOnTap(title: "Use from desktop", image: "desktopcomputer") { - activeSheet = .useFromDesktop - } } } - + Section { + if otherUsers.isEmpty { + openSheetOnTap(title: "Your chat profiles", icon: "person.crop.rectangle.stack") { + activeSheet = .chatProfiles + } + } else { + let v = userPickerRow(otherUsers, size: 44) + .padding(.leading, -8) + if #available(iOS 16.0, *) { + v + } else { + v.padding(.vertical, 4) + } + } + + openSheetOnTap(title: "Use from desktop", icon: "desktopcomputer") { + activeSheet = .useFromDesktop + } + HStack { - openSheetOnTap(title: "Settings", image: "gearshape") { + openSheetOnTap(title: "Settings", icon: "gearshape") { activeSheet = .settings } Label {} icon: { @@ -146,33 +121,92 @@ struct UserPicker: View { } } .modifier(ThemedBackground(grouped: true)) - - if #available(iOS 16.0, *) { - v.presentationDetents([.height(400)]) - } else { - v + .disabled(switchingProfile) + } + + private func userPickerRow(_ users: [UserInfo], size: CGFloat) -> some View { + HStack(spacing: 6) { + let s = ScrollView(.horizontal) { + HStack(spacing: 27) { +// Image(systemName: "person.crop.rectangle.stack.fill") +// .resizable() +// .scaledToFit() +// .frame(height: size) +// .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground).asAnotherColorFromSecondaryVariant(theme)) +// .padding([.top, .trailing], 3) +// Image(systemName: "theatermasks.fill") +// .resizable() +// .scaledToFit() +// .frame(width: size, height: size) +// .foregroundColor(.indigo) +// .padding([.top, .trailing], 3) + ForEach(users) { u in + if !u.user.hidden && u.user.userId != m.currentUser?.userId { + userView(u, size: size) + } + } + } + .padding(.leading, 2) + .padding(.trailing, 22) + } + ZStack { + if #available(iOS 16.0, *) { + s.scrollIndicators(.hidden) + } else { + s + } + HStack(spacing: 0) { + LinearGradient( + colors: [.black, .clear], + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: 2) + Color.clear + LinearGradient( + colors: [.clear, .black], + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: size) + } + .frame(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) -> some View { + private func userView(_ u: UserInfo, size: CGFloat) -> some View { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: u.user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) + ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) .padding([.top, .trailing], 3) if (u.unreadCount > 0) { - unreadBadge() + unreadBadge(u) } } + .frame(width: size) .onTapGesture { - activeUser = m.currentUser - + switchingProfile = true Task { do { try await changeActiveUserAsync_(u.user.userId, viewPwd: nil) await MainActor.run { - activeSheet = nil + switchingProfile = false + dismiss() } } catch { await MainActor.run { + switchingProfile = false AlertManager.shared.showAlertMsg( title: "Error switching profile!", message: "Error: \(responseError(error))" @@ -183,28 +217,34 @@ struct UserPicker: View { } } - private func openSheetOnTap(title: LocalizedStringKey, image: String, setActive: @escaping () -> Void) -> some View { - Button(action: setActive) { - Label { - Text(title).foregroundColor(.primary) - } icon: { - Image(systemName: image) - .resizable() + private func openSheetOnTap(title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { + openSheetOnTap(label: { + ZStack(alignment: .leading) { + Image(systemName: icon).frame(maxWidth: 24, maxHeight: 24, alignment: .center) .symbolRenderingMode(.monochrome) .foregroundColor(theme.colors.secondary) - .frame(maxWidth: 20, maxHeight: 20) + Text(title) + .foregroundColor(.primary) + .padding(.leading, 36) } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) - .contentShape(Rectangle()) - .padding(.leading, -19).padding(.vertical, -8).padding(.trailing, -32) + }, action: action) } - private func unreadBadge() -> some View { - Circle() - .frame(width: 12, height: 12) - .foregroundColor(theme.colors.primary) + private func openSheetOnTap(label: () -> V, action: @escaping () -> Void) -> some View { + 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) } } diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 936c6cb3ab..13302b6170 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -20,3 +20,41 @@ func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { presentedViewController.present(activityViewController, animated: true) } } + +extension View { + func shareSheet(item: Binding) -> some View { + sheet(item: item) { item in + Group { + if #available(iOS 16.0, *) { + ActivityView(item: item) + .presentationDetents([.medium, .large]) + } else { + ActivityView(item: item) + } + }.ignoresSafeArea() + } + } +} + +struct ShareItem: Identifiable { + let content: any Hashable + var id: Int { content.hashValue } +} + +private struct ActivityView: UIViewControllerRepresentable { + let item: ShareItem + + func makeUIViewController( + context: UIViewControllerRepresentableContext + ) -> UIActivityViewController { + UIActivityViewController( + activityItems: [item.content], + applicationActivities: nil + ) + } + + func updateUIViewController( + _ uiViewController: UIActivityViewController, + context: UIViewControllerRepresentableContext + ) { } +} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index b9d4d5ac5b..463ac4ae07 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -504,17 +504,18 @@ struct ProfilePreview: View { HStack { ProfileImage(imageStr: profileOf.image, size: 44, color: color) .padding(.trailing, 6) - .padding(.vertical, 6) - VStack(alignment: .leading) { - Text(profileOf.displayName) - .fontWeight(.bold) - .font(.title2) - if profileOf.fullName != "" && profileOf.fullName != profileOf.displayName { - Text(profileOf.fullName) - } - } + profileName().lineLimit(1) } } + + 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 { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index da7848640f..27bf214b45 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -22,6 +22,7 @@ struct UserAddressView: View { @State private var mailViewResult: Result? = nil @State private var alert: UserAddressAlert? @State private var progressIndicator = false + @State private var shareItem: ShareItem? @FocusState private var keyboardVisible: Bool private enum UserAddressAlert: Identifiable { @@ -68,6 +69,7 @@ struct UserAddressView: View { } } } + .shareSheet(item: $shareItem) } @Namespace private var bottomID @@ -245,7 +247,7 @@ struct UserAddressView: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) + shareItem = ShareItem(content: simplexChatLink(userAddress.connReqContact)) } label: { settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Text("Share address") diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index cc7c5b5e7e..330ce56e0b 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -10,7 +10,6 @@ struct UserProfilesView: View { @EnvironmentObject private var m: ChatModel @EnvironmentObject private var theme: AppTheme @Environment(\.editMode) private var editMode - @Environment(\.dismiss) var dismiss: DismissAction @AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true @AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true @State private var showDeleteConfirmation = false @@ -284,7 +283,7 @@ struct UserProfilesView: View { await MainActor.run { onboardingStageDefault.set(.step1_SimpleXInfo) m.onboardingStage = .step1_SimpleXInfo - dismiss() + dismissAllSheets() } } } else { @@ -307,7 +306,7 @@ struct UserProfilesView: View { Task { do { try await changeActiveUserAsync_(user.userId, viewPwd: userViewPassword(user)) - dismiss() + dismissAllSheets() } catch { await MainActor.run { alert = .activateUserError(error: responseError(error)) } }