Skip to content

Commit b1e4423

Browse files
authored
feat: thread list redesign (#3442)
This pull request refactors the channel message preview components to improve code modularity, maintainability, and UI consistency. The main changes involve extracting message preview logic into dedicated components, updating delivery status handling, and improving empty state indicator styling. **Component Refactoring and Consolidation** * Extracted message preview rendering logic from `ChannelPreviewMessage` into a new `ChannelMessagePreview` component, simplifying the parent component and improving reusability. [[1]](diffhunk://#diff-3baf806204bf22f2aca161a9ca9bb0d1593e82e7bdbd22dbc30e99ebf957b18aL56-L74) [[2]](diffhunk://#diff-3baf806204bf22f2aca161a9ca9bb0d1593e82e7bdbd22dbc30e99ebf957b18aL83-R62) [[3]](diffhunk://#diff-3baf806204bf22f2aca161a9ca9bb0d1593e82e7bdbd22dbc30e99ebf957b18aL118-R105) [[4]](diffhunk://#diff-a1000d2141a1e1116205bfffda5e2b2507d86dcefa0787dce539a60e4a631acbR1-R72) * Removed the legacy `MessagePreview` and `MessagePreviewUserDetails` components, consolidating their functionality into the new preview components. [[1]](diffhunk://#diff-b3eaa7b0936e533885858844b0cabcb0daf593a52cbac4c70d2db6c88e1af7c1L1-L331) [[2]](diffhunk://#diff-3baf806204bf22f2aca161a9ca9bb0d1593e82e7bdbd22dbc30e99ebf957b18aL23-L24) **Delivery Status Handling Improvements** * Renamed and refactored `ChannelListMessageDeliveryStatus` to `ChannelMessagePreviewDeliveryStatus`, updating its props and logic to work with the new message preview structure. * Improved logic for displaying delivery status and sender name, especially in one-on-one channels and when the last message is not from the current user. [[1]](diffhunk://#diff-1b6443e0db4bb3faff80217caaaf3a2153152a6d5508ade3e3572d3e9c48e393R37-R45) [[2]](diffhunk://#diff-1b6443e0db4bb3faff80217caaaf3a2153152a6d5508ade3e3572d3e9c48e393L56-R76) * Added new styling for displaying the sender's username in delivery status. **Empty State Indicator Styling** * Updated `EmptyStateIndicator` to use theme-based styles for thread empty states, ensuring consistent appearance across the app. [[1]](diffhunk://#diff-d3f418800301f531de71e9fc4f792c707a1a9046989948628c933bab565bfa60L1-R8) [[2]](diffhunk://#diff-d3f418800301f531de71e9fc4f792c707a1a9046989948628c933bab565bfa60R25-R31) [[3]](diffhunk://#diff-d3f418800301f531de71e9fc4f792c707a1a9046989948628c933bab565bfa60L61-R79) [[4]](diffhunk://#diff-d3f418800301f531de71e9fc4f792c707a1a9046989948628c933bab565bfa60R99-R109) These changes collectively improve the maintainability of the codebase and the user experience by making the message preview and delivery status components more modular and visually consistent.
1 parent afff5b7 commit b1e4423

37 files changed

+1257
-692
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { useMemo } from 'react';
2+
import { View, Text, StyleSheet } from 'react-native';
3+
4+
import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
5+
6+
import { useTheme } from '../../contexts/themeContext/ThemeContext';
7+
import { useMessagePreviewIcon, useMessagePreviewText } from '../../hooks';
8+
import { primitives } from '../../theme';
9+
10+
export type ChannelMessagePreviewProps = {
11+
message: LocalMessage | MessageResponse | DraftMessage;
12+
};
13+
14+
export const ChannelMessagePreview = ({ message }: ChannelMessagePreviewProps) => {
15+
const isMessageDeleted = message?.type === 'deleted';
16+
const {
17+
theme: { semantics },
18+
} = useTheme();
19+
const styles = useStyles({ isMessageDeleted });
20+
const MessagePreviewIcon = useMessagePreviewIcon({ message });
21+
const messagePreviewTitle = useMessagePreviewText({ message });
22+
23+
return (
24+
<View style={[styles.container]}>
25+
{MessagePreviewIcon ? (
26+
<MessagePreviewIcon
27+
height={16}
28+
stroke={isMessageDeleted ? semantics.textTertiary : semantics.textSecondary}
29+
width={16}
30+
/>
31+
) : null}
32+
<Text numberOfLines={1} style={[styles.subtitle]}>
33+
{messagePreviewTitle}
34+
</Text>
35+
</View>
36+
);
37+
};
38+
39+
const useStyles = ({ isMessageDeleted = false }: { isMessageDeleted?: boolean }) => {
40+
const {
41+
theme: {
42+
channelPreview: { messagePreview },
43+
semantics,
44+
},
45+
} = useTheme();
46+
return useMemo(() => {
47+
return StyleSheet.create({
48+
container: {
49+
flexDirection: 'row',
50+
alignItems: 'center',
51+
gap: primitives.spacingXxs,
52+
flexShrink: 1,
53+
...messagePreview.container,
54+
},
55+
subtitle: {
56+
color: isMessageDeleted ? semantics.textTertiary : semantics.textSecondary,
57+
fontSize: primitives.typographyFontSizeSm,
58+
fontWeight: primitives.typographyFontWeightRegular,
59+
includeFontPadding: false,
60+
lineHeight: primitives.typographyLineHeightNormal,
61+
flexShrink: 1,
62+
...messagePreview.subtitle,
63+
},
64+
});
65+
}, [
66+
isMessageDeleted,
67+
semantics.textTertiary,
68+
semantics.textSecondary,
69+
messagePreview.container,
70+
messagePreview.subtitle,
71+
]);
72+
};

package/src/components/ChannelPreview/ChannelListMessageDeliveryStatus.tsx renamed to package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import React, { useMemo } from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33

4-
import { LocalMessage } from 'stream-chat';
4+
import { LocalMessage, MessageResponse } from 'stream-chat';
55

66
import { ChannelPreviewProps } from './ChannelPreview';
7-
import { LastMessageType } from './hooks/useChannelPreviewData';
8-
9-
import { MessageDeliveryStatus, useMessageDeliveryStatus } from './hooks/useMessageDeliveryStatus';
107

118
import { useChatContext } from '../../contexts/chatContext/ChatContext';
129
import { useTheme } from '../../contexts/themeContext/ThemeContext';
1310
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
11+
import { MessageDeliveryStatus, useMessageDeliveryStatus } from '../../hooks';
1412
import { Check, CheckAll, Time } from '../../icons';
1513
import { primitives } from '../../theme';
1614
import { MessageStatusTypes } from '../../utils/utils';
1715

18-
export type ChannelListMessageDeliveryStatusProps = Pick<ChannelPreviewProps, 'channel'> & {
19-
lastMessage: LastMessageType;
16+
export type ChannelMessagePreviewDeliveryStatusProps = Pick<ChannelPreviewProps, 'channel'> & {
17+
message: MessageResponse | LocalMessage;
2018
};
2119

22-
export const ChannelListMessageDeliveryStatus = ({
20+
export const ChannelMessagePreviewDeliveryStatus = ({
2321
channel,
24-
lastMessage,
25-
}: ChannelListMessageDeliveryStatusProps) => {
22+
message,
23+
}: ChannelMessagePreviewDeliveryStatusProps) => {
2624
const { client } = useChatContext();
2725
const { t } = useTranslationContext();
2826
const channelConfigExists = typeof channel?.getConfig === 'function';
@@ -36,9 +34,15 @@ export const ChannelListMessageDeliveryStatus = ({
3634
},
3735
} = useTheme();
3836

37+
const membersWithoutSelf = useMemo(() => {
38+
return Object.values(channel.state?.members || {}).filter(
39+
(member) => member.user?.id !== client.user?.id,
40+
);
41+
}, [channel.state?.members, client.user?.id]);
42+
3943
const isLastMessageByCurrentUser = useMemo(() => {
40-
return lastMessage?.user?.id === client.user?.id;
41-
}, [lastMessage, client.user?.id]);
44+
return message?.user?.id === client.user?.id;
45+
}, [message, client.user?.id]);
4246

4347
const readEvents = useMemo(() => {
4448
if (!channelConfigExists) {
@@ -53,19 +57,23 @@ export const ChannelListMessageDeliveryStatus = ({
5357

5458
const { status } = useMessageDeliveryStatus({
5559
channel,
56-
lastMessage: lastMessage as LocalMessage,
60+
lastMessage: message as LocalMessage,
5761
isReadEventsEnabled: readEvents,
5862
});
5963

60-
if (!isLastMessageByCurrentUser) {
64+
if (!channel.data?.name && membersWithoutSelf.length === 1 && !isLastMessageByCurrentUser) {
6165
return null;
6266
}
6367

68+
if (!isLastMessageByCurrentUser) {
69+
return <Text style={styles.username}>{message?.user?.name || message?.user?.id}:</Text>;
70+
}
71+
6472
return (
6573
<View style={styles.container}>
66-
{lastMessage.status === MessageStatusTypes.SENDING ? (
74+
{message.status === MessageStatusTypes.SENDING ? (
6775
<Time stroke={semantics.chatTextTimestamp} height={16} width={16} {...timeIcon} />
68-
) : lastMessage.status === MessageStatusTypes.RECEIVED &&
76+
) : message.status === MessageStatusTypes.RECEIVED &&
6977
status === MessageDeliveryStatus.READ ? (
7078
<CheckAll stroke={semantics.accentPrimary} height={16} width={16} {...checkAllIcon} />
7179
) : status === MessageDeliveryStatus.DELIVERED ? (
@@ -103,6 +111,12 @@ const useStyles = () => {
103111
lineHeight: primitives.typographyLineHeightNormal,
104112
...text,
105113
},
114+
username: {
115+
color: semantics.textTertiary,
116+
fontSize: primitives.typographyFontSizeSm,
117+
fontWeight: primitives.typographyFontWeightSemiBold,
118+
lineHeight: primitives.typographyLineHeightNormal,
119+
},
106120
});
107121
}, [semantics, text, container]);
108122
};

package/src/components/ChannelPreview/ChannelPreviewMessage.tsx

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import React, { useCallback, useMemo } from 'react';
1+
import React, { useMemo } from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33

4-
import { DraftMessage, LocalMessage, MessageResponse } from 'stream-chat';
5-
4+
import { ChannelMessagePreview } from './ChannelMessagePreview';
5+
import { ChannelMessagePreviewDeliveryStatus } from './ChannelMessagePreviewDeliveryStatus';
66
import { ChannelPreviewProps } from './ChannelPreview';
77

88
import { ChannelTypingIndicatorPreview } from './ChannelTypingIndicatorPreview';
@@ -20,8 +20,6 @@ import { useTranslationContext } from '../../contexts/translationContext/Transla
2020
import { NewPoll } from '../../icons/NewPoll';
2121
import { primitives } from '../../theme';
2222
import { MessageStatusTypes } from '../../utils/utils';
23-
import { MessagePreview } from '../MessagePreview/MessagePreview';
24-
import { MessagePreviewUserDetails } from '../MessagePreview/MessagePreviewUserDetails';
2523
import { ErrorBadge } from '../ui';
2624

2725
export type ChannelPreviewMessageProps = Pick<ChannelPreviewProps, 'channel'> & {
@@ -53,25 +51,6 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
5351
const isFailedMessage =
5452
lastMessage?.status === MessageStatusTypes.FAILED || lastMessage?.type === 'error';
5553

56-
const textStyle = useMemo(() => {
57-
return [styles.subtitle];
58-
}, [styles.subtitle]);
59-
60-
const iconProps = useMemo(() => {
61-
return {
62-
width: 16,
63-
height: 16,
64-
stroke: isMessageDeleted ? semantics.textTertiary : semantics.textSecondary,
65-
};
66-
}, [isMessageDeleted, semantics.textTertiary, semantics.textSecondary]);
67-
68-
const renderMessagePreview = useCallback(
69-
(message: LocalMessage | MessageResponse | DraftMessage) => {
70-
return <MessagePreview message={message} textStyle={textStyle} iconProps={iconProps} />;
71-
},
72-
[textStyle, iconProps],
73-
);
74-
7554
if (usersTyping.length > 0) {
7655
return <ChannelTypingIndicatorPreview channel={channel} usersTyping={usersTyping} />;
7756
}
@@ -80,7 +59,7 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
8059
return (
8160
<View style={styles.container}>
8261
<Text style={styles.draftText}>{t('Draft')}:</Text>
83-
{renderMessagePreview(draftMessage)}
62+
<ChannelMessagePreview message={draftMessage} />
8463
</View>
8564
);
8665
}
@@ -115,15 +94,15 @@ export const ChannelPreviewMessage = (props: ChannelPreviewMessageProps) => {
11594
if (channel.data?.name || membersWithoutSelf.length > 1) {
11695
return (
11796
<View style={styles.container}>
118-
<MessagePreviewUserDetails channel={channel} message={lastMessage} />
119-
{renderMessagePreview(lastMessage)}
97+
<ChannelMessagePreviewDeliveryStatus channel={channel} message={lastMessage} />
98+
<ChannelMessagePreview message={lastMessage} />
12099
</View>
121100
);
122101
} else {
123102
return (
124103
<View style={styles.container}>
125-
<MessagePreviewUserDetails channel={channel} message={lastMessage} />
126-
{renderMessagePreview(lastMessage)}
104+
<ChannelMessagePreviewDeliveryStatus channel={channel} message={lastMessage} />
105+
<ChannelMessagePreview message={lastMessage} />
127106
</View>
128107
);
129108
}
Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import React from 'react';
1+
import React, { useMemo } from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33

44
import { useTheme } from '../../contexts/themeContext/ThemeContext';
55
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
6-
import { useViewport } from '../../hooks/useViewport';
7-
import { ChatIcon, MessageBubbleEmpty, MessageIcon } from '../../icons';
6+
import { MessageBubbleEmpty } from '../../icons';
7+
import { primitives } from '../../theme';
88

99
export type EmptyStateProps = {
1010
listType?: 'channel' | 'message' | 'threads' | 'default';
@@ -13,78 +13,81 @@ export type EmptyStateProps = {
1313
export const EmptyStateIndicator = ({ listType }: EmptyStateProps) => {
1414
const {
1515
theme: {
16-
colors: { black, grey, grey_gainsboro },
17-
emptyStateIndicator: {
18-
channelContainer,
19-
channelDetails,
20-
channelTitle,
21-
messageContainer,
22-
messageTitle,
23-
},
16+
emptyStateIndicator: { channelContainer, channelTitle, messageContainer, messageTitle },
17+
semantics,
2418
},
2519
} = useTheme();
26-
const { vw } = useViewport();
27-
const width = vw(33);
2820
const { t } = useTranslationContext();
21+
const styles = useStyles();
2922

3023
switch (listType) {
3124
case 'channel':
3225
return (
3326
<View style={[styles.container, channelContainer]}>
34-
<MessageIcon height={width} pathFill={grey_gainsboro} width={width} />
35-
<Text
36-
style={[styles.channelTitle, { color: black }, channelTitle]}
37-
testID='empty-channel-state-title'
38-
>
39-
{t("Let's start chatting!")}
40-
</Text>
41-
<Text
42-
style={[styles.channelDetails, { color: grey, width: vw(66) }, channelDetails]}
43-
testID='empty-channel-state-details'
44-
>
45-
{t('How about sending your first message to a friend?')}
27+
<MessageBubbleEmpty height={27} stroke={semantics.textTertiary} width={25} />
28+
<Text style={[styles.channelTitle, channelTitle]} testID='empty-channel-state-title'>
29+
{t('No conversations yet')}
4630
</Text>
4731
</View>
4832
);
4933
case 'message':
5034
return (
5135
<View style={[styles.container, messageContainer]}>
52-
<ChatIcon height={width} pathFill={grey_gainsboro} width={width} />
53-
<Text style={[styles.messageTitle, { color: grey_gainsboro }, messageTitle]}>
54-
{t('No chats here yet…')}
55-
</Text>
36+
<MessageBubbleEmpty height={27} stroke={semantics.textTertiary} width={25} />
37+
<Text style={[styles.messageTitle, messageTitle]}>{t('No chats here yet…')}</Text>
5638
</View>
5739
);
5840
case 'threads':
5941
return (
60-
<View style={[styles.container]}>
61-
<MessageBubbleEmpty height={width} pathFill={'#B4BBBA'} width={width} />
62-
<Text style={{ color: '#7E828B' }}>{t('No threads here yet')}...</Text>
42+
<View style={styles.container}>
43+
<MessageBubbleEmpty height={27} stroke={semantics.textTertiary} width={25} />
44+
<Text style={styles.threadText}>{t('Reply to a message to start a thread')}</Text>
6345
</View>
6446
);
6547
default:
66-
return <Text style={[{ color: black }, messageContainer]}>No items exist</Text>;
48+
return (
49+
<Text style={[{ color: semantics.textSecondary }, messageContainer]}>No items exist</Text>
50+
);
6751
}
6852
};
6953

70-
const styles = StyleSheet.create({
71-
channelDetails: {
72-
fontSize: 14,
73-
textAlign: 'center',
74-
},
75-
channelTitle: {
76-
fontSize: 16,
77-
paddingBottom: 8,
78-
paddingTop: 16,
79-
},
80-
container: {
81-
alignItems: 'center',
82-
flex: 1,
83-
justifyContent: 'center',
84-
},
85-
messageTitle: {
86-
fontSize: 20,
87-
fontWeight: 'bold',
88-
paddingBottom: 8,
89-
},
90-
});
54+
const useStyles = () => {
55+
const {
56+
theme: { semantics },
57+
} = useTheme();
58+
59+
return useMemo(() => {
60+
return StyleSheet.create({
61+
channelDetails: {
62+
fontSize: 14,
63+
textAlign: 'center',
64+
},
65+
channelTitle: {
66+
color: semantics.textSecondary,
67+
fontSize: primitives.typographyFontSizeMd,
68+
fontWeight: primitives.typographyFontWeightRegular,
69+
lineHeight: primitives.typographyLineHeightNormal,
70+
textAlign: 'center',
71+
paddingVertical: primitives.spacingSm,
72+
},
73+
container: {
74+
alignItems: 'center',
75+
flex: 1,
76+
justifyContent: 'center',
77+
},
78+
messageTitle: {
79+
fontSize: 20,
80+
fontWeight: 'bold',
81+
paddingBottom: 8,
82+
},
83+
threadText: {
84+
color: semantics.textSecondary,
85+
fontSize: primitives.typographyFontSizeMd,
86+
fontWeight: primitives.typographyFontWeightRegular,
87+
lineHeight: primitives.typographyLineHeightNormal,
88+
textAlign: 'center',
89+
paddingVertical: primitives.spacingSm,
90+
},
91+
});
92+
}, [semantics]);
93+
};

0 commit comments

Comments
 (0)