| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- import React, { useState } from 'react';
- import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native';
- import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
- import { Colors } from 'src/theme';
- import { API_HOST } from 'src/constants';
- import { getFontSize } from 'src/utils';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { ChatProps, WarningProps } from '../types';
- import { useNavigation } from '@react-navigation/native';
- import { NAVIGATION_PAGES } from 'src/types';
- import { usePostReportConversationMutation } from '@api/chat';
- import { useChatStore } from 'src/stores/chatStore';
- import TrashIcon from 'assets/icons/travels-screens/trash-solid.svg';
- import BanIcon from 'assets/icons/messages/ban.svg';
- import BellSlashIcon from 'assets/icons/messages/bell-slash.svg';
- import { AvatarWithInitials } from 'src/components';
- import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
- import ExitIcon from 'assets/icons/messages/exit.svg';
- import GroupIcon from 'assets/icons/messages/group-chat.svg';
- import { database } from 'src/watermelondb';
- import { BlockedUser, Chat } from 'src/watermelondb/models';
- import { Q } from '@nozbe/watermelondb';
- import {
- addDirtyAction,
- syncChatsIncremental
- } from 'src/watermelondb/features/chat/data/chat.sync';
- async function findChatRecord(chatData: ChatProps | null): Promise<Chat | null> {
- if (!chatData) return null;
- const chatsCollection = database.get<Chat>('chats');
- if (chatData.uid) {
- const res = await chatsCollection.query(Q.where('chat_uid', chatData.uid)).fetch();
- return res[0] ?? null;
- }
- if (chatData.groupToken) {
- const res = await chatsCollection
- .query(Q.where('group_chat_token', chatData.groupToken))
- .fetch();
- return res[0] ?? null;
- }
- return null;
- }
- async function upsertBlockedUserFromChat(chatData: ChatProps) {
- if (!chatData.uid) return;
- const blockedCollection = database.get<BlockedUser>('blocked_users');
- const existing = await blockedCollection.query(Q.where('user_id', chatData.uid)).fetch();
- const [firstName, ...rest] = (chatData.name ?? '').split(' ');
- const lastName = rest.join(' ');
- await database.write(async () => {
- if (existing.length > 0) {
- await existing[0].update((r) => {
- r.firstName = firstName || r.firstName;
- r.lastName = lastName || r.lastName;
- r.avatar = chatData.avatar ?? r.avatar;
- r.removed = false;
- });
- } else {
- await blockedCollection.create((r) => {
- r.userId = chatData.uid!;
- r.firstName = firstName || '';
- r.lastName = lastName || '';
- r.avatar = chatData.avatar ?? null;
- r.removed = false;
- });
- }
- });
- }
- const MoreModal = () => {
- const insets = useSafeAreaInsets();
- const navigation = useNavigation();
- const { setIsWarningModalVisible } = useChatStore();
- const [chatData, setChatData] = useState<
- | (ChatProps & {
- token: string;
- })
- | null
- >(null);
- const [name, setName] = useState<string | null>(null);
- const { mutateAsync: reportUser } = usePostReportConversationMutation();
- const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
- const handleSheetOpen = (
- payload:
- | (ChatProps & {
- token: string;
- })
- | null
- ) => {
- setChatData(payload);
- setName(
- payload?.userType === 'blocked'
- ? 'Account is blocked'
- : payload?.userType === 'not_exist'
- ? 'Account does not exist'
- : (payload?.name ?? null)
- );
- };
- const handleMute = async () => {
- if (!chatData) return;
- const newMuted = chatData.muted === 1 ? 0 : 1;
- const chatRec = await findChatRecord(chatData);
- if (chatRec) {
- await database.write(() =>
- chatRec.update((r) => {
- r.muted = newMuted;
- addDirtyAction(r, { type: 'mute', value: newMuted });
- })
- );
- }
- setChatData((prev) => (prev ? { ...prev, muted: newMuted } : prev));
- try {
- await syncChatsIncremental(chatData.token);
- } catch (e) {
- console.warn('mute sync failed (will retry later):', e);
- }
- };
- const handleBlock = async () => {
- if (!chatData) return;
- setShouldOpenWarningModal({
- visible: true,
- title: 'Block user',
- buttonTitle: 'Block',
- message: `Are you sure you want to block ${name}?\nThis user will be blocked and you will not be able to send or receive messages from him/her.`,
- action: async () => {
- const chatRec = await findChatRecord(chatData);
- if (chatRec) {
- await database.write(() =>
- chatRec.update((r) => {
- r.removed = true;
- addDirtyAction(r, { type: 'block' });
- })
- );
- }
- await upsertBlockedUserFromChat(chatData);
- try {
- await syncChatsIncremental(chatData.token);
- } catch (e) {
- console.warn('block sync failed:', e);
- }
- }
- });
- setTimeout(() => {
- SheetManager.hide('more-modal');
- setShouldOpenWarningModal(null);
- }, 300);
- };
- const handleReport = async () => {
- if (!chatData) return;
- setShouldOpenWarningModal({
- visible: true,
- title: `Report ${name}`,
- buttonTitle: 'Report',
- message: `Are you sure you want to report ${name}?\nIf you proceed, the chat history with ${name} will become visible to NomadMania admins for investigation.`,
- action: async () => {
- chatData.uid &&
- (await reportUser({
- token: chatData.token,
- reported_user_id: chatData.uid
- }));
- }
- });
- setTimeout(() => {
- SheetManager.hide('more-modal');
- setShouldOpenWarningModal(null);
- }, 300);
- };
- const handleDelete = async () => {
- if (!chatData) return;
- setShouldOpenWarningModal({
- visible: true,
- title: 'Delete conversation',
- message: `Are you sure you want to delete conversation with ${name}?\nThis conversation will be deleted for both sides.`,
- action: async () => {
- const chatRec = await findChatRecord(chatData);
- if (chatRec) {
- await database.write(() =>
- chatRec.update((r) => {
- r.removed = true;
- addDirtyAction(r, { type: 'delete' });
- })
- );
- }
- try {
- await syncChatsIncremental(chatData.token);
- } catch (e) {
- console.warn('delete sync failed:', e);
- }
- }
- });
- setTimeout(() => {
- SheetManager.hide('more-modal');
- setShouldOpenWarningModal(null);
- }, 300);
- };
- const handleLeaveGroup = async () => {
- if (!chatData) return;
- setShouldOpenWarningModal({
- visible: true,
- title: `Leave group ${name}`,
- buttonTitle: 'Leave',
- message: `Are you sure you want to leave ${name}?`,
- action: async () => {
- const chatRec = await findChatRecord(chatData);
- if (chatRec) {
- await database.write(() =>
- chatRec.update((r) => {
- r.removed = true;
- addDirtyAction(r, { type: 'leave_group' });
- })
- );
- }
- try {
- await syncChatsIncremental(chatData.token);
- } catch (e) {
- console.warn('leaveGroup sync failed:', e);
- }
- }
- });
- setTimeout(() => {
- SheetManager.hide('more-modal');
- setShouldOpenWarningModal(null);
- }, 300);
- };
- const handleDeleteGroup = async () => {
- if (!chatData) return;
- setShouldOpenWarningModal({
- visible: true,
- title: `Delete ${name}`,
- message: `Are you sure you want to delete this group chat?\nThis action will remove the chat from your history, but it won't affect other participants.`,
- action: async () => {
- const chatRec = await findChatRecord(chatData);
- if (chatRec) {
- await database.write(() =>
- chatRec.update((r) => {
- r.removed = true;
- addDirtyAction(r, { type: 'delete' });
- })
- );
- }
- try {
- await syncChatsIncremental(chatData.token);
- } catch (e) {
- console.warn('deleteGroup sync failed:', e);
- }
- }
- });
- setTimeout(() => {
- SheetManager.hide('more-modal');
- setShouldOpenWarningModal(null);
- }, 300);
- };
- return (
- <ActionSheet
- id="more-modal"
- gestureEnabled={true}
- onBeforeShow={(sheetRef) => {
- const payload = sheetRef || null;
- handleSheetOpen(payload);
- }}
- onClose={() => {
- if (shouldOpenWarningModal) {
- setIsWarningModalVisible(shouldOpenWarningModal);
- }
- }}
- containerStyle={styles.sheetContainer}
- defaultOverlayOpacity={0.5}
- indicatorStyle={{ backgroundColor: 'transparent' }}
- safeAreaInsets={{ top: insets.top, bottom: insets.bottom || 24, left: 0, right: 0 }}
- >
- {chatData && (
- <View style={[styles.container]}>
- <TouchableOpacity
- style={styles.header}
- onPress={() => {
- SheetManager.hide('more-modal');
- if (chatData?.uid) {
- navigation.navigate(
- ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: chatData.uid }] as never)
- );
- }
- }}
- disabled={chatData?.userType !== 'normal'}
- >
- {chatData?.avatar && (chatData.userType === 'normal' || !chatData.userType) ? (
- <Image source={{ uri: API_HOST + chatData.avatar }} style={styles.avatar} />
- ) : chatData.uid && (chatData.userType === 'normal' || !chatData.userType) ? (
- <AvatarWithInitials
- text={
- chatData.name
- .split(/ (.+)/)
- .map((n) => n[0])
- .join('') ?? ''
- }
- flag={API_HOST + 'flag.png'}
- size={32}
- fontSize={12}
- />
- ) : chatData.userType === 'normal' || !chatData.userType ? (
- <GroupIcon fill={Colors.DARK_BLUE} width={32} height={32} />
- ) : (
- <BanIcon fill={Colors.RED} width={32} height={32} />
- )}
- <Text
- style={[styles.name, chatData?.userType !== 'normal' ? { color: Colors.RED } : {}]}
- >
- {name}
- </Text>
- </TouchableOpacity>
- {chatData?.userType === 'normal' && (
- <View style={styles.optionsContainer}>
- <TouchableOpacity style={styles.option} onPress={handleMute}>
- <Text style={styles.optionText}>{chatData.muted === 1 ? 'Unmute' : 'Mute'}</Text>
- <BellSlashIcon fill={Colors.DARK_BLUE} />
- </TouchableOpacity>
- </View>
- )}
- {!chatData.announcement ? (
- <View style={[styles.optionsContainer, { paddingVertical: 0, gap: 0 }]}>
- {chatData?.groupToken && (
- <TouchableOpacity
- style={[styles.option, styles.dangerOption]}
- onPress={handleLeaveGroup}
- >
- <Text style={[styles.optionText, styles.dangerText]}>Leave group chat</Text>
- <ExitIcon fill={Colors.RED} width={16} />
- </TouchableOpacity>
- )}
- {chatData?.userType === 'normal' && chatData?.uid && (
- <>
- <TouchableOpacity
- style={[styles.option, styles.dangerOption]}
- onPress={handleReport}
- >
- <Text style={[styles.optionText, styles.dangerText]}>Report {name}</Text>
- <MegaphoneIcon fill={Colors.RED} />
- </TouchableOpacity>
- <TouchableOpacity
- style={[styles.option, styles.dangerOption]}
- onPress={handleBlock}
- >
- <Text style={[styles.optionText, styles.dangerText]}>Block {name}</Text>
- <BanIcon fill={Colors.RED} />
- </TouchableOpacity>
- </>
- )}
- {chatData?.uid ? (
- <TouchableOpacity
- style={[styles.option, styles.dangerOption]}
- onPress={handleDelete}
- >
- <Text style={[styles.optionText, styles.dangerText]}>Delete chat</Text>
- <TrashIcon fill={Colors.RED} width={18} height={18} />
- </TouchableOpacity>
- ) : (
- <TouchableOpacity
- style={[styles.option, styles.dangerOption]}
- onPress={handleDeleteGroup}
- >
- <Text style={[styles.optionText, styles.dangerText]}>Delete group chat</Text>
- <TrashIcon fill={Colors.RED} width={18} height={18} />
- </TouchableOpacity>
- )}
- </View>
- ) : null}
- </View>
- )}
- </ActionSheet>
- );
- };
- const styles = StyleSheet.create({
- sheetContainer: {
- borderTopLeftRadius: 15,
- borderTopRightRadius: 15
- },
- container: {
- backgroundColor: 'white',
- paddingHorizontal: 16,
- paddingTop: 8,
- gap: 16
- },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- borderBottomWidth: 1,
- borderBottomColor: Colors.FILL_LIGHT,
- gap: 8
- },
- avatar: {
- width: 32,
- height: 32,
- borderRadius: 16,
- borderWidth: 1,
- borderColor: Colors.FILL_LIGHT
- },
- name: {
- fontSize: getFontSize(14),
- fontFamily: 'montserrat-700',
- color: Colors.DARK_BLUE
- },
- optionsContainer: {
- paddingVertical: 10,
- paddingHorizontal: 8,
- gap: 16,
- borderRadius: 8,
- backgroundColor: Colors.FILL_LIGHT
- },
- option: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between'
- },
- optionText: {
- fontSize: getFontSize(12),
- fontWeight: '600',
- color: Colors.DARK_BLUE
- },
- dangerOption: {
- paddingVertical: 10,
- borderBottomWidth: 1,
- borderBlockColor: Colors.WHITE
- },
- dangerText: {
- color: Colors.RED
- }
- });
- export default MoreModal;
|