diff --git a/package/src/components/MessageInput/MessageComposerLeadingView.tsx b/package/src/components/MessageInput/MessageComposerLeadingView.tsx
new file mode 100644
index 0000000000..e45e927757
--- /dev/null
+++ b/package/src/components/MessageInput/MessageComposerLeadingView.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+
+import Animated, { LinearTransition } from 'react-native-reanimated';
+
+import { InputButtons } from './components/InputButtons';
+import { idleRecordingStateSelector } from './utils/audioRecorderSelectors';
+
+import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../hooks/useStateStore';
+
+export const MessageComposerLeadingView = () => {
+ const {
+ theme: {
+ messageInput: { inputButtonsContainer },
+ },
+ } = useTheme();
+ const { audioRecorderManager, messageInputFloating } = useMessageInputContext();
+ const { isRecordingStateIdle } = useStateStore(
+ audioRecorderManager.state,
+ idleRecordingStateSelector,
+ );
+
+ return isRecordingStateIdle ? (
+
+
+
+ ) : null;
+};
+
+const styles = StyleSheet.create({
+ inputButtonsContainer: {
+ alignSelf: 'flex-end',
+ },
+ shadow: {
+ elevation: 6,
+
+ shadowColor: 'hsla(0, 0%, 0%, 0.24)',
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.24,
+ shadowRadius: 12,
+ },
+});
diff --git a/package/src/components/MessageInput/MessageComposerTrailingView.tsx b/package/src/components/MessageInput/MessageComposerTrailingView.tsx
new file mode 100644
index 0000000000..0ff4c0341a
--- /dev/null
+++ b/package/src/components/MessageInput/MessageComposerTrailingView.tsx
@@ -0,0 +1,3 @@
+export const MessageComposerTrailingView = () => {
+ return null;
+};
diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx
index 2f7c4706ea..ad5d2cff7e 100644
--- a/package/src/components/MessageInput/MessageInput.tsx
+++ b/package/src/components/MessageInput/MessageInput.tsx
@@ -14,19 +14,18 @@ import Animated, {
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { type MessageComposerState, type TextComposerState, type UserResponse } from 'stream-chat';
+import { type UserResponse } from 'stream-chat';
-import { LinkPreviewList } from './components/LinkPreviewList';
-import { OutputButtons } from './components/OutputButtons';
+import { MicPositionProvider } from './contexts/MicPositionContext';
+import { MessageComposerLeadingView } from './MessageComposerLeadingView';
+import { MessageComposerTrailingView } from './MessageComposerTrailingView';
+import { MessageInputHeaderView } from './MessageInputHeaderView';
+import { MessageInputLeadingView } from './MessageInputLeadingView';
+import { MessageInputTrailingView } from './MessageInputTrailingView';
-import { useHasLinkPreviews } from './hooks/useLinkPreviews';
+import { audioRecorderSelector } from './utils/audioRecorderSelectors';
-import {
- ChatContextValue,
- useAttachmentManagerState,
- useChatContext,
- useOwnCapabilitiesContext,
-} from '../../contexts';
+import { ChatContextValue, useChatContext, useOwnCapabilitiesContext } from '../../contexts';
import {
AttachmentPickerContextValue,
useAttachmentPickerContext,
@@ -62,7 +61,6 @@ import { MessageInputHeightState } from '../../state-store/message-input-height-
import { primitives } from '../../theme';
import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput';
import { CreatePoll } from '../Poll/CreatePollContent';
-import { GiphyBadge } from '../ui/GiphyBadge';
import { SafeAreaViewWrapper } from '../UIComponents/SafeAreaViewWrapper';
const useStyles = () => {
@@ -191,7 +189,6 @@ type MessageInputPropsWithContext = Pick<
Pick &
Pick & {
editing: boolean;
- hasAttachments: boolean;
isKeyboardVisible: boolean;
TextInputComponent?: React.ComponentType<
TextInputProps & {
@@ -202,16 +199,6 @@ type MessageInputPropsWithContext = Pick<
recordingStatus?: string;
};
-const textComposerStateSelector = (state: TextComposerState) => ({
- command: state.command,
- mentionedUsers: state.mentionedUsers,
- suggestions: state.suggestions,
-});
-
-const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
- quotedMessage: state.quotedMessage,
-});
-
const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({
height: state.height,
});
@@ -226,7 +213,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
additionalTextInputProps,
asyncMessagesLockDistance,
asyncMessagesSlideToCancelDistance,
- AttachmentUploadPreviewList,
AudioRecorder,
AudioRecordingInProgress,
AudioRecordingLockIndicator,
@@ -237,15 +223,12 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
CreatePollContent,
disableAttachmentPicker,
editing,
- hasAttachments,
messageInputFloating,
messageInputHeightStore,
Input,
inputBoxRef,
- InputButtons,
isKeyboardVisible,
members,
- Reply,
threadList,
sendMessage,
showPollCreationDialog,
@@ -259,33 +242,21 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const styles = useStyles();
const messageComposer = useMessageComposer();
- const { clearEditingState } = useMessageComposerAPIContext();
- const onDismissEditMessage = () => {
- clearEditingState();
- };
- const { textComposer } = messageComposer;
- const { command } = useStateStore(textComposer.state, textComposerStateSelector);
- const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
const { height } = useStateStore(messageInputHeightStore.store, messageInputHeightStoreSelector);
- const hasLinkPreviews = useHasLinkPreviews();
-
const {
theme: {
semantics,
messageInput: {
attachmentSelectionBar,
container,
- contentContainer,
floatingWrapper,
focusedInputBoxContainer,
inputBoxContainer,
inputBoxWrapper,
inputContainer,
- inputButtonsContainer,
inputFloatingContainer,
- outputButtonsContainer,
suggestionsListContainer: { container: suggestionListContainer },
wrapper,
},
@@ -380,200 +351,154 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const BOTTOM_OFFSET = isKeyboardVisible ? 16 : bottom ? bottom : 16;
+ const micPositionContextValue = useMemo(
+ () => ({ micPositionX, micPositionY }),
+ [micPositionX, micPositionY],
+ );
+
return (
- <>
-
- messageInputHeightStore.setHeight(
- messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
- )
- } // BOTTOM OFFSET is the position of the input from the bottom of the screen
- style={
- messageInputFloating
- ? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
- : [
- styles.wrapper,
- {
- borderTopWidth: 1,
- backgroundColor: semantics.composerBg,
- borderColor: semantics.borderCoreDefault,
- paddingBottom: BOTTOM_OFFSET,
- },
- wrapper,
- ]
- }
- >
- {Input ? (
-
- ) : (
-
- {isRecordingStateIdle ? (
+
+ <>
+
+ messageInputHeightStore.setHeight(
+ messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
+ )
+ } // BOTTOM OFFSET is the position of the input from the bottom of the screen
+ style={
+ messageInputFloating
+ ? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
+ : [
+ styles.wrapper,
+ {
+ borderTopWidth: 1,
+ backgroundColor: semantics.composerBg,
+ borderColor: semantics.borderCoreDefault,
+ paddingBottom: BOTTOM_OFFSET,
+ },
+ wrapper,
+ ]
+ }
+ >
+ {Input ? (
+
+ ) : (
+
+
- {InputButtons && }
-
- ) : null}
-
-
- {recordingStatus === 'stopped' ? (
-
- ) : micLocked ? (
-
- ) : null}
- {isRecordingStateIdle ? (
+
+ {recordingStatus === 'stopped' ? (
+
+ ) : micLocked ? (
+
+ ) : null}
+
+
+
- {editing ? (
-
-
+ ) : (
+ <>
+
+
+
-
- ) : null}
- {quotedMessage ? (
-
-
-
- ) : null}
-
-
+ >
+ )}
+
+
- ) : null}
-
-
- {!isRecordingStateIdle ? (
-
- ) : (
- <>
- {command ? (
-
-
-
- ) : null}
-
-
- >
- )}
-
- {(recordingStatus === 'idle' || recordingStatus === 'recording') && !micLocked ? (
-
-
-
- ) : null}
-
-
-
+
+
+
+ )}
+
+
+
+ {!isRecordingStateIdle ? (
+
+
+ ) : (
+
)}
-
-
-
- {!isRecordingStateIdle ? (
-
-
-
- ) : null}
-
-
-
-
- {!disableAttachmentPicker && selectedPicker ? (
+
-
+
- ) : null}
-
- {showPollCreationDialog ? (
-
-
-
-
-
-
-
-
-
- ) : null}
- >
+
+
+ ) : null}
+
+ {showPollCreationDialog ? (
+
+
+
+
+
+
+
+
+
+ ) : null}
+ >
+
);
};
@@ -590,7 +515,6 @@ const areEqual = (
channel: prevChannel,
closePollCreationDialog: prevClosePollCreationDialog,
editing: prevEditing,
- hasAttachments: prevHasAttachments,
isKeyboardVisible: prevIsKeyboardVisible,
isOnline: prevIsOnline,
openPollCreationDialog: prevOpenPollCreationDialog,
@@ -613,7 +537,6 @@ const areEqual = (
editing: nextEditing,
isKeyboardVisible: nextIsKeyboardVisible,
isOnline: nextIsOnline,
- hasAttachments: nextHasAttachments,
openPollCreationDialog: nextOpenPollCreationDialog,
selectedPicker: nextSelectedPicker,
showPollCreationDialog: nextShowPollCreationDialog,
@@ -676,11 +599,6 @@ const areEqual = (
return false;
}
- const hasAttachmentsEqual = prevHasAttachments === nextHasAttachments;
- if (!hasAttachmentsEqual) {
- return false;
- }
-
const isKeyboardVisibleEqual = prevIsKeyboardVisible === nextIsKeyboardVisible;
if (!isKeyboardVisibleEqual) {
return false;
@@ -726,12 +644,6 @@ const MemoizedMessageInput = React.memo(
export type MessageInputProps = Partial;
-const audioRecorderSelector = (state: AudioRecorderManagerState) => ({
- micLocked: state.micLocked,
- isRecordingStateIdle: state.status === 'idle',
- recordingStatus: state.status,
-});
-
/**
* UI Component for message input
* It's a consumer of
@@ -798,7 +710,6 @@ export const MessageInput = (props: MessageInputProps) => {
const { clearEditingState } = useMessageComposerAPIContext();
const { Reply } = useMessagesContext();
- const { attachments } = useAttachmentManagerState();
const isKeyboardVisible = useKeyboardVisibility();
const { micLocked, isRecordingStateIdle, recordingStatus } = useStateStore(
@@ -854,7 +765,6 @@ export const MessageInput = (props: MessageInputProps) => {
disableAttachmentPicker,
editing,
FileSelectorIcon,
- hasAttachments: attachments.length > 0,
ImageSelectorIcon,
Input,
inputBoxRef,
diff --git a/package/src/components/MessageInput/MessageInputHeaderView.tsx b/package/src/components/MessageInput/MessageInputHeaderView.tsx
new file mode 100644
index 0000000000..44d55bd6ad
--- /dev/null
+++ b/package/src/components/MessageInput/MessageInputHeaderView.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+
+import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
+
+import { LinkPreviewList } from './components/LinkPreviewList';
+import { useHasLinkPreviews } from './hooks/useLinkPreviews';
+
+import { idleRecordingStateSelector } from './utils/audioRecorderSelectors';
+import { messageComposerStateStoreSelector } from './utils/messageComposerSelectors';
+
+import { useMessageComposerAPIContext } from '../../contexts/messageComposerContext/MessageComposerAPIContext';
+import { useHasAttachments } from '../../contexts/messageInputContext/hooks/useHasAttachments';
+import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
+import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext';
+import { useMessagesContext } from '../../contexts/messagesContext/MessagesContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../hooks/useStateStore';
+import { primitives } from '../../theme';
+
+export const MessageInputHeaderView = () => {
+ const {
+ theme: {
+ messageInput: { contentContainer },
+ },
+ } = useTheme();
+ const messageComposer = useMessageComposer();
+ const editing = !!messageComposer.editedMessage;
+ const { clearEditingState } = useMessageComposerAPIContext();
+ const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
+ const hasLinkPreviews = useHasLinkPreviews();
+ const { audioRecorderManager, AttachmentUploadPreviewList } = useMessageInputContext();
+ const { Reply } = useMessagesContext();
+ const { isRecordingStateIdle } = useStateStore(
+ audioRecorderManager.state,
+ idleRecordingStateSelector,
+ );
+ const hasAttachments = useHasAttachments();
+
+ return isRecordingStateIdle ? (
+
+ {editing ? (
+
+
+
+ ) : null}
+ {quotedMessage ? (
+
+
+
+ ) : null}
+
+
+
+ ) : null;
+};
+
+const styles = StyleSheet.create({
+ contentContainer: {
+ gap: primitives.spacingXxs,
+ overflow: 'hidden',
+ paddingHorizontal: primitives.spacingXs,
+ },
+});
diff --git a/package/src/components/MessageInput/MessageInputLeadingView.tsx b/package/src/components/MessageInput/MessageInputLeadingView.tsx
new file mode 100644
index 0000000000..2037de40d2
--- /dev/null
+++ b/package/src/components/MessageInput/MessageInputLeadingView.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import { textComposerStateSelector } from './utils/messageComposerSelectors';
+
+import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
+import { useStateStore } from '../../hooks/useStateStore';
+import { primitives } from '../../theme';
+import { GiphyBadge } from '../ui/GiphyBadge';
+
+export const MessageInputLeadingView = () => {
+ const messageComposer = useMessageComposer();
+ const { textComposer } = messageComposer;
+ const { command } = useStateStore(textComposer.state, textComposerStateSelector);
+
+ return command ? (
+
+
+
+ ) : null;
+};
+
+const styles = StyleSheet.create({
+ giphyContainer: {
+ padding: primitives.spacingXs,
+ },
+});
diff --git a/package/src/components/MessageInput/MessageInputTrailingView.tsx b/package/src/components/MessageInput/MessageInputTrailingView.tsx
new file mode 100644
index 0000000000..d0a7531f64
--- /dev/null
+++ b/package/src/components/MessageInput/MessageInputTrailingView.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+
+import { OutputButtons } from './components/OutputButtons';
+
+import { audioRecorderSelector } from './utils/audioRecorderSelectors';
+
+import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useStateStore } from '../../hooks/useStateStore';
+import { primitives } from '../../theme';
+
+export const MessageInputTrailingView = () => {
+ const {
+ theme: {
+ messageInput: { outputButtonsContainer },
+ },
+ } = useTheme();
+ const { audioRecorderManager } = useMessageInputContext();
+ const { micLocked, recordingStatus } = useStateStore(
+ audioRecorderManager.state,
+ audioRecorderSelector,
+ );
+ return (recordingStatus === 'idle' || recordingStatus === 'recording') && !micLocked ? (
+
+
+
+ ) : null;
+};
+
+const styles = StyleSheet.create({
+ outputButtonsContainer: {
+ alignSelf: 'flex-end',
+ padding: primitives.spacingXs,
+ },
+});
diff --git a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
index 4733e48b26..0f384672ee 100644
--- a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
+++ b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
@@ -40,7 +40,7 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": false,
"expanded": undefined,
"selected": undefined,
}
@@ -393,7 +393,7 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": false,
"expanded": undefined,
"selected": undefined,
}
@@ -746,7 +746,7 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": true,
"expanded": undefined,
"selected": undefined,
}
diff --git a/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap b/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap
index ce5c623954..364869e599 100644
--- a/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap
+++ b/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap
@@ -39,7 +39,7 @@ exports[`SendButton should render a SendButton 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": false,
"expanded": undefined,
"selected": undefined,
}
@@ -389,7 +389,7 @@ exports[`SendButton should render a disabled SendButton 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": true,
"expanded": undefined,
"selected": undefined,
}
diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
index 82f95e7385..63dd611ac9 100644
--- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
+++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
@@ -5,7 +5,6 @@ import { Gesture, GestureDetector, State } from 'react-native-gesture-handler';
import Animated, {
clamp,
runOnJS,
- SharedValue,
useAnimatedStyle,
useSharedValue,
withSpring,
@@ -23,7 +22,9 @@ import { useStateStore } from '../../../../hooks/useStateStore';
import { NewMic } from '../../../../icons/NewMic';
import { NativeHandlers } from '../../../../native';
import { AudioRecorderManagerState } from '../../../../state-store/audio-recorder-manager';
-import { Button } from '../../../ui';
+import { primitives } from '../../../../theme';
+import { ButtonStylesConfig, useButtonStyles } from '../../../ui/Button/hooks/useButtonStyles';
+import { useMicPositionContext } from '../../contexts/MicPositionContext';
export type AudioRecordingButtonPropsWithContext = Pick<
MessageInputContextValue,
@@ -49,11 +50,14 @@ export type AudioRecordingButtonPropsWithContext = Pick<
* Handler to determine what should happen on press of the mic button.
*/
handlePress?: () => void;
- micPositionX: SharedValue;
- micPositionY: SharedValue;
cancellableDuration: boolean;
};
+const buttonStylesConfig: ButtonStylesConfig = {
+ variant: 'secondary',
+ type: 'ghost',
+};
+
/**
* Component to display the mic button on the Message Input.
*/
@@ -69,21 +73,23 @@ export const AudioRecordingButtonWithContext = (props: AudioRecordingButtonProps
uploadVoiceRecording,
handleLongPress,
handlePress,
- micPositionX,
- micPositionY,
cancellableDuration,
status,
recording,
} = props;
+ const { micPositionX, micPositionY } = useMicPositionContext();
const activeAudioPlayer = useActiveAudioPlayer();
const scale = useSharedValue(1);
+ const pressed = useSharedValue(false);
const { t } = useTranslationContext();
const {
theme: {
messageInput: { micButtonContainer },
+ semantics,
},
} = useTheme();
+ const buttonStyles = useButtonStyles(buttonStylesConfig);
const onPressHandler = useStableCallback(() => {
if (handlePress) {
@@ -147,9 +153,9 @@ export const AudioRecordingButtonWithContext = (props: AudioRecordingButtonProps
const onTouchGestureEnd = useStableCallback(() => {
if (status === 'recording') {
if (cancellableDuration) {
- runOnJS(onEarlyReleaseHandler)();
+ onEarlyReleaseHandler();
} else {
- runOnJS(uploadVoiceRecording)(asyncMessagesMultiSendEnabled);
+ uploadVoiceRecording(asyncMessagesMultiSendEnabled);
}
}
});
@@ -160,17 +166,19 @@ export const AudioRecordingButtonWithContext = (props: AudioRecordingButtonProps
.minDuration(asyncMessagesMinimumPressDuration)
.onBegin(() => {
scale.value = withSpring(0.8, { mass: 0.5 });
+ pressed.value = true;
})
.onStart(() => {
runOnJS(onLongPressHandler)();
})
.onFinalize((e) => {
scale.value = withSpring(1, { mass: 0.5 });
+ pressed.value = false;
if (e.state === State.FAILED) {
runOnJS(onPressHandler)();
}
}),
- [asyncMessagesMinimumPressDuration, onLongPressHandler, onPressHandler, scale],
+ [asyncMessagesMinimumPressDuration, onLongPressHandler, onPressHandler, scale, pressed],
);
const panGesture = useMemo(
@@ -222,33 +230,20 @@ export const AudioRecordingButtonWithContext = (props: AudioRecordingButtonProps
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: scale.value }],
+ backgroundColor: pressed.value ? semantics.backgroundCorePressed : 'transparent',
};
});
return (
-
+
);
};
-export type AudioRecordingButtonProps = Partial & {
- micPositionX: SharedValue;
- micPositionY: SharedValue;
-};
+export type AudioRecordingButtonProps = Partial;
const MemoizedAudioRecordingButton = React.memo(
AudioRecordingButtonWithContext,
@@ -300,5 +295,11 @@ export const AudioRecordingButton = (props: AudioRecordingButtonProps) => {
AudioRecordingButton.displayName = 'AudioRecordingButton{messageInput}';
const styles = StyleSheet.create({
- container: {},
+ container: {
+ borderRadius: primitives.radiusMax,
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: 32,
+ width: 32,
+ },
});
diff --git a/package/src/components/MessageInput/components/OutputButtons/index.tsx b/package/src/components/MessageInput/components/OutputButtons/index.tsx
index 7ca7e51376..33dbdbe0e9 100644
--- a/package/src/components/MessageInput/components/OutputButtons/index.tsx
+++ b/package/src/components/MessageInput/components/OutputButtons/index.tsx
@@ -24,10 +24,7 @@ import { useStateStore } from '../../../../hooks/useStateStore';
import { AIStates, useAIState } from '../../../AITypingIndicatorView';
import { useIsCooldownActive } from '../../hooks/useIsCooldownActive';
-export type OutputButtonsProps = Partial & {
- micPositionX: Animated.SharedValue;
- micPositionY: Animated.SharedValue;
-};
+export type OutputButtonsProps = Partial;
export type OutputButtonsWithContextProps = Pick &
Pick &
@@ -43,8 +40,6 @@ export type OutputButtonsWithContextProps = Pick &
| 'StopMessageStreamingButton'
| 'StartAudioRecordingButton'
> & {
- micPositionX: Animated.SharedValue;
- micPositionY: Animated.SharedValue;
cooldownIsActive: boolean;
};
@@ -62,8 +57,6 @@ export const OutputButtonsWithContext = (props: OutputButtonsWithContextProps) =
SendButton,
StopMessageStreamingButton,
StartAudioRecordingButton,
- micPositionX,
- micPositionY,
} = props;
const {
theme: {
@@ -120,7 +113,7 @@ export const OutputButtonsWithContext = (props: OutputButtonsWithContextProps) =
key='audio-recording-button'
style={audioRecordingButtonContainer}
>
-
+
);
} else {
diff --git a/package/src/components/MessageInput/contexts/MicPositionContext.tsx b/package/src/components/MessageInput/contexts/MicPositionContext.tsx
new file mode 100644
index 0000000000..854b602bc4
--- /dev/null
+++ b/package/src/components/MessageInput/contexts/MicPositionContext.tsx
@@ -0,0 +1,34 @@
+import React, { createContext, PropsWithChildren, useContext } from 'react';
+
+import { SharedValue } from 'react-native-reanimated';
+
+import { DEFAULT_BASE_CONTEXT_VALUE } from '../../../contexts/utils/defaultBaseContextValue';
+import { isTestEnvironment } from '../../../contexts/utils/isTestEnvironment';
+
+export type MicPositionContextValue = {
+ micPositionX: SharedValue;
+ micPositionY: SharedValue;
+};
+
+const MicPositionContext = createContext(
+ DEFAULT_BASE_CONTEXT_VALUE as unknown as MicPositionContextValue,
+);
+
+export const MicPositionProvider = ({
+ children,
+ value,
+}: PropsWithChildren<{ value: MicPositionContextValue }>) => (
+ {children}
+);
+
+export const useMicPositionContext = () => {
+ const contextValue = useContext(MicPositionContext) as unknown as MicPositionContextValue;
+
+ if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) {
+ throw new Error(
+ 'The useMicPositionContext hook was called outside of the MicPositionProvider. Make sure MessageInput wraps the subtree where mic positions are used.',
+ );
+ }
+
+ return contextValue;
+};
diff --git a/package/src/components/MessageInput/utils/audioRecorderSelectors.ts b/package/src/components/MessageInput/utils/audioRecorderSelectors.ts
new file mode 100644
index 0000000000..d82b6c7de1
--- /dev/null
+++ b/package/src/components/MessageInput/utils/audioRecorderSelectors.ts
@@ -0,0 +1,11 @@
+import { AudioRecorderManagerState } from '../../../state-store/audio-recorder-manager';
+
+export const idleRecordingStateSelector = (state: AudioRecorderManagerState) => ({
+ isRecordingStateIdle: state.status === 'idle',
+});
+
+export const audioRecorderSelector = (state: AudioRecorderManagerState) => ({
+ micLocked: state.micLocked,
+ isRecordingStateIdle: state.status === 'idle',
+ recordingStatus: state.status,
+});
diff --git a/package/src/components/MessageInput/utils/messageComposerSelectors.ts b/package/src/components/MessageInput/utils/messageComposerSelectors.ts
new file mode 100644
index 0000000000..b073e1f2bb
--- /dev/null
+++ b/package/src/components/MessageInput/utils/messageComposerSelectors.ts
@@ -0,0 +1,9 @@
+import { MessageComposerState, TextComposerState } from 'stream-chat';
+
+export const textComposerStateSelector = (state: TextComposerState) => ({
+ command: state.command,
+});
+
+export const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
+ quotedMessage: state.quotedMessage,
+});
diff --git a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
index c18d5de3c1..565c589967 100644
--- a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
+++ b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
@@ -53,7 +53,7 @@ exports[`ScrollToBottomButton should render the message notification and match s
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": false,
"expanded": undefined,
"selected": undefined,
}
diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
index 68709acee3..f8132a3a27 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
@@ -1951,7 +1951,7 @@ exports[`Thread should match thread snapshot 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": false,
"expanded": undefined,
"selected": undefined,
}
@@ -2200,7 +2200,7 @@ exports[`Thread should match thread snapshot 1`] = `
{
"busy": undefined,
"checked": undefined,
- "disabled": undefined,
+ "disabled": true,
"expanded": undefined,
"selected": undefined,
}
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index e69443a57d..472bafca36 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -123,6 +123,11 @@ export * from './MessageInput/components/AttachmentPreview/AttachmentUploadPrevi
export * from './MessageInput/components/OutputButtons/CooldownTimer';
export * from './MessageInput/components/InputButtons';
export * from './MessageInput/MessageInput';
+export * from './MessageInput/MessageComposerLeadingView';
+export * from './MessageInput/MessageComposerTrailingView';
+export * from './MessageInput/MessageInputHeaderView';
+export * from './MessageInput/MessageInputLeadingView';
+export * from './MessageInput/MessageInputTrailingView';
export * from './MessageInput/components/OutputButtons/SendButton';
export * from './MessageInput/SendMessageDisallowedIndicator';
export * from './MessageInput/ShowThreadMessageInChannelButton';
@@ -133,6 +138,7 @@ export * from './MessageInput/components/AudioRecorder/AudioRecordingInProgress'
export * from './MessageInput/components/AudioRecorder/AudioRecordingLockIndicator';
export * from './MessageInput/components/AudioRecorder/AudioRecordingPreview';
export * from './MessageInput/components/AudioRecorder/AudioRecordingWaveform';
+export * from './MessageInput/contexts/MicPositionContext';
export * from './MessageInput/components/AttachmentPreview/AttachmentUnsupportedIndicator';
export * from './MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
diff --git a/package/src/components/ui/Button/Button.tsx b/package/src/components/ui/Button/Button.tsx
index c9051c3ed6..11f0d40d68 100644
--- a/package/src/components/ui/Button/Button.tsx
+++ b/package/src/components/ui/Button/Button.tsx
@@ -97,6 +97,7 @@ export const Button = ({
styles.container,
{ paddingHorizontal: buttonPadding[size] },
]}
+ disabled={disabled}
{...rest}
>
{LeftIcon ? (
diff --git a/package/src/components/ui/Button/hooks/useButtonStyles.ts b/package/src/components/ui/Button/hooks/useButtonStyles.ts
index 86f3bbb32c..e61e4754b7 100644
--- a/package/src/components/ui/Button/hooks/useButtonStyles.ts
+++ b/package/src/components/ui/Button/hooks/useButtonStyles.ts
@@ -24,13 +24,15 @@ export type ButtonStyle = {
disabledBorderColor?: ColorValue;
};
+export type ButtonStylesConfig = Pick;
+
/**
* Returns the styles for the button based on the button style and type.
* @param buttonStyle - The style of the button.
* @param type - The type of the button.
* @returns The styles for the button.
*/
-export const useButtonStyles = ({ variant, type }: Pick) => {
+export const useButtonStyles = ({ variant, type }: ButtonStylesConfig) => {
const {
theme: { semantics },
} = useTheme();
diff --git a/package/src/contexts/messageInputContext/hooks/useHasAttachments.ts b/package/src/contexts/messageInputContext/hooks/useHasAttachments.ts
new file mode 100644
index 0000000000..33e6d9c91b
--- /dev/null
+++ b/package/src/contexts/messageInputContext/hooks/useHasAttachments.ts
@@ -0,0 +1,15 @@
+import type { AttachmentManagerState } from 'stream-chat';
+
+import { useMessageComposer } from './useMessageComposer';
+
+import { useStateStore } from '../../../hooks/useStateStore';
+
+const stateSelector = (state: AttachmentManagerState) => ({
+ hasAttachments: state.attachments.length > 0,
+});
+
+export const useHasAttachments = () => {
+ const { attachmentManager } = useMessageComposer();
+ const { hasAttachments } = useStateStore(attachmentManager.state, stateSelector);
+ return hasAttachments;
+};