Skip to content

Commit ae50c38

Browse files
Merge pull request #619 from GetStream/thread-typing-events
CRNS-196: Support typing events in threads
2 parents e193e51 + bc69d7f commit ae50c38

File tree

5 files changed

+127
-31
lines changed

5 files changed

+127
-31
lines changed

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { SafeAreaView } from 'react-native-safe-area-context';
44
import {
55
Channel,
66
Thread,
7+
ThreadContextValue,
78
useAttachmentPickerContext,
89
useTheme,
10+
useTypingString,
911
} from 'stream-chat-react-native';
1012

1113
import { ScreenHeader } from '../components/ScreenHeader';
@@ -34,6 +36,31 @@ type ThreadScreenRouteProp = RouteProp<StackNavigatorParamList, 'ThreadScreen'>;
3436
type ThreadScreenProps = {
3537
route: ThreadScreenRouteProp;
3638
};
39+
40+
export type ThreadHeaderProps = {
41+
thread: ThreadContextValue<
42+
LocalAttachmentType,
43+
LocalChannelType,
44+
LocalCommandType,
45+
LocalEventType,
46+
LocalMessageType,
47+
LocalReactionType,
48+
LocalUserType
49+
>['thread'];
50+
};
51+
52+
const ThreadHeader: React.FC<ThreadHeaderProps> = ({ thread }) => {
53+
const typing = useTypingString();
54+
55+
return (
56+
<ScreenHeader
57+
inSafeArea
58+
titleText='Thread Reply'
59+
subtitleText={typing ? typing : `with ${thread?.user?.name}`}
60+
/>
61+
);
62+
};
63+
3764
export const ThreadScreen: React.FC<ThreadScreenProps> = ({
3865
route: {
3966
params: { channel, thread },
@@ -68,11 +95,7 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
6895
thread={thread}
6996
>
7097
<View style={styles.container}>
71-
<ScreenHeader
72-
inSafeArea
73-
subtitleText={`with ${thread?.user?.name}`}
74-
titleText='Thread Reply'
75-
/>
98+
<ThreadHeader thread={thread} />
7699
<Thread<
77100
LocalAttachmentType,
78101
LocalChannelType,

src/components/MessageList/TypingIndicatorContainer.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React, { PropsWithChildren } from 'react';
22
import { StyleSheet, View } from 'react-native';
33

4+
import { filterTypingUsers } from './utils/filterTypingUsers';
5+
46
import {
57
ChatContextValue,
68
useChatContext,
79
} from '../../contexts/chatContext/ChatContext';
810
import { useTheme } from '../../contexts/themeContext/ThemeContext';
11+
import {
12+
ThreadContextValue,
13+
useThreadContext,
14+
} from '../../contexts/threadContext/ThreadContext';
915
import {
1016
TypingContextValue,
1117
useTypingContext,
@@ -39,7 +45,8 @@ type TypingIndicatorContainerPropsWithContext<
3945
Re extends UnknownType = DefaultReactionType,
4046
Us extends DefaultUserType = DefaultUserType
4147
> = Pick<TypingContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'typing'> &
42-
Pick<ChatContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'client'>;
48+
Pick<ChatContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'client'> &
49+
Pick<ThreadContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'thread'>;
4350

4451
const TypingIndicatorContainerWithContext = <
4552
At extends UnknownType = DefaultAttachmentType,
@@ -54,19 +61,16 @@ const TypingIndicatorContainerWithContext = <
5461
TypingIndicatorContainerPropsWithContext<At, Ch, Co, Ev, Me, Re, Us>
5562
>,
5663
) => {
57-
const { children, client, typing } = props;
64+
const { children, client, thread, typing } = props;
5865

5966
const {
6067
theme: {
6168
messageList: { typingIndicatorContainer },
6269
},
6370
} = useTheme();
64-
const typingUsers = Object.values(typing);
71+
const typingUsers = filterTypingUsers({ client, thread, typing });
6572

66-
if (
67-
!typingUsers.length ||
68-
(typingUsers.length === 1 && typingUsers[0].user?.id === client?.user?.id)
69-
) {
73+
if (!typingUsers.length) {
7074
return null;
7175
}
7276

@@ -107,9 +111,13 @@ export const TypingIndicatorContainer = <
107111
) => {
108112
const { typing } = useTypingContext<At, Ch, Co, Ev, Me, Re, Us>();
109113
const { client } = useChatContext<At, Ch, Co, Ev, Me, Re, Us>();
114+
const { thread } = useThreadContext<At, Ch, Co, Ev, Me, Re, Us>();
110115

111116
return (
112-
<TypingIndicatorContainerWithContext {...{ client, typing }} {...props} />
117+
<TypingIndicatorContainerWithContext
118+
{...{ client, thread, typing }}
119+
{...props}
120+
/>
113121
);
114122
};
115123

src/components/MessageList/hooks/useTypingString.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { filterTypingUsers } from '../utils/filterTypingUsers';
2+
13
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
4+
import { useThreadContext } from '../../../contexts/threadContext/ThreadContext';
25
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
36
import { useTypingContext } from '../../../contexts/typingContext/TypingContext';
47

@@ -23,34 +26,24 @@ export const useTypingString = <
2326
Us extends UnknownType = DefaultUserType
2427
>() => {
2528
const { client } = useChatContext<At, Ch, Co, Ev, Me, Re, Us>();
29+
const { thread } = useThreadContext<At, Ch, Co, Ev, Me, Re, Us>();
2630
const { t } = useTranslationContext();
2731
const { typing } = useTypingContext<At, Ch, Co, Ev, Me, Re, Us>();
2832

29-
const typingKeys = Object.keys(typing);
30-
const nonSelfUsers: string[] = [];
31-
typingKeys.forEach((typingKey) => {
32-
if (client?.user?.id === typing?.[typingKey]?.user?.id) {
33-
return;
34-
}
35-
const user =
36-
typing?.[typingKey]?.user?.name || typing?.[typingKey]?.user?.id;
37-
if (user) {
38-
nonSelfUsers.push(user);
39-
}
40-
});
33+
const filteredTypingUsers = filterTypingUsers({ client, thread, typing });
4134

42-
if (nonSelfUsers.length === 1) {
43-
return t('{{ user }} is typing', { user: nonSelfUsers[0] });
35+
if (filteredTypingUsers.length === 1) {
36+
return t('{{ user }} is typing', { user: filteredTypingUsers[0] });
4437
}
4538

46-
if (nonSelfUsers.length > 1) {
39+
if (filteredTypingUsers.length > 1) {
4740
/**
4841
* Joins the multiple names with number after first name
4942
* example: "Dan and Neil"
5043
*/
5144
return t('{{ firstUser }} and {{ nonSelfUserLength }} more are typing', {
52-
firstUser: nonSelfUsers[0],
53-
nonSelfUserLength: nonSelfUsers.length - 1,
45+
firstUser: filteredTypingUsers[0],
46+
nonSelfUserLength: filteredTypingUsers.length - 1,
5447
});
5548
}
5649

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { ChatContextValue } from '../../../contexts/chatContext/ChatContext';
2+
import type { ThreadContextValue } from '../../../contexts/threadContext/ThreadContext';
3+
import type { TypingContextValue } from '../../../contexts/typingContext/TypingContext';
4+
import type {
5+
DefaultAttachmentType,
6+
DefaultChannelType,
7+
DefaultCommandType,
8+
DefaultEventType,
9+
DefaultMessageType,
10+
DefaultReactionType,
11+
DefaultUserType,
12+
UnknownType,
13+
} from '../../../types/types';
14+
15+
type FilterTypingUsersParams<
16+
At extends UnknownType = DefaultAttachmentType,
17+
Ch extends UnknownType = DefaultChannelType,
18+
Co extends string = DefaultCommandType,
19+
Ev extends UnknownType = DefaultEventType,
20+
Me extends UnknownType = DefaultMessageType,
21+
Re extends UnknownType = DefaultReactionType,
22+
Us extends UnknownType = DefaultUserType
23+
> = Pick<TypingContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'typing'> &
24+
Pick<ChatContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'client'> &
25+
Pick<ThreadContextValue<At, Ch, Co, Ev, Me, Re, Us>, 'thread'>;
26+
27+
export const filterTypingUsers = <
28+
At extends UnknownType = DefaultAttachmentType,
29+
Ch extends UnknownType = DefaultChannelType,
30+
Co extends string = DefaultCommandType,
31+
Ev extends UnknownType = DefaultEventType,
32+
Me extends UnknownType = DefaultMessageType,
33+
Re extends UnknownType = DefaultReactionType,
34+
Us extends UnknownType = DefaultUserType
35+
>({
36+
client,
37+
thread,
38+
typing,
39+
}: FilterTypingUsersParams<At, Ch, Co, Ev, Me, Re, Us>) => {
40+
const nonSelfUsers: string[] = [];
41+
42+
if (!client || !client.user || !typing) return nonSelfUsers;
43+
44+
const typingKeys = Object.keys(typing);
45+
46+
typingKeys.forEach((typingKey) => {
47+
if (!typing[typingKey]) return;
48+
49+
/** removes own typing events */
50+
if (client.user?.id === typing[typingKey].user?.id) {
51+
return;
52+
}
53+
54+
const isRegularEvent = !typing[typingKey].parent_id && !thread?.id;
55+
const isCurrentThreadEvent = typing[typingKey].parent_id === thread?.id;
56+
57+
/** filters different threads events */
58+
if (!isRegularEvent && !isCurrentThreadEvent) {
59+
return;
60+
}
61+
62+
const user = typing[typingKey].user?.name || typing[typingKey].user?.id;
63+
if (user) {
64+
nonSelfUsers.push(user);
65+
}
66+
});
67+
68+
return nonSelfUsers;
69+
};

src/contexts/messageInputContext/MessageInputContext.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,10 @@ export const MessageInputProvider = <
547547
setText(newText);
548548

549549
if (newText && channel) {
550-
logChatPromiseExecution(channel.keystroke(), 'start typing event');
550+
logChatPromiseExecution(
551+
channel.keystroke(thread?.id),
552+
'start typing event',
553+
);
551554
}
552555

553556
if (value.onChangeText) {

0 commit comments

Comments
 (0)