Skip to content

Commit

Permalink
Implemented pinning channels in the demo app
Browse files Browse the repository at this point in the history
  • Loading branch information
martinmitrevski committed Jan 2, 2024
1 parent b4b1d4c commit b908015
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 1 deletion.
18 changes: 17 additions & 1 deletion DemoAppSwiftUI/DemoAppSwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ struct DemoAppSwiftUIApp: App {

@ObservedObject var appState = AppState.shared
@ObservedObject var notificationsHandler = NotificationsHandler.shared

@State var channelListController: ChatChannelListController?

var body: some Scene {
WindowGroup {
Expand All @@ -26,17 +28,31 @@ struct DemoAppSwiftUIApp: App {
if notificationsHandler.notificationChannelId != nil {
ChatChannelListView(
viewFactory: DemoAppFactory.shared,
channelListController: channelListController,
selectedChannelId: notificationsHandler.notificationChannelId
)
} else {
ChatChannelListView(
viewFactory: DemoAppFactory.shared
viewFactory: DemoAppFactory.shared,
channelListController: channelListController
)
}
}
}
.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)
]
)
channelListController = chatClient.channelListController(query: channelListQuery)
}
notificationsHandler.setupRemoteNotifications()
}
}
Expand Down
182 changes: 182 additions & 0 deletions DemoAppSwiftUI/PinChannelHelpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//
// Copyright © 2023 Stream.io Inc. All rights reserved.
//

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
@Injected(\.colors) private var colors
@Injected(\.utils) private var utils
@Injected(\.images) private var images
@Injected(\.chatClient) private var chatClient

var channel: ChatChannel
var channelName: String
var injectedChannelInfo: InjectedChannelInfo?
var avatar: UIImage
var onlineIndicatorShown: Bool
var disabled = false
var onItemTap: (ChatChannel) -> Void

public var body: some View {
Button {
onItemTap(channel)
} label: {
HStack {
ChannelAvatarView(
avatar: avatar,
showOnlineIndicator: onlineIndicatorShown
)

VStack(alignment: .leading, spacing: 4) {
HStack {
ChatTitleView(name: channelName)

Spacer()

if injectedChannelInfo == nil && channel.unreadCount != .noUnread {
UnreadIndicatorView(
unreadCount: channel.unreadCount.messages
)
}
}

HStack {
subtitleView

Spacer()

HStack(spacing: 4) {
if shouldShowReadEvents {
MessageReadIndicatorView(
readUsers: channel.readUsers(
currentUserId: chatClient.currentUserId,
message: channel.latestMessages.first
),
showReadCount: false
)
}
SubtitleText(text: injectedChannelInfo?.timestamp ?? channel.timestampText)
.accessibilityIdentifier("timestampView")
}
}
}
}
.padding(.all, 8)
}
.foregroundColor(.black)
.disabled(disabled)
.background(channel.isPinned ? Color(colors.pinnedBackground) : .clear)
.id("\(channel.id)-\(channel.isPinned ? "pinned" : "not-pinned")-base")
}

private var subtitleView: some View {
HStack(spacing: 4) {
if let image = image {
Image(uiImage: image)
.customizable()
.frame(maxHeight: 12)
.foregroundColor(Color(colors.subtitleText))
} else {
if channel.shouldShowTypingIndicator {
TypingIndicatorView()
}
}
SubtitleText(text: injectedChannelInfo?.subtitle ?? channel.subtitleText)
Spacer()
}
.accessibilityIdentifier("subtitleView")
}

private var shouldShowReadEvents: Bool {
if let message = channel.latestMessages.first,
message.isSentByCurrentUser,
!message.isDeleted {
return channel.config.readEventsEnabled
}

return false
}

private var image: UIImage? {
if channel.isMuted {
return images.muted
}
return nil
}
}

struct DemoAppChatChannelNavigatableListItem<ChannelDestination: View>: View {
private var channel: ChatChannel
private var channelName: String
private var avatar: UIImage
private var disabled: Bool
private var onlineIndicatorShown: Bool
@Binding private var selectedChannel: ChannelSelectionInfo?
private var channelDestination: (ChannelSelectionInfo) -> ChannelDestination
private var onItemTap: (ChatChannel) -> Void

init(
channel: ChatChannel,
channelName: String,
avatar: UIImage,
onlineIndicatorShown: Bool,
disabled: Bool = false,
selectedChannel: Binding<ChannelSelectionInfo?>,
channelDestination: @escaping (ChannelSelectionInfo) -> ChannelDestination,
onItemTap: @escaping (ChatChannel) -> Void
) {
self.channel = channel
self.channelName = channelName
self.channelDestination = channelDestination
self.onItemTap = onItemTap
self.avatar = avatar
self.onlineIndicatorShown = onlineIndicatorShown
self.disabled = disabled
_selectedChannel = selectedChannel
}

public var body: some View {
ZStack {
DemoAppChatChannelListItem(
channel: channel,
channelName: channelName,
injectedChannelInfo: injectedChannelInfo,
avatar: avatar,
onlineIndicatorShown: onlineIndicatorShown,
disabled: disabled,
onItemTap: onItemTap
)

NavigationLink(
tag: channel.channelSelectionInfo,
selection: $selectedChannel
) {
LazyView(channelDestination(channel.channelSelectionInfo))
} label: {
EmptyView()
}
}
.id("\(channel.id)-\(channel.isPinned ? "pinned" : "not-pinned")-base")
}

private var injectedChannelInfo: InjectedChannelInfo? {
selectedChannel?.channel.cid.rawValue == channel.cid.rawValue ? selectedChannel?.injectedChannelInfo : nil
}
}
71 changes: 71 additions & 0 deletions DemoAppSwiftUI/ViewFactoryExamples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,77 @@ class DemoAppFactory: ViewFactory {
func makeChannelListHeaderViewModifier(title: String) -> some ChannelListHeaderViewModifier {
CustomChannelModifier(title: title)
}

func supportedMoreChannelActions(
for channel: ChatChannel,
onDismiss: @escaping () -> Void,
onError: @escaping (Error) -> Void
) -> [ChannelAction] {
var actions = ChannelAction.defaultActions(
for: channel,
chatClient: chatClient,
onDismiss: onDismiss,
onError: onError
)
let pinChannel = ChannelAction(
title: channel.isPinned ? "Unpin Channel" : "Pin Channel",
iconName: "pin.fill",
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()
}
}
},
confirmationPopup: nil,
isDestructive: false
)
actions.insert(pinChannel, at: actions.count - 2)
return actions
}

