22/* eslint-disable camelcase */
33import { useAuthContext } from '@app/context/AuthContext' ;
44import { 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' ;
79import { logger } from '@app/utils/logger' ;
810import { generateNonce } from '@app/utils/string/generateNonce' ;
911import { memo , useEffect , useRef , useState , JSX } from 'react' ;
@@ -21,7 +23,7 @@ import Animated, {
2123import { createStyleSheet , useStyles } from 'react-native-unistyles' ;
2224import { ChatUserstate } from 'tmi.js' ;
2325import { Typography } from '../Typography' ;
24- import { ChatMessageV2 , ChatMessageV2Props } from './ChatMessageV2 ' ;
26+ import { ChatMessage , ChatMessageProps } from './ChatMessage ' ;
2527
2628export interface FormattedChatMessage {
2729 user : ChatUserstate ;
@@ -37,6 +39,17 @@ interface ChatProps {
3739export 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+
211261Chat . displayName = 'Chat' ;
212262
213263const 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