123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- import React, { useState, useEffect, useRef, useCallback } from 'react';
- import {
- View,
- Text,
- TouchableOpacity,
- Image,
- Platform,
- TouchableHighlight,
- } from 'react-native';
- import {
- AvatarWithInitials,
- HorizontalTabView,
- Input,
- PageWrapper,
- 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';
- const TypingIndicator = () => {
- const [dots, setDots] = useState('');
- useEffect(() => {
- const interval = setInterval(() => {
- setDots((prevDots) => {
- if (prevDots.length >= 3) {
- return '';
- }
- return prevDots + '.';
- });
- }, 500);
- return () => clearInterval(interval);
- }, []);
- return <Text style={styles.typingText}>Typing{dots}</Text>;
- };
- const MessagesScreen = () => {
- 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 [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 }>({});
- 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);
- };
- const handleWebSocketMessage = (data: any) => {
- switch (data.action) {
- case 'new_message':
- refetch();
- break;
- case 'is_typing':
- if (data.conversation_with) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.conversation_with]: true
- }));
- }
- break;
- case 'stopped_typing':
- if (data.conversation_with) {
- setTypingUsers((prev) => ({
- ...prev,
- [data.conversation_with]: 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 }) => {
- return (
- <SwipeableRow
- chat={{
- uid: item.uid,
- name: item.name,
- avatar: item.avatar,
- pin: item.pin,
- archive: item.archive,
- muted: item.muted
- }}
- token={token}
- onRowOpen={handleRowOpen}
- refetch={refetch}
- refetchBlocked={refetchBlocked}
- >
- <TouchableHighlight
- key={`${item.uid}-${typingUsers[item.uid]}`}
- activeOpacity={0.8}
- onPress={() =>
- navigation.navigate(
- ...([
- NAVIGATION_PAGES.CHAT,
- {
- id: item.uid,
- name: item.name,
- avatar: item.avatar
- }
- ] as never)
- )
- }
- underlayColor={Colors.FILL_LIGHT}
- >
- <View style={styles.chatItem}>
- {item.avatar ? (
- <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
- ) : (
- <AvatarWithInitials
- text={
- item.name
- .split(/ (.+)/)
- .map((n) => n[0])
- .join('') ?? ''
- }
- flag={API_HOST + item?.flag}
- size={54}
- />
- )}
- <View style={{ flex: 1, gap: 6 }}>
- <View style={[styles.rowContainer, { alignItems: 'center' }]}>
- <Text style={styles.chatName}>{item.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 !== item.uid && item.status === 3 ? (
- <ReadIcon fill={Colors.DARK_BLUE} />
- ) : item.sent_by !== item.uid && (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 }]}>
- {typingUsers[item.uid] ? (
- <TypingIndicator />
- ) : (
- <Text numberOfLines={2} style={styles.chatMessage}>
- {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 (
- <PageWrapper style={{ 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}
- />
- </PageWrapper>
- );
- };
- export default MessagesScreen;
|