Skip to content

Commit c603666

Browse files
implemented injecting commands
1 parent 17f5087 commit c603666

File tree

6 files changed

+184
-27
lines changed

6 files changed

+184
-27
lines changed

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,15 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
133133
}
134134
.overlay(
135135
viewModel.typingSuggestion != nil ? CommandsContainerView(
136-
suggestedUsers: viewModel.suggestedUsers,
137-
userSelected: viewModel.mentionedUserSelected
136+
suggestions: viewModel.suggestions,
137+
handleCommand: { commandInfo in
138+
viewModel.handleCommand(
139+
for: $viewModel.text,
140+
selectedRangeLocation: $viewModel.selectedRangeLocation,
141+
typingSuggestion: $viewModel.typingSuggestion,
142+
extraData: commandInfo
143+
)
144+
}
138145
)
139146
.offset(y: -composerHeight)
140147
.animation(nil) : nil,

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,22 @@ public class MessageComposerViewModel: ObservableObject {
7878
}
7979
}
8080

81-
@Published private(set) var typingSuggestion: TypingSuggestion?
81+
@Published var typingSuggestion: TypingSuggestion?
8282

8383
@Published var filePickerShown = false
8484
@Published var cameraPickerShown = false
8585
@Published var errorShown = false
8686
@Published var showReplyInChannel = false
87-
@Published var suggestedUsers = [ChatUser]()
87+
@Published var suggestions = [String: Any]()
8888

8989
private let channelController: ChatChannelController
9090
private var messageController: ChatMessageController?
9191

92-
private let typingSuggester = TypingSuggester(options: .init(symbol: "@"))
9392
private let mentionsSuggester: MentionsSuggester
9493
private var cancellables = Set<AnyCancellable>()
94+
private lazy var commandsHandler = CommandsHandler(commands: [
95+
MentionsSuggester(channelController: channelController)
96+
])
9597

9698
public init(
9799
channelController: ChatChannelController,
@@ -302,6 +304,20 @@ public class MessageComposerViewModel: ObservableObject {
302304
}
303305
}
304306

