Skip to content

Commit 59dd920

Browse files
committed
fix(app): refactor chat
1 parent 8122b05 commit 59dd920

31 files changed

Lines changed: 2550 additions & 2377 deletions

src/components/Chat/Chat.tsx

Lines changed: 399 additions & 1657 deletions
Large diffs are not rendered by default.
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { SanitisiedEmoteSet } from '@app/services/seventv-service';
2+
import { ChatUser } from '@app/store/chatStore';
3+
import { useCallback, useEffect, useState, forwardRef, useMemo } from 'react';
4+
import {
5+
TextInput,
6+
TextInputProps,
7+
View,
8+
LayoutChangeEvent,
9+
} from 'react-native';
10+
import { StyleSheet } from 'react-native-unistyles';
11+
import { ChatInput } from './components/ChatInput';
12+
import { EmoteSuggestions } from './components/EmoteSuggestions';
13+
import { UserSuggestions } from './components/UserSuggestions';
14+
import { useEmoteSuggestions } from './hooks/useEmoteSuggestions';
15+
import { useSuggestionAnimations } from './hooks/useSuggestionAnimations';
16+
import { useUserSuggestions } from './hooks/useUserSuggestions';
17+
import { useWordInfo } from './hooks/useWordInfo';
18+
19+
export type SuggestionType = 'emote' | 'user';
20+
21+
interface ChatComposerProps extends TextInputProps {
22+
onEmoteSelect?: (emote: SanitisiedEmoteSet) => void;
23+
maxSuggestions?: number;
24+
prioritizeChannelEmotes?: boolean;
25+
placeholder?: string;
26+
}
27+
28+
export const ChatComposer = forwardRef<TextInput, ChatComposerProps>(
29+
(
30+
{
31+
onEmoteSelect,
32+
onChangeText,
33+
maxSuggestions = 50,
34+
prioritizeChannelEmotes = true,
35+
value = '',
36+
placeholder = 'Search emotes...',
37+
...textFieldProps
38+
},
39+
ref,
40+
) => {
41+
const [isFocused, setIsFocused] = useState(false);
42+
const [showSuggestions, setShowSuggestions] = useState(false);
43+
const [cursorPosition, setCursorPosition] = useState(0);
44+
const [inputLayout, setInputLayout] = useState({
45+
x: 0,
46+
y: 0,
47+
width: 0,
48+
height: 0,
49+
});
50+
51+
const { wordInfo, isUserMention, isEmoteSearch } = useWordInfo({
52+
text: value,
53+
cursorPosition,
54+
});
55+
56+
const { filteredEmotes } = useEmoteSuggestions({
57+
searchTerm: wordInfo.searchTerm,
58+
maxSuggestions,
59+
prioritizeChannelEmotes,
60+
});
61+
62+
const { filteredUsers } = useUserSuggestions({
63+
searchTerm: wordInfo.word,
64+
enabled: isUserMention,
65+
});
66+
67+
const handleHideComplete = useCallback(() => {
68+
setShowSuggestions(false);
69+
}, []);
70+
71+
const { opacity, scale, translateY, hide } = useSuggestionAnimations({
72+
shouldShow: showSuggestions,
73+
onHideComplete: handleHideComplete,
74+
});
75+
76+
const shouldShowEmoteSuggestions =
77+
isFocused &&
78+
isEmoteSearch &&
79+
filteredEmotes.length > 0 &&
80+
wordInfo.word.length > 0;
81+
82+
const shouldShowUserSuggestions =
83+
isFocused && isUserMention && filteredUsers.length > 0;
84+
85+
const validUsers = useMemo(
86+
() =>
87+
filteredUsers.filter((user): user is typeof user => user !== undefined),
88+
[filteredUsers],
89+
);
90+
91+
useEffect(() => {
92+
if (shouldShowEmoteSuggestions) {
93+
setShowSuggestions(true);
94+
} else if (!shouldShowEmoteSuggestions && !shouldShowUserSuggestions) {
95+
setShowSuggestions(false);
96+
}
97+
}, [shouldShowEmoteSuggestions, shouldShowUserSuggestions]);
98+
99+
const handleInputLayout = useCallback((event: LayoutChangeEvent) => {
100+
const { x, y, width, height } = event.nativeEvent.layout;
101+
setInputLayout({ x, y, width, height });
102+
}, []);
103+
104+
const handleTextChange = useCallback(
105+
(text: string) => {
106+
onChangeText?.(text);
107+
},
108+
[onChangeText],
109+
);
110+
111+
const handleSelectionChange = useCallback(
112+
(event: {
113+
nativeEvent: { selection: { start: number; end: number } };
114+
}) => {
115+
setCursorPosition(event.nativeEvent.selection.start);
116+
},
117+
[],
118+
);
119+
120+
const handleFocus = useCallback(() => {
121+
setIsFocused(true);
122+
}, []);
123+
124+
const handleBlur = useCallback(() => {
125+
setIsFocused(false);
126+
}, []);
127+
128+
const handleEmotePress = useCallback(
129+
(emote: SanitisiedEmoteSet) => {
130+
const beforeWord = value.substring(0, wordInfo.start);
131+
const afterWord = value.substring(wordInfo.end);
132+
const newText = `${beforeWord}${emote.name}${afterWord}`;
133+
const newCursorPosition = wordInfo.start + emote.name.length;
134+
135+
onEmoteSelect?.(emote);
136+
onChangeText?.(newText);
137+
138+
setShowSuggestions(false);
139+
hide();
140+
141+
setTimeout(() => {
142+
setCursorPosition(newCursorPosition);
143+
}, 10);
144+
},
145+
[value, wordInfo, onEmoteSelect, onChangeText, hide],
146+
);
147+
148+
const handleUserSelect = useCallback(
149+
(user: ChatUser) => {
150+
const beforeWord = value.substring(0, wordInfo.start);
151+
const afterWord = value.substring(wordInfo.end);
152+
const newText = `${beforeWord}${user.name} ${afterWord}`;
153+
const newCursorPosition = wordInfo.start + user.name.length + 1;
154+
155+
onChangeText?.(newText);
156+
157+
setTimeout(() => {
158+
setCursorPosition(newCursorPosition);
159+
}, 10);
160+
},
161+
[value, wordInfo, onChangeText],
162+
);
163+
return (
164+
<View style={styles.mainContainer}>
165+
<EmoteSuggestions
166+
emotes={filteredEmotes}
167+
handleEmotePress={handleEmotePress}
168+
showSuggestions={showSuggestions}
169+
setShowSuggestions={setShowSuggestions}
170+
inputLayout={inputLayout}
171+
suggestionOpacity={opacity}
172+
suggestionScale={scale}
173+
suggestionTranslateY={translateY}
174+
/>
175+
<UserSuggestions
176+
users={validUsers}
177+
showUserSuggestions={shouldShowUserSuggestions}
178+
handleUserSelect={handleUserSelect}
179+
/>
180+
<View style={styles.inputWrapper}>
181+
<ChatInput
182+
ref={ref}
183+
value={value}
184+
placeholder={placeholder}
185+
onChangeText={handleTextChange}
186+
onSelectionChange={handleSelectionChange}
187+
onLayout={handleInputLayout}
188+
onFocus={handleFocus}
189+
onBlur={handleBlur}
190+
{...textFieldProps}
191+
/>
192+
</View>
193+
</View>
194+
);
195+
},
196+
);
197+
198+
ChatComposer.displayName = 'ChatComposer';
199+
200+
const styles = StyleSheet.create(() => ({
201+
inputWrapper: {
202+
width: '100%',
203+
},
204+
mainContainer: {
205+
width: '100%',
206+
},
207+
}));
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import Input, { InputProps } from '@app/components/Input/Input';
2+
import { forwardRef, useCallback } from 'react';
3+
import { TextInput, View, LayoutChangeEvent, FocusEvent } from 'react-native';
4+
import { StyleSheet } from 'react-native-unistyles';
5+
6+
interface ChatInputProps extends Omit<InputProps, 'onChangeText'> {
7+
value?: string;
8+
onChangeText?: (text: string) => void;
9+
onSelectionChange?: (event: {
10+
nativeEvent: { selection: { start: number; end: number } };
11+
}) => void;
12+
onLayout?: (event: LayoutChangeEvent) => void;
13+
onFocus?: (e: FocusEvent) => void;
14+
onBlur?: (e: FocusEvent) => void;
15+
}
16+
17+
export const ChatInput = forwardRef<TextInput, ChatInputProps>(
18+
(
19+
{
20+
value = '',
21+
onChangeText,
22+
onSelectionChange,
23+
onLayout,
24+
onFocus,
25+
onBlur,
26+
placeholder = 'Send a message...',
27+
...textFieldProps
28+
},
29+
ref,
30+
) => {
31+
const handleTextChange = useCallback(
32+
(text: string) => {
33+
onChangeText?.(text);
34+
},
35+
[onChangeText],
36+
);
37+
38+
return (
39+
<View style={styles.container} onLayout={onLayout}>
40+
<Input
41+
ref={ref}
42+
{...textFieldProps}
43+
value={value}
44+
placeholder={placeholder}
45+
onChangeText={handleTextChange}
46+
onSelectionChange={onSelectionChange}
47+
onFocus={onFocus}
48+
onBlur={onBlur}
49+
multiline
50+
scrollEnabled
51+
textAlignVertical="top"
52+
style={[styles.input, textFieldProps.style]}
53+
/>
54+
</View>
55+
);
56+
},
57+
);
58+
59+
ChatInput.displayName = 'ChatInput';
60+
61+
const styles = StyleSheet.create(theme => ({
62+
container: {
63+
backgroundColor: theme.colors.gray.ui,
64+
borderRadius: theme.radii.lg,
65+
},
66+
input: {
67+
fontSize: 15,
68+
paddingTop: 10,
69+
paddingBottom: 10,
70+
paddingHorizontal: 14,
71+
minHeight: 40,
72+
maxHeight: 100,
73+
},
74+
}));

0 commit comments

Comments
 (0)