From 0c007f8db0684a2715d0acf7b393521e4bdd37cb Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 11 Dec 2024 22:02:30 +0000 Subject: [PATCH] ios: chat tags poc --- apps/ios/Shared/Model/ChatModel.swift | 38 ++++ apps/ios/Shared/Model/SimpleXAPI.swift | 31 +++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 2 +- .../Views/ChatList/ChatListNavLink.swift | 195 +++++++++++++++++- .../Shared/Views/ChatList/ChatListView.swift | 120 ++++++++++- .../Shared/Views/NewChat/NewChatView.swift | 1 + apps/ios/SimpleXChat/APITypes.swift | 31 +++ apps/ios/SimpleXChat/ChatTypes.swift | 12 +- 8 files changed, 418 insertions(+), 12 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 6b6b0ac03f..7ee80a6176 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -100,6 +100,44 @@ class ItemsModel: ObservableObject { } } +class ChatTagsModel: ObservableObject { + static let shared = ChatTagsModel() + + let presetTags: [ChatTagFilter] = [ + .presetTag(icon: "star", activeIcon: "star.fill", filter: { c in c.chatSettings?.favorite ?? false }), + .presetTag(icon: "person", activeIcon: "person.fill", filter: { if case .direct = $0 { true } else { false }}), + .presetTag(icon: "person.2", activeIcon: "person.2.fill", filter: { filterGroupChat($0) }), + .presetTag(icon: "briefcase", activeIcon: "briefcase.fill", filter: { filterBusinessChat($0) }) + ] + + @Published var tags: [ChatTagFilter] = [] + @Published var selectedTag: ChatTagFilter? +} + +private func filterBusinessChat(_ cInfo: ChatInfo) -> Bool { + if case let .group(gInfo) = cInfo { + return switch gInfo.businessChat?.chatType { + case .none: false + case .business: true + case .customer: true + } + } else { + return false + } +} + +private func filterGroupChat(_ cInfo: ChatInfo) -> Bool { + if case let .group(gInfo) = cInfo { + return switch gInfo.businessChat?.chatType { + case .none: true + case .business: false + case .customer: false + } + } else { + return false + } +} + class NetworkModel: ObservableObject { // map of connections network statuses, key is agent connection id @Published var networkStatuses: Dictionary = [:] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 51be3191ec..9af3101c9a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -313,6 +313,13 @@ func apiGetChatsAsync() async throws -> [ChatData] { return try apiChatsResponse(await chatSendCmd(.apiGetChats(userId: userId))) } +func apiGetChatTags() async throws -> [ChatTag] { + let userId = try currentUserId("apiGetChatTags") + let r = await chatSendCmd(.apiGetChatTags(userId: userId)) + if case let .chatTags(_, chatTags) = r { return chatTags } + throw r +} + private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { if case let .apiChats(_, chats) = r { return chats } throw r @@ -368,6 +375,30 @@ func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: Ch return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) } +func apiCreateChatTag(type: ChatType, id: Int64, tag: ChatTagData) async throws -> ([ChatTag], [Int64]) { + let r = await chatSendCmd(.apiCreateChatTag(type: type, id: id, tag: tag)) + if case let .tagsUpdated(_, userTags, chatTags) = r { + return (userTags, chatTags) + } + throw r +} + +func apiTagChat(type: ChatType, id: Int64, tagId: Int64) async throws -> ([ChatTag], [Int64]) { + let r = await chatSendCmd(.apiTagChat(type: type, id: id, tagId: tagId)) + if case let .tagsUpdated(_, userTags, chatTags) = r { + return (userTags, chatTags) + } + throw r +} + +func apiUntagChat(type: ChatType, id: Int64, tagId: Int64) async throws -> ([ChatTag], [Int64]) { + let r = await chatSendCmd(.apiUntagChat(type: type, id: id, tagId: tagId)) + if case let .chatUntagged(_, userTags, chatTags) = r { + return (userTags, chatTags) + } + throw r +} + func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? { let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages) return await processSendMessageCmd(toChatType: type, cmd: cmd) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 9b6b9b73e8..1c3203920a 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -332,7 +332,7 @@ struct ChatInfoView: View { .sheet(item: $sheet) { if #available(iOS 16.0, *) { $0.content - .presentationDetents([.fraction(0.4)]) + .presentationDetents([.fraction($0.fraction)]) } else { $0.content } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 6c5dad1f74..7d08a1a091 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -122,6 +122,7 @@ struct ChatListNavLink: View { markReadButton() toggleFavoriteButton() toggleNtfsButton(chat: chat) + tagChatButton(chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !chat.chatItems.isEmpty { @@ -140,6 +141,7 @@ struct ChatListNavLink: View { deleteLabel } .tint(.red) + untagChatButton(chat) } .frame(height: dynamicRowHeight) } @@ -149,7 +151,7 @@ struct ChatListNavLink: View { .sheet(item: $sheet) { if #available(iOS 16.0, *) { $0.content - .presentationDetents([.fraction(0.4)]) + .presentationDetents([.fraction($0.fraction)]) } else { $0.content } @@ -306,7 +308,41 @@ struct ChatListNavLink: View { } .tint(Color.orange) } + + private func tagChatButton(_ chat: Chat) -> some View { + Button { + sheet = SomeSheet( + content: { + AnyView( + ChatListTag(chat: chat) + ) + }, + id: "tag sheet", + fraction: 0.7 + ) + } label: { + SwipeLabel(NSLocalizedString("Tag", comment: "swipe action"), systemImage: "tag.fill", inverted: oneHandUI) + } + .tint(Color.indigo) + } + private func untagChatButton(_ chat: Chat) -> some View { + Button { + sheet = SomeSheet( + content: { + AnyView( + ChatListTag(chat: chat, untag: true) + ) + }, + id: "tag sheet", + fraction: 0.7 + ) + } label: { + SwipeLabel(NSLocalizedString("Untag", comment: "swipe action"), systemImage: "tag.fill", inverted: oneHandUI) + } + .tint(Color.yellow.opacity(0.8)) + } + private func clearNoteFolderButton() -> some View { Button { AlertManager.shared.showAlert(clearNoteFolderAlert()) @@ -484,6 +520,163 @@ struct ChatListNavLink: View { } } +struct ChatListTag: View { + var chat: Chat + var untag: Bool = false + @EnvironmentObject var chatTagsModel: ChatTagsModel + @EnvironmentObject var m: ChatModel + @State private var emoji: String = "" + @State private var name: String = "" + + var chatTagsIds: [Int64] { chat.chatInfo.contact?.chatTags ?? chat.chatInfo.groupInfo?.chatTags ?? [] } + + var body: some View { + List { + if untag { + untagView() + } else { + tagView() + } + } + } + + @ViewBuilder private func tagView() -> some View { + Section { + TextField("Emoji..", text: $emoji) + TextField("Tag name...", text: $name) + Button { + createChatTag() + } label: { + Text("Create tag") + } + } + + let tagsToPick = chatTagsModel.tags.compactMap { tag in + if case let .chatTag(emoji, text, tagId) = tag, !chatTagsIds.contains(tagId) { + return (emoji, text, tagId) + } else { + return nil + } + } + + if !tagsToPick.isEmpty { + Section { + ForEach(tagsToPick, id: \.0) { tag in + let (emoji, text, tagId) = tag + Button { + tagChat(tagId) + } label: { + Text("\(emoji) \(text)") + } + } + } header: { + Text("Choose existing") + } + } + } + + @ViewBuilder private func untagView() -> some View { + Section { + ForEach(chatTagsModel.tags) { tag in + if case let .chatTag(emoji, text, tagId) = tag, chatTagsIds.contains(tagId) { + Button { + untagChat(tagId) + } label: { + Text("\(emoji) \(text)") + } + } else { + EmptyView() + } + } + } header: { + Text("Choose existing") + } + } + + private func createChatTag() { + Task { + do { + let (userTags, chatTags) = try await apiCreateChatTag( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + tag: ChatTagData(emoji: emoji, text: name) + ) + + await MainActor.run { + chatTagsModel.tags = userTags.map { + .chatTag(emoji: $0.chatTagEmoji, text: $0.chatTagText, tagId: $0.chatTagId) + } + updateChatTags(chat: chat, chatTags: chatTags) + } + } catch let error { + showAlert( + NSLocalizedString("Error creating tag", comment: "alert title"), + message: responseError(error) + ) + } + } + } + + private func tagChat(_ tagId: Int64) { + Task { + do { + let (userTags, chatTags) = try await apiTagChat( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + tagId: tagId + ) + + await MainActor.run { + chatTagsModel.tags = userTags.map { + .chatTag(emoji: $0.chatTagEmoji, text: $0.chatTagText, tagId: $0.chatTagId) + } + updateChatTags(chat: chat, chatTags: chatTags) + } + } catch let error { + showAlert( + NSLocalizedString("Error tagging chat", comment: "alert title"), + message: responseError(error) + ) + } + } + } + + private func untagChat(_ tagId: Int64) { + Task { + do { + let (userTags, chatTags) = try await apiUntagChat( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + tagId: tagId + ) + + await MainActor.run { + chatTagsModel.tags = userTags.map { + .chatTag(emoji: $0.chatTagEmoji, text: $0.chatTagText, tagId: $0.chatTagId) + } + updateChatTags(chat: chat, chatTags: chatTags) + } + } catch let error { + showAlert( + NSLocalizedString("Error untagging chat", comment: "alert title"), + message: responseError(error) + ) + } + + } + } + + private func updateChatTags(chat: Chat, chatTags: [Int64]) { + if var contact = chat.chatInfo.contact { + contact.chatTags = chatTags + m.updateContact(contact) + } else if var group = chat.chatInfo.groupInfo { + group.chatTags = chatTags + m.updateGroup(group) + } + } +} + func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { Alert( title: Text("Reject contact request"), diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index b18e9295b9..d3fbb4b20c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -117,6 +117,7 @@ struct ChatListView: View { @State private var searchChatFilteredBySimplexLink: String? = nil @State private var scrollToSearchBar = false @State private var userPickerShown: Bool = false + @StateObject private var chatTagsModel = ChatTagsModel.shared @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true @@ -144,6 +145,7 @@ struct ChatListView: View { destination: chatView ) { chatListView } } + .environmentObject(chatTagsModel) .modifier( Sheet(isPresented: $userPickerShown) { UserPicker(userPickerShown: $userPickerShown, activeSheet: $activeUserPickerSheet) @@ -394,13 +396,13 @@ struct ChatListView: View { let s = searchString() return s == "" && !showUnreadAndFavorites ? chatModel.chats.filter { chat in - !chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card + filterByTag(chat) && !chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card } : chatModel.chats.filter { chat in let cInfo = chat.chatInfo switch cInfo { case let .direct(contact): - return !contact.chatDeleted && chatContactType(chat: chat) != ContactType.card && ( + return filterByTag(chat) && !contact.chatDeleted && chatContactType(chat: chat) != ContactType.card && ( s == "" ? filtered(chat) : (viewNameContains(cInfo, s) || @@ -409,29 +411,50 @@ struct ChatListView: View { ) case let .group(gInfo): return s == "" - ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited) - : viewNameContains(cInfo, s) + ? filterByTag(chat) && (filtered(chat) || gInfo.membership.memberStatus == .memInvited) + : filterByTag(chat) && viewNameContains(cInfo, s) case .local: - return s == "" || viewNameContains(cInfo, s) + return filterByTag(chat) && (s == "" || viewNameContains(cInfo, s)) case .contactRequest: - return s == "" || viewNameContains(cInfo, s) + return filterByTag(chat) && (s == "" || viewNameContains(cInfo, s)) case let .contactConnection(conn): - return s != "" && conn.localAlias.localizedLowercase.contains(s) + return filterByTag(chat) && (s != "" && conn.localAlias.localizedLowercase.contains(s)) case .invalidJSON: return false } } } - + func searchString() -> String { searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase } - + func filtered(_ chat: Chat) -> Bool { (chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadChat || (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) } + + func filterByTag(_ chat: Chat) -> Bool { + if let tag = chatTagsModel.selectedTag { + switch tag { + case let .presetTag(_, _, filter): + return filter(chat.chatInfo) + case let .chatTag(_, _, tagId): + let cInfo = chat.chatInfo + switch cInfo { + case let .direct(contact): + return contact.chatTags.contains(tagId) + case let .group(gInfo): + return gInfo.chatTags.contains(tagId) + default: + return false + } + } + } else { + return true + } + } func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { cInfo.chatViewName.localizedLowercase.contains(s) @@ -500,6 +523,7 @@ struct SubsStatusIndicator: View { struct ChatListSearchBar: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatTagsModel: ChatTagsModel @Binding var searchMode: Bool @FocusState.Binding var searchFocussed: Bool @Binding var searchText: String @@ -512,6 +536,7 @@ struct ChatListSearchBar: View { var body: some View { VStack(spacing: 12) { + ScrollView([.horizontal], showsIndicators: false) { ChatTagsView() } HStack(spacing: 12) { HStack(spacing: 4) { Image(systemName: "magnifyingglass") @@ -605,6 +630,83 @@ struct ChatListSearchBar: View { } } +enum ChatTagFilter: Identifiable, Equatable { + static func == (lhs: ChatTagFilter, rhs: ChatTagFilter) -> Bool { + switch (lhs, rhs) { + case let (.presetTag(icon1, activeIcon1, _), .presetTag(icon2, activeIcon2, _)): + return icon1 == icon2 && activeIcon1 == activeIcon2 + case let (.chatTag(emoji1, text1, tagId1), .chatTag(emoji2, text2, tagId2)): + return emoji1 == emoji2 && text1 == text2 && tagId1 == tagId2 + default: + return false + } + } + + case presetTag(icon: String, activeIcon: String, filter: (_ cInfo: ChatInfo) -> Bool) + case chatTag(emoji: String, text: String, tagId: Int64) + + public var id: String { + switch self { + case let .presetTag(icon, _, _): "preset \(icon)" + case let .chatTag(emoji, _, _): "chatTag \(emoji)" + } + } +} + +struct ChatTagsView: View { + @EnvironmentObject var chatTagsModel: ChatTagsModel + + var body: some View { + let tags = chatTagsModel.presetTags + chatTagsModel.tags + HStack { + ForEach(tags, id: \.id) { tag in + let current = chatTagsModel.selectedTag == tag + let color: Color = current ? .accentColor : .secondary + ZStack { + switch tag { + case let .presetTag(icon, activeIcon, _): + Image(systemName: current ? activeIcon : icon) + .foregroundColor(color) + case let .chatTag(emoji, text, _): + HStack(spacing: 4) { + Text(emoji) + ZStack { + Text(text).fontWeight(.medium).foregroundColor(.clear) + Text(text).fontWeight(current ? .medium : .regular).foregroundColor(color) + } + } + } + } + .onTapGesture { + if (chatTagsModel.selectedTag == tag) { + chatTagsModel.selectedTag = nil + } else { + chatTagsModel.selectedTag = tag + } + } + } + }.task { + getChatTags() + } + } + + private func getChatTags() { + Task { + do { + let chatTags = try await apiGetChatTags() + + await MainActor.run { + self.chatTagsModel.tags = chatTags.map { + .chatTag(emoji: $0.chatTagEmoji, text: $0.chatTagText, tagId: $0.chatTagId) + } + } + } catch let error { + AlertManager.shared.showAlertMsg(title: "Error", message: "\(responseError(error))") + } + } + } +} + func chatStoppedIcon() -> some View { Button { AlertManager.shared.showAlertMsg( diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index e18d932278..6e898f4cdf 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -25,6 +25,7 @@ struct SomeActionSheet: Identifiable { struct SomeSheet: Identifiable { @ViewBuilder var content: Content var id: String + var fraction = 0.4 } private enum NewChatViewAlert: Identifiable { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 884993f542..7f8913debe 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -40,10 +40,14 @@ public enum ChatCommand { case testStorageEncryption(key: String) case apiSaveSettings(settings: AppSettings) case apiGetSettings(settings: AppSettings) + case apiGetChatTags(userId: Int64) case apiGetChats(userId: Int64) case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage]) + case apiCreateChatTag(type: ChatType, id: Int64, tag: ChatTagData) + case apiTagChat(type: ChatType, id: Int64, tagId: Int64) + case apiUntagChat(type: ChatType, id: Int64, tagId: Int64) case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage]) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) @@ -198,6 +202,7 @@ public enum ChatCommand { case let .testStorageEncryption(key): return "/db test key \(key)" case let .apiSaveSettings(settings): return "/_save app settings \(encodeJSON(settings))" case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))" + case let .apiGetChatTags(userId): return "/_get tags \(userId)" case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") @@ -206,6 +211,9 @@ public enum ChatCommand { let msgs = encodeJSON(composedMessages) let ttlStr = ttl != nil ? "\(ttl!)" : "default" return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)" + case let .apiCreateChatTag(type, id, tag): return "/_create tag \(ref(type, id)) \(encodeJSON(tag))" + case let .apiTagChat(type, id, tagId): return "/_tag \(ref(type, id)) \(tagId)" + case let .apiUntagChat(type, id, tagId): return "/_untag \(ref(type, id)) \(tagId)" case let .apiCreateChatItems(noteFolderId, composedMessages): let msgs = encodeJSON(composedMessages) return "/_create *\(noteFolderId) json \(msgs)" @@ -367,10 +375,14 @@ public enum ChatCommand { case .testStorageEncryption: return "testStorageEncryption" case .apiSaveSettings: return "apiSaveSettings" case .apiGetSettings: return "apiGetSettings" + case .apiGetChatTags: return "apiGetChatTags" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" case .apiGetChatItemInfo: return "apiGetChatItemInfo" case .apiSendMessages: return "apiSendMessages" + case .apiCreateChatTag: return "apiCreateChatTag" + case .apiTagChat: return "apiTagChat" + case .apiUntagChat: return "apiUntagChat" case .apiCreateChatItems: return "apiCreateChatItems" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" @@ -564,6 +576,7 @@ public enum ChatResponse: Decodable, Error { case chatSuspended case apiChats(user: UserRef, chats: [ChatData]) case apiChat(user: UserRef, chat: ChatData) + case chatTags(user: UserRef, userTags: [ChatTag]) case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) case serverOperatorConditions(conditions: ServerOperatorConditions) @@ -590,6 +603,8 @@ public enum ChatResponse: Decodable, Error { case contactCode(user: UserRef, contact: Contact, connectionCode: String) case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) + case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) + case chatUntagged(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) @@ -741,6 +756,7 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return "chatSuspended" case .apiChats: return "apiChats" case .apiChat: return "apiChat" + case .chatTags: return "chatTags" case .chatItemInfo: return "chatItemInfo" case .serverTestResult: return "serverTestResult" case .serverOperatorConditions: return "serverOperators" @@ -767,6 +783,8 @@ public enum ChatResponse: Decodable, Error { case .contactCode: return "contactCode" case .groupMemberCode: return "groupMemberCode" case .connectionVerified: return "connectionVerified" + case .tagsUpdated: return "tagsUpdated" + case .chatUntagged: return "chatUntagged" case .invitation: return "invitation" case .connectionIncognitoUpdated: return "connectionIncognitoUpdated" case .connectionUserChanged: return "connectionUserChanged" @@ -914,6 +932,7 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return noDetails case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat): return withUser(u, String(describing: chat)) + case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))") case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))" @@ -942,6 +961,8 @@ public enum ChatResponse: Decodable, Error { case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)") case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") + case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") + case let .chatUntagged(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)") case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\newUserId: \(String(describing: newUser.userId))") @@ -1172,6 +1193,16 @@ public enum ChatPagination { } } +public struct ChatTagData: Encodable { + public var emoji: String + public var text: String + + public init(emoji: String, text: String) { + self.emoji = emoji + self.text = text + } +} + public struct ComposedMessage: Encodable { public var fileSource: CryptoFile? var quotedItemId: Int64? diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index da1ce24b73..537e28dfa3 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1545,6 +1545,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool + public var chatTags: [Int64] public var uiThemes: ThemeModeOverrides? public var chatDeleted: Bool @@ -1615,6 +1616,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { createdAt: .now, updatedAt: .now, contactGrpInvSent: false, + chatTags: [], chatDeleted: false ) } @@ -1910,6 +1912,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } public var localAlias: String { "" } + public var chatTags: [Int64] public var isOwner: Bool { return membership.memberRole == .owner && membership.memberCurrent @@ -1932,7 +1935,8 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { hostConnCustomUserProfileId: nil, chatSettings: ChatSettings.defaults, createdAt: .now, - updatedAt: .now + updatedAt: .now, + chatTags: [] ) } @@ -4210,6 +4214,12 @@ public enum ChatItemTTL: Identifiable, Comparable, Hashable { } } +public struct ChatTag: Decodable, Hashable { + public var chatTagId: Int64 + public var chatTagText: String + public var chatTagEmoji: String +} + public struct ChatItemInfo: Decodable, Hashable { public var itemVersions: [ChatItemVersion] public var memberDeliveryStatuses: [MemberDeliveryStatus]?