From 60c37f0d1d03be234e0f40f97044daa48ae30f99 Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 13 Nov 2024 11:41:39 +0000 Subject: [PATCH] ios: user profiles move auth to change actions, show unread counts (#5170) * ios: user profiles move auth to change actions, show unread count per profile * simpler approach and add profile protection * not show muted icon * refactor * not needed * fix * simpler fix * deadline --------- Co-authored-by: Evgeny Poberezkin --- .../Shared/Views/ChatList/UserPicker.swift | 18 ++- .../Views/UserSettings/UserProfilesView.swift | 128 ++++++++++++------ 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index cfcfe851f3..dbe10ad997 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -124,7 +124,7 @@ struct UserPicker: View { ZStack(alignment: .topTrailing) { ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground)) if (u.unreadCount > 0) { - unreadBadge(u).offset(x: 4, y: -4) + UnreadBadge(userInfo: u).offset(x: 4, y: -4) } } .padding(.trailing, 6) @@ -169,15 +169,21 @@ struct UserPicker: View { } } } - - private func unreadBadge(_ u: UserInfo) -> some View { +} + +struct UnreadBadge: View { + var userInfo: UserInfo + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + + var body: some View { let size = dynamicSize(userFont).chatInfoSize - return unreadCountText(u.unreadCount) - .font(userFont <= .xxxLarge ? .caption : .caption2) + unreadCountText(userInfo.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) + .background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary) .cornerRadius(dynamicSize(userFont).unreadCorner) } } diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 330ce56e0b..c3dce183bb 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -21,6 +21,7 @@ struct UserProfilesView: View { @State private var profileHidden = false @State private var profileAction: UserProfileAction? @State private var actionPassword = "" + @State private var navigateToProfileCreate = false var trimmedSearchTextOrPassword: String { searchTextOrPassword.trimmingCharacters(in: .whitespaces)} @@ -55,17 +56,6 @@ struct UserProfilesView: View { } var body: some View { - if authorized { - userProfilesView() - } else { - Button(action: runAuth) { Label("Unlock", systemImage: "lock") } - .onAppear(perform: runAuth) - } - } - - private func runAuth() { authorize(NSLocalizedString("Open user profiles", comment: "authentication reason"), $authorized) } - - private func userProfilesView() -> some View { List { if profileHidden { Button { @@ -77,12 +67,14 @@ struct UserProfilesView: View { Section { let users = filteredUsers() let v = ForEach(users) { u in - userView(u.user) + userView(u) } if #available(iOS 16, *) { v.onDelete { indexSet in if let i = indexSet.first { - confirmDeleteUser(users[i].user) + withAuth { + confirmDeleteUser(users[i].user) + } } } } else { @@ -90,12 +82,22 @@ struct UserProfilesView: View { } if trimmedSearchTextOrPassword == "" { - NavigationLink { - CreateProfile() - } label: { + NavigationLink( + destination: CreateProfile(), + isActive: $navigateToProfileCreate + ) { Label("Add profile", systemImage: "plus") + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 38) + .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) + .contentShape(Rectangle()) + .onTapGesture { + withAuth { + self.navigateToProfileCreate = true + } + } + .padding(.leading, -16).padding(.vertical, -8).padding(.trailing, -32) } - .frame(height: 38) } } footer: { Text("Tap to activate profile.") @@ -189,7 +191,25 @@ struct UserProfilesView: View { private var visibleUsersCount: Int { m.users.filter({ u in !u.user.hidden }).count } - + + private func withAuth(_ action: @escaping () -> Void) { + if authorized { + action() + } else { + authenticate( + reason: NSLocalizedString("Change user profiles", comment: "authentication reason") + ) { laResult in + switch laResult { + case .success, .unavailable: + authorized = true + AppSheetState.shared.scenePhaseActive = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: action) + case .failed: authorized = false + } + } + } + } + private func correctPassword(_ user: User, _ pwd: String) -> Bool { if let ph = user.viewPwdHash { return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash @@ -213,8 +233,10 @@ struct UserProfilesView: View { passwordField settingsRow("trash", color: theme.colors.secondary) { Button("Delete chat profile", role: .destructive) { - profileAction = nil - Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + withAuth { + profileAction = nil + Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } @@ -231,8 +253,10 @@ struct UserProfilesView: View { passwordField settingsRow("lock.open", color: theme.colors.secondary) { Button("Unhide chat profile") { - profileAction = nil - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + withAuth{ + profileAction = nil + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } + } } .disabled(!actionEnabled(user)) } @@ -255,11 +279,13 @@ struct UserProfilesView: View { private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View { Button(title, role: .destructive) { - if let user = userToDelete { - if passwordEntryRequired(user) { - profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) - } else { - alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + withAuth { + if let user = userToDelete { + if passwordEntryRequired(user) { + profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } else { + alert = .deleteUser(user: user, delSMPQueues: delSMPQueues) + } } } } @@ -301,7 +327,8 @@ struct UserProfilesView: View { } } - @ViewBuilder private func userView(_ user: User) -> some View { + @ViewBuilder private func userView(_ userInfo: UserInfo) -> some View { + let user = userInfo.user let v = Button { Task { do { @@ -319,12 +346,19 @@ struct UserProfilesView: View { Spacer() if user.activeUser { Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) - } else if user.hidden { - Image(systemName: "lock").foregroundColor(theme.colors.secondary) - } else if !user.showNtfs { - Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) } else { - Image(systemName: "checkmark").foregroundColor(.clear) + if userInfo.unreadCount > 0 { + UnreadBadge(userInfo: userInfo) + } + if user.hidden { + Image(systemName: "lock").foregroundColor(theme.colors.secondary) + } else if userInfo.unreadCount == 0 { + if !user.showNtfs { + Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) + } else { + Image(systemName: "checkmark").foregroundColor(.clear) + } + } } } } @@ -332,30 +366,38 @@ struct UserProfilesView: View { .swipeActions(edge: .leading, allowsFullSwipe: true) { if user.hidden { Button("Unhide") { - if passwordEntryRequired(user) { - profileAction = .unhideUser(user: user) - } else { - setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + withAuth { + if passwordEntryRequired(user) { + profileAction = .unhideUser(user: user) + } else { + setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: trimmedSearchTextOrPassword) } + } } } .tint(.green) } else { if visibleUsersCount > 1 { Button("Hide") { - selectedUser = user + withAuth { + selectedUser = user + } } .tint(.gray) } Group { if user.showNtfs { Button("Mute") { - setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { - try await apiMuteUser(user.userId) + withAuth { + setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) { + try await apiMuteUser(user.userId) + } } } } else { Button("Unmute") { - setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + withAuth { + setUserPrivacy(user) { try await apiUnmuteUser(user.userId) } + } } } } @@ -367,7 +409,9 @@ struct UserProfilesView: View { } else { v.swipeActions(edge: .trailing, allowsFullSwipe: true) { Button("Delete", role: .destructive) { - confirmDeleteUser(user) + withAuth { + confirmDeleteUser(user) + } } } }