diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx
index b71b8c6a67..4bdc24765c 100644
--- a/examples/SampleApp/App.tsx
+++ b/examples/SampleApp/App.tsx
@@ -60,6 +60,7 @@ import { Toast } from './src/components/ToastComponent/Toast';
import { useClientNotificationsToastHandler } from './src/hooks/useClientNotificationsToastHandler';
import AsyncStore from './src/utils/AsyncStore.ts';
import {
+ MessageInputFloatingConfigItem,
MessageListImplementationConfigItem,
MessageListModeConfigItem,
MessageListPruningConfigItem,
@@ -106,6 +107,9 @@ const App = () => {
const [messageListPruning, setMessageListPruning] = useState<
MessageListPruningConfigItem['value'] | undefined
>(undefined);
+ const [messageInputFloating, setMessageInputFloating] = useState<
+ MessageInputFloatingConfigItem['value'] | undefined
+ >(undefined);
const colorScheme = useColorScheme();
const streamChatTheme = useStreamChatTheme();
const streami18n = new Streami18n();
@@ -161,6 +165,10 @@ const App = () => {
'@stream-rn-sampleapp-messagelist-pruning',
{ value: undefined },
);
+ const messageInputFloatingStoredValue = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messageinput-floating',
+ { value: false },
+ );
setMessageListImplementation(
messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
);
@@ -168,6 +176,9 @@ const App = () => {
setMessageListPruning(
messageListPruningStoredValue?.value as MessageListPruningConfigItem['value'],
);
+ setMessageInputFloating(
+ messageInputFloatingStoredValue?.value as MessageInputFloatingConfigItem['value'],
+ );
};
getMessageListConfig();
return () => {
@@ -232,6 +243,7 @@ const App = () => {
logout,
switchUser,
messageListImplementation,
+ messageInputFloating: messageInputFloating ?? false,
messageListMode,
messageListPruning,
}}
diff --git a/examples/SampleApp/src/components/SecretMenu.tsx b/examples/SampleApp/src/components/SecretMenu.tsx
index 0037645b82..aff708538f 100644
--- a/examples/SampleApp/src/components/SecretMenu.tsx
+++ b/examples/SampleApp/src/components/SecretMenu.tsx
@@ -26,6 +26,7 @@ export type NotificationConfigItem = { label: string; name: string; id: string }
export type MessageListImplementationConfigItem = { label: string; id: 'flatlist' | 'flashlist' };
export type MessageListModeConfigItem = { label: string; mode: 'default' | 'livestream' };
export type MessageListPruningConfigItem = { label: string; value: 100 | 500 | 1000 | undefined };
+export type MessageInputFloatingConfigItem = { label: string; value: boolean };
const messageListImplementationConfigItems: MessageListImplementationConfigItem[] = [
{ label: 'FlatList', id: 'flatlist' },
@@ -44,6 +45,11 @@ const messageListPruningConfigItems: MessageListPruningConfigItem[] = [
{ label: '1000 Messages', value: 1000 },
];
+const messageInputFloatingConfigItems: MessageInputFloatingConfigItem[] = [
+ { label: 'Normal', value: false },
+ { label: 'Floating', value: true },
+];
+
export const SlideInView = ({
visible,
children,
@@ -161,6 +167,23 @@ const SecretMenuMessageListImplementationConfigItem = ({
);
+const SecretMenuMessageInputFloatingConfigItem = ({
+ messageInputFloatingConfigItem,
+ storeMessageInputFloating,
+ isSelected,
+}: {
+ messageInputFloatingConfigItem: MessageInputFloatingConfigItem;
+ storeMessageInputFloating: (item: MessageInputFloatingConfigItem) => void;
+ isSelected: boolean;
+}) => (
+ storeMessageInputFloating(messageInputFloatingConfigItem)}
+ >
+ {messageInputFloatingConfigItem.label}
+
+);
+
const SecretMenuMessageListModeConfigItem = ({
messageListModeConfigItem,
storeMessageListMode,
@@ -218,6 +241,8 @@ export const SecretMenu = ({
const [selectedMessageListPruning, setSelectedMessageListPruning] = useState<
MessageListPruningConfigItem['value'] | null
>(null);
+ const [selectedMessageInputFloating, setSelectedMessageInputFloating] =
+ useState(false);
const {
theme: {
colors: { black, grey },
@@ -250,12 +275,19 @@ export const SecretMenu = ({
'@stream-rn-sampleapp-messagelist-pruning',
messageListPruningConfigItems[0],
);
+ const messageInputFloating = await AsyncStore.getItem(
+ '@stream-rn-sampleapp-messageinput-floating',
+ messageInputFloatingConfigItems[0],
+ );
setSelectedProvider(notificationProvider?.id ?? notificationConfigItems[0].id);
setSelectedMessageListImplementation(
messageListImplementation?.id ?? messageListImplementationConfigItems[0].id,
);
setSelectedMessageListMode(messageListMode?.mode ?? messageListModeConfigItems[0].mode);
setSelectedMessageListPruning(messageListPruning?.value);
+ setSelectedMessageInputFloating(
+ messageInputFloating?.value ?? messageInputFloatingConfigItems[0].value,
+ );
};
getSelectedConfig();
}, [notificationConfigItems]);
@@ -283,6 +315,11 @@ export const SecretMenu = ({
setSelectedMessageListPruning(item.value);
}, []);
+ const storeMessageInputFloating = useCallback(async (item: MessageInputFloatingConfigItem) => {
+ await AsyncStore.setItem('@stream-rn-sampleapp-messageinput-floating', item);
+ setSelectedMessageInputFloating(item.value);
+ }, []);
+
const removeAllDevices = useCallback(async () => {
const { devices } = await chatClient.getDevices(chatClient.userID);
for (const device of devices ?? []) {
@@ -335,6 +372,22 @@ export const SecretMenu = ({
+
+
+
+ Message Input Floating
+
+ {messageInputFloatingConfigItems.map((item) => (
+
+ ))}
+
+
+
diff --git a/examples/SampleApp/src/context/AppContext.ts b/examples/SampleApp/src/context/AppContext.ts
index 9ec7c80990..b0e7921d67 100644
--- a/examples/SampleApp/src/context/AppContext.ts
+++ b/examples/SampleApp/src/context/AppContext.ts
@@ -4,6 +4,7 @@ import type { StreamChat } from 'stream-chat';
import type { LoginConfig } from '../types';
import {
+ MessageInputFloatingConfigItem,
MessageListImplementationConfigItem,
MessageListModeConfigItem,
MessageListPruningConfigItem,
@@ -15,6 +16,7 @@ type AppContextType = {
logout: () => void;
switchUser: (userId?: string) => void;
messageListImplementation: MessageListImplementationConfigItem['id'];
+ messageInputFloating: MessageInputFloatingConfigItem['value'];
messageListMode: MessageListModeConfigItem['mode'];
messageListPruning: MessageListPruningConfigItem['value'];
};
diff --git a/examples/SampleApp/src/screens/ChannelListScreen.tsx b/examples/SampleApp/src/screens/ChannelListScreen.tsx
index 796406673d..8bc0d965cf 100644
--- a/examples/SampleApp/src/screens/ChannelListScreen.tsx
+++ b/examples/SampleApp/src/screens/ChannelListScreen.tsx
@@ -9,7 +9,7 @@ import {
View,
} from 'react-native';
import { useNavigation, useScrollToTop } from '@react-navigation/native';
-import { ChannelList, CircleClose, Search, useTheme } from 'stream-chat-react-native';
+import { ChannelList, CircleClose, useTheme } from 'stream-chat-react-native';
import { Channel } from 'stream-chat';
import { ChannelPreview } from '../components/ChannelPreview';
import { ChatScreenHeader } from '../components/ChatScreenHeader';
@@ -19,6 +19,7 @@ import { usePaginatedSearchedMessages } from '../hooks/usePaginatedSearchedMessa
import type { ChannelSort } from 'stream-chat';
import { useStreamChatContext } from '../context/StreamChatContext';
+import { Search } from '../icons/Search';
const styles = StyleSheet.create({
channelListContainer: {
diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx
index ba4898e133..70b489cca4 100644
--- a/examples/SampleApp/src/screens/ChannelScreen.tsx
+++ b/examples/SampleApp/src/screens/ChannelScreen.tsx
@@ -20,7 +20,6 @@ import {
} from 'stream-chat-react-native';
import { Pressable, StyleSheet, View } from 'react-native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useAppContext } from '../context/AppContext';
import { ScreenHeader } from '../components/ScreenHeader';
@@ -122,9 +121,13 @@ export const ChannelScreen: React.FC = ({
params: { channel: channelFromProp, channelId, messageId },
},
}) => {
- const { chatClient, messageListImplementation, messageListMode, messageListPruning } =
- useAppContext();
- const { bottom } = useSafeAreaInsets();
+ const {
+ chatClient,
+ messageListImplementation,
+ messageListMode,
+ messageListPruning,
+ messageInputFloating,
+ } = useAppContext();
const {
theme: { colors },
} = useTheme();
@@ -218,11 +221,12 @@ export const ChannelScreen: React.FC = ({
}
return (
-
+
({ parentMessage: nextValue.parentMessage }) as const;
@@ -84,6 +86,8 @@ export const ThreadScreen: React.FC = ({
const { client: chatClient } = useChatContext();
const { t } = useTranslationContext();
const { setThread } = useStreamChatContext();
+ const { messageInputFloating } = useAppContext();
+ const headerHeight = useHeaderHeight();
const onPressMessage: NonNullable['onPressMessage']> = (
payload,
@@ -115,14 +119,15 @@ export const ThreadScreen: React.FC = ({
}, [setThread]);
return (
-
+
= ({
-
+
);
};
diff --git a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
index a49bfe7c7e..ac23dd1054 100644
--- a/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
+++ b/package/src/components/AutoCompleteInput/AutoCompleteInput.tsx
@@ -3,7 +3,6 @@ import {
I18nManager,
TextInput as RNTextInput,
StyleSheet,
- TextInputContentSizeChangeEvent,
TextInputProps,
TextInputSelectionChangeEvent,
} from 'react-native';
@@ -35,7 +34,7 @@ type AutoCompleteInputPropsWithContext = TextInputProps &
* This is currently passed in from MessageInput to avoid rerenders
* that would happen if we put this in the MessageInputContext
*/
- cooldownActive?: boolean;
+ cooldownRemainingSeconds?: number;
TextInputComponent?: React.ComponentType<
TextInputProps & {
ref: React.Ref | undefined;
@@ -55,18 +54,19 @@ const configStateSelector = (state: MessageComposerConfig) => ({
});
const MAX_NUMBER_OF_LINES = 5;
+const LINE_HEIGHT = 20;
+const PADDING_VERTICAL = 12;
const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext) => {
const {
channel,
- cooldownActive = false,
+ cooldownRemainingSeconds,
setInputBoxRef,
t,
TextInputComponent = RNTextInput,
...rest
} = props;
const [localText, setLocalText] = useState('');
- const [textHeight, setTextHeight] = useState(0);
const messageComposer = useMessageComposer();
const { textComposer } = messageComposer;
const { command, text } = useStateStore(textComposer.state, textComposerStateSelector);
@@ -115,15 +115,12 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
} = useTheme();
const placeholderText = useMemo(() => {
- return command ? t('Search') : cooldownActive ? t('Slow mode ON') : t('Send a message');
- }, [command, cooldownActive, t]);
-
- const handleContentSizeChange = useCallback(
- ({ nativeEvent: { contentSize } }: TextInputContentSizeChangeEvent) => {
- setTextHeight(contentSize.height);
- },
- [],
- );
+ return command
+ ? t('Search')
+ : cooldownRemainingSeconds
+ ? `Slow mode, wait ${cooldownRemainingSeconds}s...`
+ : t('Send a message');
+ }, [command, cooldownRemainingSeconds, t]);
return (
{
- const { channel: prevChannel, cooldownActive: prevCooldownActive, t: prevT } = prevProps;
- const { channel: nextChannel, cooldownActive: nextCooldownActive, t: nextT } = nextProps;
+ const {
+ channel: prevChannel,
+ cooldownRemainingSeconds: prevCooldownRemainingSeconds,
+ t: prevT,
+ } = prevProps;
+ const {
+ channel: nextChannel,
+ cooldownRemainingSeconds: nextCooldownRemainingSeconds,
+ t: nextT,
+ } = nextProps;
const tEqual = prevT === nextT;
if (!tEqual) {
return false;
}
- const cooldownActiveEqual = prevCooldownActive === nextCooldownActive;
- if (!cooldownActiveEqual) {
+ const cooldownRemainingSecondsEqual =
+ prevCooldownRemainingSeconds === nextCooldownRemainingSeconds;
+ if (!cooldownRemainingSecondsEqual) {
return false;
}
@@ -206,6 +211,8 @@ const styles = StyleSheet.create({
flex: 1,
fontSize: 16,
includeFontPadding: false, // for android vertical text centering
+ lineHeight: 20,
+ paddingLeft: 16,
paddingVertical: 12,
textAlignVertical: 'center', // for android vertical text centering
},
diff --git a/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js b/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
index c3591fc026..945581876e 100644
--- a/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
+++ b/package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.js
@@ -113,29 +113,6 @@ describe('AutoCompleteInput', () => {
});
});
- it('should style the text input with maxHeight that is set by the layout', async () => {
- const channelProps = { channel };
- const props = { numberOfLines: 10 };
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- const input = queryByTestId('auto-complete-text-input');
-
- act(() => {
- fireEvent(input, 'contentSizeChange', {
- nativeEvent: {
- contentSize: { height: 100 },
- },
- });
- });
-
- await waitFor(() => {
- expect(input.props.style[1].maxHeight).toBe(1000);
- });
- });
-
it('should call the textComposer setSelection when the onSelectionChange is triggered', async () => {
const { textComposer } = channel.messageComposer;
@@ -166,12 +143,12 @@ describe('AutoCompleteInput', () => {
// TODO: Add a test for command
it.each([
- { cooldownActive: false, result: 'Send a message' },
- { cooldownActive: true, result: 'Slow mode ON' },
+ { cooldownRemainingSeconds: undefined, result: 'Send a message' },
+ { cooldownRemainingSeconds: 10, result: 'Slow mode, wait 10s...' },
])('should have the placeholderText as Slow mode ON when cooldown is active', async (data) => {
const channelProps = { channel };
const props = {
- cooldownActive: data.cooldownActive,
+ cooldownRemainingSeconds: data.cooldownRemainingSeconds,
};
renderComponent({ channelProps, client, props });
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index 6afeff2d77..fe0469bd88 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -173,9 +173,7 @@ import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageS
import { ReactionListBottom as ReactionListBottomDefault } from '../Message/MessageSimple/ReactionList/ReactionListBottom';
import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageSimple/ReactionList/ReactionListTop';
import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
-import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
-import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/AttachmentUploadPreviewList';
-import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
+import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList';
import { AttachmentUploadProgressIndicator as AttachmentUploadProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
import { AudioAttachmentUploadPreview as AudioAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview';
import { FileAttachmentUploadPreview as FileAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview';
@@ -187,12 +185,10 @@ import { AudioRecordingLockIndicator as AudioRecordingLockIndicatorDefault } fro
import { AudioRecordingPreview as AudioRecordingPreviewDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingPreview';
import { AudioRecordingWaveform as AudioRecordingWaveformDefault } from '../MessageInput/components/AudioRecorder/AudioRecordingWaveform';
import { CommandInput as CommandInputDefault } from '../MessageInput/components/CommandInput';
-import { InputEditingStateHeader as InputEditingStateHeaderDefault } from '../MessageInput/components/InputEditingStateHeader';
-import { InputReplyStateHeader as InputReplyStateHeaderDefault } from '../MessageInput/components/InputReplyStateHeader';
-import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/CooldownTimer';
-import { InputButtons as InputButtonsDefault } from '../MessageInput/InputButtons';
-import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/MoreOptionsButton';
-import { SendButton as SendButtonDefault } from '../MessageInput/SendButton';
+import { InputButtons as InputButtonsDefault } from '../MessageInput/components/InputButtons';
+import { AttachButton as AttachButtonDefault } from '../MessageInput/components/InputButtons/AttachButton';
+import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/components/OutputButtons/CooldownTimer';
+import { SendButton as SendButtonDefault } from '../MessageInput/components/OutputButtons/SendButton';
import { SendMessageDisallowedIndicator as SendMessageDisallowedIndicatorDefault } from '../MessageInput/SendMessageDisallowedIndicator';
import { ShowThreadMessageInChannelButton as ShowThreadMessageInChannelButtonDefault } from '../MessageInput/ShowThreadMessageInChannelButton';
import { StopMessageStreamingButton as DefaultStopMessageStreamingButton } from '../MessageInput/StopMessageStreamingButton';
@@ -608,7 +604,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
channel,
children,
client,
- CommandsButton = CommandsButtonDefault,
compressImageQuality,
CooldownTimer = CooldownTimerDefault,
CreatePollContent,
@@ -671,9 +666,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
InlineUnreadIndicator = InlineUnreadIndicatorDefault,
Input,
InputButtons = InputButtonsDefault,
- InputEditingStateHeader = InputEditingStateHeaderDefault,
CommandInput = CommandInputDefault,
- InputReplyStateHeader = InputReplyStateHeaderDefault,
isAttachmentEqual,
isMessageAIGenerated = () => false,
keyboardBehavior,
@@ -708,6 +701,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageDeleted = MessageDeletedDefault,
MessageEditedTimestamp = MessageEditedTimestampDefault,
MessageError = MessageErrorDefault,
+ messageInputFloating = false,
MessageFooter = MessageFooterDefault,
MessageHeader,
messageId,
@@ -729,7 +723,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
MessageUserReactions = MessageUserReactionsDefault,
MessageUserReactionsAvatar = MessageUserReactionsAvatarDefault,
MessageUserReactionsItem = MessageUserReactionsItemDefault,
- MoreOptionsButton = MoreOptionsButtonDefault,
myMessageTheme,
NetworkDownIndicator = NetworkDownIndicatorDefault,
// TODO: Think about this one
@@ -1866,7 +1859,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
CameraSelectorIcon,
channelId,
CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
@@ -1884,9 +1876,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
openPollCreationDialog,
SendButton,
sendMessage,
diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
index 5c5d4a0607..197d1419b8 100644
--- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
+++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts
@@ -30,7 +30,6 @@ export const useCreateInputMessageInputContext = ({
channelId,
CameraSelectorIcon,
CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
@@ -48,9 +47,7 @@ export const useCreateInputMessageInputContext = ({
ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
openPollCreationDialog,
SendButton,
sendMessage,
@@ -96,7 +93,6 @@ export const useCreateInputMessageInputContext = ({
AutoCompleteSuggestionList,
CameraSelectorIcon,
CommandInput,
- CommandsButton,
compressImageQuality,
CooldownTimer,
CreatePollContent,
@@ -114,9 +110,7 @@ export const useCreateInputMessageInputContext = ({
ImageSelectorIcon,
Input,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
openPollCreationDialog,
SendButton,
sendMessage,
diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx
index bddd71b580..7f02d27310 100644
--- a/package/src/components/Message/MessageSimple/MessageContent.tsx
+++ b/package/src/components/Message/MessageSimple/MessageContent.tsx
@@ -27,8 +27,6 @@ import {
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
-import { useViewport } from '../../../hooks/useViewport';
-
import { checkMessageEquality, checkQuotedMessageEquality } from '../../../utils/utils';
import { Poll } from '../../Poll/Poll';
import { useMessageData } from '../hooks/useMessageData';
@@ -180,7 +178,6 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
},
},
} = useTheme();
- const { vw } = useViewport();
const onLayout: (event: LayoutChangeEvent) => void = ({
nativeEvent: {
@@ -325,7 +322,7 @@ const MessageContentWithContext = (props: MessageContentPropsWithContext) => {
key={`quoted_reply_${messageContentOrderIndex}`}
style={[styles.replyContainer, replyContainer]}
>
-
+
)
);
diff --git a/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx b/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx
deleted file mode 100644
index d14f51a2be..0000000000
--- a/package/src/components/MessageInput/AttachmentUploadPreviewList.tsx
+++ /dev/null
@@ -1,254 +0,0 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { FlatList, LayoutChangeEvent, StyleSheet, View } from 'react-native';
-
-import {
- isLocalAudioAttachment,
- isLocalFileAttachment,
- isLocalImageAttachment,
- isLocalVoiceRecordingAttachment,
- isVideoAttachment,
- LocalAttachment,
- LocalImageAttachment,
-} from 'stream-chat';
-
-import { useMessageComposer } from '../../contexts';
-import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
-import {
- MessageInputContextValue,
- useMessageInputContext,
-} from '../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { isSoundPackageAvailable } from '../../native';
-
-const IMAGE_PREVIEW_SIZE = 100;
-const FILE_PREVIEW_HEIGHT = 60;
-
-export type AttachmentUploadPreviewListPropsWithContext = Pick<
- MessageInputContextValue,
- | 'AudioAttachmentUploadPreview'
- | 'FileAttachmentUploadPreview'
- | 'ImageAttachmentUploadPreview'
- | 'VideoAttachmentUploadPreview'
->;
-
-/**
- * AttachmentUploadPreviewList
- * UI Component to preview the files set for upload
- */
-const UnMemoizedAttachmentUploadListPreview = (
- props: AttachmentUploadPreviewListPropsWithContext,
-) => {
- const [flatListWidth, setFlatListWidth] = useState(0);
- const flatListRef = useRef | null>(null);
- const {
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- ImageAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- } = props;
- const { attachmentManager } = useMessageComposer();
- const { attachments } = useAttachmentManagerState();
- const {
- theme: {
- colors: { grey_whisper },
- messageInput: {
- attachmentSeparator,
- attachmentUploadPreviewList: { filesFlatList, imagesFlatList, wrapper },
- },
- },
- } = useTheme();
-
- const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
- const fileUploads = useMemo(() => {
- return attachments.filter((attachment) => !isLocalImageAttachment(attachment));
- }, [attachments]);
-
- const renderImageItem = useCallback(
- ({ item }: { item: LocalImageAttachment }) => {
- return (
-
- );
- },
- [
- ImageAttachmentUploadPreview,
- attachmentManager.removeAttachments,
- attachmentManager.uploadAttachment,
- ],
- );
-
- const renderFileItem = useCallback(
- ({ item }: { item: LocalAttachment }) => {
- if (isLocalImageAttachment(item)) {
- // This is already handled in the `renderImageItem` above, so we return null here to avoid duplication.
- return null;
- } else if (isLocalVoiceRecordingAttachment(item)) {
- return (
-
- );
- } else if (isLocalAudioAttachment(item)) {
- if (isSoundPackageAvailable()) {
- return (
-
- );
- } else {
- return (
-
- );
- }
- } else if (isVideoAttachment(item)) {
- return (
-
- );
- } else if (isLocalFileAttachment(item)) {
- return (
-
- );
- } else return null;
- },
- [
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- attachmentManager.removeAttachments,
- attachmentManager.uploadAttachment,
- flatListWidth,
- ],
- );
-
- useEffect(() => {
- if (fileUploads.length && flatListRef.current) {
- setTimeout(() => flatListRef.current?.scrollToEnd(), 1);
- }
- }, [fileUploads.length]);
-
- const onLayout = useCallback(
- (event: LayoutChangeEvent) => {
- if (flatListRef.current) {
- setFlatListWidth(event.nativeEvent.layout.width);
- }
- },
- [flatListRef],
- );
-
- if (!attachments.length) {
- return null;
- }
-
- return (
-
- {imageUploads.length ? (
- ({
- index,
- length: IMAGE_PREVIEW_SIZE + 8,
- offset: (IMAGE_PREVIEW_SIZE + 8) * index,
- })}
- horizontal
- keyExtractor={(item) => item.localMetadata.id}
- renderItem={renderImageItem}
- style={[styles.imagesFlatList, imagesFlatList]}
- />
- ) : null}
- {imageUploads.length && fileUploads.length ? (
-
- ) : null}
- {fileUploads.length ? (
- ({
- index,
- length: FILE_PREVIEW_HEIGHT + 8,
- offset: (FILE_PREVIEW_HEIGHT + 8) * index,
- })}
- keyExtractor={(item) => item.localMetadata.id}
- onLayout={onLayout}
- ref={flatListRef}
- renderItem={renderFileItem}
- style={[styles.filesFlatList, filesFlatList]}
- testID={'file-upload-preview'}
- />
- ) : null}
-
- );
-};
-
-export type AttachmentUploadPreviewListProps = Partial;
-
-const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
- UnMemoizedAttachmentUploadListPreview,
-);
-
-/**
- * AttachmentUploadPreviewList
- * UI Component to preview the files set for upload
- */
-export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => {
- const {
- AudioAttachmentUploadPreview,
- FileAttachmentUploadPreview,
- ImageAttachmentUploadPreview,
- VideoAttachmentUploadPreview,
- } = useMessageInputContext();
- return (
-
- );
-};
-
-const styles = StyleSheet.create({
- attachmentSeparator: {
- borderBottomWidth: 1,
- marginVertical: 8,
- },
- filesFlatList: { maxHeight: FILE_PREVIEW_HEIGHT * 2.5 + 16 },
- imagesFlatList: {},
- wrapper: {
- paddingTop: 12,
- },
-});
-
-AttachmentUploadPreviewList.displayName =
- 'AttachmentUploadPreviewList{messageInput{attachmentUploadPreviewList}}';
diff --git a/package/src/components/MessageInput/CommandsButton.tsx b/package/src/components/MessageInput/CommandsButton.tsx
deleted file mode 100644
index 7ca093dece..0000000000
--- a/package/src/components/MessageInput/CommandsButton.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { useCallback } from 'react';
-import type { GestureResponderEvent, PressableProps } from 'react-native';
-import { Pressable } from 'react-native';
-
-import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Lightning } from '../../icons/Lightning';
-
-export type CommandsButtonProps = {
- /** Function that opens commands selector. */
- handleOnPress?: PressableProps['onPress'];
-};
-
-export const CommandsButton = (props: CommandsButtonProps) => {
- const { handleOnPress } = props;
- const messageComposer = useMessageComposer();
- const { textComposer } = messageComposer;
-
- const onPressHandler = useCallback(
- async (event: GestureResponderEvent) => {
- if (handleOnPress) {
- handleOnPress(event);
- return;
- }
-
- await textComposer.handleChange({
- selection: {
- end: 1,
- start: 1,
- },
- text: '/',
- });
- },
- [handleOnPress, textComposer],
- );
-
- const {
- theme: {
- colors: { grey },
- messageInput: { commandsButton },
- },
- } = useTheme();
-
- return (
-
-
-
- );
-};
-
-CommandsButton.displayName = 'CommandsButton{messageInput}';
diff --git a/package/src/components/MessageInput/CooldownTimer.tsx b/package/src/components/MessageInput/CooldownTimer.tsx
deleted file mode 100644
index 36c524d2db..0000000000
--- a/package/src/components/MessageInput/CooldownTimer.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from 'react';
-import { StyleSheet, Text, View } from 'react-native';
-
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-
-export type CooldownTimerProps = {
- seconds: number;
-};
-
-const CONTAINER_SIZE = 24;
-const CONTAINER_HORIZONTAL_PADDING = 6;
-const EXTRA_CHARACTER_PADDING = CONTAINER_SIZE - CONTAINER_HORIZONTAL_PADDING * 2;
-
-/**
- * To avoid the container jumping between sizes when there are more
- * than one character in the width of the container since we aren't
- * using a monospaced font.
- */
-const normalizeWidth = (seconds: number) =>
- CONTAINER_SIZE + EXTRA_CHARACTER_PADDING * (`${seconds}`.length - 1);
-
-/**
- * Renders an amount of seconds left for a cooldown to finish.
- *
- * See `useCountdown` for an example of how to set a countdown
- * to use as the source of `seconds`.
- **/
-export const CooldownTimer = (props: CooldownTimerProps) => {
- const { seconds } = props;
- const {
- theme: {
- colors: { black, grey_gainsboro },
- messageInput: {
- cooldownTimer: { container, text },
- },
- },
- } = useTheme();
-
- return (
-
-
- {seconds}
-
-
- );
-};
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- borderRadius: CONTAINER_SIZE / 2,
- height: CONTAINER_SIZE,
- justifyContent: 'center',
- minWidth: CONTAINER_SIZE,
- paddingHorizontal: CONTAINER_HORIZONTAL_PADDING,
- },
- text: { fontSize: 16, fontWeight: '600' },
-});
diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx
index 37e594f47e..5c6080e790 100644
--- a/package/src/components/MessageInput/MessageInput.tsx
+++ b/package/src/components/MessageInput/MessageInput.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Modal, StyleSheet, TextInput, TextInputProps, View } from 'react-native';
+import React, { useEffect } from 'react';
+import { Modal, Platform, StyleSheet, TextInput, TextInputProps, View } from 'react-native';
import {
Gesture,
@@ -9,19 +9,30 @@ import {
} from 'react-native-gesture-handler';
import Animated, {
Extrapolation,
+ FadeIn,
+ FadeOut,
interpolate,
+ LinearTransition,
runOnJS,
useAnimatedStyle,
useSharedValue,
withSpring,
+ ZoomIn,
+ ZoomOut,
} from 'react-native-reanimated';
import { type MessageComposerState, type TextComposerState, type UserResponse } from 'stream-chat';
+import { OutputButtons } from './components/OutputButtons';
import { useAudioRecorder } from './hooks/useAudioRecorder';
import { useCountdown } from './hooks/useCountdown';
-import { ChatContextValue, useChatContext, useOwnCapabilitiesContext } from '../../contexts';
+import {
+ ChatContextValue,
+ useAttachmentManagerState,
+ useChatContext,
+ useOwnCapabilitiesContext,
+} from '../../contexts';
import {
AttachmentPickerContextValue,
useAttachmentPickerContext,
@@ -34,9 +45,7 @@ import {
MessageComposerAPIContextValue,
useMessageComposerAPIContext,
} from '../../contexts/messageComposerContext/MessageComposerAPIContext';
-import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useMessageComposerHasSendableData } from '../../contexts/messageInputContext/hooks/useMessageComposerHasSendableData';
import {
MessageInputContextValue,
useMessageInputContext,
@@ -52,47 +61,75 @@ import {
useTranslationContext,
} from '../../contexts/translationContext/TranslationContext';
+import { useKeyboardVisibility } from '../../hooks/useKeyboardVisibility';
import { useStateStore } from '../../hooks/useStateStore';
import { isAudioRecorderAvailable, NativeHandlers } from '../../native';
-import { AIStates, useAIState } from '../AITypingIndicatorView';
+import {
+ MessageInputHeightState,
+ messageInputHeightStore,
+ setMessageInputHeight,
+} from '../../state-store/message-input-height-store';
import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput';
import { CreatePoll } from '../Poll/CreatePollContent';
import { SafeAreaViewWrapper } from '../UIComponents/SafeAreaViewWrapper';
const styles = StyleSheet.create({
- attachmentSeparator: {
- borderBottomWidth: 1,
- marginBottom: 10,
- },
- autoCompleteInputContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- },
- composerContainer: {
+ container: {
alignItems: 'center',
flexDirection: 'row',
+ gap: 8,
justifyContent: 'space-between',
},
- container: {
- borderTopWidth: 1,
- padding: 10,
+ contentContainer: {
+ gap: 4,
+ overflow: 'hidden',
+ paddingHorizontal: 8,
+ },
+ floatingWrapper: {
+ left: 0,
+ paddingHorizontal: 24,
+ position: 'absolute',
+ right: 0,
},
inputBoxContainer: {
- borderRadius: 20,
+ flex: 1,
+ },
+ inputBoxWrapper: {
+ borderRadius: 24,
borderWidth: 1,
flex: 1,
- marginHorizontal: 10,
+ flexDirection: 'row',
},
- micButtonContainer: {},
- optionsContainer: {
+ inputButtonsContainer: {
+ alignSelf: 'flex-end',
+ },
+ inputContainer: {
+ alignItems: 'center',
flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ micButtonContainer: {},
+ outputButtonsContainer: {
+ alignSelf: 'flex-end',
+ padding: 8,
+ },
+ shadow: {
+ elevation: 6,
+
+ shadowColor: 'hsla(0, 0%, 0%, 0.24)',
+ shadowOffset: { height: 4, width: 0 },
+ shadowOpacity: 0.24,
+ shadowRadius: 12,
},
- replyContainer: { paddingBottom: 0, paddingHorizontal: 8, paddingTop: 8 },
- sendButtonContainer: {},
suggestionsListContainer: {
position: 'absolute',
width: '100%',
},
+ wrapper: {
+ borderTopWidth: 1,
+ paddingHorizontal: 24,
+ paddingTop: 24,
+ },
});
type MessageInputPropsWithContext = Pick<
@@ -125,14 +162,13 @@ type MessageInputPropsWithContext = Pick<
| 'Input'
| 'inputBoxRef'
| 'InputButtons'
- | 'InputEditingStateHeader'
| 'CameraSelectorIcon'
| 'CreatePollIcon'
| 'FileSelectorIcon'
+ | 'messageInputFloating'
| 'ImageSelectorIcon'
| 'VideoRecorderSelectorIcon'
| 'CommandInput'
- | 'InputReplyStateHeader'
| 'SendButton'
| 'ShowThreadMessageInChannelButton'
| 'StartAudioRecordingButton'
@@ -148,6 +184,8 @@ type MessageInputPropsWithContext = Pick<
Pick &
Pick & {
editing: boolean;
+ hasAttachments: boolean;
+ isKeyboardVisible: boolean;
TextInputComponent?: React.ComponentType<
TextInputProps & {
ref: React.Ref | undefined;
@@ -157,7 +195,6 @@ type MessageInputPropsWithContext = Pick<
const textComposerStateSelector = (state: TextComposerState) => ({
command: state.command,
- hasText: !!state.text,
mentionedUsers: state.mentionedUsers,
suggestions: state.suggestions,
});
@@ -166,6 +203,10 @@ const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
quotedMessage: state.quotedMessage,
});
+const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({
+ height: state.height,
+});
+
const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
const {
AttachmentPickerSelectionBar,
@@ -186,58 +227,59 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
AudioRecordingLockIndicator,
AudioRecordingPreview,
AutoCompleteSuggestionList,
- channel,
closeAttachmentPicker,
closePollCreationDialog,
cooldownEndsAt,
- CooldownTimer,
CreatePollContent,
disableAttachmentPicker,
editing,
+ hasAttachments,
+ messageInputFloating,
Input,
inputBoxRef,
InputButtons,
- InputEditingStateHeader,
CommandInput,
- InputReplyStateHeader,
+ isKeyboardVisible,
isOnline,
members,
Reply,
threadList,
- SendButton,
sendMessage,
showPollCreationDialog,
ShowThreadMessageInChannelButton,
StartAudioRecordingButton,
- StopMessageStreamingButton,
TextInputComponent,
watchers,
} = props;
const messageComposer = useMessageComposer();
+ const { clearEditingState } = useMessageComposerAPIContext();
+ const onDismissEditMessage = () => {
+ clearEditingState();
+ };
const { textComposer } = messageComposer;
- const { command, hasText } = useStateStore(textComposer.state, textComposerStateSelector);
+ const { command } = useStateStore(textComposer.state, textComposerStateSelector);
const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
- const { attachments } = useAttachmentManagerState();
- const hasSendableData = useMessageComposerHasSendableData();
-
- const [height, setHeight] = useState(0);
+ const { height } = useStateStore(messageInputHeightStore, messageInputHeightStoreSelector);
const {
theme: {
colors: { border, grey_whisper, white, white_smoke },
messageInput: {
attachmentSelectionBar,
- autoCompleteInputContainer,
- composerContainer,
container,
+ contentContainer,
+ floatingWrapper,
focusedInputBoxContainer,
inputBoxContainer,
+ inputBoxWrapper,
+ inputContainer,
+ inputButtonsContainer,
+ inputFloatingContainer,
micButtonContainer,
- optionsContainer,
- replyContainer,
- sendButtonContainer,
+ outputButtonsContainer,
suggestionsListContainer: { container: suggestionListContainer },
+ wrapper,
},
},
} = useTheme();
@@ -323,11 +365,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
} = useAudioRecorder();
const asyncAudioEnabled = audioRecordingEnabled && isAudioRecorderAvailable();
- const showSendingButton = hasText || attachments.length || command;
-
- const isSendingButtonVisible = useMemo(() => {
- return asyncAudioEnabled ? showSendingButton && !recording : true;
- }, [asyncAudioEnabled, recording, showSendingButton]);
const micPositionX = useSharedValue(0);
const micPositionY = useSharedValue(0);
@@ -418,24 +455,31 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
],
}));
- const { aiState } = useAIState(channel);
-
- const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]);
- const shouldDisplayStopAIGeneration =
- [AIStates.Thinking, AIStates.Generating].includes(aiState) && !!StopMessageStreamingButton;
+ const BOTTOM_OFFSET = isKeyboardVisible ? 24 : Platform.OS === 'ios' ? 32 : 24;
return (
<>
- setHeight(newHeight)}
- style={[styles.container, { backgroundColor: white, borderColor: border }, container]}
+ }) => setMessageInputHeight(messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight)} // 24 is the position of the input from the bottom of the screen
+ style={
+ messageInputFloating
+ ? [styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
+ : [
+ styles.wrapper,
+ {
+ backgroundColor: white,
+ borderColor: border,
+ paddingBottom: BOTTOM_OFFSET,
+ },
+ wrapper,
+ ]
+ }
>
- {editing && }
- {quotedMessage && !editing && }
{recording && (
<>
{
>
)}
-
+
{Input ? (
) : (
@@ -480,76 +524,118 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
/>
) : (
<>
-
+
{InputButtons && }
-
- {quotedMessage && (
-
-
+
+
+ {editing ? (
+
+
+
+ ) : null}
+ {quotedMessage ? (
+
+
+
+ ) : null}
+
- )}
-
-
- {command ? (
-
- ) : (
-
-
+
+
+ {command ? (
+
+ ) : (
+
+ )}
+
+
+
+
- )}
-
+
+
>
)}
- {shouldDisplayStopAIGeneration ? (
-
- ) : isSendingButtonVisible ? (
- cooldownRemainingSeconds ? (
-
- ) : (
-
-
-
- )
- ) : null}
- {audioRecordingEnabled && isAudioRecorderAvailable() && !micLocked && (
+ {asyncAudioEnabled && !micLocked ? (
-
-
+
+
+
+
- )}
+ ) : null}
>
)}
-
+
-
+
-
+
{!disableAttachmentPicker && selectedPicker ? (
- {
]}
>
-
+
) : null}
{showPollCreationDialog ? (
@@ -600,6 +686,8 @@ const areEqual = (
closePollCreationDialog: prevClosePollCreationDialog,
cooldownEndsAt: prevCooldownEndsAt,
editing: prevEditing,
+ hasAttachments: prevHasAttachments,
+ isKeyboardVisible: prevIsKeyboardVisible,
isOnline: prevIsOnline,
openPollCreationDialog: prevOpenPollCreationDialog,
selectedPicker: prevSelectedPicker,
@@ -617,7 +705,9 @@ const areEqual = (
closePollCreationDialog: nextClosePollCreationDialog,
cooldownEndsAt: nextCooldownEndsAt,
editing: nextEditing,
+ isKeyboardVisible: nextIsKeyboardVisible,
isOnline: nextIsOnline,
+ hasAttachments: nextHasAttachments,
openPollCreationDialog: nextOpenPollCreationDialog,
selectedPicker: nextSelectedPicker,
showPollCreationDialog: nextShowPollCreationDialog,
@@ -677,6 +767,16 @@ const areEqual = (
return false;
}
+ const hasAttachmentsEqual = prevHasAttachments === nextHasAttachments;
+ if (!hasAttachmentsEqual) {
+ return false;
+ }
+
+ const isKeyboardVisibleEqual = prevIsKeyboardVisible === nextIsKeyboardVisible;
+ if (!isKeyboardVisibleEqual) {
+ return false;
+ }
+
const isOnlineEqual = prevIsOnline === nextIsOnline;
if (!isOnlineEqual) {
return false;
@@ -754,9 +854,8 @@ export const MessageInput = (props: MessageInputProps) => {
Input,
inputBoxRef,
InputButtons,
- InputEditingStateHeader,
CommandInput,
- InputReplyStateHeader,
+ messageInputFloating,
openPollCreationDialog,
SendButton,
sendMessage,
@@ -775,6 +874,8 @@ export const MessageInput = (props: MessageInputProps) => {
const { clearEditingState } = useMessageComposerAPIContext();
const { Reply } = useMessagesContext();
+ const { attachments } = useAttachmentManagerState();
+ const isKeyboardVisible = useKeyboardVisibility();
const { t } = useTranslationContext();
@@ -823,14 +924,15 @@ export const MessageInput = (props: MessageInputProps) => {
disableAttachmentPicker,
editing,
FileSelectorIcon,
+ hasAttachments: attachments.length > 0,
ImageSelectorIcon,
Input,
inputBoxRef,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
+ isKeyboardVisible,
isOnline,
members,
+ messageInputFloating,
openPollCreationDialog,
Reply,
selectedPicker,
diff --git a/package/src/components/MessageInput/MoreOptionsButton.tsx b/package/src/components/MessageInput/MoreOptionsButton.tsx
deleted file mode 100644
index f02227da61..0000000000
--- a/package/src/components/MessageInput/MoreOptionsButton.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import { Pressable } from 'react-native';
-
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { CircleRight } from '../../icons/CircleRight';
-
-export type MoreOptionsButtonProps = {
- /** Function that opens attachment options bottom sheet */
- handleOnPress?: () => void;
-};
-
-export const MoreOptionsButton = (props: MoreOptionsButtonProps) => {
- const { handleOnPress } = props;
-
- const {
- theme: {
- colors: { accent_blue },
- messageInput: { moreOptionsButton },
- },
- } = useTheme();
-
- return (
- [moreOptionsButton, { opacity: pressed ? 0.8 : 1 }]}
- testID='more-options-button'
- >
-
-
- );
-};
-
-MoreOptionsButton.displayName = 'MoreOptionsButton{messageInput}';
diff --git a/package/src/components/MessageInput/SendButton.tsx b/package/src/components/MessageInput/SendButton.tsx
deleted file mode 100644
index b5b9959cf4..0000000000
--- a/package/src/components/MessageInput/SendButton.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React, { useCallback } from 'react';
-
-import { Pressable } from 'react-native';
-
-import { TextComposerState } from 'stream-chat';
-
-import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
-import {
- MessageInputContextValue,
- useMessageInputContext,
-} from '../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { useStateStore } from '../../hooks/useStateStore';
-import { Search } from '../../icons/Search';
-import { SendRight } from '../../icons/SendRight';
-import { SendUp } from '../../icons/SendUp';
-
-export type SendButtonProps = Partial> & {
- /** Disables the button */
- disabled: boolean;
-};
-
-const textComposerStateSelector = (state: TextComposerState) => ({
- command: state.command,
-});
-
-export const SendButton = (props: SendButtonProps) => {
- const { disabled = false, sendMessage: propsSendMessage } = props;
- const { sendMessage: sendMessageFromContext } = useMessageInputContext();
- const sendMessage = propsSendMessage || sendMessageFromContext;
- const messageComposer = useMessageComposer();
- const { textComposer } = messageComposer;
- const { command } = useStateStore(textComposer.state, textComposerStateSelector);
-
- const {
- theme: {
- colors: { accent_blue, grey_gainsboro },
- messageInput: { searchIcon, sendButton, sendRightIcon, sendUpIcon },
- },
- } = useTheme();
-
- const onPressHandler = useCallback(() => {
- if (disabled) {
- return;
- }
- sendMessage();
- }, [disabled, sendMessage]);
-
- return (
-
- {command ? (
-
- ) : disabled ? (
-
- ) : (
-
- )}
-
- );
-};
-
-SendButton.displayName = 'SendButton{messageInput}';
diff --git a/package/src/components/MessageInput/__tests__/AttachButton.test.js b/package/src/components/MessageInput/__tests__/AttachButton.test.js
index d0201770aa..dbd9ae7305 100644
--- a/package/src/components/MessageInput/__tests__/AttachButton.test.js
+++ b/package/src/components/MessageInput/__tests__/AttachButton.test.js
@@ -8,7 +8,7 @@ import { initiateClientWithChannels } from '../../../mock-builders/api/initiateC
import * as NativeHandler from '../../../native';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
-import { AttachButton } from '../AttachButton';
+import { AttachButton } from '../components/InputButtons/AttachButton';
const renderComponent = ({ channelProps, client, props }) => {
return render(
diff --git a/package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.js b/package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.js
index 5532989480..91c86b386a 100644
--- a/package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.js
+++ b/package/src/components/MessageInput/__tests__/AttachmentUploadPreviewList.test.js
@@ -14,7 +14,7 @@ import {
import { FileState } from '../../../utils/utils';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
-import { AttachmentUploadPreviewList } from '../AttachmentUploadPreviewList';
+import { AttachmentUploadPreviewList } from '../components/AttachmentPreview/AttachmentUploadPreviewList';
jest.mock('../../../native.ts', () => {
const { View } = require('react-native');
diff --git a/package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.js b/package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.js
index 00a93050cb..4aebfdb773 100644
--- a/package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.js
+++ b/package/src/components/MessageInput/__tests__/AudioAttachmentUploadPreview.test.js
@@ -9,7 +9,7 @@ import { generateAudioAttachment } from '../../../mock-builders/attachments';
import { FileState } from '../../../utils/utils';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
-import { AttachmentUploadPreviewList } from '../AttachmentUploadPreviewList';
+import { AttachmentUploadPreviewList } from '../components/AttachmentPreview/AttachmentUploadPreviewList';
jest.mock('../../../native.ts', () => {
const View = require('react-native').View;
diff --git a/package/src/components/MessageInput/__tests__/CommandsButton.test.js b/package/src/components/MessageInput/__tests__/CommandsButton.test.js
deleted file mode 100644
index cd226cb652..0000000000
--- a/package/src/components/MessageInput/__tests__/CommandsButton.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import React from 'react';
-
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
-
-import { OverlayProvider } from '../../../contexts';
-
-import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
-import { Channel } from '../../Channel/Channel';
-import { Chat } from '../../Chat/Chat';
-import { CommandsButton } from '../CommandsButton';
-
-const renderComponent = ({ client, channel, props }) => {
- return render(
-
-
-
-
-
-
- ,
- );
-};
-
-describe('CommandsButton', () => {
- let client;
- let channel;
-
- beforeEach(async () => {
- const { client: chatClient, channels } = await initiateClientWithChannels();
- client = chatClient;
- channel = channels[0];
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- cleanup();
- });
-
- it('should render component', async () => {
- const props = {};
- renderComponent({ channel, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('commands-button')).toBeTruthy();
- });
- });
-
- it('should call handleOnPress callback when the button is clicked if passed', async () => {
- const handleOnPress = jest.fn();
- const props = { handleOnPress };
-
- renderComponent({ channel, client, props });
-
- const { getByTestId, queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('commands-button')).toBeTruthy();
- });
-
- act(() => {
- fireEvent.press(getByTestId('commands-button'));
- });
-
- await waitFor(() => {
- expect(handleOnPress).toHaveBeenCalled();
- });
- });
-});
diff --git a/package/src/components/MessageInput/__tests__/InputButtons.test.js b/package/src/components/MessageInput/__tests__/InputButtons.test.js
index 501fb72dba..d25e38492d 100644
--- a/package/src/components/MessageInput/__tests__/InputButtons.test.js
+++ b/package/src/components/MessageInput/__tests__/InputButtons.test.js
@@ -1,15 +1,13 @@
import React from 'react';
-import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
+import { act, cleanup, render, screen, waitFor } from '@testing-library/react-native';
import { OverlayProvider } from '../../../contexts';
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
-import { generateImageAttachment } from '../../../mock-builders/attachments';
-import { FileState } from '../../../utils/utils';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
-import { InputButtons } from '../InputButtons';
+import { InputButtons } from '../components/InputButtons/index';
const renderComponent = ({ channelProps, client, props }) => {
return render(
@@ -42,24 +40,6 @@ describe('InputButtons', () => {
});
});
- // TODO: Add it back once the command inject PR is merged
- it.skip('should return null if the commands are set on the textComposer', async () => {
- const props = {};
- const channelProps = { channel };
-
- channel.messageComposer.textComposer.setCommand({ description: 'Ban a user', name: 'ban' });
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('more-options-button')).toBeFalsy();
- expect(queryByTestId('commands-button')).toBeFalsy();
- expect(queryByTestId('attach-button')).toBeFalsy();
- });
- });
-
it('should return null if hasCommands is false and hasAttachmentUploadCapabilities is false', async () => {
const props = {};
const channelProps = {
@@ -75,28 +55,11 @@ describe('InputButtons', () => {
const { queryByTestId } = screen;
await waitFor(() => {
- expect(queryByTestId('more-options-button')).toBeFalsy();
expect(queryByTestId('commands-button')).toBeFalsy();
expect(queryByTestId('attach-button')).toBeFalsy();
});
});
- it('should show more options when the hasCommand is true and the hasAttachmentUploadCapabilities is true', async () => {
- const props = {};
- const channelProps = {
- channel,
- };
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('commands-button')).toBeTruthy();
- expect(queryByTestId('attach-button')).toBeTruthy();
- });
- });
-
it('should show only attach button when the hasCommand is false and the hasAttachmentUploadCapabilities is true', async () => {
const props = {};
const channelProps = {
@@ -114,25 +77,6 @@ describe('InputButtons', () => {
});
});
- it('should show only commands button when the hasCommand is true and the hasAttachmentUploadCapabilities is false', async () => {
- const props = {};
- const channelProps = {
- channel,
- overrideOwnCapabilities: {
- uploadFile: false,
- },
- };
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('commands-button')).toBeTruthy();
- expect(queryByTestId('attach-button')).toBeFalsy();
- });
- });
-
it('should not show commands buttons when there is text in the textComposer', async () => {
const props = {};
const channelProps = {
@@ -146,62 +90,4 @@ describe('InputButtons', () => {
expect(queryByTestId('commands-button')).toBeFalsy();
});
});
-
- it('should show more options button when there is text in the textComposer', async () => {
- const props = {};
- const channelProps = {
- channel,
- };
- channel.messageComposer.textComposer.setText('hello');
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('more-options-button')).toBeTruthy();
- });
-
- act(() => {
- fireEvent.press(queryByTestId('more-options-button'));
- });
-
- await waitFor(() => {
- // Falsy, because the textComposer has text. This is a good test.
- expect(queryByTestId('commands-button')).toBeFalsy();
- expect(queryByTestId('attach-button')).toBeTruthy();
- });
- });
-
- it('should show more options button when there is attachments', async () => {
- const props = {};
- const channelProps = {
- channel,
- };
- channel.messageComposer.attachmentManager.upsertAttachments([
- generateImageAttachment({
- localMetadata: {
- id: 'image-attachment',
- uploadState: FileState.UPLOADING,
- },
- }),
- ]);
-
- renderComponent({ channelProps, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('more-options-button')).toBeTruthy();
- });
-
- act(() => {
- fireEvent.press(queryByTestId('more-options-button'));
- });
-
- await waitFor(() => {
- expect(queryByTestId('commands-button')).toBeTruthy();
- expect(queryByTestId('attach-button')).toBeTruthy();
- });
- });
});
diff --git a/package/src/components/MessageInput/__tests__/MessageInput.test.js b/package/src/components/MessageInput/__tests__/MessageInput.test.js
index 634472fb1a..8cea3bef40 100644
--- a/package/src/components/MessageInput/__tests__/MessageInput.test.js
+++ b/package/src/components/MessageInput/__tests__/MessageInput.test.js
@@ -9,7 +9,6 @@ import { OverlayProvider } from '../../../contexts/overlayContext/OverlayProvide
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
-import { NativeHandlers } from '../../../native';
import { AttachmentPickerSelectionBar } from '../../AttachmentPicker/components/AttachmentPickerSelectionBar';
import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSelectorIcon';
import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon';
@@ -90,59 +89,60 @@ describe('MessageInput', () => {
});
});
- it('should start the audio recorder on long press and cleanup on unmount', async () => {
- renderComponent({
- channelProps: { audioRecordingEnabled: true, channel },
- client,
- props: {},
- });
-
- const { queryByTestId, unmount } = screen;
-
- const audioButton = queryByTestId('audio-button');
-
- act(() => {
- fireEvent(audioButton, 'longPress');
- });
-
- await waitFor(() => {
- expect(NativeHandlers.Audio.startRecording).toHaveBeenCalledTimes(1);
- expect(NativeHandlers.Audio.stopRecording).not.toHaveBeenCalled();
- expect(queryByTestId('recording-active-container')).toBeTruthy();
- expect(Alert.alert).not.toHaveBeenCalledWith('Hold to start recording.');
- });
-
- await act(() => {
- unmount();
- });
-
- await waitFor(() => {
- expect(NativeHandlers.Audio.stopRecording).toHaveBeenCalledTimes(1);
- });
- });
-
- it('should trigger an alert if a normal press happened on audio recording', async () => {
- renderComponent({
- channelProps: { audioRecordingEnabled: true, channel },
- client,
- props: {},
- });
-
- const { queryByTestId } = screen;
-
- const audioButton = queryByTestId('audio-button');
-
- act(() => {
- fireEvent.press(audioButton);
- });
-
- await waitFor(() => {
- expect(NativeHandlers.Audio.startRecording).not.toHaveBeenCalled();
- expect(NativeHandlers.Audio.stopRecording).not.toHaveBeenCalled();
- expect(queryByTestId('recording-active-container')).not.toBeTruthy();
- // This is sort of a brittle test, but there doesn't seem to be another way
- // to target alerts. The reason why it's here is because we had a bug with it.
- expect(Alert.alert).toHaveBeenCalledWith('Hold to start recording.');
- });
- });
+ // TODO: Once the async audio design is done, fix it
+ // it('should start the audio recorder on long press and cleanup on unmount', async () => {
+ // renderComponent({
+ // channelProps: { audioRecordingEnabled: false, channel }, // TODO: Once the async audio design is done, fix it
+ // client,
+ // props: {},
+ // });
+
+ // const { getByLabelText, unmount } = screen;
+
+ // const audioButton = getByLabelText('Start recording');
+
+ // act(() => {
+ // fireEvent(audioButton, 'longPress');
+ // });
+
+ // await waitFor(() => {
+ // expect(NativeHandlers.Audio.startRecording).toHaveBeenCalledTimes(1);
+ // expect(NativeHandlers.Audio.stopRecording).not.toHaveBeenCalled();
+ // expect(queryByTestId('recording-active-container')).toBeTruthy();
+ // expect(Alert.alert).not.toHaveBeenCalledWith('Hold to start recording.');
+ // });
+
+ // await act(() => {
+ // unmount();
+ // });
+
+ // await waitFor(() => {
+ // expect(NativeHandlers.Audio.stopRecording).toHaveBeenCalledTimes(1);
+ // });
+ // });
+
+ // it('should trigger an alert if a normal press happened on audio recording', async () => {
+ // renderComponent({
+ // channelProps: { audioRecordingEnabled: false, channel },
+ // client,
+ // props: {},
+ // });
+
+ // const { getByLabelText, queryByTestId } = screen;
+
+ // const audioButton = getByLabelText('Start recording');
+
+ // act(() => {
+ // fireEvent.press(audioButton);
+ // });
+
+ // await waitFor(() => {
+ // expect(NativeHandlers.Audio.startRecording).not.toHaveBeenCalled();
+ // expect(NativeHandlers.Audio.stopRecording).not.toHaveBeenCalled();
+ // expect(queryByTestId('recording-active-container')).not.toBeTruthy();
+ // // This is sort of a brittle test, but there doesn't seem to be another way
+ // // to target alerts. The reason why it's here is because we had a bug with it.
+ // expect(Alert.alert).toHaveBeenCalledWith('Hold to start recording.');
+ // });
+ // });
});
diff --git a/package/src/components/MessageInput/__tests__/SendButton.test.js b/package/src/components/MessageInput/__tests__/SendButton.test.js
index e501588eb6..f237aad828 100644
--- a/package/src/components/MessageInput/__tests__/SendButton.test.js
+++ b/package/src/components/MessageInput/__tests__/SendButton.test.js
@@ -7,7 +7,7 @@ import { OverlayProvider } from '../../../contexts';
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
import { Channel } from '../../Channel/Channel';
import { Chat } from '../../Chat/Chat';
-import { SendButton } from '../SendButton';
+import { SendButton } from '../components/OutputButtons/SendButton';
const renderComponent = ({ client, channel, props }) => {
return render(
@@ -60,7 +60,6 @@ describe('SendButton', () => {
await waitFor(() => {
expect(sendMessage).toHaveBeenCalledTimes(1);
- expect(getByTestId('send-up')).toBeDefined();
});
const snapshot = toJSON();
@@ -90,7 +89,6 @@ describe('SendButton', () => {
await waitFor(() => {
expect(sendMessage).toHaveBeenCalledTimes(0);
- expect(getByTestId('send-right')).toBeDefined();
});
const snapshot = toJSON();
@@ -99,21 +97,4 @@ describe('SendButton', () => {
expect(snapshot).toMatchSnapshot();
});
});
-
- // TODO: Add it back once the command inject PR is merged
- it.skip('should show search button if the command is enabled', async () => {
- const sendMessage = jest.fn();
-
- const props = { sendMessage };
-
- channel.messageComposer.textComposer.setCommand({ description: 'Ban a user', name: 'ban' });
-
- renderComponent({ channel, client, props });
-
- const { queryByTestId } = screen;
-
- await waitFor(() => {
- expect(queryByTestId('search-icon')).toBeTruthy();
- });
- });
});
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 ce8d93e13d..8c62fffa57 100644
--- a/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
+++ b/package/src/components/MessageInput/__tests__/__snapshots__/AttachButton.test.js.snap
@@ -51,6 +51,20 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
onStartShouldSetResponder={[Function]}
style={
[
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 24,
+ "height": 48,
+ "width": 48,
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 1,
+ },
{},
]
}
@@ -58,14 +72,17 @@ exports[`AttachButton should call handleAttachButtonPress when the button is cli
>
-
-
-
-
-
-
-
-
+ }
+ strokeLinecap={1}
+ strokeLinejoin={1}
+ strokeWidth={1.5}
+ />
@@ -401,6 +398,20 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
onStartShouldSetResponder={[Function]}
style={
[
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 24,
+ "height": 48,
+ "width": 48,
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 1,
+ },
{},
]
}
@@ -408,14 +419,17 @@ exports[`AttachButton should render a enabled AttachButton 1`] = `
>
-
-
-
-
-
-
-
-
+ }
+ strokeLinecap={1}
+ strokeLinejoin={1}
+ strokeWidth={1.5}
+ />
@@ -751,6 +745,20 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
onStartShouldSetResponder={[Function]}
style={
[
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 24,
+ "height": 48,
+ "width": 48,
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 1,
+ },
{},
]
}
@@ -758,14 +766,17 @@ exports[`AttachButton should render an disabled AttachButton 1`] = `
>
-
-
-
-
-
-
-
-
+ }
+ strokeLinecap={1}
+ strokeLinejoin={1}
+ strokeWidth={1.5}
+ />
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 193309cc73..9ba722b90a 100644
--- a/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap
+++ b/package/src/components/MessageInput/__tests__/__snapshots__/SendButton.test.js.snap
@@ -50,6 +50,20 @@ exports[`SendButton should render a SendButton 1`] = `
onStartShouldSetResponder={[Function]}
style={
[
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 16,
+ "height": 32,
+ "width": 32,
+ },
+ {
+ "backgroundColor": "#005FFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 1,
+ },
{},
]
}
@@ -57,14 +71,18 @@ exports[`SendButton should render a SendButton 1`] = `
>
-
-
@@ -375,6 +394,20 @@ exports[`SendButton should render a disabled SendButton 1`] = `
onStartShouldSetResponder={[Function]}
style={
[
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 16,
+ "height": 32,
+ "width": 32,
+ },
+ {
+ "backgroundColor": "#005FFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 1,
+ },
{},
]
}
@@ -382,14 +415,18 @@ exports[`SendButton should render a disabled SendButton 1`] = `
>
-
-
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUnsupportedIndicator.tsx b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUnsupportedIndicator.tsx
index 6ff419c2f3..096d293824 100644
--- a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUnsupportedIndicator.tsx
+++ b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUnsupportedIndicator.tsx
@@ -3,11 +3,9 @@ import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
-import { Warning } from '../../../../icons/Warning';
+import { ErrorCircle } from '../../../../icons/ErrorCircle';
import { Progress, ProgressIndicatorTypes } from '../../../../utils/utils';
-const WARNING_ICON_SIZE = 16;
-
export type AttachmentUnsupportedIndicatorProps = {
/** Type of active indicator */
indicatorType?: Progress;
@@ -21,7 +19,7 @@ export const AttachmentUnsupportedIndicator = ({
}: AttachmentUnsupportedIndicatorProps) => {
const {
theme: {
- colors: { accent_red, grey_dark, overlay, white },
+ colors: { accent_error, overlay },
messageInput: {
attachmentUnsupportedIndicator: { container, text, warningIcon },
},
@@ -42,16 +40,14 @@ export const AttachmentUnsupportedIndicator = ({
container,
]}
>
-
-
- {t('Not supported')}
-
+ {t('Not supported')}
);
};
@@ -61,7 +57,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
flexDirection: 'row',
marginTop: 4,
- paddingHorizontal: 2,
},
imageStyle: {
borderRadius: 16,
@@ -70,13 +65,11 @@ const styles = StyleSheet.create({
},
warningIconStyle: {
borderRadius: 24,
- marginTop: 6,
},
warningText: {
alignItems: 'center',
color: 'black',
- fontSize: 10,
- justifyContent: 'center',
+ fontSize: 12,
marginHorizontal: 4,
},
});
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx
new file mode 100644
index 0000000000..c10db5e1e0
--- /dev/null
+++ b/package/src/components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList.tsx
@@ -0,0 +1,245 @@
+import React, { useCallback } from 'react';
+import { FlatList, StyleSheet, View } from 'react-native';
+
+import Animated, { FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated';
+
+import {
+ isLocalAudioAttachment,
+ isLocalFileAttachment,
+ isLocalImageAttachment,
+ isLocalVoiceRecordingAttachment,
+ isVideoAttachment,
+ LocalAttachment,
+} from 'stream-chat';
+
+import { useMessageComposer } from '../../../../contexts';
+import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState';
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { isSoundPackageAvailable } from '../../../../native';
+
+const IMAGE_PREVIEW_SIZE = 100;
+const FILE_PREVIEW_HEIGHT = 60;
+
+export type AttachmentUploadListPreviewPropsWithContext = Pick<
+ MessageInputContextValue,
+ | 'AudioAttachmentUploadPreview'
+ | 'FileAttachmentUploadPreview'
+ | 'ImageAttachmentUploadPreview'
+ | 'VideoAttachmentUploadPreview'
+>;
+
+const ItemSeparatorComponent = () => {
+ const {
+ theme: {
+ messageInput: {
+ attachmentUploadPreviewList: { itemSeparator },
+ },
+ },
+ } = useTheme();
+ return ;
+};
+
+const getItemLayout = (data: ArrayLike | null | undefined, index: number) => {
+ const item = data?.[index];
+ if (item && isLocalImageAttachment(item as LocalAttachment)) {
+ return {
+ index,
+ length: IMAGE_PREVIEW_SIZE + 8,
+ offset: (IMAGE_PREVIEW_SIZE + 8) * index,
+ };
+ }
+ return {
+ index,
+ length: FILE_PREVIEW_HEIGHT + 8,
+ offset: (FILE_PREVIEW_HEIGHT + 8) * index,
+ };
+};
+
+/**
+ * AttachmentUploadPreviewList
+ * UI Component to preview the files set for upload
+ */
+const UnMemoizedAttachmentUploadPreviewList = (
+ props: AttachmentUploadListPreviewPropsWithContext,
+) => {
+ const {
+ AudioAttachmentUploadPreview,
+ FileAttachmentUploadPreview,
+ ImageAttachmentUploadPreview,
+ VideoAttachmentUploadPreview,
+ } = props;
+ const { attachmentManager } = useMessageComposer();
+ const { attachments } = useAttachmentManagerState();
+ const {
+ theme: {
+ messageInput: {
+ attachmentUploadPreviewList: { flatList },
+ },
+ },
+ } = useTheme();
+
+ const renderItem = useCallback(
+ ({ item }: { item: LocalAttachment }) => {
+ if (isLocalImageAttachment(item)) {
+ return (
+
+
+
+ );
+ } else if (isLocalVoiceRecordingAttachment(item)) {
+ return (
+
+
+
+ );
+ } else if (isLocalAudioAttachment(item)) {
+ if (isSoundPackageAvailable()) {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+ } else if (isVideoAttachment(item)) {
+ return (
+
+
+
+ );
+ } else if (isLocalFileAttachment(item)) {
+ return (
+
+
+
+ );
+ } else return null;
+ },
+ [
+ AudioAttachmentUploadPreview,
+ FileAttachmentUploadPreview,
+ ImageAttachmentUploadPreview,
+ VideoAttachmentUploadPreview,
+ attachmentManager.removeAttachments,
+ attachmentManager.uploadAttachment,
+ ],
+ );
+
+ if (!attachments.length) {
+ return null;
+ }
+
+ return (
+ item.localMetadata.id}
+ renderItem={renderItem}
+ showsHorizontalScrollIndicator={false}
+ style={[styles.flatList, flatList]}
+ testID={'attachment-upload-preview-list'}
+ />
+ );
+};
+
+export type AttachmentUploadPreviewListProps = Partial;
+
+const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
+ UnMemoizedAttachmentUploadPreviewList,
+);
+
+/**
+ * AttachmentUploadPreviewList
+ * UI Component to preview the files set for upload
+ */
+export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => {
+ const {
+ AudioAttachmentUploadPreview,
+ FileAttachmentUploadPreview,
+ ImageAttachmentUploadPreview,
+ VideoAttachmentUploadPreview,
+ } = useMessageInputContext();
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ flatList: {
+ overflow: 'visible',
+ },
+ itemSeparator: {
+ width: 8,
+ },
+ wrapper: {},
+});
+
+AttachmentUploadPreviewList.displayName =
+ 'AttachmentUploadPreviewList{messageInput{attachmentUploadPreviewList}}';
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/DismissAttachmentUpload.tsx b/package/src/components/MessageInput/components/AttachmentPreview/DismissAttachmentUpload.tsx
index 43158d4b92..631bd8efd8 100644
--- a/package/src/components/MessageInput/components/AttachmentPreview/DismissAttachmentUpload.tsx
+++ b/package/src/components/MessageInput/components/AttachmentPreview/DismissAttachmentUpload.tsx
@@ -3,14 +3,14 @@ import React from 'react';
import { Pressable, PressableProps, StyleSheet } from 'react-native';
import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
-import { Close } from '../../../../icons';
+import { NewClose } from '../../../../icons/NewClose';
type DismissAttachmentUploadProps = PressableProps;
export const DismissAttachmentUpload = ({ onPress }: DismissAttachmentUploadProps) => {
const {
theme: {
- colors: { overlay, white },
+ colors: { white },
messageInput: {
dismissAttachmentUpload: { dismiss, dismissIcon, dismissIconColor },
},
@@ -22,21 +22,24 @@ export const DismissAttachmentUpload = ({ onPress }: DismissAttachmentUploadProp
onPress={onPress}
style={({ pressed }) => [
styles.dismiss,
- { backgroundColor: overlay, opacity: pressed ? 0.8 : 1 },
+ {
+ borderColor: white,
+ opacity: pressed ? 0.8 : 1,
+ },
dismiss,
]}
testID='remove-upload-preview'
>
-
+
);
};
const styles = StyleSheet.create({
dismiss: {
- borderRadius: 24,
- position: 'absolute',
- right: 8,
- top: 8,
+ backgroundColor: '#384047',
+ borderRadius: 16,
+ borderWidth: 2,
+ overflow: 'hidden',
},
});
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx b/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx
index 856bc31e87..9e913ad692 100644
--- a/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx
+++ b/package/src/components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview.tsx
@@ -14,7 +14,6 @@ import { useChatContext } from '../../../../contexts/chatContext/ChatContext';
import { useMessagesContext } from '../../../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { UploadAttachmentPreviewProps } from '../../../../types/types';
-import { getTrimmedAttachmentTitle } from '../../../../utils/getTrimmedAttachmentTitle';
import {
getDurationLabelFromDuration,
getIndicatorTypeForFileState,
@@ -26,13 +25,10 @@ export type FileAttachmentUploadPreviewProps
| LocalVideoAttachment
| LocalAudioAttachment
- > & {
- flatListWidth: number;
- };
+ >;
export const FileAttachmentUploadPreview = ({
attachment,
- flatListWidth,
handleRetry,
removeAttachments,
}: FileAttachmentUploadPreviewProps) => {
@@ -71,7 +67,7 @@ export const FileAttachmentUploadPreview = ({
-
+
-
+
- {getTrimmedAttachmentTitle(attachment.title)}
+ {attachment.title}
{indicatorType === ProgressIndicatorTypes.NOT_SUPPORTED ? (
@@ -120,17 +110,26 @@ export const FileAttachmentUploadPreview = ({
-
+
+
+
);
};
const styles = StyleSheet.create({
+ dismissWrapper: { position: 'absolute', right: 0, top: 0 },
fileContainer: {
borderRadius: 12,
borderWidth: 1,
flexDirection: 'row',
- paddingHorizontal: 8,
+ gap: 12,
+ maxWidth: 224, // TODO: Not sure how to omit this
+ padding: 16,
+ },
+ fileContent: {
+ flexShrink: 1,
+ justifyContent: 'space-between',
},
fileIcon: {
alignItems: 'center',
@@ -138,24 +137,19 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
filenameText: {
- fontSize: 14,
- fontWeight: 'bold',
+ fontSize: 12,
+ fontWeight: '600',
+ },
+ fileNameTextContainer: {
+ flexShrink: 1,
},
fileSizeText: {
fontSize: 12,
- marginTop: 10,
- },
- fileTextContainer: {
- justifyContent: 'space-around',
- marginVertical: 10,
- paddingHorizontal: 10,
},
overlay: {
borderRadius: 12,
- marginTop: 2,
},
wrapper: {
- flexDirection: 'row',
- marginHorizontal: 8,
+ padding: 4,
},
});
diff --git a/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx b/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx
index 7c29cc5715..c476a9b330 100644
--- a/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx
+++ b/package/src/components/MessageInput/components/AttachmentPreview/ImageAttachmentUploadPreview.tsx
@@ -13,7 +13,7 @@ import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { UploadAttachmentPreviewProps } from '../../../../types/types';
import { getIndicatorTypeForFileState, ProgressIndicatorTypes } from '../../../../utils/utils';
-const IMAGE_PREVIEW_SIZE = 100;
+const IMAGE_PREVIEW_SIZE = 72;
export type ImageAttachmentUploadPreviewProps> =
UploadAttachmentPreviewProps>;
@@ -32,7 +32,7 @@ export const ImageAttachmentUploadPreview = ({
const {
theme: {
messageInput: {
- imageAttachmentUploadPreview: { itemContainer, upload },
+ imageAttachmentUploadPreview: { container, upload, wrapper },
},
},
} = useTheme();
@@ -54,10 +54,10 @@ export const ImageAttachmentUploadPreview = ({
}, []);
return (
-
+
+ {indicatorType === ProgressIndicatorTypes.NOT_SUPPORTED ? (
+
+ ) : null}
-
- {indicatorType === ProgressIndicatorTypes.NOT_SUPPORTED ? (
-
- ) : null}
+
+
+
);
};
const styles = StyleSheet.create({
+ container: {
+ borderColor: '#E2E6EA',
+ borderRadius: 12,
+ borderWidth: 1,
+ flexDirection: 'row',
+ overflow: 'hidden',
+ },
+ dismissWrapper: { position: 'absolute', right: 0, top: 0 },
fileSizeText: {
fontSize: 12,
paddingHorizontal: 10,
},
- flatList: { paddingBottom: 12 },
- itemContainer: {
- flexDirection: 'row',
- height: IMAGE_PREVIEW_SIZE,
- marginLeft: 8,
- },
upload: {
- borderRadius: 10,
height: IMAGE_PREVIEW_SIZE,
width: IMAGE_PREVIEW_SIZE,
},
+ wrapper: {
+ padding: 4,
+ },
});
diff --git a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
index 0186f0d571..39782431dd 100644
--- a/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
+++ b/package/src/components/MessageInput/components/AudioRecorder/AudioRecordingButton.tsx
@@ -1,13 +1,13 @@
import React from 'react';
-import { Alert, Linking, Pressable, StyleSheet } from 'react-native';
+import { Alert, Linking } from 'react-native';
+import { IconButton } from '../../../../components/ui/IconButton';
import {
MessageInputContextValue,
useMessageInputContext,
} from '../../../../contexts/messageInputContext/MessageInputContext';
-import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
-import { Mic } from '../../../../icons/Mic';
+import { NewMic } from '../../../../icons/NewMic';
import { AudioRecordingReturnType, NativeHandlers } from '../../../../native';
export type AudioRecordingButtonProps = Partial<
@@ -45,7 +45,6 @@ export type AudioRecordingButtonProps = Partial<
export const AudioRecordingButton = (props: AudioRecordingButtonProps) => {
const {
asyncMessagesMinimumPressDuration: propAsyncMessagesMinimumPressDuration,
- buttonSize,
handleLongPress,
handlePress,
permissionsGranted,
@@ -58,14 +57,6 @@ export const AudioRecordingButton = (props: AudioRecordingButtonProps) => {
const asyncMessagesMinimumPressDuration =
propAsyncMessagesMinimumPressDuration || contextAsyncMessagesMinimumPressDuration;
- const {
- theme: {
- colors: { grey, light_gray, white },
- messageInput: {
- audioRecordingButton: { container, micIcon },
- },
- },
- } = useTheme();
const { t } = useTranslationContext();
const onPressHandler = () => {
@@ -103,33 +94,17 @@ export const AudioRecordingButton = (props: AudioRecordingButtonProps) => {
};
return (
- [
- styles.container,
- {
- backgroundColor: pressed ? light_gray : white,
- height: buttonSize || 40,
- width: buttonSize || 40,
- },
- container,
- ]}
- testID='audio-button'
- >
-
-
+ size='sm'
+ type='secondary'
+ />
);
};
-const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- borderRadius: 50,
- justifyContent: 'center',
- marginLeft: 8,
- },
-});
-
AudioRecordingButton.displayName = 'AudioRecordingButton{messageInput}';
diff --git a/package/src/components/MessageInput/components/CommandInput.tsx b/package/src/components/MessageInput/components/CommandInput.tsx
index c789f657f1..f8c6c58f8b 100644
--- a/package/src/components/MessageInput/components/CommandInput.tsx
+++ b/package/src/components/MessageInput/components/CommandInput.tsx
@@ -43,8 +43,8 @@ export const CommandInput = ({
theme: {
colors: { accent_blue, grey, white },
messageInput: {
- autoCompleteInputContainer,
commandInput: { closeButton, container, text },
+ inputContainer,
},
},
} = useTheme();
@@ -61,13 +61,13 @@ export const CommandInput = ({
const commandName = (command.name ?? '').toUpperCase();
return (
-
+
{commandName}
-
+
{
} = props;
const {
theme: {
- colors: { accent_blue, grey },
messageInput: { attachButton },
},
} = useTheme();
@@ -84,15 +82,16 @@ const AttachButtonWithContext = (props: AttachButtonPropsWithContext) => {
return (
<>
-
-
-
+ type='secondary'
+ />
{showAttachButtonPicker ? (
;
export type InputButtonsWithContextProps = Pick<
MessageInputContextValue,
| 'AttachButton'
- | 'CommandsButton'
| 'hasCameraPicker'
| 'hasCommands'
| 'hasFilePicker'
| 'hasImagePicker'
- | 'MoreOptionsButton'
| 'toggleAttachmentPicker'
> &
Pick &
Pick;
-const textComposerStateSelector = (state: TextComposerState) => ({
- command: state.command,
- hasText: !!state.text,
-});
-
export const InputButtonsWithContext = (props: InputButtonsWithContextProps) => {
const {
AttachButton,
- CommandsButton,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
- MoreOptionsButton,
uploadFile: ownCapabilitiesUploadFile,
} = props;
- const { textComposer } = useMessageComposer();
- const { command, hasText } = useStateStore(textComposer.state, textComposerStateSelector);
-
- const [showMoreOptions, setShowMoreOptions] = useState(true);
- const { attachments } = useAttachmentManagerState();
-
- const shouldShowMoreOptions = hasText || attachments.length;
-
- useEffect(() => {
- setShowMoreOptions(!shouldShowMoreOptions);
- }, [shouldShowMoreOptions]);
const {
theme: {
@@ -68,36 +43,18 @@ export const InputButtonsWithContext = (props: InputButtonsWithContextProps) =>
},
} = useTheme();
- const handleShowMoreOptions = useCallback(() => {
- setShowMoreOptions(true);
- }, [setShowMoreOptions]);
-
const hasAttachmentUploadCapabilities =
(hasCameraPicker || hasFilePicker || hasImagePicker) && ownCapabilitiesUploadFile;
- const showCommandsButton = hasCommands && !hasText;
-
- if (command) {
- return null;
- }
if (!hasAttachmentUploadCapabilities && !hasCommands) {
return null;
}
- return !showMoreOptions ? (
-
- ) : (
- <>
- {hasAttachmentUploadCapabilities ? (
-
-
-
- ) : null}
- {showCommandsButton ? : null}
- >
- );
+ return hasAttachmentUploadCapabilities ? (
+
+
+
+ ) : null;
};
const areEqual = (
@@ -106,7 +63,6 @@ const areEqual = (
) => {
const {
hasCameraPicker: prevHasCameraPicker,
- hasCommands: prevHasCommands,
hasFilePicker: prevHasFilePicker,
hasImagePicker: prevHasImagePicker,
selectedPicker: prevSelectedPicker,
@@ -114,7 +70,6 @@ const areEqual = (
const {
hasCameraPicker: nextHasCameraPicker,
- hasCommands: nextHasCommands,
hasFilePicker: nextHasFilePicker,
hasImagePicker: nextHasImagePicker,
selectedPicker: nextSelectedPicker,
@@ -132,10 +87,6 @@ const areEqual = (
return false;
}
- if (prevHasCommands !== nextHasCommands) {
- return false;
- }
-
if (prevSelectedPicker !== nextSelectedPicker) {
return false;
}
@@ -151,12 +102,10 @@ const MemoizedInputButtonsWithContext = React.memo(
export const InputButtons = (props: InputButtonsProps) => {
const {
AttachButton,
- CommandsButton,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
- MoreOptionsButton,
toggleAttachmentPicker,
} = useMessageInputContext();
const { selectedPicker } = useAttachmentPickerContext();
@@ -166,12 +115,10 @@ export const InputButtons = (props: InputButtonsProps) => {
{
};
const styles = StyleSheet.create({
- attachButtonContainer: { paddingRight: 5 },
+ attachButtonContainer: {},
});
diff --git a/package/src/components/MessageInput/components/InputEditingStateHeader.tsx b/package/src/components/MessageInput/components/InputEditingStateHeader.tsx
deleted file mode 100644
index cb05599899..0000000000
--- a/package/src/components/MessageInput/components/InputEditingStateHeader.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React, { useCallback } from 'react';
-import { Pressable, StyleSheet, Text, View } from 'react-native';
-
-import {
- MessageComposerAPIContextValue,
- useMessageComposerAPIContext,
-} from '../../../contexts/messageComposerContext/MessageComposerAPIContext';
-import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-
-import { CircleClose, Edit } from '../../../icons';
-
-export type InputEditingStateHeaderProps = Partial<
- Pick
->;
-
-export const InputEditingStateHeader = ({
- clearEditingState: propClearEditingState,
-}: InputEditingStateHeaderProps) => {
- const messageComposer = useMessageComposer();
- const { t } = useTranslationContext();
- const { clearEditingState: contextClearEditingState } = useMessageComposerAPIContext();
-
- const clearEditingState = propClearEditingState || contextClearEditingState;
-
- const {
- theme: {
- colors: { black, grey, grey_gainsboro },
- messageInput: {
- editingStateHeader: { editingBoxHeader, editingBoxHeaderTitle },
- },
- },
- } = useTheme();
-
- const onCloseHandler = useCallback(() => {
- if (clearEditingState) {
- clearEditingState();
- }
- messageComposer.restore();
- }, [clearEditingState, messageComposer]);
-
- return (
-
-
-
- {t('Editing Message')}
-
- [{ opacity: pressed ? 0.8 : 1 }]}
- testID='close-button'
- >
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- editingBoxHeader: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'space-between',
- paddingBottom: 10,
- },
- editingBoxHeaderTitle: {
- fontSize: 14,
- fontWeight: 'bold',
- },
-});
-
-InputEditingStateHeader.displayName = 'EditingStateHeader{messageInput}';
diff --git a/package/src/components/MessageInput/components/InputReplyStateHeader.tsx b/package/src/components/MessageInput/components/InputReplyStateHeader.tsx
deleted file mode 100644
index 8fd09bf49d..0000000000
--- a/package/src/components/MessageInput/components/InputReplyStateHeader.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React from 'react';
-import { Pressable, StyleSheet, Text, View } from 'react-native';
-
-import { useMessageComposer } from '../../../contexts/messageInputContext/hooks/useMessageComposer';
-import { useTheme } from '../../../contexts/themeContext/ThemeContext';
-import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
-
-import { CircleClose, CurveLineLeftUp } from '../../../icons';
-
-export const InputReplyStateHeader = () => {
- const { t } = useTranslationContext();
- const messageComposer = useMessageComposer();
- const {
- theme: {
- colors: { black, grey, grey_gainsboro },
- messageInput: {
- editingStateHeader: { editingBoxHeader, editingBoxHeaderTitle },
- },
- },
- } = useTheme();
-
- const onCloseHandler = () => {
- messageComposer.setQuotedMessage(null);
- };
-
- return (
-
-
-
- {t('Reply to Message')}
-
- [{ opacity: pressed ? 0.8 : 1 }]}
- testID='close-button'
- >
-
-
-
- );
-};
-
-const styles = StyleSheet.create({
- replyBoxHeader: {
- alignItems: 'center',
- flexDirection: 'row',
- justifyContent: 'space-between',
- paddingBottom: 8,
- },
- replyBoxHeaderTitle: {
- fontSize: 14,
- fontWeight: 'bold',
- },
-});
-
-InputReplyStateHeader.displayName = 'ReplyStateHeader{messageInput}';
diff --git a/package/src/components/MessageInput/components/OutputButtons/CooldownTimer.tsx b/package/src/components/MessageInput/components/OutputButtons/CooldownTimer.tsx
new file mode 100644
index 0000000000..ecf6105a2e
--- /dev/null
+++ b/package/src/components/MessageInput/components/OutputButtons/CooldownTimer.tsx
@@ -0,0 +1,49 @@
+import React, { useCallback } from 'react';
+import { StyleSheet, Text } from 'react-native';
+
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { IconButton } from '../../../ui/IconButton';
+
+export type CooldownTimerProps = {
+ seconds: number;
+};
+
+/**
+ * Renders an amount of seconds left for a cooldown to finish.
+ *
+ * See `useCountdown` for an example of how to set a countdown
+ * to use as the source of `seconds`.
+ **/
+export const CooldownTimer = (props: CooldownTimerProps) => {
+ const { seconds } = props;
+ const {
+ theme: {
+ messageInput: {
+ cooldownTimer: { text },
+ },
+ },
+ } = useTheme();
+
+ const icon = useCallback(() => {
+ return (
+
+ {seconds}
+
+ );
+ }, [seconds, text]);
+
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ text: { color: '#B8BEC4', fontSize: 16, fontWeight: '600' },
+});
diff --git a/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx b/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx
new file mode 100644
index 0000000000..48adc924f5
--- /dev/null
+++ b/package/src/components/MessageInput/components/OutputButtons/EditButton.tsx
@@ -0,0 +1,47 @@
+import React, { useCallback } from 'react';
+
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { NewTick } from '../../../../icons/NewTick';
+import { IconButton } from '../../../ui/IconButton';
+
+export type EditButtonProps = Partial> & {
+ /** Disables the button */
+ disabled: boolean;
+};
+
+export const EditButton = (props: EditButtonProps) => {
+ const { disabled = false, sendMessage: propsSendMessage } = props;
+ const { sendMessage: sendMessageFromContext } = useMessageInputContext();
+ const sendMessage = propsSendMessage || sendMessageFromContext;
+
+ const {
+ theme: {
+ messageInput: { editButton },
+ },
+ } = useTheme();
+
+ const onPressHandler = useCallback(() => {
+ if (disabled) {
+ return;
+ }
+ sendMessage();
+ }, [disabled, sendMessage]);
+
+ return (
+
+ );
+};
+
+EditButton.displayName = 'EditButton{messageInput}';
diff --git a/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx b/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx
new file mode 100644
index 0000000000..b0ccabad26
--- /dev/null
+++ b/package/src/components/MessageInput/components/OutputButtons/SendButton.tsx
@@ -0,0 +1,47 @@
+import React, { useCallback } from 'react';
+
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../../../contexts/messageInputContext/MessageInputContext';
+import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
+import { SendRight } from '../../../../icons/SendRight';
+import { IconButton } from '../../../ui/IconButton';
+
+export type SendButtonProps = Partial> & {
+ /** Disables the button */
+ disabled: boolean;
+};
+
+export const SendButton = (props: SendButtonProps) => {
+ const { disabled = false, sendMessage: propsSendMessage } = props;
+ const { sendMessage: sendMessageFromContext } = useMessageInputContext();
+ const sendMessage = propsSendMessage || sendMessageFromContext;
+
+ const {
+ theme: {
+ messageInput: { sendButton },
+ },
+ } = useTheme();
+
+ const onPressHandler = useCallback(() => {
+ if (disabled) {
+ return;
+ }
+ sendMessage();
+ }, [disabled, sendMessage]);
+
+ return (
+
+ );
+};
+
+SendButton.displayName = 'SendButton{messageInput}';
diff --git a/package/src/components/MessageInput/components/OutputButtons/index.tsx b/package/src/components/MessageInput/components/OutputButtons/index.tsx
new file mode 100644
index 0000000000..dda8a74d56
--- /dev/null
+++ b/package/src/components/MessageInput/components/OutputButtons/index.tsx
@@ -0,0 +1,202 @@
+import React, { useCallback } from 'react';
+
+import Animated, { ZoomIn, ZoomOut } from 'react-native-reanimated';
+
+import { TextComposerState } from 'stream-chat';
+
+import { EditButton } from './EditButton';
+
+import {
+ ChannelContextValue,
+ ChatContextValue,
+ useChannelContext,
+ useChatContext,
+ useMessageComposerHasSendableData,
+ useTheme,
+} from '../../../../contexts';
+import { useAttachmentManagerState } from '../../../../contexts/messageInputContext/hooks/useAttachmentManagerState';
+import { useMessageComposer } from '../../../../contexts/messageInputContext/hooks/useMessageComposer';
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../../../contexts/messageInputContext/MessageInputContext';
+import { useStateStore } from '../../../../hooks/useStateStore';
+import { AIStates, useAIState } from '../../../AITypingIndicatorView';
+import { AudioRecordingButton } from '../../components/AudioRecorder/AudioRecordingButton';
+import { useCountdown } from '../../hooks/useCountdown';
+
+export type OutputButtonsProps = Partial;
+
+export type OutputButtonsWithContextProps = Pick &
+ Pick &
+ Pick<
+ MessageInputContextValue,
+ | 'asyncMessagesMinimumPressDuration'
+ | 'asyncMessagesSlideToCancelDistance'
+ | 'asyncMessagesLockDistance'
+ | 'asyncMessagesMultiSendEnabled'
+ | 'audioRecordingEnabled'
+ | 'cooldownEndsAt'
+ | 'CooldownTimer'
+ | 'SendButton'
+ | 'StopMessageStreamingButton'
+ | 'StartAudioRecordingButton'
+ >;
+
+const textComposerStateSelector = (state: TextComposerState) => ({
+ command: state.command,
+ hasText: !!state.text,
+});
+
+export const OutputButtonsWithContext = (props: OutputButtonsWithContextProps) => {
+ const {
+ channel,
+ cooldownEndsAt,
+ CooldownTimer,
+ isOnline,
+ SendButton,
+ StopMessageStreamingButton,
+ } = props;
+ const {
+ theme: {
+ messageInput: {
+ audioRecordingButtonContainer,
+ cooldownButtonContainer,
+ editButtonContainer,
+ sendButtonContainer,
+ },
+ },
+ } = useTheme();
+
+ const messageComposer = useMessageComposer();
+ const editing = !!messageComposer.editedMessage;
+ const { textComposer } = messageComposer;
+ const { command, hasText } = useStateStore(textComposer.state, textComposerStateSelector);
+ const { attachments } = useAttachmentManagerState();
+ const hasSendableData = useMessageComposerHasSendableData();
+
+ const showSendingButton = hasText || attachments.length || command;
+
+ const { seconds: cooldownRemainingSeconds } = useCountdown(cooldownEndsAt);
+
+ const { aiState } = useAIState(channel);
+ const stopGenerating = useCallback(() => channel?.stopAIResponse(), [channel]);
+ const shouldDisplayStopAIGeneration =
+ [AIStates.Thinking, AIStates.Generating].includes(aiState) && !!StopMessageStreamingButton;
+
+ if (shouldDisplayStopAIGeneration) {
+ return ;
+ }
+
+ if (editing) {
+ return (
+
+
+
+ );
+ }
+
+ if (cooldownRemainingSeconds) {
+ return (
+
+
+
+ );
+ }
+
+ if (showSendingButton) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+const areEqual = (
+ prevProps: OutputButtonsWithContextProps,
+ nextProps: OutputButtonsWithContextProps,
+) => {
+ const { channel: prevChannel, cooldownEndsAt: prevCooldownEndsAt } = prevProps;
+
+ const { channel: nextChannel, cooldownEndsAt: nextCooldownEndsAt } = nextProps;
+
+ if (prevChannel?.cid !== nextChannel?.cid) {
+ return false;
+ }
+
+ const cooldownEndsAtEqual = prevCooldownEndsAt === nextCooldownEndsAt;
+ if (!cooldownEndsAtEqual) {
+ return false;
+ }
+
+ return true;
+};
+
+const MemoizedOutputButtonsWithContext = React.memo(
+ OutputButtonsWithContext,
+ areEqual,
+) as typeof OutputButtonsWithContext;
+
+export const OutputButtons = (props: OutputButtonsProps) => {
+ const { isOnline } = useChatContext();
+ const { channel } = useChannelContext();
+ const {
+ audioRecordingEnabled,
+ asyncMessagesMinimumPressDuration,
+ asyncMessagesSlideToCancelDistance,
+ asyncMessagesLockDistance,
+ asyncMessagesMultiSendEnabled,
+ cooldownEndsAt,
+ CooldownTimer,
+ SendButton,
+ StopMessageStreamingButton,
+ StartAudioRecordingButton,
+ } = useMessageInputContext();
+
+ return (
+
+ );
+};
diff --git a/package/src/components/MessageList/MessageFlashList.tsx b/package/src/components/MessageList/MessageFlashList.tsx
index bac31e5441..02c618be86 100644
--- a/package/src/components/MessageList/MessageFlashList.tsx
+++ b/package/src/components/MessageList/MessageFlashList.tsx
@@ -8,6 +8,8 @@ import {
ViewToken,
} from 'react-native';
+import Animated, { LinearTransition } from 'react-native-reanimated';
+
import type { FlashListProps, FlashListRef } from '@shopify/flash-list';
import type { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat';
@@ -30,6 +32,10 @@ import {
ImageGalleryContextValue,
useImageGalleryContext,
} from '../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../contexts/messageInputContext/MessageInputContext';
import {
MessageListItemContextValue,
MessageListItemProvider,
@@ -49,7 +55,11 @@ import {
import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext';
import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext';
-import { useStableCallback } from '../../hooks';
+import { useStableCallback, useStateStore } from '../../hooks';
+import {
+ MessageInputHeightState,
+ messageInputHeightStore,
+} from '../../state-store/message-input-height-store';
import { FileTypes } from '../../types/types';
import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper';
@@ -96,6 +106,10 @@ const getPreviousLastMessage = (messages: LocalMessage[], newMessage?: MessageRe
return previousLastMessage;
};
+const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({
+ height: state.height,
+});
+
type MessageFlashListPropsWithContext = Pick<
AttachmentPickerContextValue,
'closePicker' | 'selectedPicker' | 'setSelectedPicker'
@@ -124,6 +138,7 @@ type MessageFlashListPropsWithContext = Pick<
| 'maximumMessageLimit'
> &
Pick &
+ Pick &
Pick &
Pick &
Pick<
@@ -287,6 +302,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
loadMoreThread,
markRead,
maximumMessageLimit,
+ messageInputFloating,
myMessageTheme,
readEvents,
NetworkDownIndicator,
@@ -312,6 +328,11 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
} = props;
const flashListRef = useRef | null>(null);
+ const { height: messageInputHeight } = useStateStore(
+ messageInputHeightStore,
+ messageInputHeightStoreSelector,
+ );
+
const [hasMoved, setHasMoved] = useState(false);
const [scrollToBottomButtonVisible, setScrollToBottomButtonVisible] = useState(false);
const [isUnreadNotificationOpen, setIsUnreadNotificationOpen] = useState(false);
@@ -339,7 +360,14 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const {
colors: { white_snow },
- messageList: { container, contentContainer, listContainer },
+ messageList: {
+ container,
+ contentContainer,
+ listContainer,
+ scrollToBottomButtonContainer,
+ stickyHeaderContainer,
+ unreadMessagesNotificationContainer,
+ },
} = theme;
const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]);
@@ -935,8 +963,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
const visibleLength = nativeEvent.layoutMeasurement.height;
const contentLength = nativeEvent.contentSize.height;
- // Show scrollToBottom button once scroll position goes beyond 150.
- const isScrollAtStart = contentLength - visibleLength - offset < 150;
+ const isScrollAtStart = contentLength - visibleLength - offset < messageInputHeight;
const notLatestSet = channel.state.messages !== channel.state.latestMessages;
@@ -1025,8 +1052,12 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
);
const flatListContentContainerStyle = useMemo(
- () => [styles.contentContainer, contentContainer],
- [contentContainer],
+ () => [
+ styles.contentContainer,
+ { paddingBottom: messageInputFloating ? messageInputHeight : 0 },
+ contentContainer,
+ ],
+ [contentContainer, messageInputFloating, messageInputHeight],
);
const currentListHeightRef = useRef(undefined);
@@ -1102,7 +1133,7 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
/>
)}
-
+
{messageListLengthAfterUpdate && StickyHeader ? (
) : null}
@@ -1112,14 +1143,27 @@ const MessageFlashListWithContext = (props: MessageFlashListPropsWithContext) =>
)}
-
+
+
+
{isUnreadNotificationOpen && !threadList ? (
-
+
+
+
) : null}
);
@@ -1180,6 +1224,7 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
const { loadMore, loadMoreRecent } = usePaginatedMessageListContext();
const { loadMoreRecentThread, loadMoreThread, thread, threadInstance } = useThreadContext();
const { readEvents } = useOwnCapabilitiesContext();
+ const { messageInputFloating } = useMessageInputContext();
return (
{
markRead,
maximumMessageLimit,
Message,
+ messageInputFloating,
MessageSystem,
myMessageTheme,
NetworkDownIndicator,
@@ -1241,7 +1287,6 @@ export const MessageFlashList = (props: MessageFlashListProps) => {
const styles = StyleSheet.create({
container: {
- alignItems: 'center',
flex: 1,
width: '100%',
},
@@ -1259,8 +1304,20 @@ const styles = StyleSheet.create({
flex: 1,
width: '100%',
},
- stickyHeader: {
+ scrollToBottomButtonContainer: {
+ bottom: 8,
position: 'absolute',
+ right: 24,
+ },
+ stickyHeaderContainer: {
+ left: 0,
+ position: 'absolute',
+ right: 0,
top: 0,
},
+ unreadMessagesNotificationContainer: {
+ alignSelf: 'center',
+ position: 'absolute',
+ top: 8,
+ },
});
diff --git a/package/src/components/MessageList/MessageList.tsx b/package/src/components/MessageList/MessageList.tsx
index ac6693a396..7960e11735 100644
--- a/package/src/components/MessageList/MessageList.tsx
+++ b/package/src/components/MessageList/MessageList.tsx
@@ -9,6 +9,8 @@ import {
ViewToken,
} from 'react-native';
+import Animated, { LinearTransition } from 'react-native-reanimated';
+
import type { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat';
import { useMessageList } from './hooks/useMessageList';
@@ -32,6 +34,10 @@ import {
ImageGalleryContextValue,
useImageGalleryContext,
} from '../../contexts/imageGalleryContext/ImageGalleryContext';
+import {
+ MessageInputContextValue,
+ useMessageInputContext,
+} from '../../contexts/messageInputContext/MessageInputContext';
import {
MessageListItemContextValue,
MessageListItemProvider,
@@ -52,6 +58,11 @@ import { mergeThemes, useTheme } from '../../contexts/themeContext/ThemeContext'
import { ThreadContextValue, useThreadContext } from '../../contexts/threadContext/ThreadContext';
import { useStableCallback } from '../../hooks';
+import { useStateStore } from '../../hooks/useStateStore';
+import {
+ MessageInputHeightState,
+ messageInputHeightStore,
+} from '../../state-store/message-input-height-store';
import { FileTypes } from '../../types/types';
import { MessageWrapper } from '../Message/MessageSimple/MessageWrapper';
@@ -61,7 +72,6 @@ const WAIT_FOR_SCROLL_TIMEOUT = 0;
const MAX_RETRIES_AFTER_SCROLL_FAILURE = 10;
const styles = StyleSheet.create({
container: {
- alignItems: 'center',
flex: 1,
width: '100%',
},
@@ -79,10 +89,22 @@ const styles = StyleSheet.create({
flex: 1,
width: '100%',
},
- stickyHeader: {
+ scrollToBottomButtonContainer: {
+ bottom: 8,
+ position: 'absolute',
+ right: 24,
+ },
+ stickyHeaderContainer: {
+ left: 0,
position: 'absolute',
+ right: 0,
top: 0,
},
+ unreadMessagesNotificationContainer: {
+ alignSelf: 'center',
+ position: 'absolute',
+ top: 8,
+ },
});
const keyExtractor = (item: LocalMessage) => {
@@ -160,6 +182,7 @@ type MessageListPropsWithContext = Pick<
| 'TypingIndicatorContainer'
| 'UnreadMessagesNotification'
> &
+ Pick &
Pick<
ThreadContextValue,
'loadMoreRecentThread' | 'loadMoreThread' | 'thread' | 'threadInstance'
@@ -236,6 +259,10 @@ const renderItem = ({ item: message }: { item: LocalMessage }) => {
return ;
};
+const messageInputHeightStoreSelector = (state: MessageInputHeightState) => ({
+ height: state.height,
+});
+
/**
* The message list component renders a list of messages. It consumes the following contexts:
*
@@ -276,6 +303,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
loadMoreThread,
markRead,
maximumMessageLimit,
+ messageInputFloating,
myMessageTheme,
NetworkDownIndicator,
noGroupByUser,
@@ -301,10 +329,21 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
} = props;
const [isUnreadNotificationOpen, setIsUnreadNotificationOpen] = useState(false);
const { theme } = useTheme();
+ const { height: messageInputHeight } = useStateStore(
+ messageInputHeightStore,
+ messageInputHeightStoreSelector,
+ );
const {
colors: { white_snow },
- messageList: { container, contentContainer, listContainer },
+ messageList: {
+ container,
+ contentContainer,
+ listContainer,
+ stickyHeaderContainer,
+ scrollToBottomButtonContainer,
+ unreadMessagesNotificationContainer,
+ },
} = theme;
const myMessageThemeString = useMemo(() => JSON.stringify(myMessageTheme), [myMessageTheme]);
@@ -915,8 +954,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const handleScroll: ScrollViewProps['onScroll'] = useStableCallback((event) => {
const messageListHasMessages = processedMessageList.length > 0;
const offset = event.nativeEvent.contentOffset.y;
- // Show scrollToBottom button once scroll position goes beyond 150.
- const isScrollAtBottom = offset <= 150;
+ const isScrollAtBottom = offset <= messageInputHeight;
const notLatestSet = channel.state.messages !== channel.state.latestMessages;
@@ -1122,11 +1160,16 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
const flatListContentContainerStyle = useMemo(
() => [
- styles.contentContainer,
+ { paddingTop: messageInputFloating ? messageInputHeight : 0 },
+ additionalFlatListProps?.contentContainerStyle,
+ contentContainer,
+ ],
+ [
additionalFlatListProps?.contentContainerStyle,
contentContainer,
+ messageInputHeight,
+ messageInputFloating,
],
- [additionalFlatListProps?.contentContainerStyle, contentContainer],
);
if (!FlatList) {
@@ -1191,7 +1234,7 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
/>
)}
-
+
{messageListLengthAfterUpdate && StickyHeader ? (
) : null}
@@ -1201,14 +1244,30 @@ const MessageListWithContext = (props: MessageListPropsWithContext) => {
)}
-
+ {scrollToBottomButtonVisible ? (
+
+
+
+ ) : null}
+
{isUnreadNotificationOpen && !threadList ? (
-
+
+
+
) : null}
);
@@ -1261,6 +1320,7 @@ export const MessageList = (props: MessageListProps) => {
TypingIndicatorContainer,
UnreadMessagesNotification,
} = useMessagesContext();
+ const { messageInputFloating } = useMessageInputContext();
const { loadMore, loadMoreRecent } = usePaginatedMessageListContext();
const { loadMoreRecentThread, loadMoreThread, thread, threadInstance } = useThreadContext();
@@ -1294,6 +1354,7 @@ export const MessageList = (props: MessageListProps) => {
markRead,
maximumMessageLimit,
Message,
+ messageInputFloating,
MessageSystem,
myMessageTheme,
NetworkDownIndicator,
diff --git a/package/src/components/MessageList/ScrollToBottomButton.tsx b/package/src/components/MessageList/ScrollToBottomButton.tsx
index 8c15cb851c..5e7c732a70 100644
--- a/package/src/components/MessageList/ScrollToBottomButton.tsx
+++ b/package/src/components/MessageList/ScrollToBottomButton.tsx
@@ -1,29 +1,13 @@
import React from 'react';
-import { GestureResponderEvent, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { StyleSheet, Text, View } from 'react-native';
+
+import Animated, { ZoomIn, ZoomOut } from 'react-native-reanimated';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { Down } from '../../icons';
+import { NewDown } from '../../icons/NewDown';
+import { IconButton } from '../ui/IconButton';
const styles = StyleSheet.create({
- container: {
- alignItems: 'center',
- borderRadius: 20,
- elevation: 5,
- height: 40,
- justifyContent: 'center',
- shadowOffset: {
- height: 2,
- width: 0,
- },
- shadowOpacity: 0.25,
- shadowRadius: 4,
- width: 40,
- },
- touchable: {
- bottom: 20,
- position: 'absolute',
- right: 20,
- },
unreadCountNotificationContainer: {
alignItems: 'center',
borderRadius: 10,
@@ -40,16 +24,11 @@ const styles = StyleSheet.create({
textAlign: 'center',
textAlignVertical: 'center',
},
- wrapper: {
- alignItems: 'center',
- height: 50,
- justifyContent: 'flex-end',
- },
});
export type ScrollToBottomButtonProps = {
/** onPress handler */
- onPress: (event: GestureResponderEvent) => void;
+ onPress: () => void;
/** If we should show the notification or not */
showNotification?: boolean;
unreadCount?: number;
@@ -60,15 +39,12 @@ export const ScrollToBottomButton = (props: ScrollToBottomButtonProps) => {
const {
theme: {
- colors: { accent_blue, black, white },
+ colors: { accent_blue, white },
messageList: {
scrollToBottomButton: {
- chevronColor,
container,
- touchable,
unreadCountNotificationContainer,
unreadCountNotificationText,
- wrapper,
},
},
},
@@ -79,37 +55,42 @@ export const ScrollToBottomButton = (props: ScrollToBottomButtonProps) => {
}
return (
-
-
-
-
-
- {!!unreadCount && (
-
+
+
+
+ {!!unreadCount && (
+
+
-
- {unreadCount}
-
-
- )}
-
-
+ {unreadCount}
+
+
+ )}
+ >
);
};
diff --git a/package/src/components/MessageList/UnreadMessagesNotification.tsx b/package/src/components/MessageList/UnreadMessagesNotification.tsx
index 47192ee192..0872d8be42 100644
--- a/package/src/components/MessageList/UnreadMessagesNotification.tsx
+++ b/package/src/components/MessageList/UnreadMessagesNotification.tsx
@@ -4,7 +4,7 @@ import { Pressable, StyleSheet, Text } from 'react-native';
import { useChannelContext } from '../../contexts/channelContext/ChannelContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
-import { Close } from '../../icons';
+import { NewClose } from '../../icons/NewClose';
export type UnreadMessagesNotificationProps = {
/**
@@ -76,7 +76,7 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp
closeButtonContainer,
]}
>
-
+
);
@@ -84,13 +84,11 @@ export const UnreadMessagesNotification = (props: UnreadMessagesNotificationProp
const styles = StyleSheet.create({
container: {
- alignItems: 'center',
borderRadius: 20,
elevation: 4,
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 8,
- position: 'absolute',
shadowColor: '#000',
shadowOffset: {
height: 2,
@@ -98,7 +96,6 @@ const styles = StyleSheet.create({
},
shadowOpacity: 0.23,
shadowRadius: 2.62,
- top: 8,
},
text: {
fontWeight: '500',
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 286fdbf9a2..eddcde61d4 100644
--- a/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
+++ b/package/src/components/MessageList/__tests__/__snapshots__/ScrollToBottomButton.test.js.snap
@@ -1,136 +1,141 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`ScrollToBottomButton should render the message notification and match snapshot 1`] = `
-
-
-
-
-
-
-
+ }
+ strokeLinecap={1}
+ strokeLinejoin={1}
+ strokeWidth={1.5}
+ />
+
+
`;
diff --git a/package/src/components/RTLComponents/WritingDirectionAwareText.tsx b/package/src/components/RTLComponents/WritingDirectionAwareText.tsx
index 0e7e0fa17b..e34ff46d31 100644
--- a/package/src/components/RTLComponents/WritingDirectionAwareText.tsx
+++ b/package/src/components/RTLComponents/WritingDirectionAwareText.tsx
@@ -1,11 +1,11 @@
import React from 'react';
-import { I18nManager, StyleSheet, Text, ViewProps } from 'react-native';
+import { I18nManager, StyleSheet, Text, TextProps } from 'react-native';
const styles = StyleSheet.create({
defaultStyle: { writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' },
});
-export type WritingDirectionAwareTextProps = ViewProps;
+export type WritingDirectionAwareTextProps = TextProps;
export const WritingDirectionAwareText = (props: WritingDirectionAwareTextProps) => {
const { children, style, ...rest } = props;
diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx
index 1ad146941e..764345705d 100644
--- a/package/src/components/Reply/Reply.tsx
+++ b/package/src/components/Reply/Reply.tsx
@@ -1,405 +1,474 @@
-import React, { useMemo, useState } from 'react';
-
-import { Image, ImageStyle, StyleSheet, Text, View, ViewStyle } from 'react-native';
+import React, { useCallback, useMemo } from 'react';
+import { Image, StyleSheet, Text, View } from 'react-native';
import dayjs from 'dayjs';
+import { LocalMessage, MessageComposerState, PollState } from 'stream-chat';
-import merge from 'lodash/merge';
-
-import type { Attachment, MessageComposerState, PollState } from 'stream-chat';
-
-import { useChatContext, useMessageComposer } from '../../contexts';
-import { useChatConfigContext } from '../../contexts/chatConfigContext/ChatConfigContext';
-import { useMessageContext } from '../../contexts/messageContext/MessageContext';
+import { useChatContext } from '../../contexts/chatContext/ChatContext';
import {
- MessagesContextValue,
- useMessagesContext,
-} from '../../contexts/messagesContext/MessagesContext';
+ MessageContextValue,
+ useMessageContext,
+} from '../../contexts/messageContext/MessageContext';
+import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
+import { MessagesContextValue } from '../../contexts/messagesContext/MessagesContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import {
- TranslationContextValue,
- useTranslationContext,
-} from '../../contexts/translationContext/TranslationContext';
import { useStateStore } from '../../hooks';
+import { NewFile } from '../../icons/NewFile';
+import { NewLink } from '../../icons/NewLink';
+import { NewMapPin } from '../../icons/NewMapPin';
+import { NewMic } from '../../icons/NewMic';
+import { NewPhoto } from '../../icons/NewPhoto';
+import { NewPlayIcon } from '../../icons/NewPlayIcon';
+import { NewPoll } from '../../icons/NewPoll';
+import { NewVideo } from '../../icons/NewVideo';
import { FileTypes } from '../../types/types';
-import { getResizedImageUrl } from '../../utils/getResizedImageUrl';
-import { getTrimmedAttachmentTitle } from '../../utils/getTrimmedAttachmentTitle';
-import { checkQuotedMessageEquality, hasOnlyEmojis } from '../../utils/utils';
+import { checkQuotedMessageEquality } from '../../utils/utils';
+import { FileIcon } from '../Attachment/FileIcon';
+import { DismissAttachmentUpload } from '../MessageInput/components/AttachmentPreview/DismissAttachmentUpload';
-import { FileIcon as FileIconDefault } from '../Attachment/FileIcon';
-import { VideoThumbnail } from '../Attachment/VideoThumbnail';
-import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar';
-import { MessageTextContainer } from '../Message/MessageSimple/MessageTextContainer';
-
-const styles = StyleSheet.create({
- container: {
- alignItems: 'flex-end',
- flexDirection: 'row',
- },
- fileAttachmentContainer: { paddingLeft: 8, paddingVertical: 8 },
- imageAttachment: {
- borderRadius: 8,
- height: 32,
- marginLeft: 8,
- marginVertical: 8,
- width: 32,
- },
- messageContainer: {
- alignItems: 'flex-start',
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 12,
- borderTopLeftRadius: 12,
- borderTopRightRadius: 12,
- flexDirection: 'row',
- flexGrow: 1,
- flexShrink: 1,
- },
- secondaryText: {
- paddingHorizontal: 8,
- },
- text: { fontSize: 12, fontWeight: 'bold', overflow: 'hidden' },
- textContainer: { maxWidth: undefined, paddingHorizontal: 8 },
- videoThumbnailContainerStyle: {
- borderRadius: 8,
- height: 50,
- marginLeft: 8,
- marginVertical: 8,
- width: 50,
- },
- videoThumbnailImageStyle: {
- borderRadius: 10,
- },
+const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
+ quotedMessage: state.quotedMessage,
});
-export type ReplySelectorReturnType = {
- name?: string;
-};
-
-const selector = (nextValue: PollState): ReplySelectorReturnType => ({
+const selector = (nextValue: PollState) => ({
name: nextValue.name,
});
-const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
- quotedMessage: state.quotedMessage,
+const RightContent = React.memo((props: { message: LocalMessage }) => {
+ const { message } = props;
+ const attachments = message?.attachments;
+
+ if (!attachments || attachments.length > 1) {
+ return null;
+ }
+
+ const attachment = attachments?.[0];
+
+ if (attachment?.type === FileTypes.Image) {
+ return (
+
+
+
+ );
+ }
+ if (attachment?.type === FileTypes.Video) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ if (attachment?.type === FileTypes.File) {
+ return ;
+ }
+
+ return null;
});
-type ReplyPropsWithContext = Pick<
- MessagesContextValue,
- 'FileAttachmentIcon' | 'MessageAvatar' | 'quotedMessage'
-> &
- Pick & {
- attachmentSize?: number;
- styles?: Partial<{
- container: ViewStyle;
- fileAttachmentContainer: ViewStyle;
- imageAttachment: ImageStyle;
- messageContainer: ViewStyle;
- textContainer: ViewStyle;
- }>;
- };
+const SubtitleText = React.memo(({ message }: { message?: LocalMessage | null }) => {
+ const { client } = useChatContext();
+ const poll = client.polls.fromState(message?.poll_id ?? '');
+ const { name: pollName } = useStateStore(poll?.state, selector) ?? {};
+ const {
+ theme: {
+ reply: { subtitle: subtitleStyle },
+ },
+ } = useTheme();
-const getMessageType = (lastAttachment: Attachment) => {
- let messageType;
+ const subtitle = useMemo(() => {
+ const attachments = message?.attachments;
+ const audioAttachments = attachments?.filter(
+ (attachment) => attachment.type === FileTypes.Audio,
+ );
+ const imageAttachments = attachments?.filter(
+ (attachment) => attachment.type === FileTypes.Image,
+ );
+ const videoAttachments = attachments?.filter(
+ (attachment) => attachment.type === FileTypes.Video,
+ );
+ const fileAttachments = attachments?.filter((attachment) => attachment.type === FileTypes.File);
+ const voiceRecordingAttachments = attachments?.filter(
+ (attachment) => attachment.type === FileTypes.VoiceRecording,
+ );
+ const onlyImages = imageAttachments?.length && imageAttachments?.length === attachments?.length;
+ const onlyVideos = videoAttachments?.length && videoAttachments?.length === attachments?.length;
+ const onlyFiles = fileAttachments?.length && fileAttachments?.length === attachments?.length;
+ const onlyAudio = audioAttachments?.length === attachments?.length;
+ const onlyVoiceRecordings =
+ voiceRecordingAttachments?.length &&
+ voiceRecordingAttachments?.length === attachments?.length;
+
+ if (pollName) {
+ return pollName;
+ }
- const isLastAttachmentFile = lastAttachment.type === FileTypes.File;
+ if (message?.shared_location) {
+ if (
+ message?.shared_location?.end_at &&
+ new Date(message?.shared_location?.end_at) > new Date()
+ ) {
+ return 'Live Location';
+ }
+ return 'Location';
+ }
- const isLastAttachmentAudio = lastAttachment.type === FileTypes.Audio;
+ if (message?.text) {
+ return message?.text;
+ }
- const isLastAttachmentVoiceRecording = lastAttachment.type === FileTypes.VoiceRecording;
+ if (imageAttachments?.length && videoAttachments?.length) {
+ return `${imageAttachments?.length + videoAttachments.length} Media`;
+ }
- const isLastAttachmentVideo = lastAttachment.type === FileTypes.Video;
+ if (onlyImages) {
+ if (imageAttachments?.length === 1) {
+ return 'Photo';
+ } else {
+ return `${imageAttachments?.length} Photos`;
+ }
+ }
- const isLastAttachmentGiphy =
- lastAttachment?.type === FileTypes.Giphy || lastAttachment?.type === FileTypes.Imgur;
+ if (onlyVideos) {
+ if (videoAttachments?.length === 1) {
+ return 'Video';
+ } else {
+ return `${videoAttachments?.length} Videos`;
+ }
+ }
- const isLastAttachmentImageOrGiphy =
- lastAttachment?.type === FileTypes.Image &&
- !lastAttachment?.title_link &&
- !lastAttachment?.og_scrape_url;
+ if (onlyAudio) {
+ if (audioAttachments?.length === 1) {
+ return 'Audio';
+ } else {
+ return `${audioAttachments?.length} Audios`;
+ }
+ }
- const isLastAttachmentImage = lastAttachment?.image_url || lastAttachment?.thumb_url;
+ if (onlyVoiceRecordings) {
+ if (voiceRecordingAttachments?.length === 1) {
+ return `Voice message (${dayjs.duration(voiceRecordingAttachments?.[0]?.duration ?? 0, 'seconds').format('m:ss')})`;
+ } else {
+ return `${voiceRecordingAttachments?.length} Voice messages`;
+ }
+ }
- if (isLastAttachmentFile) {
- messageType = FileTypes.File;
- } else if (isLastAttachmentVideo) {
- messageType = FileTypes.Video;
- } else if (isLastAttachmentAudio) {
- messageType = FileTypes.Audio;
- } else if (isLastAttachmentVoiceRecording) {
- messageType = FileTypes.VoiceRecording;
- } else if (isLastAttachmentImageOrGiphy) {
- if (isLastAttachmentImage) {
- messageType = FileTypes.Image;
- } else {
- messageType = undefined;
+ if (onlyFiles && fileAttachments?.length === 1) {
+ return fileAttachments?.[0]?.title;
}
- } else if (isLastAttachmentGiphy) {
- messageType = FileTypes.Giphy;
- } else {
- messageType = 'other';
+
+ return `${attachments?.length} Files`;
+ }, [message?.attachments, message?.shared_location, message?.text, pollName]);
+
+ if (!subtitle) {
+ return null;
}
- return messageType;
-};
+ return (
+
+ {subtitle}
+
+ );
+});
-const ReplyWithContext = (props: ReplyPropsWithContext) => {
- const { client } = useChatContext();
+const SubtitleIcon = React.memo((props: { message?: LocalMessage | null }) => {
+ const { message } = props;
const {
- attachmentSize = 40,
- FileAttachmentIcon,
- MessageAvatar,
- quotedMessage,
- styles: stylesProp = {},
- t,
- } = props;
+ theme: {
+ reply: { pollIcon, locationIcon, linkIcon, audioIcon, fileIcon, videoIcon, photoIcon },
+ },
+ } = useTheme();
+ if (!message) {
+ return null;
+ }
+
+ const attachments = message?.attachments;
+ const audioAttachments = attachments?.filter((attachment) => attachment.type === FileTypes.Audio);
+ const imageAttachments = attachments?.filter((attachment) => attachment.type === FileTypes.Image);
+ const videoAttachments = attachments?.filter((attachment) => attachment.type === FileTypes.Video);
+ const voiceRecordingAttachments = attachments?.filter(
+ (attachment) => attachment.type === FileTypes.VoiceRecording,
+ );
+ const fileAttachments = attachments?.filter((attachment) => attachment.type === FileTypes.File);
+ const onlyAudio = audioAttachments?.length && audioAttachments?.length === attachments?.length;
+ const onlyVideos = videoAttachments?.length && videoAttachments?.length === attachments?.length;
+ const onlyVoiceRecordings =
+ voiceRecordingAttachments?.length && voiceRecordingAttachments?.length === attachments?.length;
+ const hasLink = attachments?.some(
+ (attachment) => attachment.type === FileTypes.Image && attachment.og_scrape_url,
+ );
+
+ if (message.poll_id) {
+ return (
+
+ );
+ }
+
+ if (message.shared_location) {
+ return (
+
+ );
+ }
+
+ if (hasLink) {
+ return (
+
+ );
+ }
+
+ if (onlyAudio || onlyVoiceRecordings) {
+ return (
+
+ );
+ }
+
+ if (fileAttachments?.length) {
+ return (
+
+ );
+ }
+
+ if (onlyVideos) {
+ return (
+
+ );
+ }
+
+ if (imageAttachments?.length) {
+ return (
+
+ );
+ }
- const { resizableCDNHosts } = useChatConfigContext();
+ return null;
+});
- const [error, setError] = useState(false);
+export type ReplyPropsWithContext = Pick &
+ Pick & {
+ isMyMessage: boolean;
+ onDismiss: () => void;
+ mode: 'reply' | 'edit';
+ };
+export const ReplyWithContext = (props: ReplyPropsWithContext) => {
+ const { isMyMessage, message: messageFromContext, mode, onDismiss, quotedMessage } = props;
const {
theme: {
- colors: { blue_alice, border, grey, transparent, white },
- messageSimple: {
- content: { deletedText },
- },
+ colors: { grey_whisper },
reply: {
+ wrapper,
container,
- fileAttachmentContainer,
- imageAttachment,
- markdownStyles,
- messageContainer,
- secondaryText,
- textContainer,
- videoThumbnail: {
- container: videoThumbnailContainerStyle,
- image: videoThumbnailImageStyle,
- },
+ leftContainer,
+ rightContainer,
+ title: titleStyle,
+ subtitleContainer,
+ dismissWrapper,
},
},
} = useTheme();
- const poll = client.polls.fromState(quotedMessage?.poll_id ?? '');
- const { name: pollName }: ReplySelectorReturnType = useStateStore(poll?.state, selector) ?? {};
-
- const messageText = quotedMessage ? quotedMessage.text : '';
-
- const emojiOnlyText = useMemo(() => {
- if (!messageText) {
- return false;
- }
- return hasOnlyEmojis(messageText);
- }, [messageText]);
+ const title = useMemo(
+ () =>
+ mode === 'edit'
+ ? 'Edit Message'
+ : isMyMessage
+ ? 'You'
+ : `Reply to ${quotedMessage?.user?.name}`,
+ [mode, isMyMessage, quotedMessage?.user?.name],
+ );
if (!quotedMessage) {
return null;
}
- const lastAttachment = quotedMessage.attachments?.slice(-1)[0] as Attachment;
- const messageType = lastAttachment && getMessageType(lastAttachment);
-
- const trimmedLastAttachmentTitle = getTrimmedAttachmentTitle(lastAttachment?.title);
-
- const hasImage =
- !error &&
- lastAttachment &&
- messageType !== FileTypes.File &&
- messageType !== FileTypes.Video &&
- messageType !== FileTypes.Audio &&
- messageType !== FileTypes.VoiceRecording &&
- (lastAttachment.image_url || lastAttachment.thumb_url || lastAttachment.og_scrape_url);
-
- const onlyEmojis = !lastAttachment && emojiOnlyText;
-
return (
-
-
+
- {!error && lastAttachment ? (
- messageType === FileTypes.File ||
- messageType === FileTypes.Audio ||
- messageType === FileTypes.VoiceRecording ? (
-
-
-
- ) : hasImage ? (
- setError(true)}
- source={{
- uri: getResizedImageUrl({
- height:
- (stylesProp.imageAttachment?.height as number) ||
- (imageAttachment?.height as number) ||
- styles.imageAttachment.height,
- resizableCDNHosts,
- url: (lastAttachment.image_url ||
- lastAttachment.thumb_url ||
- lastAttachment.og_scrape_url) as string,
- width:
- (stylesProp.imageAttachment?.width as number) ||
- (imageAttachment?.width as number) ||
- styles.imageAttachment.width,
- }),
- }}
- style={[styles.imageAttachment, imageAttachment, stylesProp.imageAttachment]}
- />
- ) : null
- ) : null}
- {messageType === FileTypes.Video && !lastAttachment.og_scrape_url ? (
-
- ) : null}
-
- 170
- ? `${quotedMessage.text.slice(0, 170)}...`
- : quotedMessage.text
- : messageType === FileTypes.Image
- ? t('Photo')
- : messageType === FileTypes.Video
- ? t('Video')
- : messageType === FileTypes.File ||
- messageType === FileTypes.Audio ||
- messageType === FileTypes.VoiceRecording
- ? trimmedLastAttachmentTitle || ''
- : '',
- }}
- onlyEmojis={onlyEmojis}
- styles={{
- textContainer: [
- {
- marginRight:
- hasImage || messageType === FileTypes.Video
- ? Number(
- stylesProp.imageAttachment?.height ||
- imageAttachment.height ||
- styles.imageAttachment.height,
- ) +
- Number(
- stylesProp.imageAttachment?.marginLeft ||
- imageAttachment.marginLeft ||
- styles.imageAttachment.marginLeft,
- )
- : messageType === FileTypes.File ||
- messageType === FileTypes.Audio ||
- messageType === FileTypes.VoiceRecording
- ? attachmentSize +
- Number(
- stylesProp.fileAttachmentContainer?.paddingLeft ||
- fileAttachmentContainer.paddingLeft ||
- styles.fileAttachmentContainer.paddingLeft,
- )
- : undefined,
- },
- styles.textContainer,
- textContainer,
- stylesProp.textContainer,
- ],
- }}
- />
- {messageType === FileTypes.Audio || messageType === FileTypes.VoiceRecording ? (
-
- {lastAttachment.duration
- ? dayjs.duration(lastAttachment.duration, 'second').format('mm:ss')
- : ''}
-
- ) : null}
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {!messageFromContext?.quoted_message ? (
+
+
+
+ ) : null}
);
};
const areEqual = (prevProps: ReplyPropsWithContext, nextProps: ReplyPropsWithContext) => {
- const { quotedMessage: prevQuotedMessage } = prevProps;
- const { quotedMessage: nextQuotedMessage } = nextProps;
+ const {
+ isMyMessage: prevIsMyMessage,
+ mode: prevMode,
+ quotedMessage: prevQuotedMessage,
+ } = prevProps;
+ const {
+ isMyMessage: nextIsMyMessage,
+ mode: nextMode,
+ quotedMessage: nextQuotedMessage,
+ } = nextProps;
- const quotedMessageEqual =
- !!prevQuotedMessage &&
- !!nextQuotedMessage &&
- checkQuotedMessageEquality(prevQuotedMessage, nextQuotedMessage);
+ const isMyMessageEqual = prevIsMyMessage === nextIsMyMessage;
- const quotedMessageAttachmentsEqual =
- prevQuotedMessage?.attachments?.length === nextQuotedMessage?.attachments?.length;
+ if (!isMyMessageEqual) {
+ return false;
+ }
- if (!quotedMessageAttachmentsEqual) {
+ const modeEqual = prevMode === nextMode;
+ if (!modeEqual) {
return false;
}
- if (!quotedMessageEqual) {
+ const messageEqual =
+ prevQuotedMessage &&
+ nextQuotedMessage &&
+ checkQuotedMessageEquality(prevQuotedMessage, nextQuotedMessage);
+ if (!messageEqual) {
return false;
}
return true;
};
-const MemoizedReply = React.memo(ReplyWithContext, areEqual) as typeof ReplyWithContext;
+export const MemoizedReply = React.memo(ReplyWithContext, areEqual) as typeof ReplyWithContext;
export type ReplyProps = Partial;
-/**
- * UI Component for reply
- */
export const Reply = (props: ReplyProps) => {
- const { message } = useMessageContext();
-
- const { FileAttachmentIcon = FileIconDefault, MessageAvatar = MessageAvatarDefault } =
- useMessagesContext();
+ const { message: messageFromContext } = useMessageContext();
+ const { client } = useChatContext();
const messageComposer = useMessageComposer();
- const { quotedMessage } = useStateStore(messageComposer.state, messageComposerStateStoreSelector);
+ const { quotedMessage: quotedMessageFromComposer } = useStateStore(
+ messageComposer.state,
+ messageComposerStateStoreSelector,
+ );
+
+ const onDismiss = useCallback(() => {
+ messageComposer.setQuotedMessage(null);
+ }, [messageComposer]);
+
+ const quotedMessage = messageFromContext
+ ? (messageFromContext.quoted_message as MessagesContextValue['quotedMessage'])
+ : quotedMessageFromComposer;
+
+ const isMyMessage = client.user?.id === quotedMessage?.user?.id;
- const { t } = useTranslationContext();
+ const mode = messageComposer.editedMessage ? 'edit' : 'reply';
return (
);
};
-Reply.displayName = 'Reply{reply}';
+const styles = StyleSheet.create({
+ attachmentContainer: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+ container: {
+ borderRadius: 12,
+ flexDirection: 'row',
+ padding: 8,
+ },
+ contentWrapper: {
+ backgroundColor: 'white',
+ borderColor: '#E2E6EA',
+ borderRadius: 8,
+ borderWidth: 1,
+ height: 40,
+ overflow: 'hidden',
+ width: 40,
+ },
+ dismissWrapper: {
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ iconStyle: {},
+ imageAttachment: {},
+ leftContainer: {
+ borderLeftColor: '#B8BEC4',
+ borderLeftWidth: 2,
+ flex: 1,
+ justifyContent: 'center',
+ paddingHorizontal: 8,
+ paddingVertical: 2,
+ },
+ playIconContainer: {
+ alignItems: 'center',
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
+ borderRadius: 10,
+ height: 20,
+ justifyContent: 'center',
+ width: 20,
+ },
+ rightContainer: {},
+ subtitle: {
+ color: '#384047',
+ flexShrink: 1,
+ fontSize: 12,
+ includeFontPadding: false,
+ lineHeight: 16,
+ },
+ subtitleContainer: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 4,
+ paddingTop: 4,
+ },
+ title: {
+ color: '#384047',
+ fontSize: 12,
+ fontWeight: 'bold',
+ includeFontPadding: false,
+ lineHeight: 16,
+ },
+ wrapper: {
+ padding: 4,
+ },
+});
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 b9c34b762b..36267f673e 100644
--- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
+++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap
@@ -21,7 +21,6 @@ exports[`Thread should match thread snapshot 1`] = `
style={
[
{
- "alignItems": "center",
"flex": 1,
"width": "100%",
},
@@ -39,7 +38,7 @@ exports[`Thread should match thread snapshot 1`] = `
contentContainerStyle={
[
{
- "paddingBottom": 4,
+ "paddingTop": 0,
},
undefined,
{},
@@ -1866,24 +1865,32 @@ exports[`Thread should match thread snapshot 1`] = `
-
-
-
-
-
-
-
-
+ }
+ strokeLinecap={1}
+ strokeLinejoin={1}
+ strokeWidth={1.5}
+ />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
-
-
+ accessible={true}
+ collapsable={false}
+ focusable={true}
+ onBlur={[Function]}
+ onClick={[Function]}
+ onFocus={[Function]}
+ onResponderGrant={[Function]}
+ onResponderMove={[Function]}
+ onResponderRelease={[Function]}
+ onResponderTerminate={[Function]}
+ onResponderTerminationRequest={[Function]}
+ onStartShouldSetResponder={[Function]}
+ style={
+ [
+ {
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ {
+ "borderRadius": 16,
+ "height": 32,
+ "width": 32,
+ },
+ {
+ "backgroundColor": "#FFFFFF",
+ "borderColor": "#E2E6EA",
+ "borderWidth": 0,
+ },
+ undefined,
+ ]
+ }
+ >
+
+
+
+
+
+
+
+
+
@@ -2438,6 +2406,9 @@ exports[`Thread should match thread snapshot 1`] = `
| React.ReactNode;
+ iconColor?: string;
+ onPress?: () => void;
+ size?: 'sm' | 'md' | 'lg';
+ status?: 'disabled' | 'pressed' | 'selected' | 'enabled';
+ type?: 'primary' | 'secondary' | 'destructive';
+ category?: 'ghost' | 'filled' | 'outline';
+};
+
+const sizes = {
+ lg: { borderRadius: 24, height: 48, width: 48 },
+ md: { borderRadius: 20, height: 40, width: 40 },
+ sm: {
+ borderRadius: 16,
+ height: 32,
+ width: 32,
+ },
+};
+
+const getBackgroundColor = ({
+ type,
+ status,
+}: {
+ type: IconButtonProps['type'];
+ status: IconButtonProps['status'];
+}) => {
+ if (type === 'primary') {
+ if (status === 'disabled') {
+ return '#E2E6EA';
+ } else {
+ return '#005FFF';
+ }
+ } else if (type === 'secondary') {
+ return '#FFFFFF';
+ }
+ return {
+ destructive: '#D92F26',
+ primary: '#005FFF',
+ secondary: '#FFFFFF',
+ }[type ?? 'primary'];
+};
+
+export const IconButton = (props: IconButtonProps) => {
+ const {
+ category = 'filled',
+ status = 'enabled',
+ Icon,
+ iconColor,
+ onPress,
+ size = 'md',
+ style,
+ type = 'primary',
+ ...rest
+ } = props;
+ const {
+ theme: {
+ colors: { selected: selectedColor },
+ },
+ } = useTheme();
+ return (
+ [
+ styles.container,
+ sizes[size],
+ {
+ backgroundColor:
+ status === 'selected'
+ ? selectedColor
+ : pressed
+ ? '#F5F6F7'
+ : getBackgroundColor({ status, type }),
+ borderColor: '#E2E6EA',
+ borderWidth: category === 'outline' || category === 'filled' ? 1 : 0,
+ },
+ style as StyleProp,
+ ]}
+ {...rest}
+ >
+ {typeof Icon === 'function' ? (
+
+ ) : (
+ {Icon}
+ )}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx
index 05a556a80a..e076b7ea7b 100644
--- a/package/src/contexts/messageInputContext/MessageInputContext.tsx
+++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx
@@ -34,9 +34,7 @@ import {
} from '../../components';
import { dismissKeyboard } from '../../components/KeyboardCompatibleView/KeyboardControllerAvoidingView';
import { parseLinksFromText } from '../../components/Message/MessageSimple/utils/parseLinks';
-import type { AttachButtonProps } from '../../components/MessageInput/AttachButton';
-import { AttachmentUploadPreviewListProps } from '../../components/MessageInput/AttachmentUploadPreviewList';
-import type { CommandsButtonProps } from '../../components/MessageInput/CommandsButton';
+import { AttachmentUploadPreviewListProps } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadPreviewList';
import type { AttachmentUploadProgressIndicatorProps } from '../../components/MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
import { AudioAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview';
import { FileAttachmentUploadPreviewProps } from '../../components/MessageInput/components/AttachmentPreview/FileAttachmentUploadPreview';
@@ -48,13 +46,12 @@ import type { AudioRecordingLockIndicatorProps } from '../../components/MessageI
import type { AudioRecordingPreviewProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingPreview';
import type { AudioRecordingWaveformProps } from '../../components/MessageInput/components/AudioRecorder/AudioRecordingWaveform';
import type { CommandInputProps } from '../../components/MessageInput/components/CommandInput';
-import type { InputEditingStateHeaderProps } from '../../components/MessageInput/components/InputEditingStateHeader';
-import type { CooldownTimerProps } from '../../components/MessageInput/CooldownTimer';
+import type { AttachButtonProps } from '../../components/MessageInput/components/InputButtons/AttachButton';
+import type { InputButtonsProps } from '../../components/MessageInput/components/InputButtons/index';
+import type { CooldownTimerProps } from '../../components/MessageInput/components/OutputButtons/CooldownTimer';
+import type { SendButtonProps } from '../../components/MessageInput/components/OutputButtons/SendButton';
import { useCooldown } from '../../components/MessageInput/hooks/useCooldown';
-import type { InputButtonsProps } from '../../components/MessageInput/InputButtons';
import type { MessageInputProps } from '../../components/MessageInput/MessageInput';
-import type { MoreOptionsButtonProps } from '../../components/MessageInput/MoreOptionsButton';
-import type { SendButtonProps } from '../../components/MessageInput/SendButton';
import { useStableCallback } from '../../hooks/useStableCallback';
import {
createAttachmentsCompositionMiddleware,
@@ -249,13 +246,7 @@ export type InputMessageInputContextValue = {
ImageAttachmentUploadPreview: React.ComponentType;
FileAttachmentUploadPreview: React.ComponentType;
VideoAttachmentUploadPreview: React.ComponentType;
- /**
- * Custom UI component for commands button.
- *
- * Defaults to and accepts same props as:
- * [CommandsButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/commands-button/)
- */
- CommandsButton: React.ComponentType;
+
/**
* Custom UI component to display the remaining cooldown a user will have to wait before
* being allowed to send another message. This component is displayed in place of the
@@ -280,17 +271,7 @@ export type InputMessageInputContextValue = {
/** When false, ImageSelectorIcon will be hidden */
hasImagePicker: boolean;
- InputEditingStateHeader: React.ComponentType;
CommandInput: React.ComponentType;
- InputReplyStateHeader: React.ComponentType;
- /**
- * Custom UI component for more options button.
- *
- * Defaults to and accepts same props as:
- * [MoreOptionsButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/more-options-button/)
- */
- MoreOptionsButton: React.ComponentType;
-
/**
* Custom UI component for send button.
*
@@ -358,6 +339,13 @@ export type InputMessageInputContextValue = {
*/
handleAttachButtonPress?: () => void;
+ /**
+ * Whether the message input is floating or not.
+ * @type boolean
+ * @default false
+ */
+ messageInputFloating: boolean;
+
/**
* Custom UI component for AutoCompleteInput.
* Has access to all of [MessageInputContext](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/contexts/messageInputContext/MessageInputContext.tsx)
diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts
index 7ad8d8f712..9d966bac51 100644
--- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts
+++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts
@@ -31,7 +31,6 @@ export const useCreateMessageInputContext = ({
closeAttachmentPicker,
closePollCreationDialog,
CommandInput,
- CommandsButton,
compressImageQuality,
cooldownEndsAt,
CooldownTimer,
@@ -50,9 +49,7 @@ export const useCreateMessageInputContext = ({
Input,
inputBoxRef,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
openAttachmentPicker,
openPollCreationDialog,
pickAndUploadImageFromNativePicker,
@@ -105,7 +102,6 @@ export const useCreateMessageInputContext = ({
closeAttachmentPicker,
closePollCreationDialog,
CommandInput,
- CommandsButton,
compressImageQuality,
cooldownEndsAt,
CooldownTimer,
@@ -124,9 +120,7 @@ export const useCreateMessageInputContext = ({
Input,
inputBoxRef,
InputButtons,
- InputEditingStateHeader,
- InputReplyStateHeader,
- MoreOptionsButton,
+ messageInputFloating,
openAttachmentPicker,
openPollCreationDialog,
pickAndUploadImageFromNativePicker,
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index f1b26969d9..6795d52f00 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -9,7 +9,7 @@ export const BASE_AVATAR_SIZE = 32;
export const Colors = {
accent_blue: '#005FFF',
accent_dark_blue: '#005DFF',
- accent_error: '#FF3842',
+ accent_error: '#D92F26',
accent_green: '#20E070',
accent_info: '#1FE06F',
accent_red: '#FF3742',
@@ -31,6 +31,7 @@ export const Colors = {
light_gray: '#E9EAED',
modal_shadow: '#00000099', // 99 = 60% opacity; x=0, y= 1, radius=4
overlay: '#000000CC', // CC = 80% opacity
+ selected: 'hsla(0, 0%, 0%, 0.15)',
shadow_icon: '#00000040', // 40 = 25% opacity; x=0, y=0, radius=4
static_black: '#000000',
static_white: '#ffffff',
@@ -272,17 +273,16 @@ export type Theme = {
attachButton: ViewStyle;
attachButtonContainer: ViewStyle;
attachmentSelectionBar: ViewStyle;
- attachmentSeparator: ViewStyle;
attachmentUnsupportedIndicator: {
container: ViewStyle;
warningIcon: IconProps;
text: TextStyle;
};
attachmentUploadPreviewList: {
- filesFlatList: ViewStyle;
- imagesFlatList: ViewStyle;
- wrapper: ViewStyle;
+ flatList: ViewStyle;
+ itemSeparator: ViewStyle;
};
+ audioRecordingButtonContainer: ViewStyle;
audioRecorder: {
arrowLeftIcon: IconProps;
checkContainer: ViewStyle;
@@ -295,10 +295,6 @@ export type Theme = {
sendCheckIcon: IconProps;
slideToCancelContainer: ViewStyle;
};
- audioRecordingButton: {
- container: ViewStyle;
- micIcon: IconProps;
- };
audioRecordingInProgress: {
container: ViewStyle;
durationText: TextStyle;
@@ -320,17 +316,15 @@ export type Theme = {
container: ViewStyle;
waveform: ViewStyle;
};
- autoCompleteInputContainer: ViewStyle;
commandInput: {
closeButton: ViewStyle;
container: ViewStyle;
text: TextStyle;
};
- commandsButton: ViewStyle;
- composerContainer: ViewStyle;
container: ViewStyle;
+ contentContainer: ViewStyle;
+ cooldownButtonContainer: ViewStyle;
cooldownTimer: {
- container: ViewStyle;
text: TextStyle;
};
dismissAttachmentUpload: {
@@ -338,13 +332,8 @@ export type Theme = {
dismissIcon: IconProps;
dismissIconColor: ColorValue;
};
- editingBoxContainer: ViewStyle;
- editingBoxHeader: ViewStyle;
- editingBoxHeaderTitle: TextStyle;
- editingStateHeader: {
- editingBoxHeader: ViewStyle;
- editingBoxHeaderTitle: TextStyle;
- };
+ editButton: ViewStyle;
+ editButtonContainer: ViewStyle;
fileAttachmentUploadPreview: {
fileContainer: ViewStyle;
filenameText: TextStyle;
@@ -356,34 +345,32 @@ export type Theme = {
fileUploadPreview: {
flatList: ViewStyle;
};
+ floatingWrapper: ViewStyle;
focusedInputBoxContainer: ViewStyle;
imageAttachmentUploadPreview: {
- itemContainer: ViewStyle;
+ container: ViewStyle;
upload: ImageStyle;
+ wrapper: ViewStyle;
};
- imageUploadPreview: {
- flatList: ViewStyle;
- };
+ inputContainer: ViewStyle;
inputBox: TextStyle;
inputBoxContainer: ViewStyle;
+ inputBoxWrapper: ViewStyle;
+ inputButtonsContainer: ViewStyle;
+ inputFloatingContainer: ViewStyle;
micButtonContainer: ViewStyle;
- moreOptionsButton: ViewStyle;
nativeAttachmentPicker: {
buttonContainer: ViewStyle;
buttonDimmerStyle: ViewStyle;
container: ViewStyle;
};
- optionsContainer: ViewStyle;
- replyContainer: ViewStyle;
- searchIcon: IconProps;
+ outputButtonsContainer: ViewStyle;
sendButton: ViewStyle;
sendButtonContainer: ViewStyle;
sendMessageDisallowedIndicator: {
container: ViewStyle;
text: TextStyle;
};
- sendRightIcon: IconProps;
- sendUpIcon: IconProps;
showThreadMessageInChannelButton: {
check: IconProps;
checkBoxActive: ViewStyle;
@@ -433,6 +420,7 @@ export type Theme = {
itemContainer: ViewStyle;
upload: ImageStyle;
};
+ wrapper: ViewStyle;
};
messageList: {
container: ViewStyle;
@@ -452,15 +440,15 @@ export type Theme = {
text: TextStyle;
textContainer: ViewStyle;
};
+ scrollToBottomButtonContainer: ViewStyle;
scrollToBottomButton: {
container: ViewStyle;
- touchable: ViewStyle;
unreadCountNotificationContainer: ViewStyle;
unreadCountNotificationText: TextStyle;
- wrapper: ViewStyle;
- chevronColor?: ColorValue;
};
+ stickyHeaderContainer: ViewStyle;
typingIndicatorContainer: ViewStyle;
+ unreadMessagesNotificationContainer: ViewStyle;
unreadMessagesNotification: {
closeButtonContainer: ViewStyle;
closeIcon: IconProps;
@@ -842,17 +830,21 @@ export type Theme = {
thumb: ViewStyle;
};
reply: {
+ audioIcon: IconProps;
container: ViewStyle;
- fileAttachmentContainer: ViewStyle;
- imageAttachment: ImageStyle;
- markdownStyles: MarkdownStyle;
- messageContainer: ViewStyle;
- secondaryText: ViewStyle;
- textContainer: ViewStyle;
- videoThumbnail: {
- container: ViewStyle;
- image: ImageStyle;
- };
+ dismissWrapper: ViewStyle;
+ fileIcon: IconProps;
+ leftContainer: ViewStyle;
+ locationIcon: IconProps;
+ linkIcon: IconProps;
+ photoIcon: IconProps;
+ pollIcon: IconProps;
+ rightContainer: ViewStyle;
+ title: TextStyle;
+ subtitle: TextStyle;
+ subtitleContainer: ViewStyle;
+ videoIcon: IconProps;
+ wrapper: ViewStyle;
};
screenPadding: number;
spinner: ViewStyle;
@@ -1096,16 +1088,14 @@ export const defaultTheme: Theme = {
attachButton: {},
attachButtonContainer: {},
attachmentSelectionBar: {},
- attachmentSeparator: {},
attachmentUnsupportedIndicator: {
container: {},
text: {},
warningIcon: {},
},
attachmentUploadPreviewList: {
- filesFlatList: {},
- imagesFlatList: {},
- wrapper: {},
+ flatList: {},
+ itemSeparator: {},
},
audioRecorder: {
arrowLeftIcon: {},
@@ -1119,7 +1109,7 @@ export const defaultTheme: Theme = {
sendCheckIcon: {},
slideToCancelContainer: {},
},
- audioRecordingButton: { container: {}, micIcon: {} },
+ audioRecordingButtonContainer: {},
audioRecordingInProgress: { container: {}, durationText: {} },
audioRecordingLockIndicator: { arrowUpIcon: {}, container: {}, lockIcon: {} },
audioRecordingPreview: {
@@ -1131,17 +1121,15 @@ export const defaultTheme: Theme = {
progressBar: {},
},
audioRecordingWaveform: { container: {}, waveform: {} },
- autoCompleteInputContainer: {},
commandInput: {
closeButton: {},
container: {},
text: {},
},
- commandsButton: {},
- composerContainer: {},
container: {},
+ contentContainer: {},
+ cooldownButtonContainer: {},
cooldownTimer: {
- container: {},
text: {},
},
dismissAttachmentUpload: {
@@ -1149,13 +1137,8 @@ export const defaultTheme: Theme = {
dismissIcon: {},
dismissIconColor: '',
},
- editingBoxContainer: {},
- editingBoxHeader: {},
- editingBoxHeaderTitle: {},
- editingStateHeader: {
- editingBoxHeader: {},
- editingBoxHeaderTitle: {},
- },
+ editButton: {},
+ editButtonContainer: {},
fileAttachmentUploadPreview: {
fileContainer: {},
filenameText: {},
@@ -1167,34 +1150,32 @@ export const defaultTheme: Theme = {
fileUploadPreview: {
flatList: {},
},
+ floatingWrapper: {},
focusedInputBoxContainer: {},
imageAttachmentUploadPreview: {
- itemContainer: {},
+ container: {},
upload: {},
- },
- imageUploadPreview: {
- flatList: {},
+ wrapper: {},
},
inputBox: {},
inputBoxContainer: {},
+ inputBoxWrapper: {},
+ inputButtonsContainer: {},
+ inputContainer: {},
+ inputFloatingContainer: {},
micButtonContainer: {},
- moreOptionsButton: {},
nativeAttachmentPicker: {
buttonContainer: {},
buttonDimmerStyle: {},
container: {},
},
- optionsContainer: {},
- replyContainer: {},
- searchIcon: {},
+ outputButtonsContainer: {},
sendButton: {},
sendButtonContainer: {},
sendMessageDisallowedIndicator: {
container: {},
text: {},
},
- sendRightIcon: {},
- sendUpIcon: {},
showThreadMessageInChannelButton: {
check: {},
checkBoxActive: {},
@@ -1244,6 +1225,7 @@ export const defaultTheme: Theme = {
recorderIconContainer: {},
upload: {},
},
+ wrapper: {},
},
messageList: {
container: {},
@@ -1265,11 +1247,11 @@ export const defaultTheme: Theme = {
},
scrollToBottomButton: {
container: {},
- touchable: {},
unreadCountNotificationContainer: {},
unreadCountNotificationText: {},
- wrapper: {},
},
+ scrollToBottomButtonContainer: {},
+ stickyHeaderContainer: {},
typingIndicatorContainer: {},
unreadMessagesNotification: {
closeButtonContainer: {},
@@ -1277,6 +1259,7 @@ export const defaultTheme: Theme = {
container: {},
text: {},
},
+ unreadMessagesNotificationContainer: {},
},
messageMenu: {
actionList: {
@@ -1667,17 +1650,21 @@ export const defaultTheme: Theme = {
thumb: {},
},
reply: {
+ audioIcon: {},
container: {},
- fileAttachmentContainer: {},
- imageAttachment: {},
- markdownStyles: {},
- messageContainer: {},
- secondaryText: {},
- textContainer: {},
- videoThumbnail: {
- container: {},
- image: {},
- },
+ dismissWrapper: {},
+ fileIcon: {},
+ leftContainer: {},
+ linkIcon: {},
+ locationIcon: {},
+ photoIcon: {},
+ pollIcon: {},
+ rightContainer: {},
+ subtitle: {},
+ subtitleContainer: {},
+ title: {},
+ videoIcon: {},
+ wrapper: {},
},
screenPadding: 8,
spinner: {},
diff --git a/package/src/hooks/useKeyboardVisibility.ts b/package/src/hooks/useKeyboardVisibility.ts
new file mode 100644
index 0000000000..e013d41241
--- /dev/null
+++ b/package/src/hooks/useKeyboardVisibility.ts
@@ -0,0 +1,35 @@
+import { useEffect, useState } from 'react';
+import { EventSubscription, Keyboard } from 'react-native';
+
+import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView';
+
+/**
+ * A custom hook that provides a boolean value indicating whether the keyboard is visible.
+ * @returns A boolean value indicating whether the keyboard is visible.
+ */
+export const useKeyboardVisibility = () => {
+ const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
+
+ useEffect(() => {
+ const listeners: EventSubscription[] = [];
+ if (KeyboardControllerPackage?.KeyboardEvents) {
+ listeners.push(
+ KeyboardControllerPackage.KeyboardEvents.addListener('keyboardWillShow', () =>
+ setIsKeyboardVisible(true),
+ ),
+ );
+ listeners.push(
+ KeyboardControllerPackage.KeyboardEvents.addListener('keyboardWillHide', () =>
+ setIsKeyboardVisible(false),
+ ),
+ );
+ } else {
+ listeners.push(Keyboard.addListener('keyboardWillShow', () => setIsKeyboardVisible(true)));
+ listeners.push(Keyboard.addListener('keyboardWillHide', () => setIsKeyboardVisible(false)));
+ }
+
+ return () => listeners.forEach((listener) => listener.remove());
+ }, []);
+
+ return isKeyboardVisible;
+};
diff --git a/package/src/icons/ErrorCircle.tsx b/package/src/icons/ErrorCircle.tsx
new file mode 100644
index 0000000000..358abc8f35
--- /dev/null
+++ b/package/src/icons/ErrorCircle.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import Svg, { Path } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const ErrorCircle = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewClose.tsx b/package/src/icons/NewClose.tsx
new file mode 100644
index 0000000000..d2bea91841
--- /dev/null
+++ b/package/src/icons/NewClose.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import Svg, { Path } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewClose = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewDown.tsx b/package/src/icons/NewDown.tsx
new file mode 100644
index 0000000000..0ff62e16f3
--- /dev/null
+++ b/package/src/icons/NewDown.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewDown = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewFile.tsx b/package/src/icons/NewFile.tsx
new file mode 100644
index 0000000000..615fdfa439
--- /dev/null
+++ b/package/src/icons/NewFile.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewFile = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewLink.tsx b/package/src/icons/NewLink.tsx
new file mode 100644
index 0000000000..6adb196d6e
--- /dev/null
+++ b/package/src/icons/NewLink.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewLink = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewMapPin.tsx b/package/src/icons/NewMapPin.tsx
new file mode 100644
index 0000000000..905d2684ca
--- /dev/null
+++ b/package/src/icons/NewMapPin.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewMapPin = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewMic.tsx b/package/src/icons/NewMic.tsx
new file mode 100644
index 0000000000..21c1044862
--- /dev/null
+++ b/package/src/icons/NewMic.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewMic = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewPhoto.tsx b/package/src/icons/NewPhoto.tsx
new file mode 100644
index 0000000000..6fc8a3b62a
--- /dev/null
+++ b/package/src/icons/NewPhoto.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewPhoto = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewPlayIcon.tsx b/package/src/icons/NewPlayIcon.tsx
new file mode 100644
index 0000000000..9ca5d3eca0
--- /dev/null
+++ b/package/src/icons/NewPlayIcon.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewPlayIcon = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewPlus.tsx b/package/src/icons/NewPlus.tsx
new file mode 100644
index 0000000000..e096b8ff42
--- /dev/null
+++ b/package/src/icons/NewPlus.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import Svg, { Path } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewPlus = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewPoll.tsx b/package/src/icons/NewPoll.tsx
new file mode 100644
index 0000000000..b08396ffc6
--- /dev/null
+++ b/package/src/icons/NewPoll.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewPoll = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewTick.tsx b/package/src/icons/NewTick.tsx
new file mode 100644
index 0000000000..a9b6bdd89d
--- /dev/null
+++ b/package/src/icons/NewTick.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewTick = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/NewVideo.tsx b/package/src/icons/NewVideo.tsx
new file mode 100644
index 0000000000..7129118011
--- /dev/null
+++ b/package/src/icons/NewVideo.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { Path, Svg } from 'react-native-svg';
+
+import { IconProps } from './utils/base';
+
+export const NewVideo = ({ height, width, ...rest }: IconProps) => (
+
+);
diff --git a/package/src/icons/Search.tsx b/package/src/icons/Search.tsx
index 060e210780..9b8723df59 100644
--- a/package/src/icons/Search.tsx
+++ b/package/src/icons/Search.tsx
@@ -1,12 +1,14 @@
import React from 'react';
-import { IconProps, RootPath, RootSvg } from './utils/base';
+import Svg, { Path } from 'react-native-svg';
-export const Search = (props: IconProps) => (
-
- (
+
+
);
diff --git a/package/src/icons/SendRight.tsx b/package/src/icons/SendRight.tsx
index a7be88c2c7..a01676cd4f 100644
--- a/package/src/icons/SendRight.tsx
+++ b/package/src/icons/SendRight.tsx
@@ -1,21 +1,21 @@
import React from 'react';
-import Svg, { Circle, Path } from 'react-native-svg';
+import Svg, { Path } from 'react-native-svg';
import { IconProps } from './utils/base';
-type Props = IconProps & {
- size: number;
-};
-
-export const SendRight = ({ size, ...rest }: Props) => (
-