index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import React, { useState, useEffect, useRef, useCallback } from 'react';
  2. import {
  3. View,
  4. Text,
  5. TouchableOpacity,
  6. Image,
  7. Platform,
  8. TouchableHighlight,
  9. } from 'react-native';
  10. import {
  11. AvatarWithInitials,
  12. HorizontalTabView,
  13. Input,
  14. PageWrapper,
  15. WarningModal
  16. } from 'src/components';
  17. import { NAVIGATION_PAGES } from 'src/types';
  18. import { useFocusEffect, useNavigation } from '@react-navigation/native';
  19. import AddChatIcon from 'assets/icons/messages/chat-plus.svg';
  20. import { API_HOST, WEBSOCKET_URL } from 'src/constants';
  21. import { Colors } from 'src/theme';
  22. import SwipeableRow from './Components/SwipeableRow';
  23. import { FlashList } from '@shopify/flash-list';
  24. import ReadIcon from 'assets/icons/messages/check-read.svg';
  25. import UnreadIcon from 'assets/icons/messages/check-unread.svg';
  26. import SearchModal from './Components/SearchUsersModal';
  27. import { SheetManager } from 'react-native-actions-sheet';
  28. import MoreModal from './Components/MoreModal';
  29. import SearchIcon from 'assets/icons/search.svg';
  30. import { storage, StoreType } from 'src/storage';
  31. import { usePostGetBlockedQuery, usePostGetChatsListQuery } from '@api/chat';
  32. import { Blocked, Chat } from './types';
  33. import PinIcon from 'assets/icons/messages/pin.svg';
  34. import { formatDate } from './utils';
  35. import { routes } from './constants';
  36. import { styles } from './styles';
  37. import { useChatStore } from 'src/stores/chatStore';
  38. import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
  39. import BanIcon from 'assets/icons/messages/ban.svg';
  40. import SwipeableBlockedRow from './Components/SwipeableBlockedRow';
  41. import { useMessagesStore } from 'src/stores/unreadMessagesStore';
  42. const TypingIndicator = () => {
  43. const [dots, setDots] = useState('');
  44. useEffect(() => {
  45. const interval = setInterval(() => {
  46. setDots((prevDots) => {
  47. if (prevDots.length >= 3) {
  48. return '';
  49. }
  50. return prevDots + '.';
  51. });
  52. }, 500);
  53. return () => clearInterval(interval);
  54. }, []);
  55. return <Text style={styles.typingText}>Typing{dots}</Text>;
  56. };
  57. const MessagesScreen = () => {
  58. const navigation = useNavigation();
  59. const token = storage.get('token', StoreType.STRING) as string;
  60. const [chats, setChats] = useState<Chat[]>([]);
  61. const [index, setIndex] = useState(0);
  62. const { data: chatsData, refetch } = usePostGetChatsListQuery(token, index === 2 ? 1 : 0, true);
  63. const { data: blockedData, refetch: refetchBlocked } = usePostGetBlockedQuery(token, true);
  64. const [blocked, setBlocked] = useState<Blocked[]>([]);
  65. const updateUnreadMessagesCount = useMessagesStore((state) => state.updateUnreadMessagesCount);
  66. const [filteredChats, setFilteredChats] = useState<{
  67. all: Chat[];
  68. unread: Chat[];
  69. archived: Chat[];
  70. blocked: Blocked[];
  71. }>({ all: [], unread: [], archived: [], blocked: [] });
  72. const [search, setSearch] = useState('');
  73. const openRowRef = useRef<any>(null);
  74. const { isWarningModalVisible, setIsWarningModalVisible } = useChatStore();
  75. const [typingUsers, setTypingUsers] = useState<{ [key: string]: boolean }>({});
  76. const socket = useRef<WebSocket | null>(null);
  77. const initializeSocket = () => {
  78. if (socket.current) {
  79. socket.current.close();
  80. }
  81. setTimeout(() => {
  82. socket.current = new WebSocket(WEBSOCKET_URL);
  83. socket.current.onopen = () => {
  84. socket.current?.send(JSON.stringify({ token }));
  85. };
  86. socket.current.onmessage = (event) => {
  87. const data = JSON.parse(event.data);
  88. handleWebSocketMessage(data);
  89. };
  90. socket.current.onclose = () => {
  91. console.log('WebSocket connection closed');
  92. };
  93. }, 500);
  94. };
  95. const handleWebSocketMessage = (data: any) => {
  96. switch (data.action) {
  97. case 'new_message':
  98. refetch();
  99. break;
  100. case 'is_typing':
  101. if (data.conversation_with) {
  102. setTypingUsers((prev) => ({
  103. ...prev,
  104. [data.conversation_with]: true
  105. }));
  106. }
  107. break;
  108. case 'stopped_typing':
  109. if (data.conversation_with) {
  110. setTypingUsers((prev) => ({
  111. ...prev,
  112. [data.conversation_with]: false
  113. }));
  114. }
  115. break;
  116. default:
  117. break;
  118. }
  119. };
  120. const handleRowOpen = (ref: any) => {
  121. if (openRowRef.current && openRowRef.current !== ref) {
  122. openRowRef.current.close();
  123. }
  124. openRowRef.current = ref;
  125. };
  126. useFocusEffect(() => {
  127. navigation.getParent()?.setOptions({
  128. tabBarStyle: {
  129. display: 'flex',
  130. ...Platform.select({
  131. android: {
  132. height: 58
  133. }
  134. })
  135. }
  136. });
  137. });
  138. useEffect(() => {
  139. if (chatsData && chatsData.conversations) {
  140. setChats(chatsData.conversations);
  141. }
  142. }, [chatsData]);
  143. useEffect(() => {
  144. if (blockedData && blockedData.blocked) {
  145. setBlocked(blockedData.blocked);
  146. }
  147. }, [blockedData]);
  148. useFocusEffect(
  149. useCallback(() => {
  150. refetch();
  151. initializeSocket();
  152. updateUnreadMessagesCount();
  153. return () => {
  154. if (socket.current) {
  155. socket.current.close();
  156. socket.current = null;
  157. }
  158. };
  159. }, [token])
  160. );
  161. const filterChatsByTab = () => {
  162. let filteredList = chats;
  163. if (index === 3) {
  164. setFilteredChats((prev) => ({ ...prev, blocked }));
  165. return;
  166. }
  167. if (index === 1) {
  168. filteredList = chats.filter((chat) => chat.unread_count > 0);
  169. }
  170. filteredList.sort((a, b) => {
  171. if (b.pin - a.pin !== 0) {
  172. return b.pin - a.pin;
  173. }
  174. if (b.pin_order - a.pin_order !== 0) {
  175. return b.pin_order - a.pin_order;
  176. }
  177. return new Date(b.updated).getTime() - new Date(a.updated).getTime();
  178. });
  179. setFilteredChats((prev) => ({ ...prev, [routes[index].key]: filteredList }));
  180. };
  181. useEffect(() => {
  182. filterChatsByTab();
  183. }, [chats, index, blocked]);
  184. const searchFilter = (text: string) => {
  185. if (text) {
  186. const newData =
  187. chats?.filter((item: Chat) => {
  188. const itemData = item.short ? item.short.toLowerCase() : ''.toLowerCase();
  189. const textData = text.toLowerCase();
  190. return itemData.indexOf(textData) > -1;
  191. }) ?? [];
  192. setFilteredChats((prev) => ({ ...prev, [routes[index].key]: newData }));
  193. setSearch(text);
  194. } else {
  195. filterChatsByTab();
  196. setSearch(text);
  197. }
  198. };
  199. const renderChatItem = ({ item }: { item: Chat }) => {
  200. return (
  201. <SwipeableRow
  202. chat={{
  203. uid: item.uid,
  204. name: item.name,
  205. avatar: item.avatar,
  206. pin: item.pin,
  207. archive: item.archive,
  208. muted: item.muted
  209. }}
  210. token={token}
  211. onRowOpen={handleRowOpen}
  212. refetch={refetch}
  213. refetchBlocked={refetchBlocked}
  214. >
  215. <TouchableHighlight
  216. key={`${item.uid}-${typingUsers[item.uid]}`}
  217. activeOpacity={0.8}
  218. onPress={() =>
  219. navigation.navigate(
  220. ...([
  221. NAVIGATION_PAGES.CHAT,
  222. {
  223. id: item.uid,
  224. name: item.name,
  225. avatar: item.avatar
  226. }
  227. ] as never)
  228. )
  229. }
  230. underlayColor={Colors.FILL_LIGHT}
  231. >
  232. <View style={styles.chatItem}>
  233. {item.avatar ? (
  234. <Image source={{ uri: API_HOST + item.avatar }} style={styles.avatar} />
  235. ) : (
  236. <AvatarWithInitials
  237. text={
  238. item.name
  239. .split(/ (.+)/)
  240. .map((n) => n[0])
  241. .join('') ?? ''
  242. }
  243. flag={API_HOST + item?.flag}
  244. size={54}
  245. />
  246. )}
  247. <View style={{ flex: 1, gap: 6 }}>
  248. <View style={[styles.rowContainer, { alignItems: 'center' }]}>
  249. <Text style={styles.chatName}>{item.name}</Text>
  250. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
  251. {item.pin === 1 ? <PinIcon height={12} fill={Colors.DARK_BLUE} /> : null}
  252. {item.muted === 1 ? <BellSlashIcon height={12} fill={Colors.DARK_BLUE} /> : null}
  253. {item.sent_by !== item.uid && item.status === 3 ? (
  254. <ReadIcon fill={Colors.DARK_BLUE} />
  255. ) : item.sent_by !== item.uid && (item.status === 2 || item.status === 1) ? (
  256. <UnreadIcon fill={Colors.LIGHT_GRAY} />
  257. ) : null}
  258. <Text style={styles.chatTime}>{formatDate(item.updated)}</Text>
  259. </View>
  260. </View>
  261. <View style={[styles.rowContainer, { flex: 1, gap: 6 }]}>
  262. {typingUsers[item.uid] ? (
  263. <TypingIndicator />
  264. ) : (
  265. <Text numberOfLines={2} style={styles.chatMessage}>
  266. {item.short}
  267. </Text>
  268. )}
  269. {item.unread_count > 0 ? (
  270. <View style={styles.unreadBadge}>
  271. <Text style={styles.unreadText}>
  272. {item.unread_count > 99 ? '99+' : item.unread_count}
  273. </Text>
  274. </View>
  275. ) : null}
  276. </View>
  277. </View>
  278. </View>
  279. </TouchableHighlight>
  280. </SwipeableRow>
  281. );
  282. };
  283. const renderBlockedItem = ({ item }: { item: Blocked }) => {
  284. return (
  285. <SwipeableBlockedRow
  286. data={{
  287. id: item.id,
  288. first_name: item.first_name,
  289. last_name: item.last_name,
  290. avatar: item.avatar
  291. }}
  292. token={token}
  293. onRowOpen={handleRowOpen}
  294. refetchBlocked={refetchBlocked}
  295. >
  296. <TouchableHighlight
  297. activeOpacity={0.8}
  298. onPress={() =>
  299. navigation.navigate(
  300. ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.id }] as never)
  301. )
  302. }
  303. underlayColor={Colors.FILL_LIGHT}
  304. >
  305. <View style={[styles.chatItem, { alignItems: 'center' }]}>
  306. {item.avatar ? (
  307. <Image
  308. source={{ uri: API_HOST + item.avatar }}
  309. style={[styles.avatar, { width: 30, height: 30, borderRadius: 15 }]}
  310. />
  311. ) : (
  312. <AvatarWithInitials
  313. text={item.first_name[0] + item.last_name[0]}
  314. flag={API_HOST + item?.flag}
  315. size={32}
  316. fontSize={12}
  317. />
  318. )}
  319. <View style={{ flex: 1, gap: 6 }}>
  320. <View style={[styles.rowContainer, { alignItems: 'center' }]}>
  321. <Text style={styles.chatName}>{item.first_name + ' ' + item.last_name}</Text>
  322. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
  323. <BanIcon height={12} fill={Colors.RED} />
  324. </View>
  325. </View>
  326. </View>
  327. </View>
  328. </TouchableHighlight>
  329. </SwipeableBlockedRow>
  330. );
  331. };
  332. return (
  333. <PageWrapper style={{ flex: 1, marginLeft: 0, marginRight: 0, gap: 12 }}>
  334. <View style={styles.header}>
  335. <View style={{ width: 30 }} />
  336. <Text style={styles.title}>Messages</Text>
  337. <TouchableOpacity
  338. onPress={() => SheetManager.show('search-modal')}
  339. style={{ width: 30, alignItems: 'flex-end' }}
  340. >
  341. <AddChatIcon />
  342. </TouchableOpacity>
  343. </View>
  344. {/* <View style={[{ paddingHorizontal: '4%' }]}>
  345. <Input
  346. inputMode={'search'}
  347. placeholder={'Search'}
  348. onChange={(text) => searchFilter(text)}
  349. value={search}
  350. icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
  351. height={38}
  352. />
  353. </View> */}
  354. <HorizontalTabView
  355. index={index}
  356. setIndex={setIndex}
  357. routes={routes}
  358. tabBarStyle={{ paddingHorizontal: '4%' }}
  359. renderScene={({ route }: { route: { key: keyof typeof filteredChats } }) =>
  360. route.key === 'blocked' ? (
  361. <FlashList
  362. viewabilityConfig={{
  363. waitForInteraction: true,
  364. itemVisiblePercentThreshold: 50,
  365. minimumViewTime: 1000
  366. }}
  367. data={filteredChats[route.key]}
  368. renderItem={renderBlockedItem}
  369. keyExtractor={(item, index) => `${item.id}-${index}`}
  370. estimatedItemSize={50}
  371. />
  372. ) : (
  373. <FlashList
  374. viewabilityConfig={{
  375. waitForInteraction: true,
  376. itemVisiblePercentThreshold: 50,
  377. minimumViewTime: 1000
  378. }}
  379. data={filteredChats[route.key]}
  380. renderItem={renderChatItem}
  381. keyExtractor={(item, index) => `${item.uid}-${index}`}
  382. estimatedItemSize={78}
  383. extraData={typingUsers}
  384. />
  385. )
  386. }
  387. />
  388. <SearchModal />
  389. <MoreModal />
  390. <WarningModal
  391. type={'delete'}
  392. buttonTitle={isWarningModalVisible?.buttonTitle ?? 'Delete'}
  393. isVisible={!!isWarningModalVisible}
  394. onClose={() => setIsWarningModalVisible(null)}
  395. title={isWarningModalVisible?.title}
  396. message={isWarningModalVisible?.message}
  397. action={isWarningModalVisible?.action}
  398. />
  399. </PageWrapper>
  400. );
  401. };
  402. export default MessagesScreen;