ios: user profiles view, per-user settings (#1801)
* ios: user profiles view, per-user settings * remove comment * bold profile name
This commit is contained in:
committed by
GitHub
parent
ed12ccaac2
commit
04d886e546
@@ -16,7 +16,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
||||
@Published var currentUser: User?
|
||||
@Published private(set) var users: [UserInfo] = []
|
||||
@Published var users: [UserInfo] = []
|
||||
@Published var chatInitialized = false
|
||||
@Published var chatRunning: Bool?
|
||||
@Published var chatDbChanged = false
|
||||
@@ -492,31 +492,6 @@ final class ChatModel: ObservableObject {
|
||||
return reversedChatItems[min(i - 1, maxIx)]
|
||||
}
|
||||
|
||||
func updateUsers(_ new: [UserInfo]) {
|
||||
users = new
|
||||
.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
|
||||
.sorted { first, _ in first.user.activeUser }
|
||||
}
|
||||
|
||||
func changeActiveUser(_ toUserId: Int64) {
|
||||
do {
|
||||
let activeUser = try apiSetActiveUser(toUserId)
|
||||
var users = users
|
||||
let oldActiveIndex = users.firstIndex(where: { $0.user.userId == currentUser?.userId })!
|
||||
var oldActive = users[oldActiveIndex]
|
||||
oldActive.user.activeUser = false
|
||||
users[oldActiveIndex] = oldActive
|
||||
|
||||
currentUser = activeUser
|
||||
let currentActiveIndex = users.firstIndex(where: { $0.user.userId == activeUser.userId })!
|
||||
users[currentActiveIndex] = UserInfo(user: activeUser, unreadCount: users[currentActiveIndex].unreadCount)
|
||||
updateUsers(users)
|
||||
try getUserChatData(self)
|
||||
} catch {
|
||||
logger.error("Unable to set active user: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func updateContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.connId] = status
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||
if let userId = content.userInfo["userId"] as? Int64,
|
||||
userId != chatModel.currentUser?.userId {
|
||||
chatModel.changeActiveUser(userId)
|
||||
changeActiveUser(userId)
|
||||
}
|
||||
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
|
||||
@@ -131,10 +131,12 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
throw r
|
||||
}
|
||||
|
||||
func listUsers() -> [UserInfo] {
|
||||
func listUsers() throws -> [UserInfo] {
|
||||
let r = chatSendCmdSync(.listUsers)
|
||||
if case let .usersList(users) = r { return users }
|
||||
return []
|
||||
if case let .usersList(users) = r {
|
||||
return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
|
||||
}
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetActiveUser(_ userId: Int64) throws -> User {
|
||||
@@ -917,9 +919,9 @@ func startChat() throws {
|
||||
let m = ChatModel.shared
|
||||
try setNetworkConfig(getNetCfg())
|
||||
let justStarted = try apiStartChat()
|
||||
m.updateUsers(listUsers())
|
||||
m.users = try listUsers()
|
||||
if justStarted {
|
||||
try getUserChatData(m)
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
|
||||
try refreshCallInvitations()
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||
@@ -937,7 +939,19 @@ func startChat() throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func getUserChatData(_ m: ChatModel) throws {
|
||||
func changeActiveUser(_ toUserId: Int64) {
|
||||
let m = ChatModel.shared
|
||||
do {
|
||||
m.currentUser = try apiSetActiveUser(toUserId)
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
} catch let error {
|
||||
logger.error("Unable to set active user: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
|
||||
@@ -15,7 +15,6 @@ struct ChatListView: View {
|
||||
@State private var searchText = ""
|
||||
@State private var showAddChat = false
|
||||
@State var userPickerVisible = false
|
||||
@State var selectedView: Int? = nil
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
@@ -29,9 +28,6 @@ struct ChatListView: View {
|
||||
} else {
|
||||
chatList
|
||||
}
|
||||
NavigationLink(destination: UserProfilesView().navigationTitle("Profiles"), tag: 0, selection: $selectedView) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
@@ -42,14 +38,7 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(
|
||||
showSettings: $showSettings,
|
||||
userPickerVisible: $userPickerVisible,
|
||||
manageUsers: {
|
||||
selectedView = 0
|
||||
userPickerVisible = false
|
||||
}
|
||||
)
|
||||
UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,10 @@ private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue:
|
||||
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
|
||||
|
||||
struct UserPicker: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var showSettings: Bool
|
||||
@Binding var userPickerVisible: Bool
|
||||
var manageUsers: () -> Void = {}
|
||||
@State var scrollViewContentSize: CGSize = .zero
|
||||
@State var disableScrolling: Bool = true
|
||||
private let menuButtonHeight: CGFloat = 68
|
||||
@@ -31,35 +30,9 @@ struct UserPicker: View {
|
||||
ScrollView {
|
||||
ScrollViewReader { sp in
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(chatModel.users.enumerated()), id: \.0) { i, userInfo in
|
||||
Button(action: {
|
||||
if !userInfo.user.activeUser {
|
||||
chatModel.changeActiveUser(userInfo.user.userId)
|
||||
userPickerVisible = false
|
||||
}
|
||||
}, label: {
|
||||
HStack(spacing: 0) {
|
||||
ProfileImage(imageStr: userInfo.user.image)
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.trailing, 12)
|
||||
Text(userInfo.user.chatViewName)
|
||||
.fontWeight(i == 0 ? .medium : .regular)
|
||||
.foregroundColor(.primary)
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
if i == 0 {
|
||||
Image(systemName: "checkmark")
|
||||
} else if userInfo.unreadCount > 0 {
|
||||
unreadCounter(userInfo.unreadCount)
|
||||
}
|
||||
}
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
if i < chatModel.users.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
ForEach(Array(m.users.sorted(by: { u, _ in u.user.activeUser }))) { u in
|
||||
userView(u)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
@@ -69,7 +42,7 @@ struct UserPicker: View {
|
||||
let scenes = UIApplication.shared.connectedScenes
|
||||
if let windowScene = scenes.first as? UIWindowScene {
|
||||
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
|
||||
disableScrolling = scrollViewContentSize.height + menuButtonHeight * 2 + 10 < layoutFrame.height
|
||||
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
|
||||
}
|
||||
}
|
||||
return Color.clear
|
||||
@@ -85,11 +58,6 @@ struct UserPicker: View {
|
||||
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
|
||||
.frame(maxHeight: scrollViewContentSize.height)
|
||||
|
||||
Divider()
|
||||
menuButton("Your user profiles", icon: "pencil") {
|
||||
manageUsers()
|
||||
}
|
||||
Divider()
|
||||
menuButton("Settings", icon: "gearshape") {
|
||||
showSettings = true
|
||||
withAnimation {
|
||||
@@ -108,27 +76,43 @@ struct UserPicker: View {
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
|
||||
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
|
||||
.padding(8)
|
||||
.onChange(of: [chatModel.currentUser?.chatViewName, chatModel.currentUser?.image] ) { _ in
|
||||
reloadCurrentUser()
|
||||
}
|
||||
.opacity(userPickerVisible ? 1.0 : 0.0)
|
||||
.onAppear {
|
||||
reloadUsers()
|
||||
do {
|
||||
m.users = try listUsers()
|
||||
} catch let error {
|
||||
logger.error("Error updating users \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadCurrentUser() {
|
||||
if let updatedUser = chatModel.currentUser, let index = chatModel.users.firstIndex(where: { $0.user.userId == updatedUser.userId }) {
|
||||
var users = chatModel.users
|
||||
users[index] = UserInfo(user: updatedUser, unreadCount: users[index].unreadCount)
|
||||
chatModel.updateUsers(users)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadUsers() {
|
||||
Task {
|
||||
chatModel.updateUsers(listUsers())
|
||||
}
|
||||
private func userView(_ u: UserInfo) -> some View {
|
||||
let user = u.user
|
||||
return Button(action: {
|
||||
if !user.activeUser {
|
||||
changeActiveUser(user.userId)
|
||||
userPickerVisible = false
|
||||
}
|
||||
}, label: {
|
||||
HStack(spacing: 0) {
|
||||
ProfileImage(imageStr: user.image)
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
.fontWeight(user.activeUser ? .medium : .regular)
|
||||
.foregroundColor(.primary)
|
||||
.overlay(DetermineWidth())
|
||||
Spacer()
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount)
|
||||
}
|
||||
}
|
||||
.padding(.trailing)
|
||||
.padding([.leading, .vertical], 12)
|
||||
})
|
||||
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
|
||||
}
|
||||
|
||||
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
|
||||
@@ -161,7 +145,7 @@ func unreadCounter(_ unread: Int64) -> some View {
|
||||
struct UserPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let m = ChatModel()
|
||||
m.updateUsers([UserInfo.sampleData, UserInfo.sampleData])
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return UserPicker(
|
||||
showSettings: Binding.constant(false),
|
||||
userPickerVisible: Binding.constant(true)
|
||||
|
||||
@@ -67,6 +67,23 @@ struct DatabaseView: View {
|
||||
private func chatDatabaseView() -> some View {
|
||||
List {
|
||||
let stopped = m.chatRunning == false
|
||||
Section {
|
||||
Picker("Delete messages after", selection: $chatItemTTL) {
|
||||
ForEach(ChatItemTTL.values) { ttl in
|
||||
Text(ttl.deleteAfterText).tag(ttl)
|
||||
}
|
||||
if case .seconds = chatItemTTL {
|
||||
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.disabled(m.chatDbChanged || progressIndicator)
|
||||
} header: {
|
||||
Text("Messages")
|
||||
} footer: {
|
||||
Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
}
|
||||
|
||||
Section {
|
||||
settingsRow(
|
||||
stopped ? "exclamationmark.octagon.fill" : "play.fill",
|
||||
@@ -157,22 +174,12 @@ struct DatabaseView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Delete messages after", selection: $chatItemTTL) {
|
||||
ForEach(ChatItemTTL.values) { ttl in
|
||||
Text(ttl.deleteAfterText).tag(ttl)
|
||||
}
|
||||
if case .seconds = chatItemTTL {
|
||||
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
.disabled(m.chatDbChanged || progressIndicator)
|
||||
Button("Delete files & media", role: .destructive) {
|
||||
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
||||
alert = .deleteFilesAndMedia
|
||||
}
|
||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
||||
} header: {
|
||||
Text("Data")
|
||||
Text("Files & media")
|
||||
} footer: {
|
||||
if let (fileCount, size) = appFilesCountAndSize {
|
||||
if fileCount == 0 {
|
||||
|
||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
||||
|
||||
struct CreateProfile: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State private var displayName: String = ""
|
||||
@State private var fullName: String = ""
|
||||
@FocusState private var focusDisplayName
|
||||
@@ -97,12 +97,13 @@ struct CreateProfile: View {
|
||||
)
|
||||
do {
|
||||
m.currentUser = try apiCreateActiveUser(profile)
|
||||
try startChat()
|
||||
if m.users.count == 1 {
|
||||
if m.users.isEmpty {
|
||||
try startChat()
|
||||
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
|
||||
} else {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
try getUserChatData(m)
|
||||
dismiss()
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
} catch {
|
||||
fatalError("Failed to create user or start chat: \(responseError(error))")
|
||||
|
||||
@@ -44,7 +44,7 @@ struct SMPServersView: View {
|
||||
|
||||
private func smpServersView() -> some View {
|
||||
List {
|
||||
Section("SMP servers") {
|
||||
Section {
|
||||
ForEach($servers) { srv in
|
||||
smpServerView(srv)
|
||||
}
|
||||
@@ -57,6 +57,11 @@ struct SMPServersView: View {
|
||||
Button("Add server…") {
|
||||
showAddServer = true
|
||||
}
|
||||
} header: {
|
||||
Text("SMP servers")
|
||||
} footer: {
|
||||
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
.lineLimit(10)
|
||||
}
|
||||
|
||||
Section {
|
||||
|
||||
@@ -113,12 +113,19 @@ struct SettingsView: View {
|
||||
Section("You") {
|
||||
NavigationLink {
|
||||
UserProfile()
|
||||
.navigationTitle("Your chat profile")
|
||||
.navigationTitle("Your current profile")
|
||||
} label: {
|
||||
ProfilePreview(profileOf: user)
|
||||
.padding(.leading, -8)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
UserProfilesView()
|
||||
.navigationTitle("Your chat profiles")
|
||||
} label: {
|
||||
settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") }
|
||||
}
|
||||
|
||||
incognitoRow()
|
||||
|
||||
NavigationLink {
|
||||
|
||||
@@ -9,57 +9,90 @@ import SimpleXChat
|
||||
struct UserProfilesView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Environment(\.editMode) private var editMode
|
||||
@State private var selectedUser: Int? = nil
|
||||
@State private var showAddUser: Bool? = false
|
||||
@State private var alert: UserProfilesAlert?
|
||||
|
||||
private enum UserProfilesAlert: Identifiable {
|
||||
case deleteUser(index: Int)
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case let .deleteUser(index): return "deleteUser \(index)"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Your profiles") {
|
||||
ForEach(Array(m.users.enumerated()), id: \.0) { i, userInfo in
|
||||
userProfileView(userInfo, index: i)
|
||||
.deleteDisabled(userInfo.user.activeUser)
|
||||
Section {
|
||||
ForEach(m.users) { u in
|
||||
userView(u.user)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Delete profile?"),
|
||||
message: Text("All chats and messages will be deleted - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
removeUser(index: indexSet.first!)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
if let i = indexSet.first {
|
||||
alert = .deleteUser(index: i)
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: CreateProfile(), tag: true, selection: $showAddUser) {
|
||||
Text("Add profile…")
|
||||
|
||||
NavigationLink {
|
||||
CreateProfile()
|
||||
} label: {
|
||||
Label("Add profile", systemImage: "plus")
|
||||
}
|
||||
.frame(height: 44)
|
||||
.padding(.vertical, 4)
|
||||
} footer: {
|
||||
Text("Your chat profiles are stored locally, only on your device.")
|
||||
}
|
||||
}
|
||||
.toolbar { EditButton() }
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case let .deleteUser(index):
|
||||
return Alert(
|
||||
title: Text("Delete user profile?"),
|
||||
message: Text("All chats and messages will be deleted - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
removeUser(index: index)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeUser(index: Int) {
|
||||
do {
|
||||
try apiDeleteUser(m.users[index].user.userId)
|
||||
var users = m.users
|
||||
users.remove(at: index)
|
||||
m.updateUsers(users)
|
||||
} catch {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Failed to delete the user",
|
||||
message: "Error: \(responseError(error))"
|
||||
)
|
||||
m.users.remove(at: index)
|
||||
} catch let error {
|
||||
let a = getErrorAlert(error, "Error deleting user profile")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
private func userProfileView(_ userBinding: UserInfo, index: Int) -> some View {
|
||||
let user = userBinding.user
|
||||
return NavigationLink(tag: index, selection: $selectedUser) {
|
||||
// UserPrefs(user: userBinding, index: index)
|
||||
// .navigationBarTitle(user.chatViewName)
|
||||
// .navigationBarTitleDisplayMode(.large)
|
||||
|
||||
@ViewBuilder private func userView(_ user: User) -> some View {
|
||||
Button {
|
||||
if !user.activeUser {
|
||||
changeActiveUser(user.userId)
|
||||
}
|
||||
} label: {
|
||||
Text(user.chatViewName)
|
||||
HStack {
|
||||
ProfileImage(imageStr: user.image)
|
||||
.frame(width: 44, height: 44)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.trailing, 12)
|
||||
Text(user.chatViewName)
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(user.activeUser ? .primary : .clear)
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.deleteDisabled(user.activeUser)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user