307+
func handleCommand(
308+
for text: Binding<String>,
309+
selectedRangeLocation: Binding<Int>,
310+
typingSuggestion: Binding<TypingSuggestion?>,
311+
extraData: [String: Any]
312+
) {
313+
commandsHandler.handleCommand(
314+
for: text,
315+
selectedRangeLocation: selectedRangeLocation,
316+
typingSuggestion: typingSuggestion,
317+
extraData: extraData
318+
)
319+
}
320+
305321
func mentionedUserSelected(_ chatUser: ChatUser) {
306322
guard let typingSuggestion = typingSuggestion else { return }
307323
let mentionText = self.mentionText(for: chatUser)
@@ -364,16 +380,16 @@ public class MessageComposerViewModel: ObservableObject {
364380
}
365381

366382
private func checkTypingSuggestions() {
367-
typingSuggestion = typingSuggester.typingSuggestion(
383+
typingSuggestion = commandsHandler.canHandleCommand(
368384
in: text,
369385
caretLocation: selectedRangeLocation
370386
)
371387

372388
if let typingSuggestion = typingSuggestion {
373-
mentionsSuggester.showMentionSuggestions(for: typingSuggestion.text, mentionRange: typingSuggestion.locationRange)
374-
.sink { [weak self] users in
389+
commandsHandler.showSuggestions(for: typingSuggestion)
390+
.sink { [weak self] suggestionInfo in
375391
withAnimation {
376-
self?.suggestedUsers = users
392+
self?.suggestions[suggestionInfo.key] = suggestionInfo.value
377393
}
378394
}
379395
.store(in: &cancellables)

Sources/StreamChatSwiftUI/ChatChannel/Composer/Suggestions/CommandsContainerView.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import SwiftUI
77

88
struct CommandsContainerView: View {
99

10-
var suggestedUsers: [ChatUser]
11-
var userSelected: (ChatUser) -> Void
10+
var suggestions: [String: Any]
11+
var handleCommand: ([String: Any]) -> Void
1212

1313
var body: some View {
1414
ZStack {
15-
MentionUsersView(
16-
users: suggestedUsers,
17-
userSelected: userSelected
18-
)
15+
if let suggestedUsers = suggestions["mentions"] as? [ChatUser] {
16+
MentionUsersView(
17+
users: suggestedUsers,
18+
userSelected: { user in
19+
handleCommand(["chatUser": user])
20+
}
21+
)
22+
}
1923
}
2024
}
2125
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// Copyright © 2021 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import Combine
6+
import StreamChat
7+
import SwiftUI
8+
9+
protocol CommandHandler {
10+
11+
func canHandleCommand(
12+
in text: String,
13+
caretLocation: Int
14+
) -> TypingSuggestion?
15+
16+
func showSuggestions(
17+
for typingSuggestion: TypingSuggestion
18+
) -> Future<SuggestionInfo, Never>
19+
20+
func handleCommand(
21+
for text: Binding<String>,
22+
selectedRangeLocation: Binding<Int>,
23+
typingSuggestion: Binding<TypingSuggestion?>,
24+
extraData: [String: Any]
25+
)
26+
}
27+
28+
struct SuggestionInfo {
29+
let key: String
30+
let value: Any
31+
}
32+
33+
class CommandsHandler: CommandHandler {
34+
35+
private let commands: [CommandHandler]
36+
37+
init(commands: [CommandHandler]) {
38+
self.commands = commands
39+
}
40+
41+
func canHandleCommand(in text: String, caretLocation: Int) -> TypingSuggestion? {
42+
for command in commands {
43+
if let suggestion = command.canHandleCommand(
44+
in: text,
45+
caretLocation: caretLocation
46+
) {
47+
return suggestion
48+
}
49+
}
50+
51+
return nil
52+
}
53+
54+
func showSuggestions(
55+
for typingSuggestion: TypingSuggestion
56+
) -> Future<SuggestionInfo, Never> {
57+
// TODO: picking of command
58+
commands.first!.showSuggestions(for: typingSuggestion)
59+
}
60+
61+
func handleCommand(
62+
for text: Binding<String>,
63+
selectedRangeLocation: Binding<Int>,
64+
typingSuggestion: Binding<TypingSuggestion?>,
65+
extraData: [String: Any]
66+
) {
67+
// TODO: picking of command
68+
commands.first?.handleCommand(
69+
for: text,
70+
selectedRangeLocation: selectedRangeLocation,
71+
typingSuggestion: typingSuggestion,
72+
extraData: extraData
73+
)
74+
}
75+
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/Suggestions/MentionsSuggester.swift

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import Combine
66
import StreamChat
77
import SwiftUI
88

9-
public struct MentionsSuggester {
10-
9+
public struct MentionsSuggester: CommandHandler {
10+
1111
// TODO: read from config
1212
private var mentionAllAppUsers = false
13+
private let typingSuggester = TypingSuggester(options: .init(symbol: "@"))
1314

1415
private let channelController: ChatChannelController
1516
private let userSearchController: ChatUserSearchController
@@ -19,10 +20,52 @@ public struct MentionsSuggester {
1920
userSearchController = channelController.client.userSearchController()
2021
}
2122

22-
public func showMentionSuggestions(for typingMention: String, mentionRange: NSRange) -> Future<[ChatUser], Never> {
23+
func canHandleCommand(in text: String, caretLocation: Int) -> TypingSuggestion? {
24+
typingSuggester.typingSuggestion(in: text, caretLocation: caretLocation)
25+
}
26+
27+
func handleCommand(
28+
for text: Binding<String>,
29+
selectedRangeLocation: Binding<Int>,
30+
typingSuggestion: Binding<TypingSuggestion?>,
31+
extraData: [String: Any]
32+
) {
33+
guard let chatUser = extraData["chatUser"] as? ChatUser,
34+
let typingSuggestionValue = typingSuggestion.wrappedValue else {
35+
return
36+
}
37+
38+
let mentionText = self.mentionText(for: chatUser)
39+
let newText = (text.wrappedValue as NSString).replacingCharacters(
40+
in: typingSuggestionValue.locationRange,
41+
with: mentionText
42+
)
43+
text.wrappedValue = newText
44+
45+
let newCaretLocation =
46+
selectedRangeLocation.wrappedValue + (mentionText.count - typingSuggestionValue.text.count)
47+
selectedRangeLocation.wrappedValue = newCaretLocation
48+
typingSuggestion.wrappedValue = nil
49+
}
50+
51+
func showSuggestions(
52+
for typingSuggestion: TypingSuggestion
53+
) -> Future<SuggestionInfo, Never> {
54+
showMentionSuggestions(
55+
for: typingSuggestion.text,
56+
mentionRange: typingSuggestion.locationRange
57+
)
58+
}
59+
60+
// MARK: - private
61+
62+
private func showMentionSuggestions(
63+
for typingMention: String,
64+
mentionRange: NSRange
65+
) -> Future<SuggestionInfo, Never> {
2366
guard let channel = channelController.channel,
2467
let currentUserId = channelController.client.currentUserId else {
25-
return resolve(with: [])
68+
return resolve(with: SuggestionInfo(key: "", value: []))
2669
}
2770

2871
if mentionAllAppUsers {
@@ -33,15 +76,16 @@ public struct MentionsSuggester {
3376
by: typingMention,
3477
excludingId: currentUserId
3578
)
36-
return resolve(with: users)
79+
let suggestionInfo = SuggestionInfo(key: "mentions", value: users)
80+
return resolve(with: suggestionInfo)
3781
}
3882
}
3983

4084
/// searchUsers does an autocomplete search on a list of ChatUser and returns users with `id` or `name` containing the search string
4185
/// results are returned sorted by their edit distance from the searched string
4286
/// distance is calculated using the levenshtein algorithm
4387
/// both search and name strings are normalized (lowercased and by replacing diacritics)
44-
func searchUsers(_ users: [ChatUser], by searchInput: String, excludingId: String? = nil) -> [ChatUser] {
88+
private func searchUsers(_ users: [ChatUser], by searchInput: String, excludingId: String? = nil) -> [ChatUser] {
4589
let normalize: (String) -> String = {
4690
$0.lowercased().folding(options: .diacriticInsensitive, locale: .current)
4791
}
@@ -65,7 +109,7 @@ public struct MentionsSuggester {
65109
}
66110
}
67111

68-
func queryForMentionSuggestionsSearch(typingMention term: String) -> UserListQuery {
112+
private func queryForMentionSuggestionsSearch(typingMention term: String) -> UserListQuery {
69113
UserListQuery(
70114
filter: .or([
71115
.autocomplete(.name, text: term),
@@ -74,21 +118,28 @@ public struct MentionsSuggester {
74118
sort: [.init(key: .name, isAscending: true)]
75119
)
76120
}
121+
122+
private func mentionText(for user: ChatUser) -> String {
123+
if let name = user.name, !name.isEmpty {
124+
return name
125+
} else {
126+
return user.id
127+
}
128+
}
77129

78-
// MARK: - private
79-
80-
private func resolve(with users: [ChatUser]) -> Future<[ChatUser], Never> {
130+
private func resolve(with users: SuggestionInfo) -> Future<SuggestionInfo, Never> {
81131
Future { promise in
82132
promise(.success(users))
83133
}
84134
}
85135

86-
private func searchAllUsers(for typingMention: String) -> Future<[ChatUser], Never> {
136+
private func searchAllUsers(for typingMention: String) -> Future<SuggestionInfo, Never> {
87137
Future { promise in
88138
let query = queryForMentionSuggestionsSearch(typingMention: typingMention)
89139
userSearchController.search(query: query) { _ in
90140
let users = Array(userSearchController.users)
91-
promise(.success(users))
141+
let suggestionInfo = SuggestionInfo(key: "mentions", value: users)
142+
promise(.success(suggestionInfo))
92143
}
93144
}
94145
}

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
/* Begin PBXBuildFile section */
1010
841B64C427744DB60016FF3B /* ComposerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841B64C327744DB60016FF3B /* ComposerModels.swift */; };
11+
841B64C82774BA770016FF3B /* CommandsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841B64C72774BA770016FF3B /* CommandsHandler.swift */; };
1112
842383E02767394200888CFC /* ChatChannelDataSource_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842383DF2767394200888CFC /* ChatChannelDataSource_Tests.swift */; };
1213
842383E427678A4D00888CFC /* QuotedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842383E327678A4D00888CFC /* QuotedMessageView.swift */; };
1314
842F0BB8276B3518002C400C /* QuotedMessageView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842F0BB7276B3518002C400C /* QuotedMessageView_Tests.swift */; };
@@ -293,6 +294,7 @@
293294
/* Begin PBXFileReference section */
294295
4A65451E274BA170003C5FA8 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
295296
841B64C327744DB60016FF3B /* ComposerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerModels.swift; sourceTree = "<group>"; };
297+
841B64C72774BA770016FF3B /* CommandsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandsHandler.swift; sourceTree = "<group>"; };
296298
842383DF2767394200888CFC /* ChatChannelDataSource_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatChannelDataSource_Tests.swift; sourceTree = "<group>"; };
297299
842383E327678A4D00888CFC /* QuotedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedMessageView.swift; sourceTree = "<group>"; };
298300
842E979C275E0AD000A52E7B /* StreamChatSwiftUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = StreamChatSwiftUI.xctestplan; sourceTree = "<group>"; };
@@ -887,6 +889,7 @@
887889
isa = PBXGroup;
888890
children = (
889891
84AB7B232773528200631A10 /* CommandsContainerView.swift */,
892+
841B64C72774BA770016FF3B /* CommandsHandler.swift */,
890893
84AB7B252773619F00631A10 /* MentionUsersView.swift */,
891894
84AB7B292773D97E00631A10 /* MentionsSuggester.swift */,
892895
84AB7B272773D4FD00631A10 /* TypingSuggester.swift */,
@@ -1344,6 +1347,7 @@
13441347
84F2908C276B91700045472D /* ZoomableScrollView.swift in Sources */,
13451348
842383E427678A4D00888CFC /* QuotedMessageView.swift in Sources */,
13461349
8465FD932746A95700AF091E /* PhotoAttachmentPickerView.swift in Sources */,
1350+
841B64C82774BA770016FF3B /* CommandsHandler.swift in Sources */,
13471351
8465FDC42746A95700AF091E /* ChatChannelListScreen.swift in Sources */,
13481352
8465FD7F2746A95700AF091E /* MessageTypeResolver.swift in Sources */,
13491353
8465FDA42746A95700AF091E /* NukeImageLoader.swift in Sources */,

0 commit comments

Comments
 (0)