Skip to content

Commit de735f4

Browse files
Ensure MessageComposer respects the send- capabilities (#5973)
* Ensure the message composer respects the `send-` capabilities. * Ensure the message composer respects the `send-` capabilities. * Ensure the message composer respects the `send-` capabilities. * Update CHANGELOG.md. * PR remarks. --------- Co-authored-by: André Mion <[email protected]>
1 parent 040f926 commit de735f4

File tree

12 files changed

+297
-52
lines changed

12 files changed

+297
-52
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
## stream-chat-android-ui-components
6262
### 🐞 Fixed
63+
- Fix `MessageComposerView` not respecting `send-message` and `send-reply` channel capabilities. [#5973](https://github.com/GetStream/stream-chat-android/pull/5973)
6364

6465
### ⬆️ Improved
6566

@@ -71,6 +72,7 @@
7172

7273
## stream-chat-android-compose
7374
### 🐞 Fixed
75+
- Fix `MessageComposer` not respecting `send-message` and `send-reply` channel capabilities. [#5973](https://github.com/GetStream/stream-chat-android/pull/5973)
7476

7577
### ⬆️ Improved
7678

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
3434
import androidx.compose.ui.unit.dp
3535
import io.getstream.chat.android.compose.ui.theme.ChatTheme
3636
import io.getstream.chat.android.models.Attachment
37-
import io.getstream.chat.android.models.ChannelCapabilities
37+
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
3838
import io.getstream.chat.android.ui.common.state.messages.Edit
3939
import io.getstream.chat.android.ui.common.state.messages.Reply
4040
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
@@ -68,8 +68,7 @@ public fun MessageInput(
6868
innerTrailingContent: @Composable RowScope.() -> Unit = {},
6969
) {
7070
val (value, attachments, activeAction) = messageComposerState
71-
val canSendMessage = messageComposerState.sendEnabled &&
72-
messageComposerState.ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
71+
val canSendMessage = messageComposerState.canSendMessage()
7372

7473
InputField(
7574
modifier = modifier,

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import io.getstream.chat.android.models.Command
7070
import io.getstream.chat.android.models.LinkPreview
7171
import io.getstream.chat.android.models.Message
7272
import io.getstream.chat.android.models.User
73+
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
74+
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canUploadFile
7375
import io.getstream.chat.android.ui.common.state.messages.Edit
7476
import io.getstream.chat.android.ui.common.state.messages.MessageMode
7577
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
@@ -511,10 +513,7 @@ internal fun DefaultComposerIntegrations(
511513
val isAttachmentsButtonEnabled = !hasCommandInput && !hasCommandSuggestions && !hasMentionSuggestions
512514
val isCommandsButtonEnabled = !hasTextInput && !hasAttachments
513515

514-
val canSendMessage = messageInputState.sendEnabled &&
515-
ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
516-
val canSendAttachments = messageInputState.sendEnabled &&
517-
ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
516+
val canSendMessage = messageInputState.canSendMessage()
518517

519518
val isRecording = messageInputState.recording !is RecordingState.Idle
520519

@@ -525,7 +524,8 @@ internal fun DefaultComposerIntegrations(
525524
.padding(horizontal = 4.dp),
526525
verticalAlignment = Alignment.CenterVertically,
527526
) {
528-
if (canSendAttachments) {
527+
val canUploadFile = messageInputState.canUploadFile()
528+
if (canUploadFile) {
529529
with(ChatTheme.componentFactory) {
530530
MessageComposerAttachmentsButton(
531531
enabled = isAttachmentsButtonEnabled,
@@ -556,12 +556,10 @@ internal fun DefaultComposerIntegrations(
556556
*/
557557
@Composable
558558
internal fun DefaultComposerLabel(state: MessageComposerState) {
559-
val text = with(state) {
560-
if (sendEnabled && ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)) {
561-
stringResource(id = R.string.stream_compose_message_label)
562-
} else {
563-
stringResource(id = R.string.stream_compose_cannot_send_messages_label)
564-
}
559+
val text = if (state.canSendMessage()) {
560+
stringResource(id = R.string.stream_compose_message_label)
561+
} else {
562+
stringResource(id = R.string.stream_compose_cannot_send_messages_label)
565563
}
566564

567565
Text(
@@ -621,36 +619,34 @@ internal fun DefaultMessageComposerTrailingContent(
621619
val coolDownTime = messageComposerState.coolDownTime
622620
val validationErrors = messageComposerState.validationErrors
623621
val attachments = messageComposerState.attachments
624-
val ownCapabilities = messageComposerState.ownCapabilities
625622
val isInEditMode = messageComposerState.action is Edit
626623

627624
val isRecordAudioPermissionDeclared = LocalContext.current.isPermissionDeclared(Manifest.permission.RECORD_AUDIO)
628625
val isRecordingEnabled = isRecordAudioPermissionDeclared && ChatTheme.messageComposerTheme.audioRecording.enabled
629626
val showRecordOverSend = ChatTheme.messageComposerTheme.audioRecording.showRecordButtonOverSend
630627

631-
val isSendButtonEnabled = messageComposerState.sendEnabled &&
632-
ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
628+
val canSendMessage = messageComposerState.canSendMessage()
633629
val isInputValid by lazy { (value.isNotBlank() || attachments.isNotEmpty()) && validationErrors.isEmpty() }
634630

635631
if (coolDownTime > 0 && !isInEditMode) {
636632
ChatTheme.componentFactory.MessageComposerCoolDownIndicator(modifier = Modifier, coolDownTime = coolDownTime)
637633
} else {
638634
val isRecording = messageComposerState.recording !is RecordingState.Idle
639635

640-
val sendEnabled = isSendButtonEnabled && isInputValid
641-
val sendVisible = when {
636+
val sendButtonEnabled = canSendMessage && isInputValid
637+
val sendButtonVisible = when {
642638
!isRecordingEnabled -> true
643639
isRecording -> false
644-
showRecordOverSend -> sendEnabled
640+
showRecordOverSend -> sendButtonEnabled
645641
else -> true
646642
}
647-
if (sendVisible) {
643+
if (sendButtonVisible) {
648644
Box(
649645
modifier = Modifier.heightIn(min = ComposerActionContainerMinHeight),
650646
contentAlignment = Center,
651647
) {
652648
ChatTheme.componentFactory.MessageComposerSendButton(
653-
enabled = sendEnabled,
649+
enabled = sendButtonEnabled,
654650
isInputValid = isInputValid,
655651
onClick = {
656652
if (isInputValid) {
@@ -661,12 +657,12 @@ internal fun DefaultMessageComposerTrailingContent(
661657
}
662658
}
663659

664-
val recordVisible = when {
665-
!isRecordingEnabled -> false
666-
showRecordOverSend -> !sendEnabled
660+
val recordButtonVisible = when {
661+
!canSendMessage || !isRecordingEnabled -> false
662+
showRecordOverSend -> !sendButtonEnabled
667663
else -> true
668664
}
669-
if (recordVisible) {
665+
if (recordButtonVisible) {
670666
ChatTheme.componentFactory.MessageComposerAudioRecordButton(
671667
state = messageComposerState.recording,
672668
recordingActions = recordingActions,

stream-chat-android-docs/src/main/java/io/getstream/chat/docs/java/ui/guides/AddingCustomAttachments.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.getstream.chat.docs.java.ui.guides;
22

3+
import static io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.MessageComposerCapabilitiesKt.canSendMessage;
4+
import static io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.MessageComposerCapabilitiesKt.canUploadFile;
5+
36
import android.content.Context;
47
import android.util.AttributeSet;
58
import android.view.LayoutInflater;
@@ -22,7 +25,6 @@
2225
import java.util.Map;
2326

2427
import io.getstream.chat.android.models.Attachment;
25-
import io.getstream.chat.android.models.ChannelCapabilities;
2628
import io.getstream.chat.android.models.Message;
2729
import io.getstream.chat.android.ui.ChatUI;
2830
import io.getstream.chat.android.ui.common.state.messages.Edit;
@@ -37,8 +39,8 @@
3739
import io.getstream.chat.android.ui.feature.messages.composer.attachment.preview.factory.MediaAttachmentPreviewFactory;
3840
import io.getstream.chat.android.ui.feature.messages.composer.content.MessageComposerLeadingContent;
3941
import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListListeners;
40-
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.BaseAttachmentFactory;
4142
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager;
43+
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.BaseAttachmentFactory;
4244
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.DefaultQuotedAttachmentMessageFactory;
4345
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.InnerAttachmentViewHolder;
4446
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.QuotedAttachmentFactory;
@@ -109,8 +111,8 @@ public void attachContext(@NonNull MessageComposerContext messageComposerContext
109111

110112
@Override
111113
public void renderState(@NonNull MessageComposerState state) {
112-
boolean canSendMessage = state.getOwnCapabilities().contains(ChannelCapabilities.SEND_MESSAGE);
113-
boolean canUploadFile = state.getOwnCapabilities().contains(ChannelCapabilities.UPLOAD_FILE);
114+
boolean canSendMessage = canSendMessage(state);
115+
boolean canUploadFile = canUploadFile(state);
114116
boolean hasTextInput = !state.getInputValue().isEmpty();
115117
boolean hasAttachments = !state.getAttachments().isEmpty();
116118
boolean hasCommandInput = state.getInputValue().startsWith("/");

stream-chat-android-docs/src/main/kotlin/io/getstream/chat/docs/kotlin/ui/guides/AddingCustomAttachments.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import androidx.core.view.isVisible
1010
import androidx.fragment.app.Fragment
1111
import com.google.android.material.datepicker.MaterialDatePicker
1212
import io.getstream.chat.android.models.Attachment
13-
import io.getstream.chat.android.models.ChannelCapabilities
1413
import io.getstream.chat.android.models.Message
1514
import io.getstream.chat.android.ui.ChatUI
15+
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
16+
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canUploadFile
1617
import io.getstream.chat.android.ui.common.state.messages.Edit
1718
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
1819
import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerContext
@@ -85,8 +86,8 @@ class AddingCustomAttachments {
8586
}
8687

8788
override fun renderState(state: MessageComposerState) {
88-
val canSendMessage = state.ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
89-
val canUploadFile = state.ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
89+
val canSendMessage = state.canSendMessage()
90+
val canUploadFile = state.canUploadFile()
9091
val hasTextInput = state.inputValue.isNotEmpty()
9192
val hasAttachments = state.attachments.isNotEmpty()
9293
val hasCommandInput = state.inputValue.startsWith("/")

stream-chat-android-ui-common/api/stream-chat-android-ui-common.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,11 @@ public class io/getstream/chat/android/ui/common/feature/documents/AttachmentDoc
577577
protected fun onCreate (Landroid/os/Bundle;)V
578578
}
579579

580+
public final class io/getstream/chat/android/ui/common/feature/messages/composer/capabilities/MessageComposerCapabilitiesKt {
581+
public static final fun canSendMessage (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)Z
582+
public static final fun canUploadFile (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)Z
583+
}
584+
580585
public abstract interface class io/getstream/chat/android/ui/common/feature/messages/composer/mention/CompatUserLookupHandler {
581586
public abstract fun handleCompatUserLookup (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0;
582587
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.getstream.chat.android.ui.common.feature.messages.composer.capabilities
18+
19+
import io.getstream.chat.android.models.ChannelCapabilities
20+
import io.getstream.chat.android.ui.common.state.messages.MessageMode
21+
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
22+
23+
/**
24+
* Determines whether the user can send a message in the current state.
25+
*
26+
* This function checks both channel-level capabilities and user preferences to determine
27+
* if a message can be sent. The required capability depends on the current message mode:
28+
* - In thread mode: requires [ChannelCapabilities.SEND_REPLY] capability
29+
* - In regular mode: requires [ChannelCapabilities.SEND_MESSAGE] capability
30+
*
31+
* The final decision also considers the [MessageComposerState.sendEnabled] flag, which allows
32+
* temporary disabling of sending (e.g., while uploading attachments or validating input).
33+
*
34+
* @return `true` if the user can send a message or reply based on both capabilities and
35+
* the send enabled flag; `false` otherwise.
36+
*/
37+
public fun MessageComposerState.canSendMessage(): Boolean {
38+
val canSendMessage = ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
39+
val canSendReply = ownCapabilities.contains(ChannelCapabilities.SEND_REPLY)
40+
val isInThread = messageMode is MessageMode.MessageThread
41+
val canSend = if (isInThread) {
42+
canSendReply
43+
} else {
44+
canSendMessage
45+
}
46+
// The final send capability depends on the channel capabilities, and potentially the user-set sendEnabled flag
47+
return sendEnabled && canSend
48+
}
49+
50+
/**
51+
* Determines whether the user can upload files in the current state.
52+
*
53+
* This function checks if the user has the [ChannelCapabilities.UPLOAD_FILE] capability
54+
* for the current channel. This capability allows users to attach and upload files
55+
* (documents, images, videos, etc.) through the message composer.
56+
*
57+
* @return `true` if the user has the [ChannelCapabilities.UPLOAD_FILE] capability; `false` otherwise.
58+
*/
59+
public fun MessageComposerState.canUploadFile(): Boolean {
60+
return ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
61+
}

0 commit comments

Comments
 (0)