Skip to content

Commit d562d62

Browse files
authored
Merge pull request #662 from GetStream/thread-typing-indicator
Thread typing indicator
2 parents 5639e10 + 0c5f0fe commit d562d62

File tree

4 files changed

+114
-13
lines changed

4 files changed

+114
-13
lines changed

src/components/MessageList/MessageListInner.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ const getGroupStyles = (
176176

177177
const MessageListInner = (props) => {
178178
const {
179-
TypingIndicator,
180179
EmptyStateIndicator,
181180
MessageSystem,
182181
DateSeparator,
183182
HeaderComponent,
183+
TypingIndicator,
184184
headerPosition,
185185
bottomRef,
186186
onMessageLoadCaptured,
@@ -306,7 +306,7 @@ const MessageListInner = (props) => {
306306
{...internalInfiniteScrollProps}
307307
>
308308
<ul className="str-chat__ul">{elements}</ul>
309-
{!threadList && <TypingIndicator />}
309+
<TypingIndicator threadList={threadList} />
310310
<div key="bottom" ref={bottomRef} />
311311
</InfiniteScroll>
312312
);

src/components/TypingIndicator/TypingIndicator.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,41 @@ import { Avatar as DefaultAvatar } from '../Avatar';
99
* @typedef {import('types').TypingIndicatorProps} Props
1010
* @type {React.FC<Props>}
1111
*/
12-
const TypingIndicator = ({ Avatar = DefaultAvatar, avatarSize = 32 }) => {
13-
const { typing, client, channel } = useContext(ChannelContext);
12+
const TypingIndicator = ({
13+
Avatar = DefaultAvatar,
14+
avatarSize = 32,
15+
threadList,
16+
}) => {
17+
const { channel, client, thread, typing } = useContext(ChannelContext);
1418

15-
if (!typing || !client || channel?.getConfig()?.typing_events === false)
19+
if (!typing || !client || channel?.getConfig()?.typing_events === false) {
1620
return null;
21+
}
1722

18-
const users = Object.values(typing).filter(
19-
({ user }) => user?.id !== client.user?.id,
23+
const typingInChannel = Object.values(typing).filter(
24+
({ user, parent_id }) => user?.id !== client.user?.id && parent_id == null,
25+
);
26+
27+
const typingInThread = Object.values(typing).some(
28+
(event) => event?.parent_id === thread?.id,
2029
);
2130

2231
return (
2332
<div
2433
className={`str-chat__typing-indicator ${
25-
users.length ? 'str-chat__typing-indicator--typing' : ''
34+
(threadList && typingInThread) ||
35+
(!threadList && typingInChannel.length)
36+
? 'str-chat__typing-indicator--typing'
37+
: ''
2638
}`}
2739
>
2840
<div className="str-chat__typing-indicator__avatars">
29-
{users.map(({ user }) => (
41+
{typingInChannel.map(({ user }, i) => (
3042
<Avatar
3143
image={user?.image}
3244
size={avatarSize}
3345
name={user?.name || user?.id}
34-
key={user?.id}
46+
key={`${user?.id}-${i}`}
3547
/>
3648
))}
3749
</div>

src/components/TypingIndicator/__tests__/TypingIndicator.test.js

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ afterEach(cleanup); // eslint-disable-line
1919

2020
const alice = generateUser();
2121

22-
async function renderComponent(typing = {}) {
22+
async function renderComponent(typing = {}, threadList, value = {}) {
2323
const client = await getTestClientWithUser(alice);
2424

2525
return render(
26-
<ChannelContext.Provider value={{ client, typing }}>
27-
<TypingIndicator />
26+
<ChannelContext.Provider value={{ client, typing, ...value }}>
27+
<TypingIndicator threadList={threadList} />
2828
</ChannelContext.Provider>,
2929
);
3030
}
@@ -139,4 +139,92 @@ describe('TypingIndicator', () => {
139139

140140
expect(tree).toMatchInlineSnapshot(`null`);
141141
});
142+
143+
describe('TypingIndicator in thread', () => {
144+
let client;
145+
let ch;
146+
let channel;
147+
148+
beforeEach(async () => {
149+
client = await getTestClientWithUser();
150+
ch = generateChannel({ config: { typing_events: true } });
151+
useMockedApis(client, [getOrCreateChannelApi(ch)]);
152+
channel = client.channel('messaging', ch.id);
153+
await channel.watch();
154+
});
155+
156+
afterEach(cleanup);
157+
158+
it('should render TypingIndicator if user is typing in thread', async () => {
159+
const { container } = await renderComponent(
160+
{ example: { parent_id: 'sample-thread', user: 'test-user' } },
161+
true,
162+
{
163+
client,
164+
channel,
165+
thread: { id: 'sample-thread' },
166+
},
167+
);
168+
169+
expect(
170+
container.firstChild.classList.contains(
171+
'str-chat__typing-indicator--typing',
172+
),
173+
).toBe(true);
174+
});
175+
176+
it('should not render TypingIndicator in main channel if user is typing in thread', async () => {
177+
const { container } = await renderComponent(
178+
{ example: { parent_id: 'sample-thread', user: 'test-user' } },
179+
false,
180+
{
181+
client,
182+
channel,
183+
thread: { id: 'sample-thread' },
184+
},
185+
);
186+
187+
expect(
188+
container.firstChild.classList.contains(
189+
'str-chat__typing-indicator--typing',
190+
),
191+
).toBe(false);
192+
});
193+
194+
it('should not render TypingIndicator in thread if user is typing in main channel', async () => {
195+
const { container } = await renderComponent(
196+
{ example: { user: 'test-user' } },
197+
true,
198+
{
199+
client,
200+
channel,
201+
thread: { id: 'sample-thread' },
202+
},
203+
);
204+
205+
expect(
206+
container.firstChild.classList.contains(
207+
'str-chat__typing-indicator--typing',
208+
),
209+
).toBe(false);
210+
});
211+
212+
it('should not render TypingIndicator in thread if user is typing in another thread', async () => {
213+
const { container } = await renderComponent(
214+
{ example: { parent_id: 'sample-thread-2', user: 'test-user' } },
215+
true,
216+
{
217+
client,
218+
channel,
219+
thread: { id: 'sample-thread' },
220+
},
221+
);
222+
223+
expect(
224+
container.firstChild.classList.contains(
225+
'str-chat__typing-indicator--typing',
226+
),
227+
).toBe(false);
228+
});
229+
});
142230
});

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ export interface ThreadHeaderProps {
973973
export interface TypingIndicatorProps {
974974
Avatar?: React.ElementType<AvatarProps>;
975975
avatarSize?: number;
976+
threadList?: boolean;
976977
}
977978

978979
export interface ReactionSelectorProps {

0 commit comments

Comments
 (0)