ios: menu to switch active user profile (#1758)

* ios: User chooser UI

* Change

* Changes

* update view

* fix layout/refactor

* fix preview

* wider menu, update label

* hide Your profiles button

* Clickable background that hides userChooser

* No click listener

* Better animation

* Disabled scrolling for small number of items

* Separated scrollview and buttons

* No transition

* Re-indent

* Limiting width by the longest label

* UserManagerView

* Adapted API

* Hide user chooser after selection

* Top counter,  users refactor

* Padding

* use VStack to fix layout bug

* eol

* rename: rename to getUserChatData

* update layout

* s/semibold/medium

* remove SettingsButton view

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2023-01-17 17:47:37 +00:00
committed by GitHub
parent a668bd5736
commit 153f80fe64
11 changed files with 382 additions and 53 deletions

View File

@@ -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

View File

@@ -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<Void, Never>?
private var receiveMessages = true

View File

@@ -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(),

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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<UserInfo>, 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()
}
}

View File

@@ -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 = "<group>"; };
18415835CBD939A9ABDC108A /* UserPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserPicker.swift; sourceTree = "<group>"; };
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfilesView.swift; sourceTree = "<group>"; };
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PressedButtonStyle.swift; sourceTree = "<group>"; };
3C714776281C081000CB4D4B /* WebRTCView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCView.swift; sourceTree = "<group>"; };
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
@@ -321,7 +326,6 @@
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = "<group>"; };
5CB924D327A853F100ACCCDD /* SettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButton.swift; sourceTree = "<group>"; };
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
@@ -536,6 +540,7 @@
5C6BA666289BD954009B8ECC /* DismissSheets.swift */,
5C00164328A26FBC0094D739 /* ContextMenu.swift */,
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */,
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */,
);
path = Helpers;
sourceTree = "<group>";
@@ -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 = "<group>";
@@ -656,6 +661,7 @@
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */,
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
18415835CBD939A9ABDC108A /* UserPicker.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -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;
};

View File

@@ -44,7 +44,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableAddressSanitizer = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -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

View File

@@ -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