Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.models.Attachment
import io.getstream.chat.android.models.ChannelCapabilities
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.Reply
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
Expand Down Expand Up @@ -68,8 +68,7 @@ public fun MessageInput(
innerTrailingContent: @Composable RowScope.() -> Unit = {},
) {
val (value, attachments, activeAction) = messageComposerState
val canSendMessage = messageComposerState.sendEnabled &&
messageComposerState.ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
val canSendMessage = canSendMessage(messageComposerState)

InputField(
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import io.getstream.chat.android.models.Command
import io.getstream.chat.android.models.LinkPreview
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.User
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canUploadFile
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.MessageMode
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
Expand Down Expand Up @@ -511,10 +513,7 @@ internal fun DefaultComposerIntegrations(
val isAttachmentsButtonEnabled = !hasCommandInput && !hasCommandSuggestions && !hasMentionSuggestions
val isCommandsButtonEnabled = !hasTextInput && !hasAttachments

val canSendMessage = messageInputState.sendEnabled &&
ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
val canSendAttachments = messageInputState.sendEnabled &&
ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
val canSendMessage = canSendMessage(messageInputState)

val isRecording = messageInputState.recording !is RecordingState.Idle

Expand All @@ -525,7 +524,8 @@ internal fun DefaultComposerIntegrations(
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (canSendAttachments) {
val canUploadFile = canUploadFile(messageInputState)
if (canUploadFile) {
with(ChatTheme.componentFactory) {
MessageComposerAttachmentsButton(
enabled = isAttachmentsButtonEnabled,
Expand Down Expand Up @@ -557,7 +557,7 @@ internal fun DefaultComposerIntegrations(
@Composable
internal fun DefaultComposerLabel(state: MessageComposerState) {
val text = with(state) {
if (sendEnabled && ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)) {
if (canSendMessage(state)) {
stringResource(id = R.string.stream_compose_message_label)
} else {
stringResource(id = R.string.stream_compose_cannot_send_messages_label)
Expand Down Expand Up @@ -621,36 +621,34 @@ internal fun DefaultMessageComposerTrailingContent(
val coolDownTime = messageComposerState.coolDownTime
val validationErrors = messageComposerState.validationErrors
val attachments = messageComposerState.attachments
val ownCapabilities = messageComposerState.ownCapabilities
val isInEditMode = messageComposerState.action is Edit

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

val isSendButtonEnabled = messageComposerState.sendEnabled &&
ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
val canSendMessage = canSendMessage(messageComposerState)
val isInputValid by lazy { (value.isNotBlank() || attachments.isNotEmpty()) && validationErrors.isEmpty() }

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

val sendEnabled = isSendButtonEnabled && isInputValid
val sendVisible = when {
val sendButtonEnabled = canSendMessage && isInputValid
val sendButtonVisible = when {
!isRecordingEnabled -> true
isRecording -> false
showRecordOverSend -> sendEnabled
showRecordOverSend -> sendButtonEnabled
else -> true
}
if (sendVisible) {
if (sendButtonVisible) {
Box(
modifier = Modifier.heightIn(min = ComposerActionContainerMinHeight),
contentAlignment = Center,
) {
ChatTheme.componentFactory.MessageComposerSendButton(
enabled = sendEnabled,
enabled = sendButtonEnabled,
isInputValid = isInputValid,
onClick = {
if (isInputValid) {
Expand All @@ -661,12 +659,12 @@ internal fun DefaultMessageComposerTrailingContent(
}
}

val recordVisible = when {
!isRecordingEnabled -> false
showRecordOverSend -> !sendEnabled
val recordButtonVisible = when {
!canSendMessage || !isRecordingEnabled -> false
showRecordOverSend -> !sendButtonEnabled
else -> true
}
if (recordVisible) {
if (recordButtonVisible) {
ChatTheme.componentFactory.MessageComposerAudioRecordButton(
state = messageComposerState.recording,
recordingActions = recordingActions,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.getstream.chat.docs.java.ui.guides;

import static io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.MessageComposerCapabilitiesKt.canSendMessage;
import static io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.MessageComposerCapabilitiesKt.canUploadFile;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
Expand All @@ -22,7 +25,6 @@
import java.util.Map;

import io.getstream.chat.android.models.Attachment;
import io.getstream.chat.android.models.ChannelCapabilities;
import io.getstream.chat.android.models.Message;
import io.getstream.chat.android.ui.ChatUI;
import io.getstream.chat.android.ui.common.state.messages.Edit;
Expand All @@ -37,8 +39,8 @@
import io.getstream.chat.android.ui.feature.messages.composer.attachment.preview.factory.MediaAttachmentPreviewFactory;
import io.getstream.chat.android.ui.feature.messages.composer.content.MessageComposerLeadingContent;
import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListListeners;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.BaseAttachmentFactory;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.BaseAttachmentFactory;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.DefaultQuotedAttachmentMessageFactory;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.InnerAttachmentViewHolder;
import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.QuotedAttachmentFactory;
Expand Down Expand Up @@ -109,8 +111,8 @@ public void attachContext(@NonNull MessageComposerContext messageComposerContext

@Override
public void renderState(@NonNull MessageComposerState state) {
boolean canSendMessage = state.getOwnCapabilities().contains(ChannelCapabilities.SEND_MESSAGE);
boolean canUploadFile = state.getOwnCapabilities().contains(ChannelCapabilities.UPLOAD_FILE);
boolean canSendMessage = canSendMessage(state);
boolean canUploadFile = canUploadFile(state);
boolean hasTextInput = !state.getInputValue().isEmpty();
boolean hasAttachments = !state.getAttachments().isEmpty();
boolean hasCommandInput = state.getInputValue().startsWith("/");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.google.android.material.datepicker.MaterialDatePicker
import io.getstream.chat.android.models.Attachment
import io.getstream.chat.android.models.ChannelCapabilities
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.ui.ChatUI
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canSendMessage
import io.getstream.chat.android.ui.common.feature.messages.composer.capabilities.canUploadFile
import io.getstream.chat.android.ui.common.state.messages.Edit
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState
import io.getstream.chat.android.ui.feature.messages.composer.MessageComposerContext
Expand Down Expand Up @@ -85,8 +86,8 @@ class AddingCustomAttachments {
}

override fun renderState(state: MessageComposerState) {
val canSendMessage = state.ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
val canUploadFile = state.ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
val canSendMessage = canSendMessage(state)
val canUploadFile = canUploadFile(state)
val hasTextInput = state.inputValue.isNotEmpty()
val hasAttachments = state.attachments.isNotEmpty()
val hasCommandInput = state.inputValue.startsWith("/")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,11 @@ public class io/getstream/chat/android/ui/common/feature/documents/AttachmentDoc
protected fun onCreate (Landroid/os/Bundle;)V
}

public final class io/getstream/chat/android/ui/common/feature/messages/composer/capabilities/MessageComposerCapabilitiesKt {
public static final fun canSendMessage (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)Z
public static final fun canUploadFile (Lio/getstream/chat/android/ui/common/state/messages/composer/MessageComposerState;)Z
}

public abstract interface class io/getstream/chat/android/ui/common/feature/messages/composer/mention/CompatUserLookupHandler {
public abstract fun handleCompatUserLookup (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.ui.common.feature.messages.composer.capabilities

import io.getstream.chat.android.models.ChannelCapabilities
import io.getstream.chat.android.ui.common.state.messages.MessageMode
import io.getstream.chat.android.ui.common.state.messages.composer.MessageComposerState

/**
* Determines whether the user can send a message in the current state.
*
* This function checks both channel-level capabilities and user preferences to determine
* if a message can be sent. The required capability depends on the current message mode:
* - In thread mode: requires [ChannelCapabilities.SEND_REPLY] capability
* - In regular mode: requires [ChannelCapabilities.SEND_MESSAGE] capability
*
* The final decision also considers the [MessageComposerState.sendEnabled] flag, which allows
* temporary disabling of sending (e.g., while uploading attachments or validating input).
*
* @param state The current [MessageComposerState] containing capability and mode information.
* @return `true` if the user can send a message or reply based on both capabilities and
* the send enabled flag; `false` otherwise.
*/
public fun canSendMessage(state: MessageComposerState): Boolean {
val canSendMessage = state.ownCapabilities.contains(ChannelCapabilities.SEND_MESSAGE)
val canSendReply = state.ownCapabilities.contains(ChannelCapabilities.SEND_REPLY)
val isInThread = state.messageMode is MessageMode.MessageThread
val canSend = if (isInThread) {
canSendReply
} else {
canSendMessage
}
// The final send capability depends on the channel capabilities, and potentially the user-set sendEnabled flag
return state.sendEnabled && canSend
}

/**
* Determines whether the user can upload files in the current state.
*
* This function checks if the user has the [ChannelCapabilities.UPLOAD_FILE] capability
* for the current channel. This capability allows users to attach and upload files
* (documents, images, videos, etc.) through the message composer.
*
* @param state The current [MessageComposerState] containing capability information.
* @return `true` if the user has the [ChannelCapabilities.UPLOAD_FILE] capability; `false` otherwise.
*/
public fun canUploadFile(state: MessageComposerState): Boolean {
return state.ownCapabilities.contains(ChannelCapabilities.UPLOAD_FILE)
}
Loading
Loading