Skip to content

Commit c4123de

Browse files
implemented top and bottom typing indicator
1 parent 84bbae2 commit c4123de

File tree

10 files changed

+119
-18
lines changed

10 files changed

+119
-18
lines changed

Sources/StreamChatSwiftUI/ChatChannel/ChannelHeader/ChatChannelHeaderViewModifier.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,20 @@ public struct DefaultChatChannelHeader: ToolbarContent {
3030

3131
public var body: some ToolbarContent {
3232
ToolbarItem(placement: .principal) {
33-
VStack {
33+
VStack(spacing: 2) {
3434
Text(channelNamer(channel, currentUserId) ?? "")
3535
.font(fonts.bodyBold)
36-
Text(channel.onlineInfoText(currentUserId: currentUserId))
37-
.font(fonts.footnote)
38-
.foregroundColor(Color(colors.textLowEmphasis))
36+
if !channel.currentlyTypingUsers.isEmpty
37+
&& utils.typingIndicatorPlacement == .navigationBar {
38+
HStack {
39+
TypingIndicatorView()
40+
SubtitleText(text: channel.typingIndicatorString)
41+
}
42+
} else {
43+
Text(channel.onlineInfoText(currentUserId: currentUserId))
44+
.font(fonts.footnote)
45+
.foregroundColor(Color(colors.textLowEmphasis))
46+
}
3947
}
4048
}
4149

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Copyright © 2022 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import SwiftUI
6+
7+
/// Defines the placement of the typing indicator.
8+
public enum TypingIndicatorPlacement {
9+
/// Typing indicator is shown in the navigation bar.
10+
case navigationBar
11+
/// Typing indicator is shown at the bottom of the message list.
12+
case bottomOverlay
13+
}

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ public struct ChatChannelView<Factory: ViewFactory>: View {
5959
.if(viewModel.reactionsShown, transform: { view in
6060
view.navigationBarHidden(true)
6161
})
62-
.if(!viewModel.reactionsShown && !viewModel.isMessageThread) { view in
62+
.if(viewModel.channelHeaderType == .regular) { view in
6363
view.modifier(factory.makeChannelHeaderViewModifier(for: viewModel.channel))
6464
}
65-
.if(!viewModel.reactionsShown && viewModel.isMessageThread) { view in
65+
.if(viewModel.channelHeaderType == .typingIndicator) { view in
66+
view.modifier(factory.makeChannelHeaderViewModifier(for: viewModel.channel))
67+
}
68+
.if(viewModel.channelHeaderType == .messageThread) { view in
6669
view.modifier(factory.makeMessageThreadHeaderViewModifier())
6770
}
6871

Sources/StreamChatSwiftUI/ChatChannel/ChatChannelViewModel.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,15 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
8686
}
8787
}
8888

89-
@Published public var reactionsShown = false
89+
@Published public var reactionsShown = false {
90+
didSet {
91+
checkHeaderType()
92+
}
93+
}
94+
9095
@Published public var quotedMessage: ChatMessage?
9196
@Published public var editedMessage: ChatMessage?
97+
@Published public var channelHeaderType: ChannelHeaderType = .regular
9298

9399
public var channel: ChatChannel {
94100
channelController.channel!
@@ -123,6 +129,8 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
123129
name: UIApplication.didReceiveMemoryWarningNotification,
124130
object: nil
125131
)
132+
133+
checkHeaderType()
126134
}
127135

128136
@objc
@@ -178,6 +186,7 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
178186
channelController: ChatChannelController
179187
) {
180188
messages = channelController.messages
189+
checkHeaderType()
181190
}
182191

183192
public func showReactionOverlay() {
@@ -259,6 +268,21 @@ open class ChatChannelViewModel: ObservableObject, MessagesDataSource {
259268
channelController.markRead()
260269
}
261270
}
271+
272+
private func checkHeaderType() {
273+
let type: ChannelHeaderType
274+
if !reactionsShown && isMessageThread {
275+
type = .messageThread
276+
} else if !channel.currentlyTypingUsers.isEmpty {
277+
type = .typingIndicator
278+
} else {
279+
type = .regular
280+
}
281+
282+
if type != channelHeaderType {
283+
channelHeaderType = type
284+
}
285+
}
262286
}
263287

264288
extension ChatMessage: Identifiable {
@@ -316,3 +340,13 @@ extension ChatMessage: Identifiable {
316340
return output
317341
}
318342
}
343+
344+
/// The type of header shown in the chat channel screen.
345+
public enum ChannelHeaderType {
346+
/// The regular header showing the channel name and members.
347+
case regular
348+
/// The header shown in message threads.
349+
case messageThread
350+
/// The header shown when someone is typing.
351+
case typingIndicator
352+
}

Sources/StreamChatSwiftUI/ChatChannel/MessageList/MessageListView.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ struct MessageListView<Factory: ViewFactory>: View, KeyboardReadable {
144144
if let date = currentDateString {
145145
DateIndicatorView(date: date)
146146
}
147+
148+
if !channel.currentlyTypingUsers.isEmpty
149+
&& utils.typingIndicatorPlacement == .bottomOverlay {
150+
TypingIndicatorBottomView(
151+
typingIndicatorString: channel.typingIndicatorString
152+
)
153+
}
147154
}
148155
.onReceive(keyboardPublisher) { visible in
149156
if currentDateString != nil {
@@ -241,3 +248,28 @@ public struct DateIndicatorView: View {
241248
}
242249
}
243250
}
251+
252+
struct TypingIndicatorBottomView: View {
253+
@Injected(\.colors) private var colors
254+
@Injected(\.fonts) private var fonts
255+
256+
var typingIndicatorString: String
257+
258+
var body: some View {
259+
VStack {
260+
Spacer()
261+
HStack {
262+
TypingIndicatorView()
263+
Text(typingIndicatorString)
264+
.font(.footnote)
265+
.foregroundColor(Color(colors.textLowEmphasis))
266+
Spacer()
267+
}
268+
.standardPadding()
269+
.background(
270+
Color(colors.background)
271+
.opacity(0.9)
272+
)
273+
}
274+
}
275+
}

Sources/StreamChatSwiftUI/ChatChannel/Utils/ChatChannelExtensions.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ extension ChatChannel {
2727
return L10n.Message.Title.group(memberCount, watcherCount)
2828
}
2929

30+
var typingIndicatorString: String {
31+
let typingUsers = Array(currentlyTypingUsers)
32+
if let user = typingUsers.first(where: { user in user.name != nil }), let name = user.name {
33+
return L10n.MessageList.TypingIndicator.users(name, typingUsers.count - 1)
34+
} else {
35+
// If we somehow cannot fetch any user name, we simply show that `Someone is typing`
36+
return L10n.MessageList.TypingIndicator.typingUnknown
37+
}
38+
}
39+
3040
private var lastSeenDateFormatter: (Date) -> String? {
3141
DateUtils.timeAgo
3242
}

Sources/StreamChatSwiftUI/ChatChannelList/ChatChannelListItem.swift

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public struct ChatChannelListItem: View {
8282
if channel.isMuted {
8383
return L10n.Channel.Item.muted
8484
} else if !channel.currentlyTypingUsers.isEmpty {
85-
return typingIndicatorString(for: Array(channel.currentlyTypingUsers))
85+
return channel.typingIndicatorString
8686
} else if let latestMessage = channel.latestMessages.first {
8787
return "\(latestMessage.author.name ?? latestMessage.author.id): \(latestMessage.textContent ?? latestMessage.text)"
8888
} else {
@@ -104,15 +104,6 @@ public struct ChatChannelListItem: View {
104104
return ""
105105
}
106106
}
107-
108-
private func typingIndicatorString(for typingUsers: [ChatUser]) -> String {
109-
if let user = typingUsers.first(where: { user in user.name != nil }), let name = user.name {
110-
return L10n.MessageList.TypingIndicator.users(name, typingUsers.count - 1)
111-
} else {
112-
// If we somehow cannot fetch any user name, we simply show that `Someone is typing`
113-
return L10n.MessageList.TypingIndicator.typingUnknown
114-
}
115-
}
116107
}
117108

118109
/// View for the avatar used in channels (includes online indicator overlay).

Sources/StreamChatSwiftUI/CommonViews/TypingIndicatorView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ struct TypingIndicatorView: View {
3030
)
3131
}
3232
.onAppear {
33-
isTyping = true
33+
/// NOTE: This is needed because of a glitch when the animation is performed in a navigation bar.
34+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
35+
isTyping = true
36+
}
3437
}
3538
}
3639
}

