123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- import React, { useState, useEffect, useRef, useCallback } from 'react';
- import {
- View,
- Text,
- TouchableOpacity,
- Image,
- Platform,
- TouchableHighlight,
- AppState,
- AppStateStatus
- } from 'react-native';
- import { AvatarWithInitials, HorizontalTabView, Input, WarningModal } from 'src/components';
- import { NAVIGATION_PAGES } from 'src/types';
- import { useFocusEffect, useNavigation } from '@react-navigation/native';
- import AddChatIcon from 'assets/icons/messages/chat-plus.svg';
- import { API_HOST, WEBSOCKET_URL } from 'src/constants';
- import { Colors } from 'src/theme';
- import SwipeableRow from './Components/SwipeableRow';
- import { FlashList } from '@shopify/flash-list';
- import ReadIcon from 'assets/icons/messages/check-read.svg';
- import UnreadIcon from 'assets/icons/messages/check-unread.svg';
- import SearchModal from './Components/SearchUsersModal';
- import { SheetManager } from 'react-native-actions-sheet';
- import MoreModal from './Components/MoreModal';
- import SearchIcon from 'assets/icons/search.svg';
- import { storage, StoreType } from 'src/storage';
- import { usePostGetBlockedQuery, usePostGetChatsListQuery } from '@api/chat';
- import { Blocked, Chat } from './types';
- import PinIcon from 'assets/icons/messages/pin.svg';
- import { formatDate } from './utils';
- import { routes } from './constants';
- import { styles } from './styles';
- import { useChatStore } from 'src/stores/chatStore';
- import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
- import BanIcon from 'assets/icons/messages/ban.svg';
- import SwipeableBlockedRow from './Components/SwipeableBlockedRow';
- import { useMessagesStore } from 'src/stores/unreadMessagesStore';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import GroupIcon from 'assets/icons/messages/group-chat.svg';
- const TypingIndicator = ({ name }: { name?: string }) => {
- const [dots, setDots] = useState('');
- useEffect(() => {
- const interval = setInterval(() => {
- setDots((prevDots) => {
- if (prevDots.length >= 3) {
- return '';
- }
- return prevDots + '.';
- });
- }, 500);
- return () => clearInterval(interval);
- }, []);
- return name ? (
- <Text style={styles.typingText}>
- {name} is typing{dots}
- </Text>
- ) : (
- <Text style={styles.typingText}>Typing{dots}</Text>
- );
- };
- const MessagesScreen = () => {
- const insets = useSafeAreaInsets();
- const navigation = useNavigation();
- const token = storage.get('token', StoreType.STRING) as string;
- const [chats, setChats] = useState<Chat[]>([]);
- const [index, setIndex] = useState(0);
- const { data: chatsData, refetch } = usePostGetChatsListQuery(token, index === 2 ? 1 : 0, true);
- const { data: blockedData, refetch: refetchBlocked } = usePostGetBlockedQuery(token, true);
- const [blocked, setBlocked] = useState<Blocked[]>([]);
- const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
- const currentUserId = storage.get('uid', StoreType.STRING) as string;
- const [filteredChats, setFilteredChats] = useState<{
- all: Chat[];
- unread: Chat[];
- archived: Chat[];
- blocked: Blocked[];
- }>({ all: [], unread: [], archived: [], blocked: [] });
- const [search, setSearch] = useState('');
- const openRowRef = useRef<any>(null);
- const { isWarningModalVisible, setIsWarningModalVisible } = useChatStore();
- const [typingUsers, setTypingUsers] = useState<
- { [key: string]: boolean } | { [key: string]: { firstName: string; isTyping: boolean } }
- >({});
- const appState = useRef(AppState.currentState);
- const socket = useRef<WebSocket | null>(null);
- const initializeSocket = () => {
- if (socket.current) {
- socket.current.close();
- }
- setTimeout(() => {
- socket.current = new WebSocket(WEBSOCKET_URL);
- socket.current.onopen = () => {
- socket.current?.send(JSON.stringify({ token }));
- };
- socket.current.onmessage = (event) => {
- const data = JSON.parse(event.data);
- handleWebSocketMessage(data);
- };
- socket.current.onclose = () => {
- console.log('WebSocket connection closed');
- };
- }, 500);
- };
- useEffect(() => {
- const handleAppStateChange = (nextAppState: AppStateStatus) => {
- if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
- if (!socket.current || socket.current.readyState === WebSocket.CLOSED) {
- socket.current = new WebSocket(WEBSOCKET_URL);
- socket.current.onopen = () => {
- socket.current?.send(JSON.stringify({ token }));
- };
- socket.current.onmessage = (event) => {
- const data = JSON.parse(event.data);
- handleWebSocketMessage(data);
- };
- }
- }
- };
- const subscription = AppState.addEventListener('change', handleAppStateChange);
- return () => {
- subscription.remove();
- if (socket.current) {
- socket.current.close();
- socket.current = null;
- }
- };
- }, [token]);
- useEffect(() => {
- const pingInterval = setInterval(() => {
- if (socket.current && socket.current.readyState === WebSocket.OPEN) {
- socket.current.send(JSON.stringify({ action: 'ping', conversation_with: 0 }));
- } else {
- initializeSocket();
- return () => {
- if (socket.current) {
- socket.current.close();
- socket.current = null;
- }
- };
- }
- }, 50000);
- return () => clearInterval(pingInterval);
- }, []);
- const handleWebSocketMessage = (data: any) => {
- switch (data.action) {
- case 'new_message':
- case 'messages_read':
- refetch();
- break;
- case 'is_typing':
- if (data.conversation_with) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.conversation_with]: true
- }));
- } else if (data.group_token) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.group_token]: {
- isTyping: true,
- firstName: data.name?.split(' ')[0]
- }
- }));
- }
- break;
- case 'stopped_typing':
- if (data.conversation_with) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.conversation_with]: false
- }));
- } else if (data.group_token) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.group_token]: false
- }));
- }
- break;
- default:
- break;
- }
- };
- const handleRowOpen = (ref: any) => {
- if (openRowRef.current && openRowRef.current !== ref) {
- openRowRef.current.close();
- }
- openRowRef.current = ref;
- };
- useFocusEffect(() => {
- navigation.getParent()?.setOptions({
- tabBarStyle: {
- display: 'flex',
- ...Platform.select({
- android: {
- height: 58
- }
- })
- }
- });
- });
- useEffect(() => {
- if (chatsData && chatsData.conversations) {
- setChats(chatsData.conversations);
- }
- }, [chatsData]);
- useEffect(() => {
- if (blockedData && blockedData.blocked) {
- setBlocked(blockedData.blocked);
- }
- }, [blockedData]);
- useFocusEffect(
- useCallback(() => {
- refetch();
- initializeSocket();
- updateUnreadMessagesCount();
- return () => {
- if (socket.current) {
- socket.current.close();
- socket.current = null;
- }
- };
- }, [token])
- );
- const filterChatsByTab = () => {
- let filteredList = chats;
- if (index === 3) {
- setFilteredChats((prev) => ({ ...prev, blocked }));
- return;
- }
- if (index === 1) {
- filteredList = chats.filter((chat) => chat.unread_count > 0);
- }
- filteredList.sort((a, b) => {
- if (b.pin - a.pin !== 0) {
- return b.pin - a.pin;
- }
- if (b.pin_order - a.pin_order !== 0) {
- return b.pin_order - a.pin_order;
- }
- return new Date(b.updated).getTime() - new Date(a.updated).getTime();
- });
- setFilteredChats((prev) => ({ ...prev, [routes[index].key]: filteredList }));
- };
- useEffect(() => {
- filterChatsByTab();
- }, [chats, index, blocked]);
- const searchFilter = (text: string) => {
- if (text) {
- const newData =
- chats?.filter((item: Chat) => {
- const itemData = item.short ? item.short.toLowerCase() : ''.toLowerCase();
- const textData = text.toLowerCase();
- return itemData.indexOf(textData) > -1;
- }) ?? [];
- setFilteredChats((prev) => ({ ...prev, [routes[index].key]: newData }));
- setSearch(text);
- } else {
- filterChatsByTab();
- setSearch(text);
- }
- };
- const renderChatItem = ({ item }: { item: Chat }) => {
- const name =
- item.user_type === 'blocked'
- ? 'Account is blocked'
- : item.user_type === 'not_exist'
- ? 'Account does not exist'
- : item.name;
- return (
- <SwipeableRow
- chat={{
- uid: item.uid,
- groupToken: item.group_chat_token,
- name: item.name,
- avatar: item.avatar,
- pin: item.pin,
- archive: item.archive,
- muted: item.muted,
- userType: item.user_type ?? 'normal'
- }}
- token={token}
- onRowOpen={handleRowOpen}
- refetch={refetch}
- refetchBlocked={refetchBlocked}
- >
- <TouchableHighlight
- key={
- item.uid
- ? `${item.uid}-${typingUsers[item.uid]}`
- : `${item.group_chat_token}-${typingUsers[item.group_chat_token ?? '']}`
- }
- activeOpacity={0.8}
- onPress={() => {
- if (!item.uid) {
- navigation.navigate(
- ...([
- NAVIGATION_PAGES.GROUP_CHAT,
- {
- group_token: item.group_chat_token,
- name: item.name,
- avatar: item.avatar,
- userType: item.user_type
- }
- ] as never)
- );
- } else {
- navigation.navigate(
- ...([
- NAVIGATION_PAGES.CHAT,
- {
- id: item.uid,
- name: item.name,
- avatar: item.avatar,
- userType: item.user_type
- }
- ] as never)
- );
- }
- }}
- underlayColor={Colors.FILL_LIGHT}
- >
- <View style={styles.chatItem}>
- {item.avatar && (item.user_type === 'normal' || !item.user_type) ? (
- <Image
- source={{
- uri: API_HOST + item.avatar,
- cache: item.group_chat_token ? 'reload' : 'force-cache'
- }}
- style={styles.avatar}
- />
- ) : item.uid && (item.user_type === 'normal' || !item.user_type) ? (
- <AvatarWithInitials
- text={
- item.name
- ?.split(/ (.+)/)
- .map((n) => n[0])
- .join('') ?? ''
- }
- flag={API_HOST + item?.flag}
- size={54}
- />
- ) : item.user_type === 'normal' || !item.user_type ? (
- <GroupIcon fill={Colors.DARK_BLUE} width={54} height={54} />
- ) : (
- <BanIcon fill={Colors.RED} width={54} height={54} />
- )}
- <View style={{ flex: 1, gap: 6 }}>
- <View style={[styles.rowContainer, { alignItems: 'center' }]}>
- <Text
- style={[
- styles.chatName,
- item.user_type === 'not_exist' || item.user_type === 'blocked'
- ? { color: Colors.RED }
- : {}
- ]}
- >
- {name}
- </Text>
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
- {item.pin === 1 ? <PinIcon height={12} fill={Colors.DARK_BLUE} /> : null}
- {item.muted === 1 ? <BellSlashIcon height={12} fill={Colors.DARK_BLUE} /> : null}
- {item.sent_by === +currentUserId && item.status === 3 ? (
- <ReadIcon fill={Colors.DARK_BLUE} />
- ) : item.sent_by === +currentUserId &&
- (item.status === 2 || item.status === 1) ? (
- <UnreadIcon fill={Colors.LIGHT_GRAY} />
- ) : null}
- <Text style={styles.chatTime}>{formatDate(item.updated)}</Text>
- </View>
- </View>
- <View style={[styles.rowContainer, { flex: 1, gap: 6 }]}>
- {item.uid && typingUsers[item.uid] ? (
- <TypingIndicator />
- ) : item.group_chat_token &&
- typingUsers[item.group_chat_token] &&
- (typingUsers[item.group_chat_token] as any)?.firstName ? (
- <TypingIndicator name={(typingUsers[item.group_chat_token] as any).firstName} />
- ) : (
- <Text numberOfLines={2} style={styles.chatMessage}>
- {item.attachement_name && item.attachement_name.length
- ? item.attachement_name
- : item.short}
- </Text>
- )}
- {item.unread_count > 0 ? (
- <View style={styles.unreadBadge}>
- <Text style={styles.unreadText}>
- {item.unread_count > 99 ? '99+' : item.unread_count}
- </Text>
- </View>
- ) : null}
- </View>
- </View>
- </View>
- </TouchableHighlight>
- </SwipeableRow>
- );
- };
- const renderBlockedItem = ({ item }: { item: Blocked }) => {
- return (
- <SwipeableBlockedRow
- data={{
- id: item.id,
- first_name: item.first_name,
- last_name: item.last_name,
- avatar: item.avatar
- }}
- token={token}
- onRowOpen={handleRowOpen}
- refetchBlocked={refetchBlocked}
- >
- <TouchableHighlight
- activeOpacity={0.8}
- onPress={() =>
- navigation.navigate(
- ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.id }] as never)
- )
- }
- underlayColor={Colors.FILL_LIGHT}
- >
- <View style={[styles.chatItem, { alignItems: 'center' }]}>
- {item.avatar ? (
- <Image
- source={{ uri: API_HOST + item.avatar }}
- style={[styles.avatar, { width: 30, height: 30, borderRadius: 15 }]}
- />
- ) : (
- <AvatarWithInitials
- text={item.first_name[0] + item.last_name[0]}
- flag={API_HOST + item?.flag}
- size={32}
- fontSize={12}
- />
- )}
- <View style={{ flex: 1, gap: 6 }}>
- <View style={[styles.rowContainer, { alignItems: 'center' }]}>
- <Text style={styles.chatName}>{item.first_name + ' ' + item.last_name}</Text>
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
- <BanIcon height={12} fill={Colors.RED} />
- </View>
- </View>
- </View>
- </View>
- </TouchableHighlight>
- </SwipeableBlockedRow>
- );
- };
- return (
- <View style={{ paddingTop: insets.top, flex: 1, marginLeft: 0, marginRight: 0, gap: 12 }}>
- <View style={styles.header}>
- <View style={{ width: 30 }} />
- <Text style={styles.title}>Messages</Text>
- <TouchableOpacity
- onPress={() => SheetManager.show('search-modal')}
- style={{ width: 30, alignItems: 'flex-end' }}
- >
- <AddChatIcon />
- </TouchableOpacity>
- </View>
- {/* <View style={[{ paddingHorizontal: '4%' }]}>
- <Input
- inputMode={'search'}
- placeholder={'Search'}
- onChange={(text) => searchFilter(text)}
- value={search}
- icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
- height={38}
- />
- </View> */}
- <HorizontalTabView
- index={index}
- setIndex={setIndex}
- routes={routes}
- tabBarStyle={{ paddingHorizontal: '4%' }}
- renderScene={({ route }: { route: { key: keyof typeof filteredChats } }) =>
- route.key === 'blocked' ? (
- <FlashList
- viewabilityConfig={{
- waitForInteraction: true,
- itemVisiblePercentThreshold: 50,
- minimumViewTime: 1000
- }}
- data={filteredChats[route.key]}
- renderItem={renderBlockedItem}
- keyExtractor={(item, index) => `${item.id}-${index}`}
- estimatedItemSize={50}
- />
- ) : (
- <FlashList
- viewabilityConfig={{
- waitForInteraction: true,
- itemVisiblePercentThreshold: 50,
- minimumViewTime: 1000
- }}
- data={filteredChats[route.key]}
- renderItem={renderChatItem}
- keyExtractor={(item, index) => `${item.uid}-${index}`}
- estimatedItemSize={78}
- extraData={typingUsers}
- />
- )
- }
- />
- <SearchModal />
- <MoreModal />
- <WarningModal
- type={'delete'}
- buttonTitle={isWarningModalVisible?.buttonTitle ?? 'Delete'}
- isVisible={!!isWarningModalVisible}
- onClose={() => setIsWarningModalVisible(null)}
- title={isWarningModalVisible?.title}
- message={isWarningModalVisible?.message}
- action={isWarningModalVisible?.action}
- />
- </View>
- );
- };
- export default MessagesScreen;
|