Skip to content

Commit d60dd9b

Browse files
Unread badges and loadUnreadThreads button \w icon
1 parent 386105c commit d60dd9b

File tree

9 files changed

+184
-136
lines changed

9 files changed

+184
-136
lines changed

examples/vite/src/App.tsx

+19-17
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
ThreadList,
1414
ThreadProvider,
1515
} from 'stream-chat-react';
16-
import 'stream-chat-react/css/v2/index.css';
16+
import '@stream-io/stream-chat-css/dist/v2/css/index.css';
1717

1818
const params = (new Proxy(new URLSearchParams(window.location.search), {
1919
get: (searchParams, property) => searchParams.get(property as string),
@@ -65,20 +65,22 @@ const App = () => {
6565

6666
return (
6767
<Chat client={chatClient}>
68-
{!threadOnly && (
69-
<>
70-
<ChannelList filters={filters} options={options} sort={sort} />
71-
<Channel>
72-
<Window>
73-
<ChannelHeader />
74-
<MessageList returnAllReadData />
75-
<MessageInput focus />
76-
</Window>
77-
<Thread />
78-
</Channel>
79-
</>
80-
)}
81-
{threadOnly && <Threads />}
68+
<div className='str-chat'>
69+
{!threadOnly && (
70+
<>
71+
<ChannelList filters={filters} options={options} sort={sort} />
72+
<Channel>
73+
<Window>
74+
<ChannelHeader />
75+
<MessageList returnAllReadData />
76+
<MessageInput focus />
77+
</Window>
78+
<Thread virtualized />
79+
</Channel>
80+
</>
81+
)}
82+
{threadOnly && <Threads />}
83+
</div>
8284
</Chat>
8385
);
8486
};
@@ -87,8 +89,8 @@ const Threads = () => {
8789
const [state, setState] = useState<ThreadType | undefined>(undefined);
8890

8991
return (
90-
<div className='str-chat threads'>
91-
<ThreadList onItemPointerDown={(_, thread) => setState(thread)} />
92+
<div className='str-chat__threads'>
93+
<ThreadList threadListItemProps={{ onPointerDown: (_, t) => setState(t) }} />
9294
<ThreadProvider thread={state}>
9395
<Thread virtualized />
9496
</ThreadProvider>

examples/vite/src/index.scss

+17-75
Original file line numberDiff line numberDiff line change
@@ -16,70 +16,10 @@ body,
1616
display: flex;
1717
height: 100%;
1818

19-
// .str-chat__thread-list {
20-
// width: 50%;
21-
// height: 100%;
22-
// }
23-
24-
.str-chat__thread-list-item {
25-
all: unset;
26-
box-sizing: border-box;
27-
padding-block: 14px;
28-
padding-inline: 8px;
29-
gap: 6px;
19+
& > div.str-chat {
20+
height: 100%;
3021
width: 100%;
3122
display: flex;
32-
flex-direction: column;
33-
cursor: pointer;
34-
}
35-
36-
.str-chat__thread-list-item__channel {
37-
font-size: 14px;
38-
font-weight: 400;
39-
}
40-
41-
.str-chat__thread-list-item__parent-message {
42-
font-size: 12px;
43-
font-weight: 400;
44-
overflow: hidden;
45-
white-space: nowrap;
46-
text-overflow: ellipsis;
47-
}
48-
49-
.str-chat__thread-list-item__latest-reply-container {
50-
display: flex;
51-
align-items: center;
52-
gap: 5px;
53-
}
54-
55-
.str-chat__thread-list-item__latest-reply-details {
56-
display: flex;
57-
flex-direction: column;
58-
flex-grow: 1;
59-
gap: 4px;
60-
width: 0;
61-
}
62-
63-
.str-chat__thread-list-item__latest-reply-created-by {
64-
font-weight: 500;
65-
font-size: 16px;
66-
}
67-
68-
.str-chat__thread-list-item__latest-reply-text {
69-
display: flex;
70-
font-size: 14px;
71-
font-weight: 400;
72-
justify-content: space-between;
73-
74-
& > div:first-child {
75-
overflow: hidden;
76-
white-space: nowrap;
77-
text-overflow: ellipsis;
78-
}
79-
80-
& > div:last-child {
81-
white-space: nowrap;
82-
}
8323
}
8424

8525
.str-chat__channel-list {
@@ -122,18 +62,6 @@ body,
12262
}
12363
}
12464

125-
.str-chat.threads {
126-
display: flex;
127-
height: 100%;
128-
width: 100%;
129-
130-
.vml {
131-
display: flex;
132-
flex-direction: column;
133-
width: 70%;
134-
}
135-
}
136-
13765
@media screen and (min-width: 768px) {
13866
//.str-chat__channel-list.thread-open {
13967
// &.menu-open {
@@ -177,4 +105,18 @@ body,
177105
display: none;
178106
}
179107
}
180-
}
108+
}
109+
110+
.str-chat__threads {
111+
display: flex;
112+
width: 100%;
113+
114+
.str-chat__thread {
115+
width: 100%;
116+
}
117+
118+
.str-chat__thread-list {
119+
height: 100%;
120+
max-width: 420px;
121+
}
122+
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
"@semantic-release/changelog": "^6.0.2",
149149
"@semantic-release/git": "^10.0.1",
150150
"@stream-io/rollup-plugin-node-builtins": "^2.1.5",
151-
"@stream-io/stream-chat-css": "^4.16.1",
151+
"@stream-io/stream-chat-css": "link:../stream-chat-css/",
152152
"@testing-library/jest-dom": "^6.1.4",
153153
"@testing-library/react": "^13.1.1",
154154
"@testing-library/react-hooks": "^8.0.0",

src/components/MessageList/MessageList.tsx

+2-7
Original file line numberDiff line numberDiff line change
@@ -225,18 +225,13 @@ const MessageListWithContext = <
225225
<UnreadMessagesNotification unreadCount={channelUnreadUiState?.unread_messages} />
226226
)}
227227
<div
228-
className={clsx(messageListClass, {
229-
[customClasses?.threadList || 'str-chat__thread-list']: threadList,
230-
})}
228+
className={clsx(messageListClass, customClasses?.threadList)}
231229
onScroll={onScroll}
232230
ref={setListElement}
233231
tabIndex={0}
234232
>
235233
{showEmptyStateIndicator ? (
236-
<EmptyStateIndicator
237-
key={'empty-state-indicator'}
238-
listType={threadList ? 'thread' : 'message'}
239-
/>
234+
<EmptyStateIndicator listType={threadList ? 'thread' : 'message'} />
240235
) : (
241236
<InfiniteScroll
242237
className='str-chat__message-list-scroll'

src/components/Threads/ThreadContext.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const useThreadContext = () => {
2020
const thread = useContext(ThreadContext);
2121

2222
const placeholder = useMemo(
23-
() => new Thread({ client, registerEventHandlers: false, threadData: {} }),
23+
() => new Thread({ client, registerSubscriptions: false, threadData: {} }),
2424
[client],
2525
);
2626

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { useEffect } from 'react';
2-
import { ComputeItemKey, Virtuoso } from 'react-virtuoso';
2+
import { ComputeItemKey, Virtuoso, VirtuosoProps } from 'react-virtuoso';
33

44
import type { ComponentType, PointerEvent } from 'react';
55
import type { InferStoreValueType, Thread, ThreadManager } from 'stream-chat';
66

77
import { ThreadListItem } from './ThreadListItem';
8+
import { Icon } from '../icons';
89
import { useChatContext } from '../../../context';
910
import { useSimpleStateStore } from '../hooks/useSimpleStateStore';
1011

@@ -23,40 +24,63 @@ import type { ThreadListItemProps } from './ThreadListItem';
2324
* - probably good idea to move component context up to a Chat component
2425
*/
2526

26-
const selector = (nextValue: InferStoreValueType<ThreadManager>) => [nextValue.threads] as const;
27+
const selector = (nextValue: InferStoreValueType<ThreadManager>) =>
28+
[nextValue.unreadThreads.newIds, nextValue.threads] as const;
2729

2830
const computeItemKey: ComputeItemKey<Thread, unknown> = (_, item) => item.id;
2931

3032
type ThreadListProps = {
31-
onItemPointerDown?: (event: PointerEvent<HTMLButtonElement>, thread: Thread) => void;
3233
ThreadListItem?: ComponentType<ThreadListItemProps>;
33-
// threads?: Thread[]
34+
threadListItemProps?: Omit<ThreadListItemProps, 'thread' | 'onPointerDown'> & {
35+
onPointerDown?: (event: PointerEvent<HTMLButtonElement>, thread: Thread) => void;
36+
};
37+
virtuosoProps?: VirtuosoProps<Thread, unknown>;
3438
};
3539

3640
export const ThreadList = ({
3741
ThreadListItem: PropsThreadListItem = ThreadListItem,
38-
onItemPointerDown,
42+
virtuosoProps,
43+
threadListItemProps: { onPointerDown, ...restThreadListItemProps } = {},
3944
}: ThreadListProps) => {
4045
const { client } = useChatContext();
41-
const [threads] = useSimpleStateStore(client.threads.state, selector);
46+
const [unreadThreadIds, threads] = useSimpleStateStore(client.threads.state, selector);
4247

4348
useEffect(() => {
4449
client.threads.loadNextPage();
4550
}, [client]);
4651

4752
return (
48-
<Virtuoso
49-
atBottomStateChange={(atBottom) => atBottom && client.threads.loadNextPage()}
50-
className='str-chat str-chat__thread-list'
51-
computeItemKey={computeItemKey}
52-
data={threads}
53-
itemContent={(_, thread) => (
54-
<PropsThreadListItem
55-
onPointerDown={(e) => onItemPointerDown?.(e, thread)}
56-
thread={thread}
57-
/>
53+
<div className='str-chat__thread-list-container'>
54+
{/* TODO: create a replaceable banner component, wait for BE to support "in" keyword for query threads */}
55+
{/* TODO: use query threads with limit (unreadThreadsId.length) - should be top of the list, and prepend
56+
- this does not work when we reply to an non-loaded thread and then reply to a loaded thread
57+
- querying afterwards will return only the latest, which was already in the list but not the one we need
58+
*/}
59+
{unreadThreadIds.length > 0 && (
60+
<div className='str-chat__unread-threads-banner'>
61+
{unreadThreadIds.length} unread threads
62+
<button
63+
className='str-chat__unread-threads-banner__button'
64+
onClick={client.threads.loadUnreadThreads}
65+
>
66+
<Icon.Reload />
67+
</button>
68+
</div>
5869
)}
59-
style={{ height: '100%', width: '50%' }}
60-
/>
70+
<Virtuoso
71+
atBottomStateChange={(atBottom) => atBottom && client.threads.loadNextPage()}
72+
className='str-chat__thread-list'
73+
computeItemKey={computeItemKey}
74+
data={threads}
75+
itemContent={(_, thread) => (
76+
<PropsThreadListItem
77+
onPointerDown={(e) => onPointerDown?.(e, thread)}
78+
thread={thread}
79+
{...restThreadListItemProps}
80+
/>
81+
)}
82+
{...virtuosoProps}
83+
/>
84+
</div>
6185
);
6286
};

0 commit comments

Comments
 (0)