Skip to content

Commit c8eaa8c

Browse files
committed
--wip-- [skip ci]
1 parent 3dcab62 commit c8eaa8c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+19743
-939
lines changed

bun.lock

Lines changed: 88 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,17 +205,16 @@
205205
"metro-react-native-babel-preset": "^0.74.1",
206206
"only-allow": "^1.2.1",
207207
"prettier": "^3.3.3",
208+
"react-native-oss-license": "^0.7.0",
208209
"react-native-url-polyfill": "^2.0.0",
209210
"svgo": "^3.3.2",
210211
"ts-jest": "^29.2.5",
211212
"ts-migrate": "^0.1.35",
212-
"ts-node": "^10.9.2",
213-
"ts-node-dev": "^2.0.0",
213+
"tsx": "^4.19.3",
214214
"typescript": "5.3.3",
215215
"typescript-eslint": "^8.21.0",
216216
"url-loader": "^4.1.1",
217217
"webpack": "^5.75.0",
218-
"react-native-oss-license": "^0.7.0",
219218
"webpack-bundle-analyzer": "^4.10.1",
220219
"webpack-cli": "^5.0.1",
221220
"webpack-dev-server": "^4.11.1"
@@ -249,6 +248,11 @@
249248
}
250249
},
251250
"trustedDependencies": [
252-
"core-js"
251+
"@sentry/cli",
252+
"core-js",
253+
"core-js-pure",
254+
"esbuild",
255+
"nx",
256+
"protobufjs"
253257
]
254258
}

src/components/Button/Button.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
import { createHitslop } from '@app/utils';
12
import { forwardRef } from 'react';
23
// eslint-disable-next-line no-restricted-imports
34
import { TouchableOpacity, TouchableOpacityProps, View } from 'react-native';
45

5-
export type ButtonProps = TouchableOpacityProps;
6+
export type ButtonProps = TouchableOpacityProps & {};
67

