Skip to content

Commit 894d3bb

Browse files
Adjust ThreadLIstUnseenThreadsBanner and add item highlighting
1 parent 8c7847a commit 894d3bb

5 files changed

Lines changed: 139 additions & 28 deletions

File tree

src/components/Threads/ThreadList/ThreadList.tsx

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import type { ComputeItemKey, VirtuosoProps } from 'react-virtuoso';
33
import { Virtuoso } from 'react-virtuoso';
44
import clsx from 'clsx';
55

6-
import type { Thread, ThreadManagerState } from 'stream-chat';
6+
import type { Thread, ThreadManager, ThreadManagerState } from 'stream-chat';
77

88
import { ThreadListItem as DefaultThreadListItem } from './ThreadListItem';
99
import { ThreadListEmptyPlaceholder as DefaultThreadListEmptyPlaceholder } from './ThreadListEmptyPlaceholder';
@@ -30,12 +30,12 @@ export const useThreadList = () => {
3030

3131
useEffect(() => {
3232
const handleVisibilityChange = () => {
33-
if (document.visibilityState === 'visible') {
34-
client.threads.activate();
35-
}
36-
if (document.visibilityState === 'hidden') {
37-
client.threads.deactivate();
38-
}
33+
// if (document.visibilityState === 'visible') {
34+
client.threads.activate();
35+
// }
36+
// if (document.visibilityState === 'hidden') {
37+
// client.threads.deactivate();
38+
// }
3939
};
4040

4141
handleVisibilityChange();
@@ -48,6 +48,41 @@ export const useThreadList = () => {
4848
}, [client]);
4949
};
5050

51+
const useThreadHighlighting = (threadManager: ThreadManager) => {
52+
const [threadsToHighlight, setThreadsToHighlight] = useState<
53+
Record<string, () => void>
54+
>({});
55+
56+
useEffect(() => {
57+
const unsubscribe = threadManager.state.subscribeWithSelector(
58+
(state) => state.threads,
59+
(nextThreads, previousThreads) => {
60+
if (!previousThreads) return;
61+
62+
const resetByThreadId: Record<string, () => void> = {};
63+
64+
for (const thread of nextThreads) {
65+
if (previousThreads.includes(thread)) continue;
66+
67+
resetByThreadId[thread.id] = () => {
68+
setThreadsToHighlight((pv) => {
69+
const copy = { ...pv };
70+
delete copy[thread.id];
71+
return copy;
72+
});
73+
};
74+
}
75+
76+
setThreadsToHighlight(resetByThreadId);
77+
},
78+
);
79+
80+
return unsubscribe;
81+
});
82+
83+
return threadsToHighlight;
84+
};
85+
5186
export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
5287
const { client, navOpen = true } = useChatContext();
5388
const {
@@ -58,6 +93,8 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
5893
} = useComponentContext();
5994
const { isLoading, threads } = useStateStore(client.threads.state, selector);
6095

96+
const resetByThreadId = useThreadHighlighting(client.threads);
97+
6198
useThreadList();
6299

63100
if (isLoading && !threads.length) {
@@ -93,7 +130,14 @@ export const ThreadList = ({ virtuosoProps }: ThreadListProps) => {
93130
}}
94131
computeItemKey={computeItemKey}
95132
data={threads}
96-
itemContent={(_, thread) => <ThreadListItem thread={thread} />}
133+
itemContent={(_, thread) => (
134+
<ThreadListItem
135+
thread={thread}
136+
threadListItemUIProps={{
137+
resetHighlighting: resetByThreadId[thread.id],
138+
}}
139+
/>
140+
)}
97141
// TODO: handle visibility (for a button that scrolls to the unread thread)
98142
// itemsRendered={(items) => console.log({ items })}
99143
{...virtuosoProps}

src/components/Threads/ThreadList/ThreadListItemUI.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import React, { useCallback, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo } from 'react';
2+
import clsx from 'clsx';
23

34
import type { ThreadState } from 'stream-chat';
45
import type { ComponentPropsWithoutRef } from 'react';
@@ -14,9 +15,14 @@ import { Badge } from '../../Badge';
1415
import { SummarizedMessagePreview } from '../../SummarizedMessagePreview';
1516
import { NAV_SIDEBAR_DESKTOP_BREAKPOINT } from '../../Chat';
1617

17-
export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'>;
18+
export type ThreadListItemUIProps = ComponentPropsWithoutRef<'button'> & {
19+
resetHighlighting?: () => void;
20+
};
1821

19-
export const ThreadListItemUI = (props: ThreadListItemUIProps) => {
22+
export const ThreadListItemUI = ({
23+
resetHighlighting,
24+
...props
25+
}: ThreadListItemUIProps) => {
2026
const { client } = useChatContext();
2127
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2228
const thread = useThreadListItemContext()!;
@@ -69,11 +75,26 @@ export const ThreadListItemUI = (props: ThreadListItemUIProps) => {
6975
}));
7076
}, [participants]);
7177

