diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 5a15c4946a..d8a1efddf6 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -16,6 +16,7 @@ final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get() @Published var currentUser: User? + @Published var users: [UserInfo] = [] @Published var chatInitialized = false @Published var chatRunning: Bool? @Published var chatDbChanged = false diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6c05c3386b..f80605239a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -131,6 +131,24 @@ func apiCreateActiveUser(_ p: Profile) throws -> User { throw r } +func listUsers() -> [UserInfo] { + let r = chatSendCmdSync(.listUsers) + if case let .usersList(users) = r { return users } + return [] +} + +func apiSetActiveUser(_ userId: Int64) throws -> User { + let r = chatSendCmdSync(.apiSetActiveUser(userId: userId)) + if case let .activeUser(user) = r { return user } + throw r +} + +func apiDeleteUser(_ userId: Int64) throws { + let r = chatSendCmdSync(.apiDeleteUser(userId: userId)) + if case .cmdOk = r { return } + throw r +} + func apiStartChat() throws -> Bool { let r = chatSendCmdSync(.startChat(subscribe: true, expire: true)) switch r { @@ -900,11 +918,8 @@ func startChat() throws { try setNetworkConfig(getNetCfg()) let justStarted = try apiStartChat() if justStarted { - m.userAddress = try apiGetUserAddress() - (m.userSMPServers, m.presetSMPServers) = try getUserSMPServers() - m.chatItemTTL = try getChatItemTTL() - let chats = try apiGetChats() - m.chats = chats.map { Chat.init($0) } + try getUserChatData(m) + m.users = listUsers() NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount()) try refreshCallInvitations() (m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken() @@ -922,6 +937,14 @@ func startChat() throws { chatLastStartGroupDefault.set(Date.now) } +func getUserChatData(_ m: ChatModel) throws { + m.userAddress = try apiGetUserAddress() + (m.userSMPServers, m.presetSMPServers) = try getUserSMPServers() + m.chatItemTTL = try getChatItemTTL() + let chats = try apiGetChats() + m.chats = chats.map { Chat.init($0) } +} + class ChatReceiver { private var receiveLoop: Task? private var receiveMessages = true diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index f35bb32aac..6df579d964 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -11,25 +11,46 @@ import SimpleXChat struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel - // not really used in this view @State private var showSettings = false @State private var searchText = "" @State private var showAddChat = false + @State var userPickerVisible = false + @State var selectedView: Int? = nil var body: some View { - NavigationView { - VStack { - if chatModel.chats.isEmpty { - onboardingButtons() - } - if chatModel.chats.count > 8 { - chatList.searchable(text: $searchText) - } else { - chatList + ZStack(alignment: .topLeading) { + NavigationView { + VStack { + if chatModel.chats.isEmpty { + onboardingButtons() + } + if chatModel.chats.count > 8 { + chatList.searchable(text: $searchText) + } else { + chatList + } + NavigationLink(destination: UserProfilesView().navigationTitle("Profiles"), tag: 0, selection: $selectedView) { + EmptyView() + } } } + .navigationViewStyle(.stack) + if userPickerVisible { + Rectangle().fill(.white.opacity(0.001)).onTapGesture { + withAnimation { + userPickerVisible.toggle() + } + } + } + UserPicker( + showSettings: $showSettings, + userPickerVisible: $userPickerVisible, + manageUsers: { + selectedView = 0 + userPickerVisible = false + } + ) } - .navigationViewStyle(.stack) } var chatList: some View { @@ -48,13 +69,35 @@ struct ChatListView: View { } .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .onAppear() { connectViaUrl() } + .onDisappear() { withAnimation { userPickerVisible = false } } .offset(x: -8) .listStyle(.plain) .navigationTitle("Your chats") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - SettingsButton() + Button { + withAnimation { + userPickerVisible.toggle() + } + } label: { + let user = chatModel.currentUser ?? User.sampleData + let color = Color(uiColor: .tertiarySystemGroupedBackground) + HStack(spacing: 0) { + ProfileImage(imageStr: user.image, color: color) + .frame(width: 32, height: 32) + .padding(.trailing, 6) + .padding(.vertical, 6) + let unread = chatModel.users + .filter { !$0.user.activeUser } + .reduce(0, {cnt, u in cnt + u.unreadCount}) + if unread > 0 { + unreadCounter(unread) + .padding(.leading, -12) + .padding(.top, -16) + } + } + } } ToolbarItem(placement: .principal) { if (chatModel.incognito) { @@ -75,6 +118,9 @@ struct ChatListView: View { } } } + .sheet(isPresented: $showSettings) { + SettingsView(showSettings: $showSettings) + } .background( NavigationLink( destination: chatView(), diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift new file mode 100644 index 0000000000..75d96e2fc6 --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -0,0 +1,181 @@ +// +// Created by Avently on 16.01.2023. +// Copyright (c) 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255)) +private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255)) + +struct UserPicker: View { + @EnvironmentObject var chatModel: 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 + @State var chatViewNameWidth: CGFloat = 0 + + var fillColor: Color { + colorScheme == .dark ? fillColorDark : fillColorLight + } + + var body: some View { + VStack { + Spacer().frame(height: 1) + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 0) { + ForEach(Array(chatModel.users.enumerated()), id: \.0) { i, userInfo in + Button(action: { + if !userInfo.user.activeUser { + changeActiveUser(toUser: userInfo) + } + }, 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() + } + } + } + .overlay { + GeometryReader { geo -> Color in + DispatchQueue.main.async { + scrollViewContentSize = geo.size + let layoutFrame = UIApplication.shared.windows[0].safeAreaLayoutGuide.layoutFrame + disableScrolling = scrollViewContentSize.height + menuButtonHeight * 2 + 10 < layoutFrame.height + } + return Color.clear + } + } + } + .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 { + userPickerVisible.toggle() + } + } + } + } + .clipShape(RoundedRectangle(cornerRadius: 16)) + .background( + Rectangle() + .fill(fillColor) + .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) + .onChange(of: [chatModel.currentUser?.chatViewName, chatModel.currentUser?.image] ) { _ in + reloadCurrentUser() + } + .opacity(userPickerVisible ? 1.0 : 0.0) + .onAppear { + reloadUsers() + } + } + + private func reloadCurrentUser() { + if let updatedUser = chatModel.currentUser, let index = chatModel.users.firstIndex(where: { $0.user.userId == updatedUser.userId }) { + let removed = chatModel.users.remove(at: index) + chatModel.users.insert(UserInfo(user: updatedUser, unreadCount: removed.unreadCount), at: 0) + } + } + + private func changeActiveUser(toUser: UserInfo) { + Task { + do { + let activeUser = try apiSetActiveUser(toUser.user.userId) + let oldActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == chatModel.currentUser?.userId })! + var oldActive = chatModel.users[oldActiveIndex] + oldActive.user.activeUser = false + chatModel.users[oldActiveIndex] = oldActive + + chatModel.currentUser = activeUser + let currentActiveIndex = chatModel.users.firstIndex(where: { $0.user.userId == activeUser.userId })! + let removed = chatModel.users.remove(at: currentActiveIndex) + chatModel.users.insert(UserInfo(user: activeUser, unreadCount: removed.unreadCount), at: 0) + chatModel.users = chatModel.users.map { $0 } + try getUserChatData(chatModel) + userPickerVisible = false + } catch { + logger.error("Unable to set active user: \(error.localizedDescription)") + } + } + } + + private func reloadUsers() { + Task { + chatModel.users = listUsers().sorted { one, two -> Bool in one.user.activeUser } + } + } + + private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { + Button(action: action) { + HStack(spacing: 0) { + Text(title) + .overlay(DetermineWidth()) + Spacer() + Image(systemName: icon) +// .frame(width: 24, alignment: .center) + } + .padding(.horizontal) + .padding(.vertical, 22) + .frame(height: menuButtonHeight) + } + .buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill))) + } +} + +func unreadCounter(_ unread: Int64) -> some View { + unreadCountText(Int(truncatingIfNeeded: unread)) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 4) + .frame(minWidth: 18, minHeight: 18) + .background(Color.accentColor) + .cornerRadius(10) +} + +struct UserPicker_Previews: PreviewProvider { + static var previews: some View { + let m = ChatModel() + m.users = [UserInfo.sampleData, UserInfo.sampleData] + return UserPicker( + showSettings: Binding.constant(false), + userPickerVisible: Binding.constant(true) + ) + .environmentObject(m) + } +} diff --git a/apps/ios/Shared/Views/Helpers/PressedButtonStyle.swift b/apps/ios/Shared/Views/Helpers/PressedButtonStyle.swift new file mode 100644 index 0000000000..d8c7cd4ce6 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/PressedButtonStyle.swift @@ -0,0 +1,15 @@ +// +// Created by Avently on 16.01.2023. +// Copyright (c) 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct PressedButtonStyle: ButtonStyle { + var defaultColor: Color + var pressedColor: Color + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .background(configuration.isPressed ? pressedColor : defaultColor) + } +} diff --git a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift b/apps/ios/Shared/Views/UserSettings/SettingsButton.swift deleted file mode 100644 index 7292fd4373..0000000000 --- a/apps/ios/Shared/Views/UserSettings/SettingsButton.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// SettingsButton.swift -// SimpleX -// -// Created by Evgeny Poberezkin on 31/01/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -struct SettingsButton: View { - @EnvironmentObject var chatModel: ChatModel - @State private var showSettings = false - - var body: some View { - Button { showSettings = true } label: { - Image(systemName: "gearshape") - } - .sheet(isPresented: $showSettings, content: { - SettingsView(showSettings: $showSettings) - }) - } -} - -struct SettingsButton_Previews: PreviewProvider { - static var previews: some View { - SettingsButton() - } -} diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift new file mode 100644 index 0000000000..a6ee3302c2 --- /dev/null +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -0,0 +1,56 @@ +// +// Created by Avently on 17.01.2023. +// Copyright (c) 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +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 showScanSMPServer = false + @State private var testing = false + + var body: some View { + List { + Section("Your profiles") { + ForEach(Array($m.users.enumerated()), id: \.0) { i, userInfo in + userProfileView(userInfo, index: i) + .deleteDisabled(userInfo.wrappedValue.user.activeUser) + } + .onDelete { indexSet in + do { + try apiDeleteUser(m.users[indexSet.first!].user.userId) + m.users.remove(atOffsets: indexSet) + } catch { + fatalError("Failed to delete user: \(responseError(error))") + } + } + NavigationLink(destination: CreateProfile(), tag: true, selection: $showAddUser) { + Text("Add profile…") + } + } + } + .toolbar { EditButton() } + } + + private func userProfileView(_ userBinding: Binding, index: Int) -> some View { + let user = userBinding.wrappedValue.user + return NavigationLink(tag: index, selection: $selectedUser) { +// UserPrefs(user: userBinding, index: index) +// .navigationBarTitle(user.chatViewName) +// .navigationBarTitleDisplayMode(.large) + } label: { + Text(user.chatViewName) + } + } +} + +struct UserProfilesView_Previews: PreviewProvider { + static var previews: some View { + UserProfilesView() + } +} diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2fa90fbc68..3f37d768f6 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -7,7 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 1841538E296606C74533367C /* UserPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415835CBD939A9ABDC108A /* UserPicker.swift */; }; + 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */; }; 1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841511920742C6E152E469F /* AnimatedImageView.swift */; }; + 18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */; }; 3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C714776281C081000CB4D4B /* WebRTCView.swift */; }; 3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; }; 3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; }; @@ -95,7 +98,6 @@ 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; }; - 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D327A853F100ACCCDD /* SettingsButton.swift */; }; 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; }; @@ -222,6 +224,9 @@ /* Begin PBXFileReference section */ 1841511920742C6E152E469F /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = ""; }; + 18415835CBD939A9ABDC108A /* UserPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPicker.swift; sourceTree = ""; }; + 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfilesView.swift; sourceTree = ""; }; + 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PressedButtonStyle.swift; sourceTree = ""; }; 3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = ""; }; 3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = ""; }; 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = ""; }; @@ -321,7 +326,6 @@ 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = ""; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = ""; }; - 5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = ""; }; 5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = ""; }; 5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = ""; }; @@ -536,6 +540,7 @@ 5C6BA666289BD954009B8ECC /* DismissSheets.swift */, 5C00164328A26FBC0094D739 /* ContextMenu.swift */, 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */, + 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */, ); path = Helpers; sourceTree = ""; @@ -623,7 +628,6 @@ 5CB924DF27A8678B00ACCCDD /* UserSettings */ = { isa = PBXGroup; children = ( - 5CB924D327A853F100ACCCDD /* SettingsButton.swift */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */, 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, @@ -642,6 +646,7 @@ 5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, + 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, ); path = UserSettings; sourceTree = ""; @@ -656,6 +661,7 @@ 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */, 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, + 18415835CBD939A9ABDC108A /* UserPicker.swift */, ); path = ChatList; sourceTree = ""; @@ -1053,7 +1059,6 @@ 5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */, 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */, 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, - 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */, 3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */, 6440CA00288857A10062C672 /* CIEventView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, @@ -1073,6 +1078,9 @@ 644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */, 1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */, 5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */, + 1841538E296606C74533367C /* UserPicker.swift in Sources */, + 18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */, + 1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme index 8b60dc4fae..6a1d4192e6 100644 --- a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX (iOS).xcscheme @@ -44,7 +44,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - enableAddressSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 45adff73dd..8725deede2 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -15,6 +15,9 @@ let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) + case listUsers + case apiSetActiveUser(userId: Int64) + case apiDeleteUser(userId: Int64) case startChat(subscribe: Bool, expire: Bool) case apiStopChat case apiActivateChat @@ -96,6 +99,9 @@ public enum ChatCommand { switch self { case .showActiveUser: return "/u" case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)" + case .listUsers: return "/users" + case let .apiSetActiveUser(userId): return "/_user \(userId)" + case let .apiDeleteUser(userId): return "/_delete user \(userId)" case let .startChat(subscribe, expire): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire))" case .apiStopChat: return "/_stop" case .apiActivateChat: return "/_app activate" @@ -184,6 +190,9 @@ public enum ChatCommand { switch self { case .showActiveUser: return "showActiveUser" case .createActiveUser: return "createActiveUser" + case .listUsers: return "listUsers" + case .apiSetActiveUser: return "apiSetActiveUser" + case .apiDeleteUser: return "apiDeleteUser" case .startChat: return "startChat" case .apiStopChat: return "apiStopChat" case .apiActivateChat: return "apiActivateChat" @@ -302,6 +311,7 @@ struct APIResponse: Decodable { public enum ChatResponse: Decodable, Error { case response(type: String, json: String) case activeUser(user: User) + case usersList(users: [UserInfo]) case chatStarted case chatRunning case chatStopped @@ -408,6 +418,7 @@ public enum ChatResponse: Decodable, Error { switch self { case let .response(type, _): return "* \(type)" case .activeUser: return "activeUser" + case .usersList: return "usersList" case .chatStarted: return "chatStarted" case .chatRunning: return "chatRunning" case .chatStopped: return "chatStopped" @@ -514,6 +525,7 @@ public enum ChatResponse: Decodable, Error { switch self { case let .response(_, json): return json case let .activeUser(user): return String(describing: user) + case let .usersList(users): return String(describing: users) case .chatStarted: return noDetails case .chatRunning: return noDetails case .chatStopped: return noDetails diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 2902de89d5..22007d7b2c 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -9,19 +9,21 @@ import Foundation import SwiftUI -public struct User: Decodable, NamedChat { +public struct User: Decodable, NamedChat, Identifiable { public var userId: Int64 var userContactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile public var fullPreferences: FullPreferences - var activeUser: Bool + public var activeUser: Bool public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } public var localAlias: String { get { "" } } + public var id: Int64 { userId } + public static let sampleData = User( userId: 1, userContactId: 1, @@ -32,6 +34,21 @@ public struct User: Decodable, NamedChat { ) } +public struct UserInfo: Decodable { + public var user: User + public var unreadCount: Int64 + + public init(user: User, unreadCount: Int64) { + self.user = user + self.unreadCount = unreadCount + } + + public static let sampleData = UserInfo( + user: User.sampleData, + unreadCount: 1 + ) +} + public typealias ContactName = String public typealias GroupName = String