Skip to content

Commit 6dde8f1

Browse files
[SWUI-51] Implemented Slow Mode
1 parent a84d65e commit 6dde8f1

File tree

16 files changed

+212
-8
lines changed

16 files changed

+212
-8
lines changed

Sources/StreamChatSwiftUI/ChatChannel/Composer/ComposerTextInputView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ struct ComposerTextInputView: UIViewRepresentable {
1212
@Binding var selectedRangeLocation: Int
1313

1414
var placeholder: String
15+
var editable: Bool
1516
var maxMessageLength: Int?
1617

1718
func makeUIView(context: Context) -> InputTextView {
1819
let inputTextView = InputTextView()
1920
context.coordinator.textView = inputTextView
2021
inputTextView.delegate = context.coordinator
22+
inputTextView.isEditable = editable
2123
inputTextView.layoutManager.delegate = context.coordinator
2224
inputTextView.placeholderLabel.text = placeholder
2325

@@ -29,6 +31,8 @@ struct ComposerTextInputView: UIViewRepresentable {
2931
if uiView.markedTextRange == nil {
3032
uiView.selectedRange.location = selectedRangeLocation
3133
uiView.text = text
34+
uiView.isEditable = editable
35+
uiView.placeholderLabel.text = placeholder
3236
uiView.handleTextChange()
3337
}
3438
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,16 @@ public struct MessageComposerView<Factory: ViewFactory>: View, KeyboardReadable
7171
addedCustomAttachments: viewModel.addedCustomAttachments,
7272
quotedMessage: $quotedMessage,
7373
maxMessageLength: channelConfig?.maxMessageLength,
74+
cooldownDuration: viewModel.cooldownDuration,
7475
onCustomAttachmentTap: viewModel.customAttachmentTapped(_:),
7576
shouldScroll: viewModel.inputComposerShouldScroll,
7677
removeAttachmentWithId: viewModel.removeAttachment(with:)
7778
)
7879

79-
factory.makeTrailingComposerView(enabled: viewModel.sendButtonEnabled) {
80+
factory.makeTrailingComposerView(
81+
enabled: viewModel.sendButtonEnabled,
82+
cooldownDuration: viewModel.cooldownDuration
83+
) {
8084
viewModel.sendMessage(
8185
quotedMessage: quotedMessage,
8286
editedMessage: editedMessage
@@ -181,6 +185,7 @@ public struct ComposerInputView<Factory: ViewFactory>: View {
181185
var addedCustomAttachments: [CustomAttachment]
182186
var quotedMessage: Binding<ChatMessage?>
183187
var maxMessageLength: Int?
188+
var cooldownDuration: Int
184189
var onCustomAttachmentTap: (CustomAttachment) -> Void
185190
var removeAttachmentWithId: (String) -> Void
186191

@@ -257,7 +262,8 @@ public struct ComposerInputView<Factory: ViewFactory>: View {
257262
text: $text,
258263
height: $textHeight,
259264
selectedRangeLocation: $selectedRangeLocation,
260-
placeholder: L10n.Composer.Placeholder.message,
265+
placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : L10n.Composer.Placeholder.message,
266+
editable: !isInCooldown,
261267
maxMessageLength: maxMessageLength
262268
)
263269
.frame(height: textFieldHeight)
@@ -293,4 +299,8 @@ public struct ComposerInputView<Factory: ViewFactory>: View {
293299
private var shouldAddVerticalPadding: Bool {
294300
!addedFileURLs.isEmpty || !addedAssets.isEmpty
295301
}
302+
303+
private var isInCooldown: Bool {
304+
cooldownDuration > 0
305+
}
296306
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,14 @@ open class MessageComposerViewModel: ObservableObject {
114114
@Published public var errorShown = false
115115
@Published public var showReplyInChannel = false
116116
@Published public var suggestions = [String: Any]()
117+
@Published public var cooldownDuration: Int = 0
117118

118119
private let channelController: ChatChannelController
119120
private var messageController: ChatMessageController?
120121

122+
private var timer: Timer?
123+
private var cooldownPeriod = 0
124+
121125
private var cancellables = Set<AnyCancellable>()
122126
private lazy var commandsHandler = utils
123127
.commandsConfig
@@ -141,13 +145,18 @@ open class MessageComposerViewModel: ObservableObject {
141145
) {
142146
self.channelController = channelController
143147
self.messageController = messageController
148+
listenToCooldownUpdates()
144149
}
145150

146151
public func sendMessage(
147152
quotedMessage: ChatMessage?,
148153
editedMessage: ChatMessage?,
149154
completion: @escaping () -> Void
150155
) {
156+
defer {
157+
checkChannelCooldown()
158+
}
159+
151160
if let composerCommand = composerCommand {
152161
commandsHandler.executeOnMessageSent(
153162
composerCommand: composerCommand
@@ -455,4 +464,35 @@ open class MessageComposerViewModel: ObservableObject {
455464
.store(in: &cancellables)
456465
}
457466
}
467+
468+
private func listenToCooldownUpdates() {
469+
channelController.channelChangePublisher.sink { [weak self] _ in
470+
let cooldownDuration = self?.channelController.channel?.cooldownDuration ?? 0
471+
if self?.cooldownPeriod == cooldownDuration {
472+
return
473+
}
474+
self?.cooldownPeriod = cooldownDuration
475+
self?.checkChannelCooldown()
476+
}
477+
.store(in: &cancellables)
478+
}
479+
480+
private func checkChannelCooldown() {
481+
let duration = channelController.channel?.cooldownDuration ?? 0
482+
if duration > 0 && timer == nil {
483+
cooldownDuration = duration
484+
timer = Timer.scheduledTimer(
485+
withTimeInterval: 1,
486+
repeats: true,
487+
block: { [weak self] _ in
488+
self?.cooldownDuration -= 1
489+
if self?.cooldownDuration == 0 {
490+
self?.timer?.invalidate()
491+
self?.timer = nil
492+
}
493+
}
494+
)
495+
timer?.fire()
496+
}
497+
}
458498
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// Copyright © 2022 Stream.io Inc. All rights reserved.
3+
//
4+
5+
import SwiftUI
6+
import UIKit
7+
8+
/// View displaying slow mode countdown.
9+
public struct SlowModeView: View {
10+
11+
@Injected(\.colors) private var colors
12+
@Injected(\.fonts) private var fonts
13+
14+
private let size: CGFloat = 32
15+
16+
var cooldownDuration: Int
17+
18+
public var body: some View {
19+
Text("\(cooldownDuration)")
20+
.padding(.horizontal, 8)
21+
.font(fonts.bodyBold)
22+
.frame(width: cooldownDuration < 10 ? size : nil, height: size)
23+
24+
.background(
25+
Color(
26+
colors.disabledColorForColor(colors.highlightedAccentBackground)
27+
)
28+
)
29+
.foregroundColor(Color(colors.textInverted))
30+
.clipShape(Capsule())
31+
.padding(.bottom, 2)
32+
}
33+
}

Sources/StreamChatSwiftUI/DefaultViewFactory.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ extension ViewFactory {
376376
addedCustomAttachments: [CustomAttachment],
377377
quotedMessage: Binding<ChatMessage?>,
378378
maxMessageLength: Int?,
379+
cooldownDuration: Int,
379380
onCustomAttachmentTap: @escaping (CustomAttachment) -> Void,
380381
shouldScroll: Bool,
381382
removeAttachmentWithId: @escaping (String) -> Void
@@ -392,6 +393,7 @@ extension ViewFactory {
392393
addedCustomAttachments: addedCustomAttachments,
393394
quotedMessage: quotedMessage,
394395
maxMessageLength: maxMessageLength,
396+
cooldownDuration: cooldownDuration,
395397
onCustomAttachmentTap: onCustomAttachmentTap,
396398
removeAttachmentWithId: removeAttachmentWithId
397399
)
@@ -408,6 +410,7 @@ extension ViewFactory {
408410
addedCustomAttachments: addedCustomAttachments,
409411
quotedMessage: quotedMessage,
410412
maxMessageLength: maxMessageLength,
413+
cooldownDuration: cooldownDuration,
411414
onCustomAttachmentTap: onCustomAttachmentTap,
412415
removeAttachmentWithId: removeAttachmentWithId
413416
)
@@ -416,13 +419,22 @@ extension ViewFactory {
416419

417420
public func makeTrailingComposerView(
418421
enabled: Bool,
422+
cooldownDuration: Int,
419423
onTap: @escaping () -> Void
420424
) -> some View {
421-
SendMessageButton(
422-
enabled: enabled,
423-
onTap: onTap
424-
)
425-
.padding(.bottom, 8)
425+
Group {
426+
if cooldownDuration == 0 {
427+
SendMessageButton(
428+
enabled: enabled,
429+
onTap: onTap
430+
)
431+
.padding(.bottom, 8)
432+
} else {
433+
SlowModeView(
434+
cooldownDuration: cooldownDuration
435+
)
436+
}
437+
}
426438
}
427439

428440
public func makeAttachmentPickerView(

Sources/StreamChatSwiftUI/Generated/L10n.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ internal enum L10n {
132132
internal static var giphy: String { L10n.tr("Localizable", "composer.placeholder.giphy") }
133133
/// Send a message
134134
internal static var message: String { L10n.tr("Localizable", "composer.placeholder.message") }
135+
/// Slow mode ON
136+
internal static var slowMode: String { L10n.tr("Localizable", "composer.placeholder.slow-mode") }
135137
}
136138
internal enum Quoted {
137139
/// Giphy

Sources/StreamChatSwiftUI/Resources/en.lproj/Localizable.strings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"composer.title.edit" = "Edit Message";
6868
"composer.title.reply" = "Reply to Message";
6969
"composer.placeholder.message" = "Send a message";
70+
"composer.placeholder.slow-mode" = "Slow mode ON";
7071
"composer.placeholder.giphy" = "Search GIFs";
7172
"composer.checkmark.direct-message-reply" = "Also send as direct message";
7273
"composer.checkmark.channel-reply" = "Also send in channel";

Sources/StreamChatSwiftUI/ViewFactory.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ public protocol ViewFactory: AnyObject {
374374
/// - addedCustomAttachments: list of added custom attachments.
375375
/// - quotedMessage: Optional quoted message, shown in the composer input.
376376
/// - maxMessageLength: the maximum allowed message length.
377+
/// - cooldownDuration: Duration of cooldown for sending messages, in case slow mode is enabled.
377378
/// - onCustomAttachmentTap: called when a custom attachment is tapped.
378379
/// - shouldScroll: whether the input field is scrollable.
379380
/// - removeAttachmentWithId: called when the attachment is removed from the input view.
@@ -388,6 +389,7 @@ public protocol ViewFactory: AnyObject {
388389
addedCustomAttachments: [CustomAttachment],
389390
quotedMessage: Binding<ChatMessage?>,
390391
maxMessageLength: Int?,
392+
cooldownDuration: Int,
391393
onCustomAttachmentTap: @escaping (CustomAttachment) -> Void,
392394
shouldScroll: Bool,
393395
removeAttachmentWithId: @escaping (String) -> Void
@@ -397,10 +399,12 @@ public protocol ViewFactory: AnyObject {
397399
/// Creates the trailing composer view.
398400
/// - Parameters:
399401
/// - enabled: whether the view is enabled (e.g. button).
402+
/// - cooldownDuration: Duration of cooldown for sending messages, in case slow mode is enabled.
400403
/// - onTap: called when the view is tapped.
401404
/// - Returns: view displayed in the trailing area of the message composer view.
402405
func makeTrailingComposerView(
403406
enabled: Bool,
407+
cooldownDuration: Int,
404408
onTap: @escaping () -> Void
405409
) -> TrailingComposerViewType
406410

StreamChatSwiftUI.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@
167167
848399EC275FB41B003075E4 /* ChatChannelListView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848399EB275FB41B003075E4 /* ChatChannelListView_Tests.swift */; };
168168
848399F227601231003075E4 /* ReactionsOverlayView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848399F127601231003075E4 /* ReactionsOverlayView_Tests.swift */; };
169169
8492974B27ABDDCB00A8EEB0 /* NotificationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8492974827ABDDBF00A8EEB0 /* NotificationsHandler.swift */; };
170+
8492975227B156D100A8EEB0 /* SlowModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8492975127B156D000A8EEB0 /* SlowModeView.swift */; };
171+
8492975427B1725B00A8EEB0 /* MessageComposerView_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8492975327B1725B00A8EEB0 /* MessageComposerView_Tests.swift */; };
170172
849CDD942768E0E1003C7A51 /* MessageActionsResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849CDD932768E0E1003C7A51 /* MessageActionsResolver.swift */; };
171173
84A75FBB274EA29B00225CE8 /* GiphyAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A75FBA274EA29B00225CE8 /* GiphyAttachmentView.swift */; };
172174
84AB7B1D2771F4AA00631A10 /* DiscardButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AB7B1C2771F4AA00631A10 /* DiscardButtonView.swift */; };
@@ -486,6 +488,8 @@
486488
848399F127601231003075E4 /* ReactionsOverlayView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsOverlayView_Tests.swift; sourceTree = "<group>"; };
487489
8492974727ABD97F00A8EEB0 /* DemoAppSwiftUI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DemoAppSwiftUI.entitlements; sourceTree = "<group>"; };
488490
8492974827ABDDBF00A8EEB0 /* NotificationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsHandler.swift; sourceTree = "<group>"; };
491+
8492975127B156D000A8EEB0 /* SlowModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlowModeView.swift; sourceTree = "<group>"; };
492+
8492975327B1725B00A8EEB0 /* MessageComposerView_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerView_Tests.swift; sourceTree = "<group>"; };
489493
849CDD932768E0E1003C7A51 /* MessageActionsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionsResolver.swift; sourceTree = "<group>"; };
490494
84A75FBA274EA29B00225CE8 /* GiphyAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GiphyAttachmentView.swift; sourceTree = "<group>"; };
491495
84AB7B1C2771F4AA00631A10 /* DiscardButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscardButtonView.swift; sourceTree = "<group>"; };
@@ -870,6 +874,7 @@
870874
8465FD112746A95600AF091E /* AttachmentPickerTypeView.swift */,
871875
8465FD1C2746A95600AF091E /* AttachmentPickerView.swift */,
872876
8465FD132746A95600AF091E /* ComposerTextInputView.swift */,
877+
8492975127B156D000A8EEB0 /* SlowModeView.swift */,
873878
8465FD1D2746A95600AF091E /* PhotoAttachmentPickerView.swift */,
874879
8465FD172746A95600AF091E /* AddedImageAttachmentsView.swift */,
875880
8465FD1E2746A95600AF091E /* AddedFileAttachmentsView.swift */,
@@ -1204,6 +1209,7 @@
12041209
846608E8278C98CB00D3D7B3 /* TypingIndicatorView_Tests.swift */,
12051210
841B2EF5278F108700ED619E /* MessageReadIndicatorView_Tests.swift */,
12061211
84C94D4E2758FE59007FE2B9 /* ChatChannelTestHelpers.swift */,
1212+
8492975327B1725B00A8EEB0 /* MessageComposerView_Tests.swift */,
12071213
);
12081214
path = ChatChannel;
12091215
sourceTree = "<group>";
@@ -1422,6 +1428,7 @@
14221428
files = (
14231429
8465FD962746A95700AF091E /* ReactionsOverlayViewModel.swift in Sources */,
14241430
8465FD792746A95700AF091E /* DeletedMessageView.swift in Sources */,
1431+
8492975227B156D100A8EEB0 /* SlowModeView.swift in Sources */,
14251432
84AB7B242773528300631A10 /* CommandsContainerView.swift in Sources */,
14261433
8465FDB72746A95700AF091E /* ChatMessageReactionAppeareance.swift in Sources */,
14271434
8465FDC52746A95700AF091E /* ChatChannelDeepLink.swift in Sources */,
@@ -1608,6 +1615,7 @@
16081615
84C94CE927578B93007FE2B9 /* ChatClient_Mock.swift in Sources */,
16091616
84C94D2627579511007FE2B9 /* CDNClient_Mock.swift in Sources */,
16101617
84C94D462757D1CA007FE2B9 /* ImageLoader_Mock.swift in Sources */,
1618+
8492975427B1725B00A8EEB0 /* MessageComposerView_Tests.swift in Sources */,
16111619
84C94D2A275796D0007FE2B9 /* MockNetworkURLProtocol.swift in Sources */,
16121620
84C94CCF27578B92007FE2B9 /* ChatMessageReaction_Mock.swift in Sources */,
16131621
84C94D0327578BF2007FE2B9 /* WaitFor.swift in Sources */,

StreamChatSwiftUITests/Infrastructure/TestTools/Models/ChatChannel_Mock.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public extension ChatChannel {
129129
watcherCount: Int = 0,
130130
memberCount: Int = 0,
131131
reads: [ChatChannelRead] = [],
132+
cooldownDuration: Int = 0,
132133
extraData: [String: RawJSON] = [:],
133134
latestMessages: [ChatMessage] = [],
134135
muteDetails: MuteDetails? = nil
@@ -152,6 +153,7 @@ public extension ChatChannel {
152153
watcherCount: watcherCount,
153154
memberCount: memberCount,
154155
reads: reads,
156+
cooldownDuration: cooldownDuration,
155157
extraData: extraData,
156158
latestMessages: { latestMessages },
157159
muteDetails: { muteDetails },

0 commit comments

Comments
 (0)