78+
useEffect(() => {
79+
if (!resetHighlighting) return;
80+
81+
const reset = resetHighlighting;
82+
83+
const timeout = setTimeout(() => {
84+
reset();
85+
}, 2000);
86+
87+
return () => clearTimeout(timeout);
88+
}, [resetHighlighting]);
89+
7290
return (
7391
<div className='str-chat__thread-list-item-container'>
7492
<button
7593
aria-pressed={activeThread === thread}
76-
className='str-chat__thread-list-item'
94+
className={clsx('str-chat__thread-list-item', {
95+
'str-chat__thread-list-item--highlighted':
96+
typeof resetHighlighting !== 'undefined',
97+
})}
7798
data-thread-id={thread.id}
7899
onClick={() => {
79100
if (
Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
11
import React from 'react';
2+
import clsx from 'clsx';
23

34
import type { ThreadManagerState } from 'stream-chat';
45

5-
import { IconArrowRotateClockwise } from '../../Icons';
6-
import { useChatContext } from '../../../context';
6+
import { IconArrowRotateRightLeftRepeatRefresh } from '../../Icons';
7+
import { useChatContext, useTranslationContext } from '../../../context';
78
import { useStateStore } from '../../../store';
9+
import { LoadingIndicator } from '../../Loading';
810

911
const selector = (nextValue: ThreadManagerState) => ({
12+
isLoading: nextValue.pagination.isLoading,
1013
unseenThreadIds: nextValue.unseenThreadIds,
1114
});
1215

1316
export const ThreadListUnseenThreadsBanner = () => {
1417
const { client } = useChatContext();
15-
const { unseenThreadIds } = useStateStore(client.threads.state, selector);
18+
const { t } = useTranslationContext();
19+
const { isLoading, unseenThreadIds } = useStateStore(client.threads.state, selector);
1620

1721
if (!unseenThreadIds.length) return null;
1822

1923
return (
20-
<div className='str-chat__unseen-threads-banner'>
21-
{/* TODO: translate */}
22-
{unseenThreadIds.length} unread threads
23-
<button
24-
className='str-chat__unseen-threads-banner__button'
25-
onClick={() => client.threads.reload()}
26-
>
27-
<IconArrowRotateClockwise />
28-
</button>
29-
</div>
24+
<button
25+
className={clsx('str-chat__unseen-threads-banner', {
26+
'str-chat__unseen-threads-banner--loading': isLoading,
27+
})}
28+
disabled={isLoading}
29+
onClick={() => client.threads.reload()}
30+
>
31+
{!isLoading && (
32+
<>
33+
<IconArrowRotateRightLeftRepeatRefresh />
34+
<span>
35+
{t('ThreadListUnseenThreadsBanner/unreadThreads', {
36+
count: unseenThreadIds.length,
37+
})}
38+
</span>
39+
</>
40+
)}
41+
{isLoading && (
42+
<>
43+
<LoadingIndicator />
44+
<span>{t('ThreadListUnseenThreadsBanner/loading')}</span>
45+
</>
46+
)}
47+
</button>
3048
);
3149
};

src/components/Threads/ThreadList/styling/ThreadList.scss

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,31 @@
111111
@media (prefers-reduced-motion: reduce) {
112112
transition: none;
113113
}
114+
115+
.str-chat__unseen-threads-banner {
116+
@include utils.unset-button;
117+
118+
font: var(--str-chat__metadata-emphasis-text);
119+
cursor: pointer;
120+
display: flex;
121+
color: var(--text-secondary);
122+
height: 36px;
123+
justify-content: center;
124+
align-items: center;
125+
gap: var(--spacing-xs);
126+
border-radius: var(--radius-none);
127+
background: var(--background-core-surface);
128+
position: relative;
129+
130+
& > .str-chat__icon {
131+
height: var(--icon-size-md);
132+
width: var(--icon-size-md);
133+
}
134+
135+
&:not(:disabled):hover {
136+
@include utils.overlay-after(var(--background-core-hover));
137+
}
138+
}
114139
}
115140

116141
.str-chat__thread-list {

src/components/Threads/ThreadList/styling/ThreadListItemUI.scss

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
border-bottom: 1px solid var(--border-core-subtle);
55
padding: var(--spacing-xxs);
66
max-width: 100%;
7+
8+
&:has(.str-chat__thread-list-item--highlighted) {
9+
background: var(--background-core-highlight);
10+
}
711
}
812

913
.str-chat__thread-list-item {
@@ -16,12 +20,11 @@
1620
border: none;
1721
cursor: pointer;
1822
text-align: start;
19-
background: var(--background-elevation-elevation-1);
23+
background: var(--base-transparent-0);
2024
border-radius: var(--radius-lg);
2125
width: 100%;
2226
max-width: 100%;
2327

24-
background: var(--background-elevation-elevation-1);
2528
&:not(:disabled):hover {
2629
background: var(--background-core-hover);
2730
}

0 commit comments

Comments
 (0)