func makeChannelListItem(
channel: ChatChannel,
channelName: String,
avatar: UIImage,
onlineIndicatorShown: Bool,
disabled: Bool,
selectedChannel: Binding<ChannelSelectionInfo?>,
swipedChannelId: Binding<String?>,
channelDestination: @escaping (ChannelSelectionInfo) -> ChatChannelView<DemoAppFactory>,
onItemTap: @escaping (ChatChannel) -> Void,
trailingSwipeRightButtonTapped: @escaping (ChatChannel) -> Void,
trailingSwipeLeftButtonTapped: @escaping (ChatChannel) -> Void,
leadingSwipeButtonTapped: @escaping (ChatChannel) -> Void
) -> some View {
let listItem = DemoAppChatChannelNavigatableListItem(
channel: channel,
channelName: channelName,
avatar: avatar,
onlineIndicatorShown: onlineIndicatorShown,
disabled: disabled,
selectedChannel: selectedChannel,
channelDestination: channelDestination,
onItemTap: onItemTap
)
return ChatChannelSwipeableListItem(
factory: self,
channelListItem: listItem,
swipedChannelId: swipedChannelId,
channel: channel,
numberOfTrailingItems: channel.ownCapabilities.contains(.deleteChannel) ? 2 : 1,
trailingRightButtonTapped: trailingSwipeRightButtonTapped,
trailingLeftButtonTapped: trailingSwipeLeftButtonTapped,
leadingSwipeButtonTapped: leadingSwipeButtonTapped
)
}
}

struct CustomChannelDestination: View {
Expand Down
4 changes: 4 additions & 0 deletions StreamChatSwiftUI.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
8402EAD2282BFC8700CCA696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8402EAD1282BFC8700CCA696 /* AppDelegate.swift */; };
8402EAD4282BFCCA00CCA696 /* UserCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8402EAD3282BFCCA00CCA696 /* UserCredentials.swift */; };
840A3F3828193AB20084E9CC /* ChatChannelInfoView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840A3F3728193AB20084E9CC /* ChatChannelInfoView_Tests.swift */; };
8413C4552B4409B600190AF4 /* PinChannelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8413C4542B4409B600190AF4 /* PinChannelHelpers.swift */; };
8413D90227A9654600A89432 /* SearchResultsView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8413D90127A9654600A89432 /* SearchResultsView_Tests.swift */; };
8417AAB82ADEC3E300445021 /* BottomReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8417AAB72ADEC3E300445021 /* BottomReactionsView.swift */; };
8417AE902ADED28800445021 /* ReactionsIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8417AE8F2ADED28800445021 /* ReactionsIconProvider.swift */; };
Expand Down Expand Up @@ -644,6 +645,7 @@
8402EAD1282BFC8700CCA696 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
8402EAD3282BFCCA00CCA696 /* UserCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCredentials.swift; sourceTree = "<group>"; };
840A3F3728193AB20084E9CC /* ChatChannelInfoView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelInfoView_Tests.swift; sourceTree = "<group>"; };
8413C4542B4409B600190AF4 /* PinChannelHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinChannelHelpers.swift; sourceTree = "<group>"; };
8413D90127A9654600A89432 /* SearchResultsView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView_Tests.swift; sourceTree = "<group>"; };
8417AAB72ADEC3E300445021 /* BottomReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomReactionsView.swift; sourceTree = "<group>"; };
8417AE8F2ADED28800445021 /* ReactionsIconProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsIconProvider.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1468,6 +1470,7 @@
8451617D2AE7B093000A9230 /* AppleMessageComposerView.swift */,
8451617F2AE7C4E2000A9230 /* WhatsAppChannelHeader.swift */,
8417AE912ADEDB6400445021 /* UserRepository.swift */,
8413C4542B4409B600190AF4 /* PinChannelHelpers.swift */,
84EDBC36274FE5CD0057218D /* Localizable.strings */,
8465FCCA27468B7500AF091E /* Info.plist */,
8465FCC227468B6A00AF091E /* Assets.xcassets */,
Expand Down Expand Up @@ -2784,6 +2787,7 @@
84335014274BAB15007A1B81 /* ViewFactoryExamples.swift in Sources */,
8465FCDE274694D200AF091E /* CustomChannelHeader.swift in Sources */,
8451617E2AE7B093000A9230 /* AppleMessageComposerView.swift in Sources */,
8413C4552B4409B600190AF4 /* PinChannelHelpers.swift in Sources */,
84B288D3274D23AF00DD090B /* LoginView.swift in Sources */,
84B288D5274D286500DD090B /* LoginViewModel.swift in Sources */,
8465FCBF27468B6900AF091E /* DemoAppSwiftUIApp.swift in Sources */,
Expand Down

0 comments on commit b908015

Please sign in to comment.