Skip to content

Commit 5ada56e

Browse files
feat: add unread messages divider with theme-aware styling (#1047)
1 parent 1b777fb commit 5ada56e

6 files changed

Lines changed: 120 additions & 15 deletions

File tree

packages/react/src/views/ChatBody/ChatBody.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const ChatBody = ({
4040
showRoles,
4141
messageListRef,
4242
scrollToBottom,
43+
clearUnreadDividerRef,
4344
}) => {
4445
const { classNames, styleOverrides } = useComponentOverrides('ChatBody');
4546
const { theme, mode } = useTheme();
@@ -49,6 +50,8 @@ const ChatBody = ({
4950
const [, setIsUserScrolledUp] = useState(false);
5051
const [otherUserMessage, setOtherUserMessage] = useState(false);
5152
const [isOverflowing, setIsOverflowing] = useState(false);
53+
const [firstUnreadMessageId, setFirstUnreadMessageId] = useState(null);
54+
const pendingFirstUnreadRef = useRef(null);
5255
const { RCInstance, ECOptions } = useContext(RCContext);
5356
const showAnnouncement = ECOptions?.showAnnouncement;
5457
const messages = useMessageStore((state) => state.messages);
@@ -124,6 +127,10 @@ const ChatBody = ({
124127
const isScrolledUp = messageListRef?.current?.scrollTop !== 0;
125128
if (isScrolledUp && !('pinned' in message) && !('starred' in message)) {
126129
setOtherUserMessage(true);
130+
// Track the first unread message (only set if not already tracking)
131+
if (!pendingFirstUnreadRef.current) {
132+
pendingFirstUnreadRef.current = message._id;
133+
}
127134
}
128135
}
129136
upsertMessage(message, ECOptions?.enableThreads);
@@ -177,7 +184,22 @@ const ChatBody = ({
177184
});
178185
}, []);
179186

187+
// Expose clearUnreadDivider function via ref for ChatInput to call
188+
useEffect(() => {
189+
if (clearUnreadDividerRef) {
190+
clearUnreadDividerRef.current = () => {
191+
setFirstUnreadMessageId(null);
192+
pendingFirstUnreadRef.current = null;
193+
};
194+
}
195+
}, [clearUnreadDividerRef]);
196+
180197
const handlePopupClick = () => {
198+
// Set the unread divider to show above the first unread message
199+
if (pendingFirstUnreadRef.current) {
200+
setFirstUnreadMessageId(pendingFirstUnreadRef.current);
201+
pendingFirstUnreadRef.current = null;
202+
}
181203
scrollToBottom();
182204
setIsUserScrolledUp(false);
183205
setOtherUserMessage(false);
@@ -242,6 +264,12 @@ const ChatBody = ({
242264
setPopupVisible(false);
243265
setIsUserScrolledUp(false);
244266
setOtherUserMessage(false);
267+
// Clear unread divider when scrolled to bottom
268+
if (firstUnreadMessageId) {
269+
setFirstUnreadMessageId(null);
270+
}
271+
// Also clear pending unread ref
272+
pendingFirstUnreadRef.current = null;
245273
}
246274
}, [
247275
messageListRef,
@@ -258,6 +286,7 @@ const ChatBody = ({
258286
setIsUserScrolledUp,
259287
setPopupVisible,
260288
setOtherUserMessage,
289+
firstUnreadMessageId,
261290
]);
262291

263292
const showNewMessagesPopup = () => {
@@ -392,6 +421,7 @@ const ChatBody = ({
392421
loadingOlderMessages={loadingOlderMessages}
393422
isUserAuthenticated={isUserAuthenticated}
394423
hasMoreMessages={hasMoreMessages}
424+
firstUnreadMessageId={firstUnreadMessageId}
395425
/>
396426
)}
397427

packages/react/src/views/ChatInput/ChatInput.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import useSearchMentionUser from '../../hooks/useSearchMentionUser';
3535
import formatSelection from '../../lib/formatSelection';
3636
import { parseEmoji } from '../../lib/emoji';
3737

38-
const ChatInput = ({ scrollToBottom }) => {
38+
const ChatInput = ({ scrollToBottom, clearUnreadDividerRef }) => {
3939
const { styleOverrides, classNames } = useComponentOverrides('ChatInput');
4040
const { RCInstance, ECOptions } = useRCContext();
4141
const { theme } = useTheme();
@@ -398,6 +398,10 @@ const ChatInput = ({ scrollToBottom }) => {
398398

399399
handleSendNewMessage(message);
400400
scrollToBottom();
401+
// Clear unread divider when user sends a message
402+
if (clearUnreadDividerRef?.current) {
403+
clearUnreadDividerRef.current();
404+
}
401405
};
402406

403407
const sendAttachment = (event) => {

packages/react/src/views/ChatLayout/ChatLayout.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useCallback, useState } from 'react';
1+
import React, { useEffect, useRef, useCallback } from 'react';
22
import { Box, useComponentOverrides } from '@embeddedchat/ui-elements';
33
import styles from './ChatLayout.styles';
44
import {
@@ -35,6 +35,7 @@ import useUiKitStore from '../../store/uiKitStore';
3535

3636
const ChatLayout = () => {
3737
const messageListRef = useRef(null);
38+
const clearUnreadDividerRef = useRef(null);
3839
const { classNames, styleOverrides } = useComponentOverrides('ChatBody');
3940
const { RCInstance, ECOptions } = useRCContext();
4041
const anonymousMode = ECOptions?.anonymousMode;
@@ -113,8 +114,12 @@ const ChatLayout = () => {
113114
showRoles={showRoles}
114115
messageListRef={messageListRef}
115116
scrollToBottom={scrollToBottom}
117+
clearUnreadDividerRef={clearUnreadDividerRef}
118+
/>
119+
<ChatInput
120+
scrollToBottom={scrollToBottom}
121+
clearUnreadDividerRef={clearUnreadDividerRef}
116122
/>
117-
<ChatInput scrollToBottom={scrollToBottom} />
118123
<div id="emoji-popup" />
119124
</Box>
120125

packages/react/src/views/Message/Message.styles.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,58 @@ export const getMessageDividerStyles = (theme) => {
108108
return styles;
109109
};
110110

111+
export const getUnreadMessageDividerStyles = (theme, mode) => {
112+
// Use destructive (red) for light themes, warningForeground (orange) for dark themes
113+
const dividerColor =
114+
mode === 'light'
115+
? theme.colors.destructive
116+
: theme.colors.warningForeground;
117+
118+
const styles = {
119+
divider: css`
120+
letter-spacing: 0rem;
121+
font-size: 0.75rem;
122+
font-weight: 700;
123+
line-height: 1rem;
124+
position: relative;
125+
display: flex;
126+
z-index: 1000;
127+
align-items: center;
128+
margin-top: 0.5rem;
129+
margin-bottom: 0.75rem;
130+
padding-left: 1.25rem;
131+
padding-right: 1.25rem;
132+
@media (max-width: 780px) {
133+
z-index: 1;
134+
}
135+
`,
136+
137+
dividerContent: css`
138+
margin-top: 0.5rem;
139+
margin-bottom: 0.5rem;
140+
padding-left: 0.5rem;
141+
padding-right: 0.5rem;
142+
background-color: ${theme.colors.background};
143+
color: ${dividerColor};
144+
position: absolute;
145+
left: 50%;
146+
transform: translateX(-50%);
147+
border-radius: ${theme.radius};
148+
`,
149+
150+
bar: css`
151+
display: flex;
152+
justify-content: flex-end;
153+
align-items: center;
154+
flex-grow: 1;
155+
height: 1px;
156+
background-color: ${dividerColor};
157+
`,
158+
};
159+
160+
return styles;
161+
};
162+
111163
export const getMessageHeaderStyles = (theme) => {
112164
const styles = {
113165
header: css`

packages/react/src/views/Message/MessageDivider.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ import {
66
useTheme,
77
} from '@embeddedchat/ui-elements';
88

9-
import { getMessageDividerStyles } from './Message.styles';
9+
import {
10+
getMessageDividerStyles,
11+
getUnreadMessageDividerStyles,
12+
} from './Message.styles';
1013

1114
export const MessageDivider = ({
1215
children,
1316
unreadLabel,
17+
unread = false,
1418
className = '',
1519
style = {},
1620
...props
@@ -20,8 +24,10 @@ export const MessageDivider = ({
2024
className,
2125
style
2226
);
23-
const { theme } = useTheme();
24-
const styles = getMessageDividerStyles(theme);
27+
const { theme, mode } = useTheme();
28+
const styles = unread
29+
? getUnreadMessageDividerStyles(theme, mode)
30+
: getMessageDividerStyles(theme);
2531
return (
2632
<Box
2733
role="separator"

packages/react/src/views/MessageList/MessageList.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import isMessageSequential from '../../lib/isMessageSequential';
99
import { Message } from '../Message';
1010
import isMessageLastSequential from '../../lib/isMessageLastSequential';
1111
import { MessageBody } from '../Message/MessageBody';
12+
import { MessageDivider } from '../Message/MessageDivider';
1213

1314
const MessageList = ({
1415
messages,
1516
loadingOlderMessages,
1617
isUserAuthenticated,
1718
hasMoreMessages,
19+
firstUnreadMessageId,
1820
}) => {
1921
const showReportMessage = useMessageStore((state) => state.showReportMessage);
2022
const messageToReport = useMessageStore((state) => state.messageToReport);
@@ -86,17 +88,23 @@ const MessageList = ({
8688
const sequential = isMessageSequential(msg, prev, 300);
8789
const lastSequential =
8890
sequential && isMessageLastSequential(msg, next);
91+
const showUnreadDivider =
92+
firstUnreadMessageId && msg._id === firstUnreadMessageId;
8993

9094
return (
91-
<Message
92-
key={msg._id}
93-
message={msg}
94-
newDay={newDay}
95-
sequential={sequential}
96-
lastSequential={lastSequential}
97-
type="default"
98-
showAvatar
99-
/>
95+
<React.Fragment key={msg._id}>
96+
{showUnreadDivider && (
97+
<MessageDivider unread>Unread Messages</MessageDivider>
98+
)}
99+
<Message
100+
message={msg}
101+
newDay={newDay}
102+
sequential={sequential}
103+
lastSequential={lastSequential}
104+
type="default"
105+
showAvatar
106+
/>
107+
</React.Fragment>
100108
);
101109
})}
102110
{showReportMessage && (

0 commit comments

Comments
 (0)