Sources/StreamChatSwiftUI/Utils.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class Utils {
1818
public var messageTypeResolver: MessageTypeResolving
1919
public var messageActionsResolver: MessageActionsResolving
2020
public var commandsConfig: CommandsConfig
21+
public var typingIndicatorPlacement: TypingIndicatorPlacement
2122

2223
public init(
2324
dateFormatter: DateFormatter = .makeDefault(),
@@ -30,6 +31,7 @@ public class Utils {
3031
messageTypeResolver: MessageTypeResolving = MessageTypeResolver(),
3132
messageActionResolver: MessageActionsResolving = MessageActionsResolver(),
3233
commandsConfig: CommandsConfig = DefaultCommandsConfig(),
34+
typingIndicatorPlacement: TypingIndicatorPlacement = .bottomOverlay,
3335
channelNamer: @escaping ChatChannelNamer = DefaultChatChannelNamer()
3436
) {
3537
self.dateFormatter = dateFormatter
@@ -43,5 +45,6 @@ public class Utils {
4345
self.messageTypeResolver = messageTypeResolver
4446
messageActionsResolver = messageActionResolver
4547
self.commandsConfig = commandsConfig
48+
self.typingIndicatorPlacement = typingIndicatorPlacement
4649
}
4750
}

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
8465FDD82746AA2700AF091E /* StreamChatSwiftUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 8465FD602746A95700AF091E /* StreamChatSwiftUI.h */; settings = {ATTRIBUTES = (Public, ); }; };
154154
8465FDDD2747A14700AF091E /* CustomComposerAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8465FDDC2747A14700AF091E /* CustomComposerAttachmentView.swift */; };
155155
846608E3278C303800D3D7B3 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846608E2278C303800D3D7B3 /* TypingIndicatorView.swift */; };
156+
846608E5278C865200D3D7B3 /* TypingIndicatorPlacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846608E4278C865200D3D7B3 /* TypingIndicatorPlacement.swift */; };
156157
848399EA275FB3E9003075E4 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 848399E9275FB3E9003075E4 /* SnapshotTesting */; };
157158
848399EC275FB41B003075E4 /* ChatChannelListView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848399EB275FB41B003075E4 /* ChatChannelListView_Tests.swift */; };
158159
848399F227601231003075E4 /* ReactionsOverlayView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848399F127601231003075E4 /* ReactionsOverlayView_Tests.swift */; };
@@ -455,6 +456,7 @@
455456
8465FD692746A95700AF091E /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = "<group>"; };
456457
8465FDDC2747A14700AF091E /* CustomComposerAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomComposerAttachmentView.swift; sourceTree = "<group>"; };
457458
846608E2278C303800D3D7B3 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = "<group>"; };
459+
846608E4278C865200D3D7B3 /* TypingIndicatorPlacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorPlacement.swift; sourceTree = "<group>"; };
458460
848399EB275FB41B003075E4 /* ChatChannelListView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelListView_Tests.swift; sourceTree = "<group>"; };
459461
848399F127601231003075E4 /* ReactionsOverlayView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsOverlayView_Tests.swift; sourceTree = "<group>"; };
460462
849CDD932768E0E1003C7A51 /* MessageActionsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionsResolver.swift; sourceTree = "<group>"; };
@@ -885,6 +887,7 @@
885887
children = (
886888
8465FD2F2746A95600AF091E /* ChatChannelHeaderViewModifier.swift */,
887889
84DEC8E92761089A00172876 /* MessageThreadHeaderViewModifier.swift */,
890+
846608E4278C865200D3D7B3 /* TypingIndicatorPlacement.swift */,
888891
);
889892
path = ChannelHeader;
890893
sourceTree = "<group>";
@@ -1460,6 +1463,7 @@
14601463
8465FD6F2746A95700AF091E /* StreamChat.swift in Sources */,
14611464
8465FD8F2746A95700AF091E /* AttachmentUploadingStateView.swift in Sources */,
14621465
8465FD732746A95700AF091E /* ActionItemView.swift in Sources */,
1466+
846608E5278C865200D3D7B3 /* TypingIndicatorPlacement.swift in Sources */,
14631467
8465FDA72746A95700AF091E /* KeyboardHandling.swift in Sources */,
14641468
8465FDD72746A95800AF091E /* Appearance.swift in Sources */,
14651469
8465FD8A2746A95700AF091E /* DiscardAttachmentButton.swift in Sources */,

0 commit comments

Comments
 (0)