Skip to content

Commit 862ecea

Browse files
initial work on message threads
1 parent 3c1becb commit 862ecea

18 files changed

+481
-42
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Copyright © 2021 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
import SwiftUI
7+
8+
/// View modifier for customizing the message thread header.
9+
public protocol MessageThreadHeaderViewModifier: ViewModifier {}
10+
11+
/// The default message thread header.
12+
public struct DefaultMessageThreadHeader: ToolbarContent {
13+
@Injected(\.fonts) private var fonts
14+
@Injected(\.colors) private var colors
15+
16+
public var body: some ToolbarContent {
17+
ToolbarItem(placement: .principal) {
18+
VStack {
19+
Text(L10n.Message.Actions.threadReply)
20+
.font(fonts.bodyBold)
21+
Text(L10n.Message.Threads.subtitle)
22+
.font(fonts.footnote)
23+
.foregroundColor(Color(colors.textLowEmphasis))
24+
}
25+
}
26+
}
27+
}
28+
29+
/// The default message thread header modifier.
30+
public struct DefaultMessageThreadHeaderModifier: MessageThreadHeaderViewModifier {
31+
32+
public func body(content: Content) -> some View {
33+
content.toolbar {
34+
DefaultMessageThreadHeader()
35+
}
36+
}
37+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//
2+
// Copyright © 2021 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import StreamChat
6+
7+
protocol MessagesDataSource: AnyObject {
8+
9+
func dataSource(
10+
channelDataSource: ChannelDataSource,
11+
didUpdateMessages messages: LazyCachedMapCollection<ChatMessage>
12+
)
13+
14+
func dataSource(
15+
channelDataSource: ChannelDataSource,
16+
didUpdateChannel channel: EntityChange<ChatChannel>,
17+
channelController: ChatChannelController
18+
)
19+
}
20+
21+
protocol ChannelDataSource: AnyObject {
22+
23+
var delegate: MessagesDataSource? { get set }
24+
25+
var messages: LazyCachedMapCollection<ChatMessage> { get }
26+
27+
func loadPreviousMessages(
28+
before messageId: MessageId?,
29+
limit: Int,
30+
completion: ((Error?) -> Void)?
31+
)
32+
}
33+
34+
class ChatChannelDataSource: ChannelDataSource, ChatChannelControllerDelegate {
35+
36+
let controller: ChatChannelController
37+
weak var delegate: MessagesDataSource?
38+
var messages: LazyCachedMapCollection<ChatMessage> {
39+
controller.messages
40+
}
41+
42+
init(controller: ChatChannelController) {
43+
self.controller = controller
44+
self.controller.delegate = self
45+
}
46+
47+
public func channelController(
48+
_ channelController: ChatChannelController,
49+
didUpdateMessages changes: [ListChange<ChatMessage>]
50+
) {
51+
delegate?.dataSource(
52+
channelDataSource: self,
53+
didUpdateMessages: channelController.messages
54+
)
55+
}
56+
57+
func channelController(
58+
_ channelController: ChatChannelController,
59+
didUpdateChannel channel: EntityChange<ChatChannel>
60+
) {
61+
delegate?.dataSource(
62+
channelDataSource: self,
63+
didUpdateChannel: channel,
64+
channelController: channelController
65+
)
66+
}
67+
68+
func loadPreviousMessages(
69+
before messageId: MessageId?,
70+
limit: Int,
71+
completion: ((Error?) -> Void)?
72+
) {
73+
controller.loadPreviousMessages(
74+
before: messageId,
75+
limit: limit,
76+
completion: completion
77+
)
78+
}
79+
}
80+
81+
class MessageThreadDataSource: ChannelDataSource, ChatMessageControllerDelegate {
82+
83+
let channelController: ChatChannelController
84+
let messageController: ChatMessageController
85+
weak var delegate: MessagesDataSource?
86+
var messages: LazyCachedMapCollection<ChatMessage> {
87+
messageController.replies
88+
}
89+
90+
init(
91+
channelController: ChatChannelController,
92+
messageController: ChatMessageController
93+
) {
94+
self.channelController = channelController
95+
self.messageController = messageController
96+
self.messageController.delegate = self
97+
}
98+
99+
func messageController(
100+
_ controller: ChatMessageController,
101+
didChangeReplies changes: [ListChange<ChatMessage>]
102+
) {
103+
delegate?.dataSource(channelDataSource: self, didUpdateMessages: controller.replies)
104+
}
105+
106+
func loadPreviousMessages(
107+
before messageId: MessageId?,
108+
limit: Int,
109+
completion: ((Error?) -> Void)?
110+
) {
111+
messageController.loadPreviousReplies(
112+
before: messageId,
113+
limit: limit,
114+
completion: completion
115+
)
116+
}
117+
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,28 @@ public struct ChatChannelView<Factory: ViewFactory>: View {
1414
@State private var messageDisplayInfo: MessageDisplayInfo?
1515

1616
private var factory: Factory
17+
private var isInThread: Bool
1718

1819
public init(
1920
viewFactory: Factory,
20-
channelController: ChatChannelController
21+
channelController: ChatChannelController,
22+
messageController: ChatMessageController? = nil
2123
) {
2224
_viewModel = StateObject(
23-
wrappedValue: ViewModelsFactory.makeChannelViewModel(with: channelController)
25+
wrappedValue: ViewModelsFactory.makeChannelViewModel(
26+
with: channelController,
27+
messageController: messageController
28+
)
2429
)
2530
factory = viewFactory
31+
isInThread = messageController != nil
2632
}
2733

2834
public var body: some View {
2935
VStack(spacing: 0) {
3036
MessageListView(
3137
factory: factory,
38+
channel: viewModel.channel,
3239
messages: viewModel.messages,
3340
messagesGroupingInfo: viewModel.messagesGroupingInfo,
3441
scrolledId: $viewModel.scrolledId,
@@ -52,12 +59,16 @@ public struct ChatChannelView<Factory: ViewFactory>: View {
5259
.if(viewModel.reactionsShown, transform: { view in
5360
view.navigationBarHidden(true)
5461
})
55-
.if(!viewModel.reactionsShown) { view in
62+
.if(!viewModel.reactionsShown && !isInThread) { view in
5663
view.modifier(factory.makeChannelHeaderViewModifier(for: viewModel.channel))
5764
}
65+
.if(!viewModel.reactionsShown && isInThread) { view in
66+
view.modifier(factory.makeMessageThreadHeaderViewModifier())
67+
}
5868

5969
factory.makeMessageComposerViewType(
6070
with: viewModel.channelController,
71+
messageController: viewModel.messageController,
6172
onMessageSent: viewModel.scrollToLastMessage
6273
)
6374
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import Nuke
77
import StreamChat
88
import SwiftUI
99

10-
public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelegate {
10+
public class ChatChannelViewModel: ObservableObject, MessagesDataSource {
11+
1112
@Injected(\.chatClient) private var chatClient
1213
@Injected(\.utils) private var utils
1314

15+
private var channelDataSource: ChannelDataSource
1416
private var cancellables = Set<AnyCancellable>()
1517
private var lastRefreshThreshold = 200
1618
private let refreshThreshold = 200
@@ -38,6 +40,7 @@ public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelega
3840
@Atomic private var lastMessageRead: String?
3941

4042
var channelController: ChatChannelController
43+
var messageController: ChatMessageController?
4144

4245
@Published var scrolledId: String?
4346
@Published var listId = UUID().uuidString
@@ -85,9 +88,24 @@ public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelega
8588
channelController.channel!
8689
}
8790

88-
public init(channelController: ChatChannelController) {
91+
public init(
92+
channelController: ChatChannelController,
93+
messageController: ChatMessageController? = nil
94+
) {
8995
self.channelController = channelController
90-
setupChannelController()
96+
channelController.synchronize()
97+
if let messageController = messageController {
98+
self.messageController = messageController
99+
messageController.synchronize()
100+
channelDataSource = MessageThreadDataSource(
101+
channelController: channelController,
102+
messageController: messageController
103+
)
104+
} else {
105+
channelDataSource = ChatChannelDataSource(controller: channelController)
106+
}
107+
channelDataSource.delegate = self
108+
messages = channelDataSource.messages
91109

92110
NotificationCenter.default.addObserver(
93111
self,
@@ -117,13 +135,13 @@ public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelega
117135
}
118136
}
119137

120-
public func channelController(
121-
_ channelController: ChatChannelController,
122-
didUpdateMessages changes: [ListChange<ChatMessage>]
138+
func dataSource(
139+
channelDataSource: ChannelDataSource,
140+
didUpdateMessages messages: LazyCachedMapCollection<ChatMessage>
123141
) {
124-
messages = channelController.messages
142+
self.messages = messages
125143

126-
let count = channelController.messages.count
144+
let count = messages.count
127145
if count > lastRefreshThreshold {
128146
lastRefreshThreshold = lastRefreshThreshold + refreshThreshold
129147
listId = UUID().uuidString
@@ -134,13 +152,14 @@ public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelega
134152
}
135153
}
136154

137-
public func channelController(
138-
_ channelController: ChatChannelController,
139-
didUpdateChannel channel: EntityChange<ChatChannel>
155+
func dataSource(
156+
channelDataSource: ChannelDataSource,
157+
didUpdateChannel channel: EntityChange<ChatChannel>,
158+
channelController: ChatChannelController
140159
) {
141160
messages = channelController.messages
142161
}
143-
162+
144163
func showReactionOverlay() {
145164
guard let view: UIView = topVC()?.view else {
146165
currentSnapshot = UIImage(systemName: "photo")
@@ -154,19 +173,14 @@ public class ChatChannelViewModel: ObservableObject, ChatChannelControllerDelega
154173

155174
// MARK: - private
156175

157-
private func setupChannelController() {
158-
channelController.delegate = self
159-
channelController.synchronize()
160-
messages = channelController.messages
161-
}
162-
163176
private func checkForNewMessages(index: Int) {
164-
if index < channelController.messages.count - 25 {
177+
if index < channelDataSource.messages.count - 25 {
165178
return
166179
}
167180

168181
if _loadingPreviousMessages.compareAndSwap(old: false, new: true) {
169-
channelController.loadPreviousMessages(
182+
channelDataSource.loadPreviousMessages(
183+
before: nil,
170184
limit: refreshThreshold,
171185
completion: { [weak self] _ in
172186
guard let self = self else { return }
@@ -222,13 +236,22 @@ extension ChatMessage: Identifiable {
222236
}
223237
}
224238

225-
return baseId + statesId + reactionScoresId
239+
return baseId + statesId + reactionScoresId + repliesCountId
226240
}
227241

228242
private var baseId: String {
229243
isDeleted ? "\(id)-deleted" : id
230244
}
231245

246+
var repliesCountId: String {
247+
var repliesCountId = ""
248+
if replyCount > 0 {
249+
repliesCountId = "\(replyCount)"
250+
}
251+
252+
return repliesCountId
253+
}
254+
232255
var uploadingStatesId: String {
233256
var states = imageAttachments.compactMap { $0.uploadingState?.state }
234257
states += giphyAttachments.compactMap { $0.uploadingState?.state }

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
1717
public init(
1818
viewFactory: Factory,
1919
channelController: ChatChannelController,
20+
messageController: ChatMessageController?,
2021
onMessageSent: @escaping () -> Void
2122
) {
2223
factory = viewFactory
2324
_viewModel = StateObject(
24-
wrappedValue: ViewModelsFactory.makeMessageComposerViewModel(with: channelController)
25+
wrappedValue: ViewModelsFactory.makeMessageComposerViewModel(
26+
with: channelController,
27+
messageController: messageController
28+
)
2529
)
2630
self.onMessageSent = onMessageSent
2731
}
@@ -53,6 +57,13 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
5357
}
5458
.padding(.all, 8)
5559

60+
if viewModel.sendInChannelShown {
61+
SendInChannelView(
62+
sendInChannel: $viewModel.showReplyInChannel,
63+
isDirectMessage: viewModel.isDirectChannel
64+
)
65+
}
66+
5667
factory.makeAttachmentPickerView(
5768
attachmentPickerState: $viewModel.pickerState,
5869
filePickerShown: $viewModel.filePickerShown,

0 commit comments

Comments
 (0)