Skip to content

Commit 7aaf041

Browse files
added docs for the composer commands
1 parent 4e0c0c7 commit 7aaf041

File tree

8 files changed

+264
-12
lines changed

8 files changed

+264
-12
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="ReadmeAssets/iOS_Chat_Messaging.png"/>
33
</p>
44

5-
## SwiftUI - Currently in Development 🏗
5+
## SwiftUI StreamChat SDK
66

77
The SwiftUI SDK is built on top of the [StreamChat](https://getstream.io/chat/docs/ios-swift/?language=swift) framework and it's a SwiftUI alternative to the [StreamChatUI](https://getstream.io/chat/docs/sdk/ios/) SDK. It's built completely in SwiftUI, using declarative patterns, that will be familiar to developers working with SwiftUI. The SDK includes an extensive set of performant and customizable UI components which allow you to get started quickly with little to no plumbing required.
88

Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ public enum AttachmentPickerType {
1818
case none
1919
/// Media (images, files, videos) is selected.
2020
case media
21-
/// Giphy commands are selected.
22-
case giphy
21+
/// Instant commands are selected.
22+
case instantCommands
2323
/// Custom attachment picker type.
2424
case custom
2525
}
@@ -43,7 +43,7 @@ public struct AttachmentPickerTypeView: View {
4343

4444
PickerTypeButton(
4545
pickerTypeState: $pickerTypeState,
46-
pickerType: .giphy,
46+
pickerType: .instantCommands,
4747
selected: attachmentPickerType
4848
)
4949
case .collapsed:

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public class MessageComposerViewModel: ObservableObject {
6868
switch pickerTypeState {
6969
case let .expanded(attachmentPickerType):
7070
overlayShown = attachmentPickerType == .media
71-
if attachmentPickerType == .giphy {
71+
if attachmentPickerType == .instantCommands {
7272
composerCommand = ComposerCommand(
7373
id: "instantCommands",
7474
typingSuggestion: TypingSuggestion.empty,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ public protocol CommandsConfig {
1111
/// The symbol that invokes mentions command.
1212
var mentionsSymbol: String { get }
1313

14-
/// The symbol that invokes giphy command.
15-
var giphySymbol: String { get }
14+
/// The symbol that invokes instant commands.
15+
var instantCommandsSymbol: String { get }
1616

1717
/// Creates the main commands handler.
1818
/// - Parameter channelController: the controller of the channel.
@@ -28,7 +28,7 @@ public class DefaultCommandsConfig: CommandsConfig {
2828
public init() {}
2929

3030
public let mentionsSymbol: String = "@"
31-
public let giphySymbol: String = "/"
31+
public let instantCommandsSymbol: String = "/"
3232

3333
public func makeCommandsHandler(
3434
with channelController: ChatChannelController

StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerViewModel_Tests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ class MessageComposerViewModel_Tests: XCTestCase {
366366
let viewModel = makeComposerViewModel()
367367

368368
// When
369-
viewModel.pickerTypeState = .expanded(.giphy)
369+
viewModel.pickerTypeState = .expanded(.instantCommands)
370370

371371
// Then
372372
XCTAssert(viewModel.composerCommand != nil)
@@ -406,7 +406,7 @@ class MessageComposerViewModel_Tests: XCTestCase {
406406
let viewModel = makeComposerViewModel()
407407

408408
// When
409-
viewModel.pickerTypeState = .expanded(.giphy)
409+
viewModel.pickerTypeState = .expanded(.instantCommands)
410410

411411
// Then
412412
XCTAssert(!viewModel.suggestions.isEmpty)

StreamChatSwiftUITests/Tests/ChatChannel/Suggestions/TestCommandsConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class TestCommandsConfig: CommandsConfig {
1616
}
1717

1818
public let mentionsSymbol: String = "@"
19-
public let giphySymbol: String = "/"
19+
public let instantCommandsSymbol: String = "/"
2020

2121
private let chatClient: ChatClient
2222

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
---
2+
title: Message Composer Commands
3+
---
4+
5+
## Composer Commands Overview
6+
7+
The SwiftUI SDK has support for several types of commands in the composer. For example, when a user types "@" in the input field, a list of users that can be mentioned will be displayed. Additionally, the composer supports instant commands ("/"), similar to Slack. For example, you can share a giphy, by typing the "/giphy" command, or mute/unmute users. All the symbols and text for the commands are configurable, so you can use different displaying information for the commands. Additionally, you can create your own commands, and define their rules and handling. For example, you can create a "/pay" command, to send money to a user in the chat.
8+
9+
## Modifying the Supported Commands
10+
11+
Before creating your own custom commands, let's see how you can modify the supported ones, to fit your needs. First, you can change the order of the commands, as well as remove ones which you don't want to support. Additionally, you can change the invoking symbols of the commands. In order to accomplish this, you will need to implement your own `CommandsConfig` and inject it in the `Utils` class inside the `StreamChat` object.
12+
13+
```swift
14+
public class CustomCommandsConfig: CommandsConfig {
15+
16+
public init() {}
17+
18+
// Change these properties for different symbols for the commands.
19+
public let mentionsSymbol: String = "@"
20+
public let instantCommandsSymbol: String = "/"
21+
22+
public func makeCommandsHandler(
23+
with channelController: ChatChannelController
24+
) -> CommandsHandler {
25+
// Modify the configuration of the commands here:
26+
let mentionsCommand = MentionsCommandHandler(
27+
channelController: channelController,
28+
commandSymbol: mentionsSymbol,
29+
mentionAllAppUsers: false
30+
)
31+
let giphyCommand = GiphyCommandHandler(commandSymbol: "/giphy")
32+
let muteCommand = MuteCommandHandler(
33+
channelController: channelController,
34+
commandSymbol: "/mute"
35+
)
36+
let unmuteCommand = UnmuteCommandHandler(
37+
channelController: channelController,
38+
commandSymbol: "/unmute"
39+
)
40+
41+
// Add or remove commands here, or change the order.
42+
let instantCommands = InstantCommandsHandler(
43+
commands: [giphyCommand, muteCommand, unmuteCommand]
44+
)
45+
return CommandsHandler(commands: [mentionsCommand, instantCommands])
46+
}
47+
}
48+
```
49+
50+
In the code above, you can modify the command symbols by changing the `mentionsSymbol` and `instantCommandsSymbol` properties accordingly. In the `makeCommandsHandler` method, you can change the order of the commands, or add or remove commands.
51+
52+
After you have created the `CustomCommandsConfig`, you need pass it to the `StreamChat` object in the setup step (for example in your `AppDelegate`):
53+
54+
```swift
55+
let utils = Utils(commandsConfig: CustomCommandsConfig())
56+
let streamChat = StreamChat(chatClient: chatClient, utils: utils)
57+
```
58+
59+
## Creating a Custom Command
60+
61+
In order to create a custom command, you need to create your own class implementing the `CommandHandler` protocol. After you create your own implementation, you will need to inject it in your own `CustomCommandsConfig`, as described in the above example.
62+
63+
The methods defined by the `CommandHandler` are the following:
64+
65+
```swift
66+
public protocol CommandHandler {
67+
68+
/// Identifier of the command.
69+
var id: String { get }
70+
71+
/// Display info for the command.
72+
var displayInfo: CommandDisplayInfo? { get }
73+
74+
/// Whether execution of the command replaces sending of a message.
75+
var replacesMessageSent: Bool { get }
76+
77+
/// Checks whether the command can be handled.
78+
/// - Parameters:
79+
/// - text: the user entered text.
80+
/// - caretLocation: the end location of a selected text range.
81+
/// - Returns: optional `ComposerCommand` (if the handler can handle the command).
82+
func canHandleCommand(
83+
in text: String,
84+
caretLocation: Int
85+
) -> ComposerCommand?
86+
87+
/// Returns a command handler for a command (if available).
88+
/// - Parameter command: the command whose handler will be returned.
89+
/// - Returns: Optional `CommandHandler`.
90+
func commandHandler(for command: ComposerCommand) -> CommandHandler?
91+
92+
/// Shows suggestions for the provided command.
93+
/// - Parameter command: the command whose suggestions will be shown.
94+
/// - Returns: `Future` with the suggestions, or an error.
95+
func showSuggestions(
96+
for command: ComposerCommand
97+
) -> Future<SuggestionInfo, Error>
98+
99+
/// Handles the provided command.
100+
/// - Parameters:
101+
/// - text: the user entered text.
102+
/// - selectedRangeLocation: the end location of the selected text.
103+
/// - command: binding of the command.
104+
/// - extraData: additional data that can be passed from the command.
105+
func handleCommand(
106+
for text: Binding<String>,
107+
selectedRangeLocation: Binding<Int>,
108+
command: Binding<ComposerCommand?>,
109+
extraData: [String: Any]
110+
)
111+
112+
/// Checks whether the command can be executed on message sent.
113+
/// - Parameter command: the command to be checked.
114+
/// - Returns: `Bool` whether the command can be executed.
115+
func canBeExecuted(composerCommand: ComposerCommand) -> Bool
116+
117+
/// Needs to be implemented if you need some code executed before the message is sent.
118+
/// - Parameters:
119+
/// - composerCommand: the command to be executed.
120+
/// - completion: called when the command is executed.
121+
func executeOnMessageSent(
122+
composerCommand: ComposerCommand,
123+
completion: @escaping (Error?) -> Void
124+
)
125+
}
126+
```
127+
128+
You can implement these methods to have the most customized command handling behavior. However, in most cases you will need support for a two-step command process, where in the first one, you will pick the instant command and in the second step, you will mention a user, that will be affected by your command. These can be actions both supported by the SDK (muting, banning, flagging, etc), or your own custom actions.
129+
130+
In order to re-use the two-step command process from the SDK, you will need to subclass the `TwoStepMentionCommand`. For example, let's see how the mute action can be implemented by subclassing the `TwoStepMentionCommand`.
131+
132+
```swift
133+
public class MuteCommandHandler: TwoStepMentionCommand {
134+
135+
@Injected(\.images) private var images
136+
@Injected(\.chatClient) private var chatClient
137+
138+
public init(
139+
channelController: ChatChannelController,
140+
commandSymbol: String,
141+
id: String = "/mute"
142+
) {
143+
super.init(
144+
channelController: channelController,
145+
commandSymbol: commandSymbol,
146+
id: id
147+
)
148+
let displayInfo = CommandDisplayInfo(
149+
displayName: L10n.Composer.Commands.mute,
150+
icon: images.commandMute,
151+
format: "\(id) [\(L10n.Composer.Commands.Format.username)]",
152+
isInstant: true
153+
)
154+
self.displayInfo = displayInfo
155+
}
156+
157+
override public func executeOnMessageSent(
158+
composerCommand: ComposerCommand,
159+
completion: @escaping (Error?) -> Void
160+
) {
161+
if let mutedUser = selectedUser {
162+
chatClient
163+
.userController(userId: mutedUser.id)
164+
.mute { [weak self] error in
165+
self?.selectedUser = nil
166+
completion(error)
167+
}
168+
169+
return
170+
}
171+
}
172+
}
173+
```
174+
175+
In the `init` method, we setup display info of the command. If this is not specified, the command will not appear in the instant commands suggestions popup above the composer.
176+
177+
Additionally, we only need to override the `executeOnMessageSent` method, which is called when all the data is selected and the user is allowed to execute the command. In this method, we can make use of the `selectedUser` variable, which gives us information about the mentioned user in the command. In the example, we are muting the user. You can execute your own code here, for example sending a payment to the user, or anything else that fits your app's use-cases. You only need to call the `completion` handler when you are done with the action.
178+
179+
By default, these commands don't send the message in the message list. However, you can easily change this by returning `false` in the `replacesMessageSent` variable:
180+
181+
182+
```swift
183+
override public var replacesMessageSent: Bool {
184+
return false
185+
}
186+
```
187+
188+
### Customizing the Command Suggestions Views
189+
190+
The SDK comes with a default container view, that is displayed above the composer and over the message list. You can replace this view, either to adjust the user interface, or to support different types of suggestions for your custom commands.
191+
192+
In order to do this, you will need to implement the `makeCommandsContainerView` in the `ViewFactory`:
193+
194+
```swift
195+
class CustomViewFactory: ViewFactory {
196+
197+
@Injected(\.chatClient) public var chatClient
198+
199+
public func makeCommandsContainerView(
200+
suggestions: [String: Any],
201+
handleCommand: @escaping ([String: Any]) -> Void
202+
) -> some View {
203+
CustomCommandsContainerView(
204+
suggestions: suggestions,
205+
handleCommand: handleCommand
206+
)
207+
}
208+
}
209+
```
210+
211+
In this method, you receive a `Dictionary` with the suggestions provided to the user, depending on the command that's being executed. Additionally, you get a callback, which needs to be called when the user selects something in your own custom views. This data will be passed to your custom `CommandHandler`, where you will be able to react to the command accordingly.
212+
213+
```swift
214+
struct CustomCommandsContainerView: View {
215+
216+
var suggestions: [String: Any]
217+
var handleCommand: ([String: Any]) -> Void
218+
219+
var body: some View {
220+
ZStack {
221+
if let suggestedUsers = suggestions["mentions"] as? [ChatUser] {
222+
MentionUsersView(
223+
users: suggestedUsers,
224+
userSelected: { user in
225+
handleCommand(["chatUser": user])
226+
}
227+
)
228+
}
229+
230+
if let instantCommands = suggestions["instantCommands"] as? [CommandHandler] {
231+
InstantCommandsView(
232+
instantCommands: instantCommands,
233+
commandSelected: { command in
234+
handleCommand(["instantCommand": command])
235+
}
236+
)
237+
}
238+
}
239+
}
240+
}
241+
```
242+
243+
Finally, you need to inject the `CustomViewFactory` in your view hierarchy.
244+
245+
```swift
246+
var body: some Scene {
247+
WindowGroup {
248+
ChatChannelListView(viewFactory: CustomViewFactory.shared)
249+
}
250+
}
251+
```

docusaurus/sidebars-ios.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"swiftui/components/message-composer",
6666
"swiftui/components/message-reactions",
6767
"swiftui/components/message-threads",
68-
"swiftui/components/inline-replies"
68+
"swiftui/components/inline-replies",
69+
"swiftui/components/composer-commands"
6970
]
7071
}
7172
]

0 commit comments

Comments
 (0)