diff --git a/package/src/components/ChannelPreview/ChannelMessagePreview.tsx b/package/src/components/ChannelPreview/ChannelMessagePreview.tsx
new file mode 100644
index 0000000000..0527ab3112
--- /dev/null
+++ b/package/src/components/ChannelPreview/ChannelMessagePreview.tsx
@@ -0,0 +1,72 @@
+import React, { useMemo } from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+
+import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
+
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useMessagePreviewIcon, useMessagePreviewText } from '../../hooks';
+import { primitives } from '../../theme';
+
+export type ChannelMessagePreviewProps = {
+ message: LocalMessage | MessageResponse | DraftMessage;
+};
+
+export const ChannelMessagePreview = ({ message }: ChannelMessagePreviewProps) => {
+ const isMessageDeleted = message?.type === 'deleted';
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles({ isMessageDeleted });
+ const MessagePreviewIcon = useMessagePreviewIcon({ message });
+ const messagePreviewTitle = useMessagePreviewText({ message });
+
+ return (
+
+ {MessagePreviewIcon ? (
+
+ ) : null}
+
+ {messagePreviewTitle}
+
+
+ );
+};
+
+const useStyles = ({ isMessageDeleted = false }: { isMessageDeleted?: boolean }) => {
+ const {
+ theme: {
+ channelPreview: { messagePreview },
+ semantics,
+ },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ flexShrink: 1,
+ ...messagePreview.container,
+ },
+ subtitle: {
+ color: isMessageDeleted ? semantics.textTertiary : semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightRegular,
+ includeFontPadding: false,
+ lineHeight: primitives.typographyLineHeightNormal,
+ flexShrink: 1,
+ ...messagePreview.subtitle,
+ },
+ });
+ }, [
+ isMessageDeleted,
+ semantics.textTertiary,
+ semantics.textSecondary,
+ messagePreview.container,
+ messagePreview.subtitle,
+ ]);
+};
diff --git a/package/src/components/ChannelPreview/ChannelListMessageDeliveryStatus.tsx b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx
similarity index 70%
rename from package/src/components/ChannelPreview/ChannelListMessageDeliveryStatus.tsx
rename to package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx
index b7ebb4afdb..4048c40920 100644
--- a/package/src/components/ChannelPreview/ChannelListMessageDeliveryStatus.tsx
+++ b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx
@@ -1,28 +1,26 @@
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
-import { LocalMessage } from 'stream-chat';
+import { LocalMessage, MessageResponse } from 'stream-chat';
import { ChannelPreviewProps } from './ChannelPreview';
-import { LastMessageType } from './hooks/useChannelPreviewData';
-
-import { MessageDeliveryStatus, useMessageDeliveryStatus } from './hooks/useMessageDeliveryStatus';
import { useChatContext } from '../../contexts/chatContext/ChatContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
+import { MessageDeliveryStatus, useMessageDeliveryStatus } from '../../hooks';
import { Check, CheckAll, Time } from '../../icons';
import { primitives } from '../../theme';
import { MessageStatusTypes } from '../../utils/utils';
-export type ChannelListMessageDeliveryStatusProps = Pick & {
- lastMessage: LastMessageType;
+export type ChannelMessagePreviewDeliveryStatusProps = Pick & {
+ message: MessageResponse | LocalMessage;
};
-export const ChannelListMessageDeliveryStatus = ({
+export const ChannelMessagePreviewDeliveryStatus = ({
channel,
- lastMessage,
-}: ChannelListMessageDeliveryStatusProps) => {
+ message,
+}: ChannelMessagePreviewDeliveryStatusProps) => {
const { client } = useChatContext();
const { t } = useTranslationContext();
const channelConfigExists = typeof channel?.getConfig === 'function';
@@ -36,9 +34,15 @@ export const ChannelListMessageDeliveryStatus = ({
},
} = useTheme();
+ const membersWithoutSelf = useMemo(() => {
+ return Object.values(channel.state?.members || {}).filter(
+ (member) => member.user?.id !== client.user?.id,
+ );
+ }, [channel.state?.members, client.user?.id]);
+
const isLastMessageByCurrentUser = useMemo(() => {
- return lastMessage?.user?.id === client.user?.id;
- }, [lastMessage, client.user?.id]);
+ return message?.user?.id === client.user?.id;
+ }, [message, client.user?.id]);
const readEvents = useMemo(() => {
if (!channelConfigExists) {
@@ -53,19 +57,23 @@ export const ChannelListMessageDeliveryStatus = ({
const { status } = useMessageDeliveryStatus({
channel,
- lastMessage: lastMessage as LocalMessage,
+ lastMessage: message as LocalMessage,
isReadEventsEnabled: readEvents,
});
- if (!isLastMessageByCurrentUser) {
+ if (!channel.data?.name && membersWithoutSelf.length === 1 && !isLastMessageByCurrentUser) {
return null;
}
+ if (!isLastMessageByCurrentUser) {
+ return {message?.user?.name || message?.user?.id}:;
+ }
+
return (
- {lastMessage.status === MessageStatusTypes.SENDING ? (
+ {message.status === MessageStatusTypes.SENDING ? (
- ) : lastMessage.status === MessageStatusTypes.RECEIVED &&
+ ) : message.status === MessageStatusTypes.RECEIVED &&
status === MessageDeliveryStatus.READ ? (
) : status === MessageDeliveryStatus.DELIVERED ? (
@@ -103,6 +111,12 @@ const useStyles = () => {
lineHeight: primitives.typographyLineHeightNormal,
...text,
},
+ username: {
+ color: semantics.textTertiary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ },
});
}, [semantics, text, container]);
};
diff --git a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx
index ab2b663db0..1277d46900 100644
--- a/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx
+++ b/package/src/components/ChannelPreview/ChannelPreviewMessage.tsx
@@ -1,8 +1,8 @@
-import React, { useCallback, useMemo } from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
-import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
-
+import { ChannelMessagePreview } from './ChannelMessagePreview';
+import { ChannelMessagePreviewDeliveryStatus } from './ChannelMessagePreviewDeliveryStatus';
import { ChannelPreviewProps } from './ChannelPreview';
import { ChannelTypingIndicatorPreview } from './ChannelTypingIndicatorPreview';
@@ -20,8 +20,6 @@ import { useTranslationContext } from '../../contexts/translationContext/Transla
import { NewPoll } from '../../icons/NewPoll';
import { primitives } from '../../theme';
import { MessageStatusTypes } from '../../utils/utils';
-import { MessagePreview } from '../MessagePreview/MessagePreview';
-import { MessagePreviewUserDetails } from '../MessagePreview/MessagePreviewUserDetails';
import { ErrorBadge } from '../ui';
export type ChannelPreviewMessageProps = Pick & {
@@ -53,25 +51,6 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
const isFailedMessage =
lastMessage?.status === MessageStatusTypes.FAILED || lastMessage?.type === 'error';
- const textStyle = useMemo(() => {
- return [styles.subtitle];
- }, [styles.subtitle]);
-
- const iconProps = useMemo(() => {
- return {
- width: 16,
- height: 16,
- stroke: isMessageDeleted ? semantics.textTertiary : semantics.textSecondary,
- };
- }, [isMessageDeleted, semantics.textTertiary, semantics.textSecondary]);
-
- const renderMessagePreview = useCallback(
- (message: LocalMessage | MessageResponse | DraftMessage) => {
- return ;
- },
- [textStyle, iconProps],
- );
-
if (usersTyping.length > 0) {
return ;
}
@@ -80,7 +59,7 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
return (
{t('Draft')}:
- {renderMessagePreview(draftMessage)}
+
);
}
@@ -115,15 +94,15 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
if (channel.data?.name || membersWithoutSelf.length > 1) {
return (
-
- {renderMessagePreview(lastMessage)}
+
+
);
} else {
return (
-
- {renderMessagePreview(lastMessage)}
+
+
);
}
diff --git a/package/src/components/Indicators/EmptyStateIndicator.tsx b/package/src/components/Indicators/EmptyStateIndicator.tsx
index 9e585446a8..e4b9d1845d 100644
--- a/package/src/components/Indicators/EmptyStateIndicator.tsx
+++ b/package/src/components/Indicators/EmptyStateIndicator.tsx
@@ -1,10 +1,10 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
-import { useViewport } from '../../hooks/useViewport';
-import { ChatIcon, MessageBubbleEmpty, MessageIcon } from '../../icons';
+import { MessageBubbleEmpty } from '../../icons';
+import { primitives } from '../../theme';
export type EmptyStateProps = {
listType?: 'channel' | 'message' | 'threads' | 'default';
@@ -13,78 +13,81 @@ export type EmptyStateProps = {
export const EmptyStateIndicator = ({ listType }: EmptyStateProps) => {
const {
theme: {
- colors: { black, grey, grey_gainsboro },
- emptyStateIndicator: {
- channelContainer,
- channelDetails,
- channelTitle,
- messageContainer,
- messageTitle,
- },
+ emptyStateIndicator: { channelContainer, channelTitle, messageContainer, messageTitle },
+ semantics,
},
} = useTheme();
- const { vw } = useViewport();
- const width = vw(33);
const { t } = useTranslationContext();
+ const styles = useStyles();
switch (listType) {
case 'channel':
return (
-
-
- {t("Let's start chatting!")}
-
-
- {t('How about sending your first message to a friend?')}
+
+
+ {t('No conversations yet')}
);
case 'message':
return (
-
-
- {t('No chats here yet…')}
-
+
+ {t('No chats here yet…')}
);
case 'threads':
return (
-
-
- {t('No threads here yet')}...
+
+
+ {t('Reply to a message to start a thread')}
);
default:
- return No items exist;
+ return (
+ No items exist
+ );
}
};
-const styles = StyleSheet.create({
- channelDetails: {
- fontSize: 14,
- textAlign: 'center',
- },
- channelTitle: {
- fontSize: 16,
- paddingBottom: 8,
- paddingTop: 16,
- },
- container: {
- alignItems: 'center',
- flex: 1,
- justifyContent: 'center',
- },
- messageTitle: {
- fontSize: 20,
- fontWeight: 'bold',
- paddingBottom: 8,
- },
-});
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ channelDetails: {
+ fontSize: 14,
+ textAlign: 'center',
+ },
+ channelTitle: {
+ color: semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightNormal,
+ textAlign: 'center',
+ paddingVertical: primitives.spacingSm,
+ },
+ container: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+ messageTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ paddingBottom: 8,
+ },
+ threadText: {
+ color: semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightNormal,
+ textAlign: 'center',
+ paddingVertical: primitives.spacingSm,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/MessagePreview/MessagePreview.tsx b/package/src/components/MessagePreview/MessagePreview.tsx
deleted file mode 100644
index d78befb7da..0000000000
--- a/package/src/components/MessagePreview/MessagePreview.tsx
+++ /dev/null
@@ -1,331 +0,0 @@
-import React, { useMemo } from 'react';
-import { StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native';
-
-import dayjs from 'dayjs';
-import {
- DraftMessage,
- LiveLocationPayload,
- LocalMessage,
- MessageResponse,
- PollState,
-} from 'stream-chat';
-
-import { useGroupedAttachments } from './hook/useGroupedAttachments';
-
-import { useTheme } from '../../contexts';
-
-import { useChatContext } from '../../contexts/chatContext/ChatContext';
-import { useStateStore } from '../../hooks';
-import { CircleBan } from '../../icons/CircleBan';
-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 { NewPoll } from '../../icons/NewPoll';
-import { NewVideo } from '../../icons/NewVideo';
-import { IconProps } from '../../icons/utils/base';
-import { primitives } from '../../theme';
-import { FileTypes } from '../../types/types';
-
-const selector = (nextValue: PollState) => ({
- name: nextValue.name,
-});
-
-const MessagePreviewText = React.memo(
- ({
- message,
- style,
- }: {
- message?: LocalMessage | MessageResponse | DraftMessage | null;
- style?: StyleProp;
- }) => {
- const { client } = useChatContext();
- const poll = client.polls.fromState(message?.poll_id ?? '');
- const { name: pollName } = useStateStore(poll?.state, selector) ?? {};
- const styles = useStyles();
- const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
- message?.attachments,
- );
- const attachmentsLength = message?.attachments?.length;
-
- const subtitle = useMemo(() => {
- const onlyImages = images?.length && images?.length === attachmentsLength;
- const onlyVideos = videos?.length && videos?.length === attachmentsLength;
- const onlyFiles = files?.length && files?.length === attachmentsLength;
- const onlyAudio = audios?.length === attachmentsLength;
- const onlyVoiceRecordings =
- voiceRecordings?.length && voiceRecordings?.length === attachmentsLength;
-
- if (message?.type === 'deleted') {
- return 'Message deleted';
- }
-
- if (pollName) {
- return pollName;
- }
-
- if (message?.shared_location) {
- if (
- // There is a problem with types in Draft Message, and its not able to infer the type of `end_at` correctly, so the `as` is used.
- (message?.shared_location as LiveLocationPayload)?.end_at &&
- new Date((message?.shared_location as LiveLocationPayload)?.end_at) > new Date()
- ) {
- return 'Live Location';
- }
- return 'Location';
- }
-
- if (message?.text) {
- return message?.text;
- }
-
- if (onlyImages) {
- if (images?.length === 1) {
- return 'Photo';
- } else {
- return `${images?.length} Photos`;
- }
- }
-
- if (onlyVideos) {
- if (videos?.length === 1) {
- return 'Video';
- } else {
- return `${videos?.length} Videos`;
- }
- }
-
- if (onlyAudio) {
- if (audios?.length === 1) {
- return 'Audio';
- } else {
- return `${audios?.length} Audios`;
- }
- }
-
- if (onlyVoiceRecordings) {
- if (voiceRecordings?.length === 1) {
- return `Voice message (${dayjs.duration(voiceRecordings?.[0]?.duration ?? 0, 'seconds').format('m:ss')})`;
- } else {
- return `${voiceRecordings?.length} Voice messages`;
- }
- }
-
- if (giphys?.length) {
- return 'Giphy';
- }
-
- if (onlyFiles && files?.length === 1) {
- return files?.[0]?.title;
- }
-
- return `${attachmentsLength} Files`;
- }, [
- attachmentsLength,
- audios?.length,
- files,
- giphys?.length,
- images?.length,
- message?.shared_location,
- message?.text,
- message?.type,
- pollName,
- videos?.length,
- voiceRecordings,
- ]);
-
- if (!subtitle) {
- return null;
- }
-
- return (
-
- {subtitle}
-
- );
- },
-);
-
-const MessagePreviewIcon = React.memo(
- (props: {
- message?: LocalMessage | MessageResponse | DraftMessage | null;
- iconProps?: IconProps;
- }) => {
- const { message, iconProps } = props;
- const {
- theme: { semantics },
- } = useTheme();
- const styles = useStyles();
- const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
- message?.attachments,
- );
- const attachmentsLength = message?.attachments?.length;
- if (!message) {
- return null;
- }
-
- const onlyImages = images?.length && images?.length === attachmentsLength;
- const onlyAudio = audios?.length && audios?.length === attachmentsLength;
- const onlyVideos = videos?.length && videos?.length === attachmentsLength;
- const onlyVoiceRecordings =
- voiceRecordings?.length && voiceRecordings?.length === attachmentsLength;
- const hasLink = message?.attachments?.some(
- (attachment) => attachment.type === FileTypes.Image && attachment.og_scrape_url,
- );
-
- if (message.type === 'deleted') {
- return (
-
- );
- }
-
- if (message.poll_id) {
- return (
-
- );
- }
-
- if (message.shared_location) {
- return (
-
- );
- }
-
- if (hasLink) {
- return (
-
- );
- }
-
- if (onlyAudio || onlyVoiceRecordings) {
- return (
-
- );
- }
-
- if (onlyVideos) {
- return (
-
- );
- }
-
- if (onlyImages) {
- return (
-
- );
- }
-
- if (giphys?.length) {
- return (
-
- );
- }
-
- if (files?.length || images?.length || videos?.length || audios?.length) {
- return (
-
- );
- }
-
- return null;
- },
-);
-
-export type MessagePreviewProps = {
- message: LocalMessage | MessageResponse | DraftMessage;
- iconProps?: IconProps;
- textStyle?: StyleProp;
- containerStyle?: StyleProp;
-};
-
-export const MessagePreview = ({
- message,
- iconProps,
- textStyle,
- containerStyle,
-}: MessagePreviewProps) => {
- const styles = useStyles();
-
- return (
-
-
-
-
- );
-};
-
-const useStyles = () => {
- return useMemo(() => {
- return StyleSheet.create({
- subtitle: {
- fontSize: primitives.typographyFontSizeXs,
- fontWeight: primitives.typographyFontWeightRegular,
- lineHeight: primitives.typographyLineHeightTight,
- includeFontPadding: false,
- },
- container: {
- alignItems: 'center',
- flexDirection: 'row',
- gap: primitives.spacingXxs,
- flexShrink: 1,
- },
- iconStyle: {},
- });
- }, []);
-};
diff --git a/package/src/components/MessagePreview/MessagePreviewUserDetails.tsx b/package/src/components/MessagePreview/MessagePreviewUserDetails.tsx
deleted file mode 100644
index ff953e8c4a..0000000000
--- a/package/src/components/MessagePreview/MessagePreviewUserDetails.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import React, { useMemo } from 'react';
-import { Text, StyleSheet } from 'react-native';
-
-import { Channel, LocalMessage, MessageResponse } from 'stream-chat';
-
-import { useChatContext } from '../../contexts/chatContext/ChatContext';
-import { useTheme } from '../../contexts/themeContext/ThemeContext';
-import { primitives } from '../../theme';
-import { ChannelListMessageDeliveryStatus } from '../ChannelPreview/ChannelListMessageDeliveryStatus';
-
-export type MessagePreviewUserDetailsProps = {
- message: LocalMessage | MessageResponse;
- channel: Channel;
-};
-
-export const MessagePreviewUserDetails = ({ channel, message }: MessagePreviewUserDetailsProps) => {
- const styles = useStyles();
- const { client } = useChatContext();
-
- return message?.user?.id === client.user?.id ? (
-
- ) : (
- {message?.user?.name || message?.user?.id}:
- );
-};
-
-const useStyles = () => {
- const {
- theme: { semantics },
- } = useTheme();
- return useMemo(() => {
- return StyleSheet.create({
- username: {
- color: semantics.textTertiary,
- fontSize: primitives.typographyFontSizeSm,
- fontWeight: primitives.typographyFontWeightSemiBold,
- lineHeight: primitives.typographyLineHeightNormal,
- },
- });
- }, [semantics]);
-};
diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx
index f4b70a4c0f..ea8b5446ef 100644
--- a/package/src/components/Reply/Reply.tsx
+++ b/package/src/components/Reply/Reply.tsx
@@ -8,6 +8,8 @@ import {
MessageComposerState,
} from 'stream-chat';
+import { ReplyMessageView } from './ReplyMessageView';
+
import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
import {
MessageContextValue,
@@ -22,7 +24,6 @@ import { FileTypes } from '../../types/types';
import { checkQuotedMessageEquality } from '../../utils/utils';
import { FileIcon } from '../Attachment/FileIcon';
import { AttachmentRemoveControl } from '../MessageInput/components/AttachmentPreview/AttachmentRemoveControl';
-import { MessagePreview } from '../MessagePreview/MessagePreview';
import { VideoPlayIndicator } from '../ui/VideoPlayIndicator';
const messageComposerStateStoreSelector = (state: MessageComposerState) => ({
@@ -134,13 +135,11 @@ export const ReplyWithContext = (props: ReplyPropsWithContext) => {
-
-
- {title}
-
-
+
+ {title}
+
-
+
@@ -272,7 +271,6 @@ const useStyles = () => {
right: 0,
top: 0,
},
- iconStyle: {},
leftContainer: {
borderLeftWidth: 2,
flex: 1,
@@ -284,25 +282,12 @@ const useStyles = () => {
gap: primitives.spacingXxxs,
},
rightContainer: {},
- subtitle: {
- color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
- flexShrink: 1,
- fontSize: primitives.typographyFontSizeXs,
- fontWeight: primitives.typographyFontWeightRegular,
- includeFontPadding: false,
- lineHeight: primitives.typographyLineHeightTight,
- },
- titleContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- gap: primitives.spacingXxs,
- },
title: {
- color: semantics.textPrimary,
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
fontSize: primitives.typographyFontSizeXs,
fontWeight: primitives.typographyFontWeightSemiBold,
includeFontPadding: false,
- lineHeight: 16,
+ lineHeight: primitives.typographyLineHeightTight,
},
wrapper: {
padding: primitives.spacingXxs,
diff --git a/package/src/components/Reply/ReplyMessageView.tsx b/package/src/components/Reply/ReplyMessageView.tsx
new file mode 100644
index 0000000000..f5bfaeaa71
--- /dev/null
+++ b/package/src/components/Reply/ReplyMessageView.tsx
@@ -0,0 +1,72 @@
+import React, { useMemo } from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+
+import { LocalMessage, MessageResponse } from 'stream-chat';
+
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useMessagePreviewIcon, useMessagePreviewText } from '../../hooks';
+import { primitives } from '../../theme';
+
+export type ReplyMessageViewProps = {
+ message: LocalMessage | MessageResponse;
+ isMyMessage: boolean;
+};
+
+export const ReplyMessageView = ({ message, isMyMessage }: ReplyMessageViewProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles({ isMyMessage });
+ const MessagePreviewIcon = useMessagePreviewIcon({ message });
+ const messagePreviewTitle = useMessagePreviewText({ message });
+
+ return (
+
+ {MessagePreviewIcon ? (
+
+ ) : null}
+
+ {messagePreviewTitle}
+
+
+ );
+};
+
+const useStyles = ({ isMyMessage = false }: { isMyMessage?: boolean }) => {
+ const {
+ theme: {
+ reply: { messagePreview },
+ semantics,
+ },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ flexShrink: 1,
+ ...messagePreview.container,
+ },
+ subtitle: {
+ color: isMyMessage ? semantics.chatTextOutgoing : semantics.chatTextIncoming,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightRegular,
+ includeFontPadding: false,
+ lineHeight: primitives.typographyLineHeightTight,
+ flexShrink: 1,
+ ...messagePreview.subtitle,
+ },
+ });
+ }, [
+ isMyMessage,
+ semantics.chatTextOutgoing,
+ semantics.chatTextIncoming,
+ messagePreview.container,
+ messagePreview.subtitle,
+ ]);
+};
diff --git a/package/src/components/ThreadList/ThreadList.tsx b/package/src/components/ThreadList/ThreadList.tsx
index 9fd3355074..909fa7a797 100644
--- a/package/src/components/ThreadList/ThreadList.tsx
+++ b/package/src/components/ThreadList/ThreadList.tsx
@@ -4,6 +4,7 @@ import { FlatList, View } from 'react-native';
import { Thread, ThreadManagerState } from 'stream-chat';
import { ThreadListItem } from './ThreadListItem';
+import { ThreadListItemSkeleton } from './ThreadListItemSkeleton';
import { ThreadListUnreadBanner as DefaultThreadListBanner } from './ThreadListUnreadBanner';
import { useChatContext } from '../../contexts';
@@ -37,7 +38,13 @@ export type ThreadListProps = Pick<
export const DefaultThreadListEmptyPlaceholder = () => ;
-export const DefaultThreadListLoadingIndicator = () => ;
+export const DefaultThreadListLoadingIndicator = () => (
+
+ {Array.from({ length: 10 }).map((_, index) => (
+
+ ))}
+
+);
export const DefaultThreadListLoadingNextIndicator = () => ;
const renderItem = (props: { item: Thread }) => ;
@@ -56,11 +63,7 @@ const ThreadListComponent = () => {
} = useThreadsContext();
if (isLoading) {
- return (
-
-
-
- );
+ return ;
}
return (
diff --git a/package/src/components/ThreadList/ThreadListItem.tsx b/package/src/components/ThreadList/ThreadListItem.tsx
index f02794c6bd..8ea107c79e 100644
--- a/package/src/components/ThreadList/ThreadListItem.tsx
+++ b/package/src/components/ThreadList/ThreadListItem.tsx
@@ -1,8 +1,12 @@
import React, { useCallback, useEffect, useMemo } from 'react';
-import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
import { LocalMessage, Thread, ThreadState } from 'stream-chat';
+import { ThreadListItemMessagePreview } from './ThreadListItemMessagePreview';
+
+import { ThreadMessagePreviewDeliveryStatus } from './ThreadMessagePreviewDeliveryStatus';
+
import { useChatContext, useTheme, useTranslationContext } from '../../contexts';
import {
ThreadListItemProvider,
@@ -10,10 +14,10 @@ import {
} from '../../contexts/threadsContext/ThreadListItemContext';
import { useThreadsContext } from '../../contexts/threadsContext/ThreadsContext';
import { useStateStore } from '../../hooks';
-import { MessageBubble } from '../../icons';
+import { primitives } from '../../theme';
import { getDateString } from '../../utils/i18n/getDateString';
import { useChannelPreviewDisplayName } from '../ChannelPreview/hooks/useChannelPreviewDisplayName';
-import { MessagePreview } from '../MessagePreview/MessagePreview';
+import { BadgeNotification, UserAvatarStack } from '../ui';
import { UserAvatar } from '../ui/Avatar/UserAvatar';
export type ThreadListItemProps = {
@@ -21,47 +25,6 @@ export type ThreadListItemProps = {
timestampTranslationKey?: string;
};
-const styles = StyleSheet.create({
- boldText: { fontSize: 14, fontWeight: '500' },
- contentRow: {
- flexDirection: 'row',
- marginTop: 6,
- },
- contentTextWrapper: {
- flex: 1,
- marginLeft: 8,
- },
- dateText: { alignSelf: 'flex-end' },
- headerRow: {
- flexDirection: 'row',
- },
- infoRow: {
- alignItems: 'center',
- flexDirection: 'row',
- },
- parentMessagePreviewContainer: {
- flex: 1,
- marginVertical: 2,
- },
- previewMessageContainer: {
- flex: 1,
- marginTop: 4,
- },
- touchableWrapper: {
- flex: 1,
- paddingHorizontal: 8,
- paddingVertical: 14,
- },
- unreadBubbleWrapper: {
- alignItems: 'center',
- alignSelf: 'flex-end',
- borderRadius: 50,
- height: 22,
- justifyContent: 'center',
- width: 22,
- },
-});
-
export const attachmentTypeIconMap = {
audio: '🔈',
file: '📄',
@@ -83,11 +46,10 @@ export const ThreadListItemComponent = () => {
const displayName = useChannelPreviewDisplayName(channel);
const { onThreadSelect } = useThreadsContext();
const {
- theme: {
- colors: { accent_red, text_low_emphasis, white },
- threadListItem,
- },
+ theme: { semantics },
} = useTheme();
+ const styles = useStyles();
+ const { t } = useTranslationContext();
useEffect(() => {
const unsubscribe = thread.messageComposer.registerDraftEventSubscriptions();
@@ -95,71 +57,64 @@ export const ThreadListItemComponent = () => {
}, [thread.messageComposer]);
return (
- {
- if (onThreadSelect) {
- onThreadSelect(
- { thread: parentMessage as LocalMessage, threadInstance: thread },
- channel,
- );
- }
- }}
- style={[styles.touchableWrapper, threadListItem.touchableWrapper]}
- testID='thread-list-item'
- >
-
-
-
- {displayName || 'N/A'}
-
-
-
-
-
-
- {ownUnreadMessageCount > 0 && !deletedAtDateString ? (
-
-
- {ownUnreadMessageCount}
-
-
- ) : null}
-
-
+
+ {
+ if (onThreadSelect) {
+ onThreadSelect(
+ { thread: parentMessage as LocalMessage, threadInstance: thread },
+ channel,
+ );
+ }
+ }}
+ style={({ pressed }) => [
+ styles.container,
+ { backgroundColor: pressed ? semantics.backgroundCorePressed : 'transparent' },
+ ]}
+ testID='thread-list-item'
+ >
{lastReply?.user ? (
-
+
) : null}
-
-
- {lastReply?.user?.name}
+
+
+ {displayName || 'N/A'}
-
-
-
+ {lastReply ? (
+
+
+
-
-
- {deletedAtDateString ?? dateString}
+ ) : null}
+
+
+
+ {parentMessage?.reply_count === 1
+ ? t('1 Reply')
+ : t('{{ replyCount }} Replies', {
+ replyCount: parentMessage?.reply_count,
+ })}
+ {deletedAtDateString ?? dateString}
-
-
+
+ {ownUnreadMessageCount > 0 && !deletedAtDateString ? (
+
+
+
+ ) : null}
+
+
);
};
@@ -227,3 +182,69 @@ export const ThreadListItem = (props: ThreadListItemProps) => {
);
};
+
+const useStyles = () => {
+ const {
+ theme: { threadListItem, semantics },
+ } = useTheme();
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ wrapper: {
+ flex: 1,
+ padding: primitives.spacingXxs,
+ borderBottomWidth: 1,
+ borderBottomColor: semantics.borderCoreSubtle,
+ ...threadListItem.wrapper,
+ },
+ container: {
+ flexDirection: 'row',
+ gap: primitives.spacingSm,
+ padding: primitives.spacingSm,
+ borderRadius: primitives.radiusLg,
+ ...threadListItem.container,
+ },
+ channelName: {
+ color: semantics.textTertiary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...threadListItem.channelName,
+ },
+ content: {
+ flex: 1,
+ gap: primitives.spacingXs,
+ ...threadListItem.content,
+ },
+ previewMessageContainer: {
+ flexDirection: 'row',
+ gap: primitives.spacingXxs,
+ ...threadListItem.previewMessageContainer,
+ },
+ lowerRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXs,
+ ...threadListItem.lowerRow,
+ },
+ messageRepliesText: {
+ color: semantics.textLink,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...threadListItem.messageRepliesText,
+ },
+ dateText: {
+ color: semantics.textTertiary,
+ fontSize: primitives.typographyFontSizeSm,
+ fontWeight: primitives.typographyFontWeightRegular,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...threadListItem.dateText,
+ },
+ unreadBubbleWrapper: {
+ ...threadListItem.unreadBubbleWrapper,
+ },
+ }),
+ [semantics, threadListItem],
+ );
+};
diff --git a/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx b/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx
new file mode 100644
index 0000000000..5a07d28643
--- /dev/null
+++ b/package/src/components/ThreadList/ThreadListItemMessagePreview.tsx
@@ -0,0 +1,61 @@
+import React, { useMemo } from 'react';
+import { View, Text, StyleSheet } from 'react-native';
+
+import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
+
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useMessagePreviewIcon, useMessagePreviewText } from '../../hooks';
+import { primitives } from '../../theme';
+
+export type ThreadListItemMessagePreviewProps = {
+ message: LocalMessage | MessageResponse | DraftMessage;
+};
+
+export const ThreadListItemMessagePreview = ({ message }: ThreadListItemMessagePreviewProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+ const MessagePreviewIcon = useMessagePreviewIcon({ message });
+ const messagePreviewTitle = useMessagePreviewText({ message });
+
+ return (
+
+ {MessagePreviewIcon ? (
+
+ ) : null}
+
+ {messagePreviewTitle}
+
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: {
+ threadListItem: { messagePreview },
+ semantics,
+ },
+ } = useTheme();
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ flexShrink: 1,
+ ...messagePreview.container,
+ },
+ subtitle: {
+ color: semantics.textPrimary,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightRegular,
+ includeFontPadding: false,
+ lineHeight: primitives.typographyLineHeightNormal,
+ flexShrink: 1,
+ ...messagePreview.subtitle,
+ },
+ });
+ }, [messagePreview.container, messagePreview.subtitle, semantics.textPrimary]);
+};
diff --git a/package/src/components/ThreadList/ThreadListItemSkeleton.tsx b/package/src/components/ThreadList/ThreadListItemSkeleton.tsx
new file mode 100644
index 0000000000..e3454c02dc
--- /dev/null
+++ b/package/src/components/ThreadList/ThreadListItemSkeleton.tsx
@@ -0,0 +1,281 @@
+import React, { useEffect, useMemo } from 'react';
+import { StyleSheet, useWindowDimensions, View } from 'react-native';
+import Animated, {
+ Easing,
+ useAnimatedStyle,
+ useSharedValue,
+ withRepeat,
+ withTiming,
+} from 'react-native-reanimated';
+import Svg, { Path, Rect, Defs, LinearGradient, Stop, ClipPath, G, Mask } from 'react-native-svg';
+
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+
+export const ThreadListItemSkeleton = () => {
+ const width = useWindowDimensions().width;
+ const startOffset = useSharedValue(-width);
+ const styles = useStyles();
+
+ const {
+ theme: {
+ channelListSkeleton: { animationTime = 1500, container, height = 112 },
+ semantics,
+ },
+ } = useTheme();
+
+ useEffect(() => {
+ startOffset.value = withRepeat(
+ withTiming(width, {
+ duration: animationTime,
+ easing: Easing.linear,
+ }),
+ -1,
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const animatedStyle = useAnimatedStyle(
+ () => ({
+ transform: [{ translateX: startOffset.value }],
+ }),
+ [],
+ );
+
+ return (
+
+
+
+
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ borderBottomWidth: 1,
+ flexDirection: 'row',
+ borderBottomColor: semantics.borderCoreDefault,
+ },
+ });
+ }, [semantics]);
+};
diff --git a/package/src/components/ThreadList/ThreadListUnreadBanner.tsx b/package/src/components/ThreadList/ThreadListUnreadBanner.tsx
index 6e414d1071..e128f27a55 100644
--- a/package/src/components/ThreadList/ThreadListUnreadBanner.tsx
+++ b/package/src/components/ThreadList/ThreadListUnreadBanner.tsx
@@ -1,53 +1,103 @@
-import React from 'react';
-import { StyleSheet, Text, TouchableOpacity } from 'react-native';
+import React, { useMemo, useState } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
import { ThreadManagerState } from 'stream-chat';
-import { useChatContext, useTheme } from '../../contexts';
+import { useChatContext, useTheme, useTranslationContext } from '../../contexts';
import { useStateStore } from '../../hooks';
-import { Reload } from '../../icons';
-
-const styles = StyleSheet.create({
- text: { alignSelf: 'flex-start', flex: 1, fontSize: 16 },
- touchableWrapper: {
- borderRadius: 16,
- flexDirection: 'row',
- marginHorizontal: 8,
- marginVertical: 6,
- paddingHorizontal: 16,
- paddingVertical: 14,
- },
-});
+import { Loading, Reload } from '../../icons';
+import { NewExclamationCircle } from '../../icons/NewExclamationCircle';
+import { primitives } from '../../theme';
const selector = (nextValue: ThreadManagerState) =>
({ unseenThreadIds: nextValue.unseenThreadIds }) as const;
export const ThreadListUnreadBanner = () => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
const { client } = useChatContext();
+ const { t } = useTranslationContext();
const {
- theme: {
- colors: { text_high_emphasis, white },
- threadListUnreadBanner,
- },
+ theme: { semantics },
} = useTheme();
+ const styles = useStyles();
const { unseenThreadIds } = useStateStore(client.threads.state, selector);
if (!unseenThreadIds.length) {
return null;
}
+ const handlePress = async () => {
+ try {
+ setLoading(true);
+ await client.threads.reload();
+ } catch (err) {
+ setError(err as Error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+ {t('Loading...')}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {t("Couldn't load new threads. Tap to retry")}
+
+ );
+ }
+
return (
- client.threads.reload()}
- style={[
- styles.touchableWrapper,
- { backgroundColor: text_high_emphasis },
- threadListUnreadBanner.touchableWrapper,
+ [
+ styles.container,
+ {
+ backgroundColor: pressed
+ ? semantics.backgroundCorePressed
+ : semantics.backgroundCoreSurface,
+ },
]}
>
-
- {unseenThreadIds.length} unread threads
+
+
+ {t('{{count}} new threads', { count: unseenThreadIds.length })}
-
-
+
);
};
+
+const useStyles = () => {
+ const {
+ theme: { semantics, threadListUnreadBanner },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ text: {
+ color: semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeXs,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightTight,
+ ...threadListUnreadBanner.text,
+ },
+ container: {
+ backgroundColor: semantics.backgroundCoreSurface,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: primitives.spacingSm,
+ gap: primitives.spacingXs,
+ ...threadListUnreadBanner.container,
+ },
+ });
+ }, [semantics, threadListUnreadBanner]);
+};
diff --git a/package/src/components/ThreadList/ThreadMessagePreviewDeliveryStatus.tsx b/package/src/components/ThreadList/ThreadMessagePreviewDeliveryStatus.tsx
new file mode 100644
index 0000000000..e262ca8063
--- /dev/null
+++ b/package/src/components/ThreadList/ThreadMessagePreviewDeliveryStatus.tsx
@@ -0,0 +1,122 @@
+import React, { useMemo } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+
+import { Channel, LocalMessage } from 'stream-chat';
+
+import { useChatContext } from '../../contexts/chatContext/ChatContext';
+import { useTheme } from '../../contexts/themeContext/ThemeContext';
+import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
+import { MessageDeliveryStatus, useMessageDeliveryStatus } from '../../hooks';
+import { Check, CheckAll, Time } from '../../icons';
+import { primitives } from '../../theme';
+import { MessageStatusTypes } from '../../utils/utils';
+
+export type ThreadMessagePreviewDeliveryStatusProps = {
+ channel: Channel;
+ message: LocalMessage;
+};
+
+export const ThreadMessagePreviewDeliveryStatus = ({
+ channel,
+ message,
+}: ThreadMessagePreviewDeliveryStatusProps) => {
+ const { client } = useChatContext();
+ const { t } = useTranslationContext();
+ const channelConfigExists = typeof channel?.getConfig === 'function';
+ const styles = useStyles();
+ const {
+ theme: {
+ channelPreview: {
+ messageDeliveryStatus: { checkAllIcon, checkIcon, timeIcon },
+ },
+ semantics,
+ },
+ } = useTheme();
+
+ const membersWithoutSelf = useMemo(() => {
+ return Object.values(channel.state?.members || {}).filter(
+ (member) => member.user?.id !== client.user?.id,
+ );
+ }, [channel.state?.members, client.user?.id]);
+
+ const isLastMessageByCurrentUser = useMemo(() => {
+ return message?.user?.id === client.user?.id;
+ }, [message, client.user?.id]);
+
+ const readEvents = useMemo(() => {
+ if (!channelConfigExists) {
+ return true;
+ }
+ const read_events = !channel.disconnected && !!channel?.id && channel.getConfig()?.read_events;
+ if (typeof read_events !== 'boolean') {
+ return true;
+ }
+ return read_events;
+ }, [channelConfigExists, channel]);
+
+ const { status } = useMessageDeliveryStatus({
+ channel,
+ lastMessage: message as LocalMessage,
+ isReadEventsEnabled: readEvents,
+ });
+
+ if (!channel.data?.name && membersWithoutSelf.length === 1 && !isLastMessageByCurrentUser) {
+ return null;
+ }
+
+ if (!isLastMessageByCurrentUser) {
+ return {message?.user?.name || message?.user?.id}:;
+ }
+
+ return (
+
+ {message.status === MessageStatusTypes.SENDING ? (
+
+ ) : message.status === MessageStatusTypes.RECEIVED &&
+ status === MessageDeliveryStatus.READ ? (
+
+ ) : status === MessageDeliveryStatus.DELIVERED ? (
+
+ ) : status === MessageDeliveryStatus.SENT ? (
+
+ ) : null}
+ {t('You')}:
+
+ );
+};
+
+const useStyles = () => {
+ const {
+ theme: {
+ semantics,
+ threadListItem: {
+ messagePreviewDeliveryStatus: { container, text, username },
+ },
+ },
+ } = useTheme();
+
+ return useMemo(() => {
+ return StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: primitives.spacingXxs,
+ ...container,
+ },
+ text: {
+ color: semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...text,
+ },
+ username: {
+ color: semantics.textSecondary,
+ fontSize: primitives.typographyFontSizeMd,
+ fontWeight: primitives.typographyFontWeightSemiBold,
+ lineHeight: primitives.typographyLineHeightNormal,
+ ...username,
+ },
+ });
+ }, [semantics, text, container, username]);
+};
diff --git a/package/src/components/index.ts b/package/src/components/index.ts
index a23fc62bac..c19b007b83 100644
--- a/package/src/components/index.ts
+++ b/package/src/components/index.ts
@@ -55,7 +55,7 @@ export * from './ChannelPreview/hooks/useChannelPreviewDisplayName';
export * from './ChannelPreview/hooks/useChannelPreviewDisplayPresence';
export * from './ChannelPreview/hooks/useChannelPreviewData';
export * from './ChannelPreview/hooks/useIsChannelMuted';
-export * from './ChannelPreview/hooks/useMessageDeliveryStatus';
+export * from './ChannelPreview/ChannelMessagePreviewDeliveryStatus';
export * from './Chat/Chat';
export * from './Chat/hooks/useCreateChatClient';
@@ -168,8 +168,6 @@ export * from './MessageMenu/MessageUserReactionsAvatar';
export * from './MessageMenu/MessageReactionPicker';
export * from './MessageMenu/hooks/useFetchReactions';
-export * from './MessagePreview/MessagePreview';
-
export * from './ProgressControl/ProgressControl';
export * from './ProgressControl/WaveProgressBar';
export * from './Poll';
diff --git a/package/src/components/ui/Badge/BadgeNotification.tsx b/package/src/components/ui/Badge/BadgeNotification.tsx
index 8bcc599dbf..3b6d517dea 100644
--- a/package/src/components/ui/Badge/BadgeNotification.tsx
+++ b/package/src/components/ui/Badge/BadgeNotification.tsx
@@ -5,7 +5,7 @@ import { useTheme } from '../../../contexts/themeContext/ThemeContext';
import { primitives } from '../../../theme';
export type BadgeNotificationProps = {
- type: 'primary' | 'error' | 'neutral';
+ type?: 'primary' | 'error' | 'neutral';
count: number;
size: 'sm' | 'xs';
testID?: string;
@@ -36,7 +36,7 @@ const textStyles = {
};
export const BadgeNotification = (props: BadgeNotificationProps) => {
- const { type, count, size = 'sm', testID } = props;
+ const { type = 'primary', count, size = 'sm', testID } = props;
const styles = useStyles();
const {
theme: { semantics },
diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts
index 45606ebd80..583fb7fdb7 100644
--- a/package/src/contexts/themeContext/utils/theme.ts
+++ b/package/src/contexts/themeContext/utils/theme.ts
@@ -204,6 +204,10 @@ export type Theme = {
errorText: TextStyle;
draftText: TextStyle;
};
+ messagePreview: {
+ container: ViewStyle;
+ subtitle: TextStyle;
+ };
};
dateHeader: {
container: ViewStyle;
@@ -923,6 +927,10 @@ export type Theme = {
subtitleContainer: ViewStyle;
videoIcon: IconProps;
wrapper: ViewStyle;
+ messagePreview: {
+ container: ViewStyle;
+ subtitle: TextStyle;
+ };
};
screenPadding: number;
spinner: ViewStyle;
@@ -935,22 +943,28 @@ export type Theme = {
};
};
threadListItem: {
- boldText: TextStyle;
- contentRow: ViewStyle;
- contentTextWrapper: ViewStyle;
+ wrapper: ViewStyle;
+ container: ViewStyle;
+ content: ViewStyle;
+ channelName: TextStyle;
dateText: TextStyle;
- headerRow: ViewStyle;
- infoRow: ViewStyle;
- parentMessagePreviewContainer: ViewStyle;
- parentMessageText: TextStyle;
+ lowerRow: ViewStyle;
+ messageRepliesText: TextStyle;
previewMessageContainer: ViewStyle;
- touchableWrapper: ViewStyle;
- unreadBubbleText: TextStyle;
unreadBubbleWrapper: ViewStyle;
+ messagePreview: {
+ container: ViewStyle;
+ subtitle: TextStyle;
+ };
+ messagePreviewDeliveryStatus: {
+ container: ViewStyle;
+ text: TextStyle;
+ username: TextStyle;
+ };
};
threadListUnreadBanner: {
text: TextStyle;
- touchableWrapper: ViewStyle;
+ container: ViewStyle;
};
typingIndicator: {
container: ViewStyle;
@@ -1083,6 +1097,10 @@ export const defaultTheme: Theme = {
statusContainer: {},
titleContainer: {},
wrapper: {},
+ messagePreview: {
+ container: {},
+ subtitle: {},
+ },
},
colors: Colors,
dateHeader: {
@@ -1780,6 +1798,10 @@ export const defaultTheme: Theme = {
title: {},
videoIcon: {},
wrapper: {},
+ messagePreview: {
+ container: {},
+ subtitle: {},
+ },
},
screenPadding: 16,
spinner: {},
@@ -1789,22 +1811,28 @@ export const defaultTheme: Theme = {
},
},
threadListItem: {
- boldText: {},
- contentRow: {},
- contentTextWrapper: {},
- dateText: {},
- headerRow: {},
- infoRow: {},
- parentMessagePreviewContainer: {},
- parentMessageText: {},
previewMessageContainer: {},
- touchableWrapper: {},
- unreadBubbleText: {},
unreadBubbleWrapper: {},
+ wrapper: {},
+ container: {},
+ content: {},
+ channelName: {},
+ dateText: {},
+ lowerRow: {},
+ messageRepliesText: {},
+ messagePreview: {
+ container: {},
+ subtitle: {},
+ },
+ messagePreviewDeliveryStatus: {
+ container: {},
+ text: {},
+ username: {},
+ },
},
threadListUnreadBanner: {
text: {},
- touchableWrapper: {},
+ container: {},
},
typingIndicator: {
container: {},
diff --git a/package/src/hooks/index.ts b/package/src/hooks/index.ts
index cc90b8ba17..503cf5128b 100644
--- a/package/src/hooks/index.ts
+++ b/package/src/hooks/index.ts
@@ -12,3 +12,7 @@ export * from './useInAppNotificationsState';
export * from './useRAFCoalescedValue';
export * from './useAudioPlayerControl';
export * from './useAttachmentPickerState';
+export * from './messagePreview/useMessageDeliveryStatus';
+export * from './messagePreview/useGroupedAttachments';
+export * from './messagePreview/useMessagePreviewIcon';
+export * from './messagePreview/useMessagePreviewText';
diff --git a/package/src/components/MessagePreview/hook/useGroupedAttachments.ts b/package/src/hooks/messagePreview/useGroupedAttachments.ts
similarity index 100%
rename from package/src/components/MessagePreview/hook/useGroupedAttachments.ts
rename to package/src/hooks/messagePreview/useGroupedAttachments.ts
index 16cf6ef914..48373030de 100644
--- a/package/src/components/MessagePreview/hook/useGroupedAttachments.ts
+++ b/package/src/hooks/messagePreview/useGroupedAttachments.ts
@@ -41,6 +41,8 @@ export const useGroupedAttachments = (attachments?: Attachment[]) => {
(acc, attachment) => {
if (isGiphyAttachment(attachment)) {
acc.giphys.push(attachment);
+ } else if (isVoiceRecordingAttachment(attachment)) {
+ acc.voiceRecordings.push(attachment);
} else if (isAudioAttachment(attachment)) {
acc.audios.push(attachment);
} else if (isImageAttachment(attachment)) {
@@ -49,8 +51,6 @@ export const useGroupedAttachments = (attachments?: Attachment[]) => {
acc.videos.push(attachment);
} else if (isFileAttachment(attachment)) {
acc.files.push(attachment);
- } else if (isVoiceRecordingAttachment(attachment)) {
- acc.voiceRecordings.push(attachment);
}
return acc;
diff --git a/package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts b/package/src/hooks/messagePreview/useMessageDeliveryStatus.ts
similarity index 97%
rename from package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts
rename to package/src/hooks/messagePreview/useMessageDeliveryStatus.ts
index dd89b9a665..3ee9704cd4 100644
--- a/package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts
+++ b/package/src/hooks/messagePreview/useMessageDeliveryStatus.ts
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat';
-import { useChatContext } from '../../../contexts/chatContext/ChatContext';
+import { useChatContext } from '../../contexts/chatContext/ChatContext';
export enum MessageDeliveryStatus {
NOT_SENT_BY_CURRENT_USER = 'not_sent_by_current_user',
diff --git a/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx b/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx
new file mode 100644
index 0000000000..dce65d9490
--- /dev/null
+++ b/package/src/hooks/messagePreview/useMessagePreviewIcon.tsx
@@ -0,0 +1,75 @@
+import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
+
+import { useGroupedAttachments } from './useGroupedAttachments';
+
+import { CircleBan } from '../../icons/CircleBan';
+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 { NewPoll } from '../../icons/NewPoll';
+import { NewVideo } from '../../icons/NewVideo';
+import { FileTypes } from '../../types/types';
+
+export const useMessagePreviewIcon = ({
+ message,
+}: {
+ message?: LocalMessage | MessageResponse | DraftMessage | null;
+}) => {
+ const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
+ message?.attachments,
+ );
+ const attachmentsLength = message?.attachments?.length;
+ if (!message) {
+ return null;
+ }
+
+ const onlyImages = images?.length && images?.length === attachmentsLength;
+ const onlyAudio = audios?.length && audios?.length === attachmentsLength;
+ const onlyVideos = videos?.length && videos?.length === attachmentsLength;
+ const onlyVoiceRecordings =
+ voiceRecordings?.length && voiceRecordings?.length === attachmentsLength;
+
+ const hasLink = message?.attachments?.some(
+ (attachment) => attachment.type === FileTypes.Image && attachment.og_scrape_url,
+ );
+
+ if (message.type === 'deleted') {
+ return CircleBan;
+ }
+
+ if (message.poll_id) {
+ return NewPoll;
+ }
+
+ if (message.shared_location) {
+ return NewMapPin;
+ }
+
+ if (hasLink) {
+ return NewLink;
+ }
+
+ if (onlyAudio || onlyVoiceRecordings) {
+ return NewMic;
+ }
+
+ if (onlyVideos) {
+ return NewVideo;
+ }
+
+ if (onlyImages) {
+ return NewPhoto;
+ }
+
+ if (giphys?.length) {
+ return NewFile;
+ }
+
+ if (files?.length || images?.length || videos?.length || audios?.length) {
+ return NewFile;
+ }
+
+ return null;
+};
diff --git a/package/src/hooks/messagePreview/useMessagePreviewText.tsx b/package/src/hooks/messagePreview/useMessagePreviewText.tsx
new file mode 100644
index 0000000000..34d8271685
--- /dev/null
+++ b/package/src/hooks/messagePreview/useMessagePreviewText.tsx
@@ -0,0 +1,103 @@
+import dayjs from 'dayjs';
+import {
+ DraftMessage,
+ LiveLocationPayload,
+ LocalMessage,
+ MessageResponse,
+ PollState,
+} from 'stream-chat';
+
+import { useGroupedAttachments } from './useGroupedAttachments';
+
+import { useChatContext } from '../../contexts/chatContext/ChatContext';
+import { useStateStore } from '../../hooks';
+
+const selector = (nextValue: PollState) => ({
+ name: nextValue.name,
+});
+
+export const useMessagePreviewText = ({
+ message,
+}: {
+ message?: LocalMessage | MessageResponse | DraftMessage | null;
+}) => {
+ const { client } = useChatContext();
+ const poll = client.polls.fromState(message?.poll_id ?? '');
+ const { name: pollName } = useStateStore(poll?.state, selector) ?? {};
+ const { giphys, audios, images, videos, files, voiceRecordings } = useGroupedAttachments(
+ message?.attachments,
+ );
+ const attachmentsLength = message?.attachments?.length;
+
+ const onlyImages = images?.length && images?.length === attachmentsLength;
+ const onlyVideos = videos?.length && videos?.length === attachmentsLength;
+ const onlyFiles = files?.length && files?.length === attachmentsLength;
+ const onlyAudio = audios?.length === attachmentsLength;
+ const onlyVoiceRecordings =
+ voiceRecordings?.length && voiceRecordings?.length === attachmentsLength;
+
+ if (message?.type === 'deleted') {
+ return 'Message deleted';
+ }
+
+ if (pollName) {
+ return pollName;
+ }
+
+ if (message?.shared_location) {
+ if (
+ // There is a problem with types in Draft Message, and its not able to infer the type of `end_at` correctly, so the `as` is used.
+ (message?.shared_location as LiveLocationPayload)?.end_at &&
+ new Date((message?.shared_location as LiveLocationPayload)?.end_at) > new Date()
+ ) {
+ return 'Live Location';
+ }
+ return 'Location';
+ }
+
+ if (message?.text) {
+ return message?.text;
+ }
+
+ if (onlyImages) {
+ if (images?.length === 1) {
+ return 'Photo';
+ } else {
+ return `${images?.length} Photos`;
+ }
+ }
+
+ if (onlyVideos) {
+ if (videos?.length === 1) {
+ return 'Video';
+ } else {
+ return `${videos?.length} Videos`;
+ }
+ }
+
+ if (onlyAudio) {
+ if (audios?.length === 1) {
+ return 'Audio';
+ } else {
+ return `${audios?.length} Audios`;
+ }
+ }
+
+ if (onlyVoiceRecordings) {
+ if (voiceRecordings?.length === 1) {
+ return `Voice message (${dayjs.duration(voiceRecordings?.[0]?.duration ?? 0, 'seconds').format('m:ss')})`;
+ } else {
+ return `${voiceRecordings?.length} Voice messages`;
+ }
+ }
+
+ if (giphys?.length) {
+ return 'Giphy';
+ }
+
+ if (onlyFiles && files?.length === 1) {
+ return files?.[0]?.title;
+ }
+
+ return `${attachmentsLength} Files`;
+};
diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json
index 43f607545d..eef58be35c 100644
--- a/package/src/i18n/en.json
+++ b/package/src/i18n/en.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Replied to a thread",
"View": "View",
"Reminder overdue": "Reminder overdue",
- "Poll has ended": "Poll has ended"
+ "Poll has ended": "Poll has ended",
+ "Reply to a message to start a thread": "Reply to a message to start a thread",
+ "Couldn't load new threads. Tap to retry": "Couldn't load new threads. Tap to retry",
+ "{{count}} new threads": "{{count}} new threads",
+ "No conversations yet": "No conversations yet"
}
diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json
index 981acfcc62..2fa8055f8e 100644
--- a/package/src/i18n/es.json
+++ b/package/src/i18n/es.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Respondido a un hilo",
"View": "Ver",
"Reminder overdue": "Recordatorio vencido",
- "Poll has ended": "Votación finalizada"
+ "Poll has ended": "Votación finalizada",
+ "Reply to a message to start a thread": "Responde a un mensaje para empezar un hilo",
+ "Couldn't load new threads. Tap to retry": "No se pudieron cargar nuevos hilos. Toca para reintentar",
+ "{{count}} new threads": "{{count}} nuevos hilos",
+ "No conversations yet": "No hay conversaciones todavía"
}
diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json
index 6527b6d296..284db2ccd0 100644
--- a/package/src/i18n/fr.json
+++ b/package/src/i18n/fr.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Respondido a un hilo",
"View": "Ver",
"Reminder overdue": "Recordatorio vencido",
- "Poll has ended": "Vote terminé"
+ "Poll has ended": "Vote terminé",
+ "Reply to a message to start a thread": "Répondre à un message pour commencer un fil",
+ "Couldn't load new threads. Tap to retry": "Impossible de charger les nouveaux fils. Appuyez pour réessayer",
+ "{{count}} new threads": "{{count}} nouveaux fils",
+ "No conversations yet": "Aucune conversation pour le moment"
}
diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json
index b104ee010c..09a9996cba 100644
--- a/package/src/i18n/he.json
+++ b/package/src/i18n/he.json
@@ -177,5 +177,9 @@
"Replied to a thread": "הגב/י בשרשור",
"View": "צפה",
"Reminder overdue": "הזמן פג",
- "Poll has ended": "ההצבעה הסתיימה"
+ "Poll has ended": "ההצבעה הסתיימה",
+ "Reply to a message to start a thread": "השב/י להודעה כדי להתחיל שרשור",
+ "Couldn't load new threads. Tap to retry": "לא ניתן לטעון שרשורים חדשים. הקש כדי לנסות שוב",
+ "{{count}} new threads": "{{count}} שרשורים חדשים",
+ "No conversations yet": "אין שיחות עדיין"
}
diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json
index 442f144292..f4812fded2 100644
--- a/package/src/i18n/hi.json
+++ b/package/src/i18n/hi.json
@@ -177,5 +177,9 @@
"Replied to a thread": "थ्रेड में उत्तर दिया",
"View": "देखें",
"Reminder overdue": "रीमिंडर ओवरडो",
- "Poll has ended": "वोट समाप्त"
+ "Poll has ended": "वोट समाप्त",
+ "Reply to a message to start a thread": "एक संदेश का जवाब देकर थ्रेड शुरू करें",
+ "Couldn't load new threads. Tap to retry": "नये थ्रेड्स लोड नहीं हो सके। टैप करके पुनः कोशिश करें",
+ "{{count}} new threads": "{{count}} नये थ्रेड्स",
+ "No conversations yet": "अभी तक कोई चैट नहीं है"
}
diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json
index b41c526707..e3bdf02906 100644
--- a/package/src/i18n/it.json
+++ b/package/src/i18n/it.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Respondido a un hilo",
"View": "Ver",
"Reminder overdue": "Recordatorio vencido",
- "Poll has ended": "Votazione terminata"
+ "Poll has ended": "Votazione terminata",
+ "Reply to a message to start a thread": "Rispondi a un messaggio per iniziare un thread",
+ "Couldn't load new threads. Tap to retry": "Impossibile caricare nuovi thread. Tocca per riprovare",
+ "{{count}} new threads": "{{count}} nuovi thread",
+ "No conversations yet": "Ancora nessuna conversazione"
}
diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json
index 015821c2a8..680a94799a 100644
--- a/package/src/i18n/ja.json
+++ b/package/src/i18n/ja.json
@@ -177,5 +177,9 @@
"Replied to a thread": "スレッドに返信",
"View": "表示",
"Reminder overdue": "リマインダー期限切れ",
- "Poll has ended": "投票終了"
+ "Poll has ended": "投票終了",
+ "Reply to a message to start a thread": "メッセージに返信してスレッドを開始",
+ "Couldn't load new threads. Tap to retry": "新しいスレッドを読み込めませんでした。タップして再試行",
+ "{{count}} new threads": "{{count}} 新しいスレッド",
+ "No conversations yet": "まだ会話がありません"
}
diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json
index 30d39822a4..f7b4a8d127 100644
--- a/package/src/i18n/ko.json
+++ b/package/src/i18n/ko.json
@@ -177,5 +177,9 @@
"Replied to a thread": "스레드에 답장",
"View": "보기",
"Reminder overdue": "리마인더 만료",
- "Poll has ended": "투표 종료됨"
+ "Poll has ended": "투표 종료됨",
+ "Reply to a message to start a thread": "메시지에 답장하여 스레드 시작",
+ "Couldn't load new threads. Tap to retry": "새로운 스레드를 로드할 수 없습니다. 탭하여 다시 시도",
+ "{{count}} new threads": "{{count}} 새로운 스레드",
+ "No conversations yet": "아직 대화가 없습니다"
}
diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json
index d8afc01355..3336766a97 100644
--- a/package/src/i18n/nl.json
+++ b/package/src/i18n/nl.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Replied to a thread",
"View": "View",
"Reminder overdue": "Reminder overdue",
- "Poll has ended": "Stemmen beëindigd"
+ "Poll has ended": "Stemmen beëindigd",
+ "Reply to a message to start a thread": "Antwoord op een bericht om een thread te starten",
+ "Couldn't load new threads. Tap to retry": "Kan nieuwe threads niet laden. Tik om opnieuw te proberen",
+ "{{count}} new threads": "{{count}} nieuwe threads",
+ "No conversations yet": "Nog geen gesprekken"
}
diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json
index a2f2fa827f..610206f75d 100644
--- a/package/src/i18n/pt-br.json
+++ b/package/src/i18n/pt-br.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Respondido a un hilo",
"View": "Ver",
"Reminder overdue": "Recordatorio vencido",
- "Poll has ended": "Votação encerrada"
+ "Poll has ended": "Votação encerrada",
+ "Reply to a message to start a thread": "Responder a uma mensagem para iniciar um tópico",
+ "Couldn't load new threads. Tap to retry": "Não foi possível carregar novos tópicos. Toque para tentar novamente",
+ "{{count}} new threads": "{{count}} novos tópicos",
+ "No conversations yet": "Ainda não há conversas"
}
diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json
index bffbac0807..2509f06761 100644
--- a/package/src/i18n/ru.json
+++ b/package/src/i18n/ru.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Ответил на тему",
"View": "Посмотреть",
"Reminder overdue": "Напоминание просрочено",
- "Poll has ended": "Голосование завершено"
+ "Poll has ended": "Голосование завершено",
+ "Reply to a message to start a thread": "Ответить на сообщение, чтобы начать тему",
+ "Couldn't load new threads. Tap to retry": "Не удалось загрузить новые темы. Нажмите, чтобы попробовать снова",
+ "{{count}} new threads": "{{count}} новых тем",
+ "No conversations yet": "Нет чатов"
}
diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json
index 167d288c06..d93dc63154 100644
--- a/package/src/i18n/tr.json
+++ b/package/src/i18n/tr.json
@@ -177,5 +177,9 @@
"Replied to a thread": "Konuya yanıt verildi",
"View": "Görüntüle",
"Reminder overdue": "Hatırlatıcı süresi doldu",
- "Poll has ended": "Oylama sona erdi"
+ "Poll has ended": "Oylama sona erdi",
+ "Reply to a message to start a thread": "Bir mesaja yanıt vermek için konu başlatın",
+ "Couldn't load new threads. Tap to retry": "Yeni konular yüklenemedi. Tekrar denemek için dokunun",
+ "{{count}} new threads": "{{count}} yeni konu",
+ "No conversations yet": "Henüz konuşma yok"
}
diff --git a/package/src/icons/Edit.tsx b/package/src/icons/Edit.tsx
index a54c991e61..ad88a86ab9 100644
--- a/package/src/icons/Edit.tsx
+++ b/package/src/icons/Edit.tsx
@@ -4,21 +4,29 @@ import Svg, { Mask, Path } from 'react-native-svg';
import { IconProps } from './utils/base';
-export const Edit = (props: IconProps) => (
-
);
diff --git a/package/src/icons/Reload.tsx b/package/src/icons/Reload.tsx
index a1500c68e2..b1d0c62b20 100644
--- a/package/src/icons/Reload.tsx
+++ b/package/src/icons/Reload.tsx
@@ -1,12 +1,17 @@
import React from 'react';
-import { IconProps, RootPath, RootSvg } from './utils/base';
+import { Path, Svg } from 'react-native-svg';
-export const Reload = (props: IconProps) => (
-
- (
+
+
-
+
);