From 9adc3cf885edc0089f903ffde3536c2206fedf4c Mon Sep 17 00:00:00 2001 From: Toomas Vahter Date: Wed, 11 Dec 2024 10:05:04 +0200 Subject: [PATCH] Demo app changes for pinned and archived channels (#683) * Use native pinning support for channels * Add archive and unarchive actions. Add predefined channel queries and action sheet for selecting different queries --- .../BlockedUsersView.swift | 0 .../BlockedUsersViewModel.swift | 0 .../ChannelListQueryIdentifier.swift | 26 ++++ .../ChooseChannelQueryView.swift | 21 ++++ .../CustomChannelHeader.swift | 13 +- .../{ => ChannelHeader}/NewChatView.swift | 0 .../NewChatViewModel.swift | 0 DemoAppSwiftUI/DemoAppSwiftUIApp.swift | 116 +++++++++++++----- DemoAppSwiftUI/PinChannelHelpers.swift | 12 -- DemoAppSwiftUI/ViewFactoryExamples.swift | 59 +++++++-- StreamChatSwiftUI.xcodeproj/project.pbxproj | 30 +++-- 11 files changed, 217 insertions(+), 60 deletions(-) rename DemoAppSwiftUI/{ => ChannelHeader}/BlockedUsersView.swift (100%) rename DemoAppSwiftUI/{ => ChannelHeader}/BlockedUsersViewModel.swift (100%) create mode 100644 DemoAppSwiftUI/ChannelHeader/ChannelListQueryIdentifier.swift create mode 100644 DemoAppSwiftUI/ChannelHeader/ChooseChannelQueryView.swift rename DemoAppSwiftUI/{ => ChannelHeader}/CustomChannelHeader.swift (88%) rename DemoAppSwiftUI/{ => ChannelHeader}/NewChatView.swift (100%) rename DemoAppSwiftUI/{ => ChannelHeader}/NewChatViewModel.swift (100%) diff --git a/DemoAppSwiftUI/BlockedUsersView.swift b/DemoAppSwiftUI/ChannelHeader/BlockedUsersView.swift similarity index 100% rename from DemoAppSwiftUI/BlockedUsersView.swift rename to DemoAppSwiftUI/ChannelHeader/BlockedUsersView.swift diff --git a/DemoAppSwiftUI/BlockedUsersViewModel.swift b/DemoAppSwiftUI/ChannelHeader/BlockedUsersViewModel.swift similarity index 100% rename from DemoAppSwiftUI/BlockedUsersViewModel.swift rename to DemoAppSwiftUI/ChannelHeader/BlockedUsersViewModel.swift diff --git a/DemoAppSwiftUI/ChannelHeader/ChannelListQueryIdentifier.swift b/DemoAppSwiftUI/ChannelHeader/ChannelListQueryIdentifier.swift new file mode 100644 index 00000000..e01d7e51 --- /dev/null +++ b/DemoAppSwiftUI/ChannelHeader/ChannelListQueryIdentifier.swift @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation +import StreamChat + +enum ChannelListQueryIdentifier: String, CaseIterable, Identifiable { + case initial + case archived + case pinned + case unarchivedAndPinnedSorted + + var id: String { + rawValue + } + + var title: String { + switch self { + case .initial: "Initial Channels" + case .archived: "Archived Channels" + case .pinned: "Pinned Channels" + case .unarchivedAndPinnedSorted: "Sort by Pinned and Ignore Archived Channels" + } + } +} diff --git a/DemoAppSwiftUI/ChannelHeader/ChooseChannelQueryView.swift b/DemoAppSwiftUI/ChannelHeader/ChooseChannelQueryView.swift new file mode 100644 index 00000000..b78c9a00 --- /dev/null +++ b/DemoAppSwiftUI/ChannelHeader/ChooseChannelQueryView.swift @@ -0,0 +1,21 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import StreamChat +import StreamChatSwiftUI +import SwiftUI + +struct ChooseChannelQueryView: View { + static let queryIdentifiers = ChannelListQueryIdentifier.allCases.sorted(using: KeyPathComparator(\.title)) + + var body: some View { + ForEach(Self.queryIdentifiers) { queryIdentifier in + Button { + AppState.shared.setChannelQueryIdentifier(queryIdentifier) + } label: { + Text(queryIdentifier.title) + } + } + } +} diff --git a/DemoAppSwiftUI/CustomChannelHeader.swift b/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift similarity index 88% rename from DemoAppSwiftUI/CustomChannelHeader.swift rename to DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift index e18f21a0..e5f12ad6 100644 --- a/DemoAppSwiftUI/CustomChannelHeader.swift +++ b/DemoAppSwiftUI/ChannelHeader/CustomChannelHeader.swift @@ -53,6 +53,7 @@ struct CustomChannelModifier: ChannelListHeaderViewModifier { var title: String + @State var isChooseChannelQueryShown = false @State var isNewChatShown = false @State var logoutAlertShown = false @State var actionsPopupShown = false @@ -99,11 +100,14 @@ struct CustomChannelModifier: ChannelListHeaderViewModifier { ) } .confirmationDialog("", isPresented: $actionsPopupShown) { - Button("Blocked users") { + Button("Choose Channel Query") { + isChooseChannelQueryShown = true + } + Button("Show Blocked Users") { blockedUsersShown = true } - Button("Logout") { + Button("Logout", role: .destructive) { logoutAlertShown = true } @@ -111,6 +115,11 @@ struct CustomChannelModifier: ChannelListHeaderViewModifier { } message: { Text("Select an action") } + .confirmationDialog("", isPresented: $isChooseChannelQueryShown) { + ChooseChannelQueryView() + } message: { + Text("Choose a channel query") + } } } } diff --git a/DemoAppSwiftUI/NewChatView.swift b/DemoAppSwiftUI/ChannelHeader/NewChatView.swift similarity index 100% rename from DemoAppSwiftUI/NewChatView.swift rename to DemoAppSwiftUI/ChannelHeader/NewChatView.swift diff --git a/DemoAppSwiftUI/NewChatViewModel.swift b/DemoAppSwiftUI/ChannelHeader/NewChatViewModel.swift similarity index 100% rename from DemoAppSwiftUI/NewChatViewModel.swift rename to DemoAppSwiftUI/ChannelHeader/NewChatViewModel.swift diff --git a/DemoAppSwiftUI/DemoAppSwiftUIApp.swift b/DemoAppSwiftUI/DemoAppSwiftUIApp.swift index 4f3927c7..e7ab3f24 100644 --- a/DemoAppSwiftUI/DemoAppSwiftUIApp.swift +++ b/DemoAppSwiftUI/DemoAppSwiftUIApp.swift @@ -2,6 +2,7 @@ // Copyright © 2024 Stream.io Inc. All rights reserved. // +import Combine import StreamChat import StreamChatSwiftUI import SwiftUI @@ -39,25 +40,11 @@ struct DemoAppSwiftUIApp: App { .tabItem { Label("Threads", systemImage: "text.bubble") } .badge(appState.unreadCount.threads) } + .id(appState.contentIdentifier) } } .onChange(of: appState.userState) { newValue in if newValue == .loggedIn { - /* - if let currentUserId = chatClient.currentUserId { - let pinnedByKey = ChatChannel.isPinnedBy(keyForUserId: currentUserId) - let channelListQuery = ChannelListQuery( - filter: .containMembers(userIds: [currentUserId]), - sort: [ - .init(key: .custom(keyPath: \.isPinned, key: pinnedByKey), isAscending: true), - .init(key: .lastMessageAt), - .init(key: .updatedAt) - ] - ) - appState.channelListController = chatClient.channelListController(query: channelListQuery) - } - */ - appState.currentUserController = chatClient.currentUserController() notificationsHandler.setupRemoteNotifications() } } @@ -86,28 +73,55 @@ struct DemoAppSwiftUIApp: App { } class AppState: ObservableObject, CurrentChatUserControllerDelegate { + @Injected(\.chatClient) var chatClient: ChatClient - @Published var userState: UserState = .launchAnimation { - willSet { - if newValue == .notLoggedIn && userState == .loggedIn { - channelListController = nil - } - } - } - + // Recreate the content view when channel query changes. + @Published private(set) var contentIdentifier: String = "" + + @Published var userState: UserState = .launchAnimation @Published var unreadCount: UnreadCount = .noUnread - var channelListController: ChatChannelListController? - var currentUserController: CurrentChatUserController? { - didSet { - currentUserController?.delegate = self - currentUserController?.synchronize() - } - } + private(set) var channelListController: ChatChannelListController? + private(set) var currentUserController: CurrentChatUserController? + private var cancellables = Set() static let shared = AppState() - private init() {} + private init() { + $userState + .removeDuplicates() + .filter { $0 == .notLoggedIn } + .sink { [weak self] _ in + self?.didLogout() + } + .store(in: &cancellables) + $userState + .removeDuplicates() + .filter { $0 == .loggedIn } + .sink { [weak self] _ in + self?.didLogin() + } + .store(in: &cancellables) + } + + private func didLogout() { + channelListController = nil + currentUserController = nil + } + + private func didLogin() { + setChannelQueryIdentifier(.initial) + + currentUserController = chatClient.currentUserController() + currentUserController?.delegate = self + currentUserController?.synchronize() + } + + func setChannelQueryIdentifier(_ identifier: ChannelListQueryIdentifier) { + let query = AppState.channelListQuery(forIdentifier: identifier, chatClient: chatClient) + channelListController = chatClient.channelListController(query: query) + contentIdentifier = identifier.rawValue + } func currentUserController(_ controller: CurrentChatUserController, didChangeCurrentUserUnreadCount: UnreadCount) { unreadCount = didChangeCurrentUserUnreadCount @@ -125,3 +139,43 @@ enum UserState { case notLoggedIn case loggedIn } + +extension AppState { + private static func channelListQuery( + forIdentifier identifier: ChannelListQueryIdentifier, + chatClient: ChatClient + ) -> ChannelListQuery { + guard let currentUserId = chatClient.currentUserId else { fatalError("Not logged in") } + switch identifier { + case .initial: + return ChannelListQuery( + filter: .containMembers(userIds: [currentUserId]) + ) + case .unarchivedAndPinnedSorted: + return ChannelListQuery( + filter: .and([ + .containMembers(userIds: [currentUserId]), + .equal(.archived, to: false) + ]), + sort: [ + .init(key: .pinnedAt, isAscending: false), + .init(key: .default) + ] + ) + case .archived: + return ChannelListQuery( + filter: .and([ + .containMembers(userIds: [currentUserId]), + .equal(.archived, to: true) + ]) + ) + case .pinned: + return ChannelListQuery( + filter: .and([ + .containMembers(userIds: [currentUserId]), + .equal(.pinned, to: true) + ]) + ) + } + } +} diff --git a/DemoAppSwiftUI/PinChannelHelpers.swift b/DemoAppSwiftUI/PinChannelHelpers.swift index ceca3dba..73d11a23 100644 --- a/DemoAppSwiftUI/PinChannelHelpers.swift +++ b/DemoAppSwiftUI/PinChannelHelpers.swift @@ -6,18 +6,6 @@ import StreamChat import StreamChatSwiftUI import SwiftUI -extension ChatChannel { - static func isPinnedBy(keyForUserId userId: UserId) -> String { - "is_pinned_by_\(userId)" - } - - var isPinned: Bool { - guard let userId = membership?.id else { return false } - let key = Self.isPinnedBy(keyForUserId: userId) - return extraData[key]?.boolValue ?? false - } -} - struct DemoAppChatChannelListItem: View { @Injected(\.fonts) private var fonts diff --git a/DemoAppSwiftUI/ViewFactoryExamples.swift b/DemoAppSwiftUI/ViewFactoryExamples.swift index 90e37e15..34f986ed 100644 --- a/DemoAppSwiftUI/ViewFactoryExamples.swift +++ b/DemoAppSwiftUI/ViewFactoryExamples.swift @@ -31,7 +31,9 @@ class DemoAppFactory: ViewFactory { onDismiss: onDismiss, onError: onError ) + let archiveChannel = archiveChannelAction(for: channel, onDismiss: onDismiss, onError: onError) let pinChannel = pinChannelAction(for: channel, onDismiss: onDismiss, onError: onError) + actions.insert(archiveChannel, at: actions.count - 2) actions.insert(pinChannel, at: actions.count - 2) return actions } @@ -76,6 +78,40 @@ class DemoAppFactory: ViewFactory { ShowProfileModifier(messageModifierInfo: messageModifierInfo, mentionsHandler: mentionsHandler) } + private func archiveChannelAction( + for channel: ChatChannel, + onDismiss: @escaping () -> Void, + onError: @escaping (Error) -> Void + ) -> ChannelAction { + ChannelAction( + title: channel.isArchived ? "Unarchive Channel" : "Archive Channel", + iconName: "archivebox", + action: { [weak self] in + guard let self else { return } + let channelController = self.chatClient.channelController(for: channel.cid) + if channel.isArchived { + channelController.unarchive { error in + if let error = error { + onError(error) + } else { + onDismiss() + } + } + } else { + channelController.archive { error in + if let error = error { + onError(error) + } else { + onDismiss() + } + } + } + }, + confirmationPopup: nil, + isDestructive: false + ) + } + private func pinChannelAction( for channel: ChatChannel, onDismiss: @escaping () -> Void, @@ -87,14 +123,21 @@ class DemoAppFactory: ViewFactory { action: { [weak self] in guard let self else { return } let channelController = self.chatClient.channelController(for: channel.cid) - let userId = channelController.channel?.membership?.id ?? "" - let pinnedKey = ChatChannel.isPinnedBy(keyForUserId: userId) - let newState = !channel.isPinned - channelController.partialChannelUpdate(extraData: [pinnedKey: .bool(newState)]) { error in - if let error = error { - onError(error) - } else { - onDismiss() + if channel.isPinned { + channelController.unpin { error in + if let error = error { + onError(error) + } else { + onDismiss() + } + } + } else { + channelController.pin { error in + if let error = error { + onError(error) + } else { + onDismiss() + } } } }, diff --git a/StreamChatSwiftUI.xcodeproj/project.pbxproj b/StreamChatSwiftUI.xcodeproj/project.pbxproj index 1a73e0ab..978c0bc8 100644 --- a/StreamChatSwiftUI.xcodeproj/project.pbxproj +++ b/StreamChatSwiftUI.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 402C54492B6AAC0100672BFB /* StreamChatSwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8465FBB52746873A00AF091E /* StreamChatSwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4F077EF82C85E05700F06D83 /* DelayedRenderingViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */; }; 4F198FDD2C0480EC00148F49 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */; }; + 4F65F1862D06EEA7009F69A8 /* ChooseChannelQueryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F65F1852D06EEA5009F69A8 /* ChooseChannelQueryView.swift */; }; + 4F65F18A2D071798009F69A8 /* ChannelListQueryIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F65F1892D071798009F69A8 /* ChannelListQueryIdentifier.swift */; }; 4F6D83352C0F05040098C298 /* PollCommentsViewModel_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */; }; 4F6D83512C1079A00098C298 /* AlertBannerViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */; }; 4F6D83542C1094220098C298 /* AlertBannerViewModifier_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */; }; @@ -597,6 +599,8 @@ 4A65451E274BA170003C5FA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 4F077EF72C85E05700F06D83 /* DelayedRenderingViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedRenderingViewModifier.swift; sourceTree = ""; }; 4F198FDC2C0480EC00148F49 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; }; + 4F65F1852D06EEA5009F69A8 /* ChooseChannelQueryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseChannelQueryView.swift; sourceTree = ""; }; + 4F65F1892D071798009F69A8 /* ChannelListQueryIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelListQueryIdentifier.swift; sourceTree = ""; }; 4F6D83342C0F05040098C298 /* PollCommentsViewModel_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollCommentsViewModel_Tests.swift; sourceTree = ""; }; 4F6D83502C1079A00098C298 /* AlertBannerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier.swift; sourceTree = ""; }; 4F6D83532C1094220098C298 /* AlertBannerViewModifier_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertBannerViewModifier_Tests.swift; sourceTree = ""; }; @@ -1169,6 +1173,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4F65F18B2D0724F3009F69A8 /* ChannelHeader */ = { + isa = PBXGroup; + children = ( + 846B8E2B2C5B8117006A6249 /* BlockedUsersView.swift */, + 846B8E2D2C5B8130006A6249 /* BlockedUsersViewModel.swift */, + 4F65F1892D071798009F69A8 /* ChannelListQueryIdentifier.swift */, + 4F65F1852D06EEA5009F69A8 /* ChooseChannelQueryView.swift */, + 8465FCD7274694D200AF091E /* CustomChannelHeader.swift */, + 84335015274BABF3007A1B81 /* NewChatView.swift */, + 84335017274BAD4B007A1B81 /* NewChatViewModel.swift */, + ); + path = ChannelHeader; + sourceTree = ""; + }; 4F6D83522C108D470098C298 /* CommonViews */ = { isa = PBXGroup; children = ( @@ -1595,11 +1613,11 @@ 8465FCBD27468B6900AF091E /* DemoAppSwiftUI */ = { isa = PBXGroup; children = ( + 4F65F18B2D0724F3009F69A8 /* ChannelHeader */, 8492974727ABD97F00A8EEB0 /* DemoAppSwiftUI.entitlements */, 8465FCBE27468B6900AF091E /* DemoAppSwiftUIApp.swift */, 8465FCD3274694D200AF091E /* AppDelegate.swift */, 8465FCD6274694D200AF091E /* CustomAttachment.swift */, - 8465FCD7274694D200AF091E /* CustomChannelHeader.swift */, 8492974827ABDDBF00A8EEB0 /* NotificationsHandler.swift */, 8465FCD4274694D200AF091E /* DemoUser.swift */, 8465FCD2274694D200AF091E /* LaunchAnimationState.swift */, @@ -1607,8 +1625,6 @@ 8465FCD5274694D200AF091E /* SceneDelegate.swift */, 84B288D2274D23AF00DD090B /* LoginView.swift */, 84B288D4274D286500DD090B /* LoginViewModel.swift */, - 84335015274BABF3007A1B81 /* NewChatView.swift */, - 84335017274BAD4B007A1B81 /* NewChatViewModel.swift */, 84B288CC274C544B00DD090B /* CreateGroupView.swift */, 84B288D0274CEDD000DD090B /* GroupNameView.swift */, 84B288CE274C545900DD090B /* CreateGroupViewModel.swift */, @@ -1619,8 +1635,6 @@ 8451617F2AE7C4E2000A9230 /* WhatsAppChannelHeader.swift */, 8417AE912ADEDB6400445021 /* UserRepository.swift */, 8413C4542B4409B600190AF4 /* PinChannelHelpers.swift */, - 846B8E2B2C5B8117006A6249 /* BlockedUsersView.swift */, - 846B8E2D2C5B8130006A6249 /* BlockedUsersViewModel.swift */, 84EDBC36274FE5CD0057218D /* Localizable.strings */, 8465FCCA27468B7500AF091E /* Info.plist */, 8465FCC227468B6A00AF091E /* Assets.xcassets */, @@ -3036,6 +3050,7 @@ 845161802AE7C4E2000A9230 /* WhatsAppChannelHeader.swift in Sources */, 8465FCDC274694D200AF091E /* SceneDelegate.swift in Sources */, 8465FCD9274694D200AF091E /* LaunchAnimationState.swift in Sources */, + 4F65F18A2D071798009F69A8 /* ChannelListQueryIdentifier.swift in Sources */, 84B288D1274CEDD000DD090B /* GroupNameView.swift in Sources */, 84335014274BAB15007A1B81 /* ViewFactoryExamples.swift in Sources */, 8465FCDE274694D200AF091E /* CustomChannelHeader.swift in Sources */, @@ -3048,6 +3063,7 @@ 8465FCDB274694D200AF091E /* DemoUser.swift in Sources */, 8465FCDD274694D200AF091E /* CustomAttachment.swift in Sources */, 8492974B27ABDDCB00A8EEB0 /* NotificationsHandler.swift in Sources */, + 4F65F1862D06EEA7009F69A8 /* ChooseChannelQueryView.swift in Sources */, 8465FCDA274694D200AF091E /* AppDelegate.swift in Sources */, 8465FCD8274694D200AF091E /* LaunchScreen.swift in Sources */, 84B288CF274C545900DD090B /* CreateGroupViewModel.swift in Sources */, @@ -3823,8 +3839,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/GetStream/stream-chat-swift.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.68.0; + branch = develop; + kind = branch; }; }; E3A1C01A282BAC66002D1E26 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {