Skip to content

Commit 222f93d

Browse files
Fix composer not being locked after the channel was frozen (#1015)
1 parent 1af66ba commit 222f93d

File tree

13 files changed

+128
-16
lines changed

13 files changed

+128
-16
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6+
### 🐞 Fixed
7+
- Fix composer not being locked after the channel was frozen [#1015](https://github.com/GetStream/stream-chat-swiftui/pull/1015)
8+
69
### 🔄 Changed
710

811
# [4.90.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.90.0)

Sources/StreamChatSwiftUI/ChatChannel/Composer/AttachmentPickerTypeView.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public struct AttachmentPickerTypeView: View {
5050
HStack(spacing: 16) {
5151
switch pickerTypeState {
5252
case let .expanded(attachmentPickerType):
53-
if composerViewModel.channelController.channel?.canUploadFile == true {
53+
if composerViewModel.channelController.channel?.canUploadFile == true && composerViewModel.isSendMessageEnabled {
5454
PickerTypeButton(
5555
pickerTypeState: $pickerTypeState,
5656
pickerType: .media,
@@ -60,7 +60,7 @@ public struct AttachmentPickerTypeView: View {
6060
.accessibilityIdentifier("PickerTypeButtonMedia")
6161
}
6262

63-
if commandsAvailable {
63+
if commandsAvailable && composerViewModel.isSendMessageEnabled {
6464
PickerTypeButton(
6565
pickerTypeState: $pickerTypeState,
6666
pickerType: .instantCommands,
@@ -70,16 +70,18 @@ public struct AttachmentPickerTypeView: View {
7070
.accessibilityIdentifier("PickerTypeButtonCommands")
7171
}
7272
case .collapsed:
73-
Button {
74-
withAnimation {
75-
pickerTypeState = .expanded(.none)
73+
if composerViewModel.isSendMessageEnabled {
74+
Button {
75+
withAnimation {
76+
pickerTypeState = .expanded(.none)
77+
}
78+
} label: {
79+
Image(uiImage: images.shrinkInputArrow)
80+
.renderingMode(.template)
81+
.foregroundColor(Color(colors.highlightedAccentBackground))
7682
}
77-
} label: {
78-
Image(uiImage: images.shrinkInputArrow)
79-
.renderingMode(.template)
80-
.foregroundColor(Color(colors.highlightedAccentBackground))
83+
.accessibilityIdentifier("PickerTypeButtonCollapsed")
8184
}
82-
.accessibilityIdentifier("PickerTypeButtonCollapsed")
8385
}
8486
}
8587
.accessibilityElement(children: .contain)

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerView.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,8 @@ public struct ComposerInputView<Factory: ViewFactory>: View, KeyboardReadable {
375375
text: $text,
376376
height: $textHeight,
377377
selectedRangeLocation: $selectedRangeLocation,
378-
placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : L10n.Composer.Placeholder.message,
379-
editable: !isInCooldown,
378+
placeholder: isInCooldown ? L10n.Composer.Placeholder.slowMode : (isChannelFrozen ? L10n.Composer.Placeholder.messageDisabled : L10n.Composer.Placeholder.message),
379+
editable: !isInputDisabled,
380380
maxMessageLength: maxMessageLength,
381381
currentHeight: textFieldHeight
382382
)
@@ -435,4 +435,12 @@ public struct ComposerInputView<Factory: ViewFactory>: View, KeyboardReadable {
435435
private var isInCooldown: Bool {
436436
cooldownDuration > 0
437437
}
438+
439+
private var isChannelFrozen: Bool {
440+
!viewModel.isSendMessageEnabled
441+
}
442+
443+
private var isInputDisabled: Bool {
444+
isInCooldown || isChannelFrozen
445+
}
438446
}

Sources/StreamChatSwiftUI/ChatChannel/Composer/MessageComposerViewModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,11 @@ open class MessageComposerViewModel: ObservableObject {
444444
}
445445
}
446446

447+
/// A Boolean value indicating whether sending message is enabled.
448+
public var isSendMessageEnabled: Bool {
449+
channelController.channel?.canSendMessage ?? true
450+
}
451+
447452
public var sendButtonEnabled: Bool {
448453
if let composerCommand = composerCommand,
449454
let handler = commandsHandler.commandHandler(for: composerCommand) {

Sources/StreamChatSwiftUI/ChatChannel/Composer/TrailingComposerView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct TrailingComposerView: View {
1717

1818
public var body: some View {
1919
Group {
20-
if viewModel.cooldownDuration == 0 {
20+
if viewModel.cooldownDuration == 0 && viewModel.isSendMessageEnabled {
2121
HStack(spacing: 16) {
2222
SendMessageButton(
2323
enabled: viewModel.sendButtonEnabled,
@@ -28,7 +28,7 @@ public struct TrailingComposerView: View {
2828
}
2929
}
3030
.padding(.bottom, 8)
31-
} else {
31+
} else if viewModel.cooldownDuration > 0 {
3232
SlowModeView(
3333
cooldownDuration: viewModel.cooldownDuration
3434
)

Sources/StreamChatSwiftUI/Generated/L10n.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ internal enum L10n {
257257
internal static var giphy: String { L10n.tr("Localizable", "composer.placeholder.giphy") }
258258
/// Send a message
259259
internal static var message: String { L10n.tr("Localizable", "composer.placeholder.message") }
260+
/// You can't send messages in this channel
261+
internal static var messageDisabled: String { L10n.tr("Localizable", "composer.placeholder.messageDisabled") }
260262
/// Slow mode ON
261263
internal static var slowMode: String { L10n.tr("Localizable", "composer.placeholder.slow-mode") }
262264
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"composer.title.edit" = "Edit Message";
122122
"composer.title.reply" = "Reply to Message";
123123
"composer.placeholder.message" = "Send a message";
124+
"composer.placeholder.messageDisabled" = "You can't send messages in this channel";
124125
"composer.placeholder.slow-mode" = "Slow mode ON";
125126
"composer.placeholder.giphy" = "Search GIFs";
126127
"composer.checkmark.direct-message-reply" = "Also send as direct message";

StreamChatSwiftUITests/Tests/ChatChannel/ChatChannelTestHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class ChatChannelTestHelpers {
1717
let config = ChannelConfig(commands: [Command(name: "giphy", description: "", set: "", args: "")])
1818
let channel = chatChannel ?? ChatChannel.mockDMChannel(
1919
config: config,
20-
ownCapabilities: [.uploadFile],
20+
ownCapabilities: [.sendMessage, .uploadFile],
2121
lastActiveWatchers: lastActiveWatchers
2222
)
2323
let channelQuery = ChannelQuery(cid: channel.cid)

StreamChatSwiftUITests/Tests/ChatChannel/MessageComposerView_Tests.swift

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ class MessageComposerView_Tests: StreamChatTestCase {
204204
// Given
205205
let factory = DefaultViewFactory.shared
206206
let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
207-
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile])
207+
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.sendMessage, .uploadFile])
208208
let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil)
209209

210210
// When
@@ -234,6 +234,97 @@ class MessageComposerView_Tests: StreamChatTestCase {
234234
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
235235
}
236236

237+
// MARK: - Frozen Channel Tests
238+
239+
func test_messageComposerView_frozenChannel() {
240+
// Given
241+
let factory = DefaultViewFactory.shared
242+
let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
243+
// Create a channel without sendMessage capability (simulating frozen channel)
244+
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents])
245+
let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil)
246+
247+
// When
248+
let view = MessageComposerView(
249+
viewFactory: factory,
250+
viewModel: viewModel,
251+
channelController: mockChannelController,
252+
messageController: nil,
253+
quotedMessage: .constant(nil),
254+
editedMessage: .constant(nil),
255+
onMessageSent: {}
256+
)
257+
.frame(width: defaultScreenSize.width, height: 100)
258+
259+
// Then
260+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
261+
}
262+
263+
func test_composerInputView_frozenChannel() {
264+
// Given
265+
let factory = DefaultViewFactory.shared
266+
let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
267+
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents])
268+
let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil)
269+
270+
// When
271+
let view = ComposerInputView(
272+
factory: factory,
273+
text: .constant(""),
274+
selectedRangeLocation: .constant(0),
275+
command: .constant(nil),
276+
addedAssets: [],
277+
addedFileURLs: [],
278+
addedCustomAttachments: [],
279+
quotedMessage: .constant(nil),
280+
cooldownDuration: 0,
281+
onCustomAttachmentTap: { _ in },
282+
removeAttachmentWithId: { _ in }
283+
)
284+
.environmentObject(viewModel)
285+
.frame(width: defaultScreenSize.width, height: 100)
286+
287+
// Then
288+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
289+
}
290+
291+
func test_leadingComposerView_frozenChannel() {
292+
// Given
293+
let factory = DefaultViewFactory.shared
294+
let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
295+
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents])
296+
let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil)
297+
298+
// When
299+
let pickerTypeState: Binding<PickerTypeState> = .constant(.expanded(.none))
300+
let view = factory.makeLeadingComposerView(state: pickerTypeState, channelConfig: nil)
301+
.environmentObject(viewModel)
302+
.frame(width: 36, height: 36)
303+
304+
// Then
305+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
306+
}
307+
308+
func test_trailingComposerView_frozenChannel() {
309+
// Given
310+
let factory = DefaultViewFactory.shared
311+
let mockChannelController = ChatChannelTestHelpers.makeChannelController(chatClient: chatClient)
312+
mockChannelController.channel_mock = .mockDMChannel(ownCapabilities: [.uploadFile, .readEvents])
313+
let viewModel = MessageComposerViewModel(channelController: mockChannelController, messageController: nil)
314+
315+
// When
316+
let view = factory.makeTrailingComposerView(
317+
enabled: true,
318+
cooldownDuration: 0,
319+
onTap: {}
320+
)
321+
.environmentObject(viewModel)
322+
.frame(width: 100, height: 40)
323+
324+
// Then
325+
assertSnapshot(matching: view, as: .image(perceptualPrecision: precision))
326+
}
327+
237328
func test_composerInputView_inputTextView() {
238329
// Given
239330
let view = InputTextView(
20.6 KB
Loading

0 commit comments

Comments
 (0)