78
export const Button = forwardRef<View, ButtonProps>(
8-
({ children, onPress, style, ...touchableProps }, ref) => (
9+
(
10+
{
11+
children,
12+
onPress,
13+
style,
14+
hitSlop = createHitslop(10),
15+
...touchableProps
16+
},
17+
ref,
18+
) => (
919
<TouchableOpacity
1020
ref={ref}
1121
{...touchableProps}
22+
hitSlop={hitSlop}
1223
style={style}
1324
onPress={onPress}
1425
>

src/components/Chat/Chat.tsx

Lines changed: 131 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
/* eslint-disable camelcase */
33
import { useAuthContext } from '@app/context/AuthContext';
44
import { useAppNavigation, useTmiClient } from '@app/hooks';
5-
import { useChatStore } from '@app/store/chatStore';
6-
import { replaceWithEmotesV2 } from '@app/utils/chat/replaceTextWithEmotesV2';
5+
import { ChatUser, useChatStore } from '@app/store/chatStore';
6+
import { generateRandomTwitchColor } from '@app/utils';
7+
import { findBadges } from '@app/utils/chat/findBadges';
8+
import { replaceTextWithEmotes } from '@app/utils/chat/replaceTextWithEmotes';
79
import { logger } from '@app/utils/logger';
810
import { generateNonce } from '@app/utils/string/generateNonce';
911
import { memo, useEffect, useRef, useState, JSX } from 'react';
@@ -21,7 +23,7 @@ import Animated, {
2123
import { createStyleSheet, useStyles } from 'react-native-unistyles';
2224
import { ChatUserstate } from 'tmi.js';
2325
import { Typography } from '../Typography';
24-
import { ChatMessageV2, ChatMessageV2Props } from './ChatMessageV2';
26+
import { ChatMessage, ChatMessageProps } from './ChatMessage';
2527

2628
export interface FormattedChatMessage {
2729
user: ChatUserstate;
@@ -37,6 +39,17 @@ interface ChatProps {
3739
export const Chat = memo(({ channelName, channelId }: ChatProps) => {
3840
const { authState, user } = useAuthContext();
3941
const navigation = useAppNavigation();
42+
const client = useTmiClient({
43+
options: {
44+
clientId: process.env.TWITCH_CLIENT_ID as string,
45+
},
46+
channels: [channelName],
47+
identity: {
48+
username: user?.display_name ?? '',
49+
password: authState?.token.accessToken,
50+
},
51+
});
52+
4053
const {
4154
sevenTvChannelEmotes,
4255
twitchChannelEmotes,
@@ -46,11 +59,20 @@ export const Chat = memo(({ channelName, channelId }: ChatProps) => {
4659
loadChannelResources,
4760
twitchGlobalEmotes,
4861
clearChannelResources,
49-
loading,
62+
status,
63+
ttvUsers,
64+
setTTvUsers,
65+
twitchChannelBadges,
66+
twitchGlobalBadges,
67+
ffzGlobalBadges,
5068
} = useChatStore();
5169

52-
const loadChat = async () => {
70+
navigation.addListener('beforeRemove', () => {
71+
void client.disconnect();
5372
clearChannelResources();
73+
});
74+
75+
const loadChat = async () => {
5476
await loadChannelResources(channelId);
5577
};
5678

@@ -61,19 +83,16 @@ export const Chat = memo(({ channelName, channelId }: ChatProps) => {
6183
// eslint-disable-next-line react-hooks/exhaustive-deps
6284
}, []);
6385

64-
const flashListRef = useRef<FlatList<ChatMessageV2Props>>(null);
65-
const messagesRef = useRef<ChatMessageV2Props[]>([]);
86+
const flashListRef = useRef<FlatList<ChatMessageProps>>(null);
87+
const messagesRef = useRef<ChatMessageProps[]>([]);
6688
const { styles } = useStyles(stylesheet);
6789

68-
// Get screen width & height to detect orientation
6990
const { width, height } = useWindowDimensions();
7091
const isLandscape = width > height;
7192

72-
// Reanimated shared values
7393
const chatWidth = useSharedValue(isLandscape ? width * 0.4 : width);
7494
const chatHeight = useSharedValue(isLandscape ? height : 600);
7595

76-
// Animate layout changes
7796
useEffect(() => {
7897
chatWidth.value = withTiming(isLandscape ? width * 0.4 : width, {
7998
duration: 300,
@@ -89,92 +108,121 @@ export const Chat = memo(({ channelName, channelId }: ChatProps) => {
89108
height: chatHeight.value,
90109
}));
91110

92-
const client = useTmiClient({
93-
options: {
94-
clientId: process.env.TWITCH_CLIENT_ID as string,
95-
// skipUpdatingEmotesets: true,
96-
},
97-
channels: [channelName],
98-
identity: {
99-
username: user?.display_name,
100-
password: authState?.token.accessToken,
101-
},
102-
connection: {
103-
reconnect: !__DEV__,
104-
},
105-
});
111+
const [messages, setMessages] = useState<ChatMessageProps[]>([]);
112+
const [connectionError, setConnectionError] = useState<string | null>(null);
113+
const [isConnecting, setIsConnecting] = useState(false);
106114

107-
const [messages, setMessages] = useState<ChatMessageV2Props[]>([]);
108-
109-
const connectToChat = () => {
110-
void client.connect();
111-
112-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
113-
client.on('message', (_channel, tags, text, _self) => {
114-
const userstate = tags;
115-
116-
const message_id = userstate.id || '0';
117-
const message_nonce = generateNonce();
118-
119-
const replacedMessage = replaceWithEmotesV2({
120-
bttvChannelEmotes: [],
121-
bttvGlobalEmotes: [],
122-
ffzChannelEmotes,
123-
ffzGlobalEmotes,
124-
inputString: text.trim(),
125-
sevenTvChannelEmotes,
126-
sevenTvGlobalEmotes,
127-
twitchChannelEmotes,
128-
twitchGlobalEmotes,
129-
userstate,
130-
});
115+
const connectToChat = async () => {
116+
if (isConnecting) return;
131117

132-
logger.chat.info('replacedMessage', replacedMessage);
118+
try {
119+
setIsConnecting(true);
120+
setConnectionError(null);
121+
await client.connect();
133122

134-
const newMessage: ChatMessageV2Props = {
135-
userstate,
136-
message: replacedMessage,
137-
channel: '',
138-
message_id,
139-
message_nonce,
140-
sender: '',
141-
};
123+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
124+
client.on('message', (_channel, tags, text, _self) => {
125+
const userstate = tags;
142126

143-
// Append the new message to the existing messages
144-
messagesRef.current = [...messagesRef.current, newMessage];
145-
setMessages([...messagesRef.current]);
127+
const message_id = userstate.id || '0';
128+
const message_nonce = generateNonce();
146129

147-
// Scroll to the end of the chat
148-
flashListRef.current?.scrollToEnd({ animated: false });
149-
});
130+
const replacedMessage = replaceTextWithEmotes({
131+
bttvChannelEmotes: [],
132+
bttvGlobalEmotes: [],
133+
ffzChannelEmotes,
134+
ffzGlobalEmotes,
135+
inputString: text.trimEnd(),
136+
sevenTvChannelEmotes,
137+
sevenTvGlobalEmotes,
138+
twitchChannelEmotes,
139+
twitchGlobalEmotes,
140+
userstate,
141+
});
150142

151-
client.on('clearchat', () => {
152-
messagesRef.current = [];
153-
setMessages([]);
154-
flashListRef.current?.scrollToEnd({ animated: false });
155-
});
143+
const replacedBadges = findBadges(
144+
userstate,
145+
twitchChannelBadges,
146+
twitchGlobalBadges,
147+
ffzGlobalBadges,
148+
);
156149

157-
navigation.addListener('blur', () => {
158-
void client.disconnect();
159-
});
150+
const foundTtvUser = ttvUsers.find(
151+
u => u.name === `@${userstate.username}`,
152+
);
153+
154+
if (!foundTtvUser) {
155+
const ttvUser: ChatUser = {
156+
name: `@${userstate.username}`,
157+
userId: userstate['user-id'] ?? '',
158+
color:
159+
userstate.color ?? generateRandomTwitchColor(userstate.username),
160+
avatar: '',
161+
};
162+
163+
setTTvUsers([ttvUser]);
164+
}
165+
166+
const newMessage: ChatMessageProps = {
167+
userstate,
168+
message: replacedMessage,
169+
badges: replacedBadges,
170+
channel: '',
171+
message_id,
172+
message_nonce,
173+
sender: '',
174+
};
175+
176+
// Append the new message to the existing messages
177+
messagesRef.current = [...messagesRef.current, newMessage];
178+
setMessages([...messagesRef.current]);
179+
180+
// Scroll to the end of the chat
181+
flashListRef.current?.scrollToEnd({ animated: false });
182+
});
183+
184+
client.on('clearchat', () => {
185+
messagesRef.current = [];
186+
setMessages([]);
187+
flashListRef.current?.scrollToEnd({ animated: false });
188+
});
189+
190+
client.on('disconnected', reason => {
191+
logger.chat.info('Disconnected from chat:', reason);
192+
});
193+
} catch (error) {
194+
logger.chat.error('Failed to connect to chat:', error);
195+
setConnectionError('Failed to connect to chat');
196+
} finally {
197+
setIsConnecting(false);
198+
}
160199
};
161200

162201
useEffect(() => {
163-
connectToChat();
164-
return () => {
165-
void client.disconnect();
166-
};
202+
if (status === 'fulfilled') {
203+
void connectToChat();
204+
}
167205
// eslint-disable-next-line react-hooks/exhaustive-deps
168-
}, []);
206+
}, [status]);
169207

170-
if (loading) {
208+
if (status === 'loading') {
171209
return (
172210
<View>
173211
<Typography>Loading emotes</Typography>
174212
</View>
175213
);
176214
}
177215

216+
if (status === 'error' || connectionError) {
217+
return (
218+
<View>
219+
<Typography>
220+
Error: {connectionError || 'Error getting emotes'}
221+
</Typography>
222+
</View>
223+
);
224+
}
225+
178226
return (
179227
<SafeAreaView style={styles.safeArea}>
180228
<Typography style={styles.header}>Chat</Typography>
@@ -184,10 +232,11 @@ export const Chat = memo(({ channelName, channelId }: ChatProps) => {
184232
ref={flashListRef}
185233
keyExtractor={(_, index) => index.toString()}
186234
renderItem={({ item }) => (
187-
<ChatMessageV2
235+
<ChatMessage
188236
channel={item.channel}
189237
message={item.message}
190238
userstate={item.userstate}
239+
badges={item.badges}
191240
message_id={item.message_id}
192241
message_nonce={item.message_nonce}
193242
sender={item.sender}
@@ -208,6 +257,7 @@ export const Chat = memo(({ channelName, channelId }: ChatProps) => {
208257
</SafeAreaView>
209258
);
210259
});
260+
211261
Chat.displayName = 'Chat';
212262

213263
const stylesheet = createStyleSheet(theme => ({
@@ -231,12 +281,12 @@ const stylesheet = createStyleSheet(theme => ({
231281
},
232282
messageContainer: {
233283
flexDirection: 'row',
234-
flexWrap: 'wrap', // Ensure text wraps
284+
flexWrap: 'wrap',
235285
// alignItems: 'flex-start',
236-
width: '100%', // Ensure it fits within the screen
286+
width: '100%',
237287
paddingVertical: theme.spacing.xs,
238288
paddingHorizontal: theme.spacing.sm,
239-
borderRadius: theme.radii.sm, // Optional: Add rounded corners
289+
borderRadius: theme.radii.sm,
240290
},
241291
pausedOverlay: {
242292
position: 'absolute',

0 commit comments

Comments
 (0)