Skip to content

Commit 98fca5d

Browse files
implemented giphy commands
1 parent d8a2388 commit 98fca5d

20 files changed

+484
-45
lines changed

Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public struct AttachmentPickerTypeView: View {
4646
pickerType: .giphy,
4747
selected: attachmentPickerType
4848
)
49-
.disabled(true)
5049
case .collapsed:
5150
Button {
5251
withAnimation {

Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerTextInputView.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct ComposerTextInputView: UIViewRepresentable {
1010
@Binding var text: String
1111
@Binding var height: CGFloat
1212
@Binding var selectedRangeLocation: Int
13+
@Binding var isFirstResponder: Bool
1314

1415
var placeholder: String
1516

@@ -29,21 +30,30 @@ struct ComposerTextInputView: UIViewRepresentable {
2930
uiView.selectedRange.location = selectedRangeLocation
3031
uiView.text = text
3132
uiView.handleTextChange()
33+
switch isFirstResponder {
34+
case true: uiView.becomeFirstResponder()
35+
case false: uiView.resignFirstResponder()
36+
}
3237
}
3338
}
3439
}
3540

3641
func makeCoordinator() -> Coordinator {
37-
Coordinator(textInput: self)
42+
Coordinator(textInput: self, isFirstResponder: $isFirstResponder)
3843
}
3944

4045
class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
4146
weak var textView: InputTextView?
4247

4348
var textInput: ComposerTextInputView
49+
var isFirstResponder: Binding<Bool>
4450

45-
init(textInput: ComposerTextInputView) {
51+
init(
52+
textInput: ComposerTextInputView,
53+
isFirstResponder: Binding<Bool>
54+
) {
4655
self.textInput = textInput
56+
self.isFirstResponder = isFirstResponder
4757
}
4858

4959
func textViewDidChange(_ textView: UITextView) {
@@ -59,6 +69,14 @@ struct ComposerTextInputView: UIViewRepresentable {
5969
true
6070
}
6171

72+
func textViewDidBeginEditing(_ textView: UITextView) {
73+
isFirstResponder.wrappedValue = true
74+
}
75+
76+
func textViewDidEndEditing(_ textView: UITextView) {
77+
isFirstResponder.wrappedValue = false
78+
}
79+
6280
func layoutManager(
6381
_ layoutManager: NSLayoutManager,
6482
didCompleteLayoutFor textContainer: NSTextContainer?,

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
6060
factory.makeComposerInputView(
6161
text: $viewModel.text,
6262
selectedRangeLocation: $viewModel.selectedRangeLocation,
63+
isFirstResponder: $viewModel.isFirstResponder,
64+
command: $viewModel.composerCommand,
6365
addedAssets: viewModel.addedAssets,
6466
addedFileURLs: viewModel.addedFileURLs,
6567
addedCustomAttachments: viewModel.addedCustomAttachments,
@@ -122,7 +124,9 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
122124
.onReceive(keyboardPublisher) { visible in
123125
if visible {
124126
withAnimation(.easeInOut(duration: 0.02)) {
125-
viewModel.pickerTypeState = .expanded(.none)
127+
if viewModel.composerCommand == nil {
128+
viewModel.pickerTypeState = .expanded(.none)
129+
}
126130
}
127131
}
128132
}
@@ -160,10 +164,14 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
160164
/// View for the composer's input (text and media).
161165
public struct ComposerInputView<Factory: ViewFactory>: View {
162166
@Injected(\.colors) private var colors
167+
@Injected(\.fonts) private var fonts
168+
@Injected(\.images) private var images
163169

164170
var factory: Factory
165171
@Binding var text: String
166172
@Binding var selectedRangeLocation: Int
173+
@Binding var isFirstResponder: Bool
174+
@Binding var command: ComposerCommand?
167175
var addedAssets: [AddedAsset]
168176
var addedFileURLs: [URL]
169177
var addedCustomAttachments: [CustomAttachment]
@@ -224,13 +232,52 @@ public struct ComposerInputView<Factory: ViewFactory>: View {
224232
)
225233
}
226234

227-
ComposerTextInputView(
228-
text: $text,
229-
height: $textHeight,
230-
selectedRangeLocation: $selectedRangeLocation,
231-
placeholder: L10n.Composer.Placeholder.message
232-
)
233-
.frame(height: textFieldHeight)
235+
if let command = command,
236+
let displayInfo = command.displayInfo,
237+
displayInfo.isInstant == true {
238+
HStack {
239+
HStack(spacing: 0) {
240+
Image(uiImage: images.smallBolt)
241+
Text(displayInfo.displayName.uppercased())
242+
}
243+
.padding(.horizontal, 8)
244+
.font(fonts.footnoteBold)
245+
.frame(height: 24)
246+
.background(Color.blue)
247+
.foregroundColor(.white)
248+
.cornerRadius(16)
249+
250+
ComposerTextInputView(
251+
text: $text,
252+
height: $textHeight,
253+
selectedRangeLocation: $selectedRangeLocation,
254+
isFirstResponder: $isFirstResponder,
255+
placeholder: L10n.Composer.Placeholder.message
256+
)
257+
.frame(height: textFieldHeight)
258+
.overlay(
259+
HStack {
260+
Spacer()
261+
Button {
262+
self.command = nil
263+
} label: {
264+
DiscardButtonView(
265+
color: Color(colors.background7)
266+
)
267+
}
268+
}
269+
)
270+
}
271+
} else {
272+
ComposerTextInputView(
273+
text: $text,
274+
height: $textHeight,
275+
selectedRangeLocation: $selectedRangeLocation,
276+
isFirstResponder: $isFirstResponder,
277+
placeholder: L10n.Composer.Placeholder.message
278+
)
279+
.frame(height: textFieldHeight)
280+
}
234281
}
235282
.padding(.vertical, shouldAddVerticalPadding ? 8 : 0)
236283
.padding(.leading, 8)

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ public class MessageComposerViewModel: ObservableObject {
4040
channelController.sendKeystrokeEvent()
4141
checkTypingSuggestions()
4242
} else {
43-
composerCommand = nil
43+
if composerCommand?.displayInfo?.isInstant == false {
44+
composerCommand = nil
45+
}
4446
selectedRangeLocation = 0
47+
suggestions = [String: Any]()
4548
}
4649
}
4750
}
4851

4952
@Published var selectedRangeLocation: Int = 0
53+
@Published var isFirstResponder = false
5054

5155
@Published var addedFileURLs = [URL]() {
5256
didSet {
@@ -64,7 +68,17 @@ public class MessageComposerViewModel: ObservableObject {
6468
didSet {
6569
switch pickerTypeState {
6670
case let .expanded(attachmentPickerType):
67-
overlayShown = attachmentPickerType != .none
71+
overlayShown = attachmentPickerType == .media
72+
if attachmentPickerType == .giphy {
73+
composerCommand = ComposerCommand(
74+
id: "instantCommands",
75+
typingSuggestion: TypingSuggestion.empty,
76+
displayInfo: nil
77+
)
78+
showTypingSuggestions()
79+
} else {
80+
composerCommand = nil
81+
}
6882
case .collapsed:
6983
log.debug("Collapsed state shown, no changes to overlay.")
7084
}
@@ -79,7 +93,20 @@ public class MessageComposerViewModel: ObservableObject {
7993
}
8094
}
8195

82-
@Published var composerCommand: ComposerCommand?
96+
@Published var composerCommand: ComposerCommand? {
97+
didSet {
98+
if oldValue?.id != composerCommand?.id &&
99+
composerCommand?.displayInfo?.isInstant == true {
100+
text = ""
101+
if isFirstResponder == false {
102+
isFirstResponder = true
103+
}
104+
}
105+
if oldValue != nil && composerCommand == nil {
106+
pickerTypeState = .expanded(.none)
107+
}
108+
}
109+
}
83110

84111
@Published var filePickerShown = false
85112
@Published var cameraPickerShown = false
@@ -97,6 +124,16 @@ public class MessageComposerViewModel: ObservableObject {
97124
with: channelController
98125
)
99126

127+
private var messageText: String {
128+
if let composerCommand = composerCommand,
129+
let displayInfo = composerCommand.displayInfo,
130+
displayInfo.isInstant == true {
131+
return "\(composerCommand.id) \(text)"
132+
} else {
133+
return text
134+
}
135+
}
136+
100137
public init(
101138
channelController: ChatChannelController,
102139
messageController: ChatMessageController?
@@ -134,7 +171,7 @@ public class MessageComposerViewModel: ObservableObject {
134171

135172
if let messageController = messageController {
136173
messageController.createNewReply(
137-
text: text,
174+
text: messageText,
138175
attachments: attachments,
139176
showReplyInChannel: showReplyInChannel,
140177
quotedMessageId: quotedMessage?.id
@@ -148,7 +185,7 @@ public class MessageComposerViewModel: ObservableObject {
148185
}
149186
} else {
150187
channelController.createNewMessage(
151-
text: text,
188+
text: messageText,
152189
attachments: attachments,
153190
quotedMessageId: quotedMessage?.id
154191
) { [weak self] in
@@ -357,6 +394,7 @@ public class MessageComposerViewModel: ObservableObject {
357394
addedAssets = []
358395
addedFileURLs = []
359396
addedCustomAttachments = []
397+
composerCommand = nil
360398
}
361399

362400
private func checkPickerSelectionState() {
@@ -366,11 +404,19 @@ public class MessageComposerViewModel: ObservableObject {
366404
}
367405

368406
private func checkTypingSuggestions() {
407+
if composerCommand?.displayInfo?.isInstant == true {
408+
// If an instant command is selected, don't check again.
409+
return
410+
}
369411
composerCommand = commandsHandler.canHandleCommand(
370412
in: text,
371413
caretLocation: selectedRangeLocation
372414
)
373415

416+
showTypingSuggestions()
417+
}
418+
419+
private func showTypingSuggestions() {
374420
if let composerCommand = composerCommand {
375421
commandsHandler.showSuggestions(for: composerCommand)
376422
.sink { _ in

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public class DefaultCommandsConfig: CommandsConfig {
3838
commandSymbol: mentionsSymbol,
3939
mentionAllAppUsers: false
4040
)
41-
return CommandsHandler(commands: [mentionsCommand])
41+
let giphyCommand = GiphyCommandHandler(
42+
channelController: channelController,
43+
commandSymbol: "/giphy"
44+
)
45+
let instantCommands = InstantCommandsHandler(commands: [giphyCommand])
46+
return CommandsHandler(commands: [mentionsCommand, instantCommands])
4247
}
4348
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ struct CommandsContainerView: View {
2121
}
2222
)
2323
}
24+
25+
if let instantCommands = suggestions["instantCommands"] as? [CommandHandler] {
26+
InstantCommandsView(
27+
instantCommands: instantCommands,
28+
commandSelected: { command in
29+
handleCommand(["instantCommand": command])
30+
}
31+
)
32+
}
2433
}
2534
}
2635
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public protocol CommandHandler {
1212
/// Identifier of the command.
1313
var id: String { get }
1414

15+
var displayInfo: CommandDisplayInfo? { get }
16+
1517
/// Checks whether the command can be handled.
1618
/// - Parameters:
1719
/// - text: the user entered text.
@@ -49,6 +51,7 @@ public struct ComposerCommand {
4951
let id: String
5052
/// Typing suggestion that invokes the command.
5153
let typingSuggestion: TypingSuggestion
54+
let displayInfo: CommandDisplayInfo?
5255
}
5356

5457
/// Provides information about the suggestion.
@@ -59,12 +62,20 @@ public struct SuggestionInfo {
5962
let value: Any
6063
}
6164

65+
public struct CommandDisplayInfo {
66+
let displayName: String
67+
let icon: UIImage
68+
let format: String
69+
let isInstant: Bool
70+
}
71+
6272
/// Main commands handler - decides which commands to invoke.
6373
/// Command is matched if there's an id matching.
6474
public class CommandsHandler: CommandHandler {
6575

6676
private let commands: [CommandHandler]
6777
public let id: String = "main"
78+
public var displayInfo: CommandDisplayInfo?
6879

6980
init(commands: [CommandHandler]) {
7081
self.commands = commands

0 commit comments

Comments
 (0)