mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 19:05:27 +00:00
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 <evgeny@poberezkin.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user