import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { View, Text, Image, TouchableOpacity, Linking, Dimensions, FlatList, Platform, ActivityIndicator } from 'react-native'; import { styles } from './styles'; import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native'; import { Colors } from 'src/theme'; import FileViewer from 'react-native-file-viewer'; import * as FileSystem from 'expo-file-system'; import * as DocumentPicker from 'react-native-document-picker'; import * as ImagePicker from 'expo-image-picker'; import { ScrollView } from 'react-native-gesture-handler'; import { NAVIGATION_PAGES } from 'src/types'; import { API_HOST, APP_VERSION } from 'src/constants'; import { StoreType, storage } from 'src/storage'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as Progress from 'react-native-progress'; import ChevronLeft from 'assets/icons/chevron-left.svg'; import MapSvg from 'assets/icons/travels-screens/map-location.svg'; import AddImgSvg from 'assets/icons/travels-screens/add-img.svg'; import ShareIcon from 'assets/icons/share.svg'; import GigtIcon from 'assets/icons/events/gift.svg'; import CalendarCrossedIcon from 'assets/icons/events/calendar-crossed.svg'; import CalendarCheckIcon from 'assets/icons/events/calendar-check.svg'; import CalendarIcon from 'assets/icons/events/calendar-solid.svg'; import EarthIcon from 'assets/icons/travels-section/earth.svg'; import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg'; import LocationIcon from 'assets/icons/bottom-navigation/map.svg'; import FileIcon from 'assets/icons/events/file-solid.svg'; import ImageIcon from 'assets/icons/events/image.svg'; import { getFontSize } from 'src/utils'; import { EventAttachments, EventData, EventPhotos, useGetEventQuery, usePostDeleteFileMutation, usePostEventAddFileMutation, usePostJoinEventMutation, usePostUnjoinEventMutation, usePostUploadPhotoMutation, usePostUploadTempFileMutation } from '@api/events'; import { AvatarWithInitials, Input, Loading, WarningModal } from 'src/components'; import moment from 'moment'; import { renderSpotsText } from '../EventsScreen/utils'; import { useWindowDimensions } from 'react-native'; import RenderHtml, { HTMLElementModel, TNode } from 'react-native-render-html'; import { PhotoItem } from './PhotoItem'; import Share from 'react-native-share'; import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants'; import { Dropdown } from 'react-native-searchable-dropdown-kj'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import Tooltip from 'react-native-walkthrough-tooltip'; import WebView from 'react-native-webview'; type TempFile = { filetype: string; name: string; temp_name: string; isSending: boolean; type: 1 | 2 | 3; description: string; }; const fileWidth = Dimensions.get('window').width / 5; const EventScreen = ({ route }: { route: any }) => { const eventUrl = route.params?.url; const token = (storage.get('token', StoreType.STRING) as string) ?? null; const currentUserId = (storage.get('uid', StoreType.NUMBER) as number) ?? 0; const navigation = useNavigation(); const { width: windowWidth } = useWindowDimensions(); const contentWidth = windowWidth * 0.9; const scrollViewRef = useRef(null); const { data, refetch } = useGetEventQuery(token, eventUrl, true); const { mutateAsync: joinEvent } = usePostJoinEventMutation(); const { mutateAsync: unjoinEvent } = usePostUnjoinEventMutation(); const { mutateAsync: uploadTempFile } = usePostUploadTempFileMutation(); const { mutateAsync: saveFile } = usePostEventAddFileMutation(); const { mutateAsync: deleteFile } = usePostDeleteFileMutation(); const { mutateAsync: uploadPhoto } = usePostUploadPhotoMutation(); const [isExpanded, setIsExpanded] = useState(false); const [tooltipUser, setTooltipUser] = useState(null); const [event, setEvent] = useState(null); const [registrationInfo, setRegistrationInfo] = useState<{ color: string; name: string } | null>( null ); const [filteredParticipants, setFilteredParticipants] = useState( [] ); const [maxVisibleParticipants, setMaxVisibleParticipants] = useState(0); const [maxVisibleParticipantsWithGap, setMaxVisibleParticipantsWithGap] = useState(0); const [joined, setJoined] = useState<0 | 1>(0); const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({}); const [myTempFiles, setMyTempFiles] = useState([]); const [myFiles, setMyFiles] = useState([]); const [photos, setPhotos] = useState<(EventPhotos & { isSending?: boolean })[]>([]); const [isUploading, setIsUploading] = useState(false); const [modalInfo, setModalInfo] = useState({ visible: false, type: 'success', title: '', message: '', buttonTitle: 'OK', action: () => {} }); useEffect(() => { if (data && data.data) { setEvent(data.data); setJoined(data.data.joined); setMyFiles(data.data.files ?? []); setPhotos(data.data.photos); const partisipantsWidth = contentWidth / 2; setMaxVisibleParticipants(Math.floor(partisipantsWidth / 22)); setMaxVisibleParticipantsWithGap(Math.floor(partisipantsWidth / 32)); setFilteredParticipants(data.data.participants_data); setRegistrationInfo(() => { if (data.data.full) { return { color: Colors.LIGHT_GRAY, name: 'FULL' }; } else if (data.data.settings.type === 2) { return { color: Colors.ORANGE, name: 'TOUR' }; } else if (data.data.settings.type === 3) { return { color: Colors.DARK_BLUE, name: 'CONF' }; } return null; }); } }, [data]); useFocusEffect( useCallback(() => { refetch(); }, [navigation]) ); const handlePreviewDocument = useCallback(async (url: string, fileName: string) => { try { const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR); if (!dirExist.exists) { await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true }); } const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`; const fileExists = await FileSystem.getInfoAsync(fileUri); if (fileExists.exists && fileExists.size > 1024) { await FileViewer.open(fileUri, { showOpenWithDialog: true, showAppsSuggestions: true }); return; } const downloadResumable = FileSystem.createDownloadResumable( url, fileUri, {}, (downloadProgress) => { const progress = downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite; setUploadProgress((prev) => ({ ...prev, [fileName]: progress * 100 })); } ); const { uri: localUri } = await FileSystem.downloadAsync(url, fileUri, { headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS } }); await FileViewer.open(localUri, { showOpenWithDialog: true, showAppsSuggestions: true }); } catch (error) { console.error('Error previewing document:', error); } finally { setUploadProgress((prev) => { const newProgress = { ...prev }; delete newProgress[fileName]; return newProgress; }); } }, []); const handleUploadFile = useCallback(async () => { try { const response = await DocumentPicker.pick({ type: [DocumentPicker.types.allFiles], allowMultiSelection: true }); setIsUploading(true); for (const res of response) { let file: any = { uri: res.uri, name: res.name, type: res.type }; if ((file.name && !file.name.includes('.')) || !file.type) { file = { ...file, type: file.type || 'application/octet-stream' }; } await uploadTempFile( { token, file, onUploadProgress: (progressEvent) => { // if (progressEvent.lengthComputable) { // const progress = Math.round( // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100 // ); // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress })); // } } }, { onSuccess: (result) => { setMyTempFiles((prev) => [ { ...result, type: 1, description: '', isSending: false }, ...prev ]); setIsUploading(false); }, onError: (error) => { console.error('Upload error:', error); } } ); } } catch { setIsUploading(false); } finally { setIsUploading(false); } }, [token]); const handleUploadPhoto = useCallback(async () => { if (!event) return; try { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!perm.granted) { console.warn('Permission for gallery not granted'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsMultipleSelection: true, quality: 1, selectionLimit: 4 }); if (!result.canceled && result.assets) { const files = result.assets.map((asset) => ({ uri: asset.uri, type: asset.mimeType ?? 'image', name: asset.uri ? (asset.uri.split('/').pop() as string) : 'image' })); for (const file of files) { const staticPhoto: any = { id: new Date().getTime(), filetype: file.type, uid: +currentUserId, name: '', avatar: null, isSending: true, preview: 1, data: 1 }; setPhotos((prev) => [staticPhoto, ...prev]); await uploadPhoto( { token, event_id: event.id, file, onUploadProgress: (progressEvent) => { // if (progressEvent.lengthComputable) { // const progress = Math.round( // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100 // ); // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress })); // } } }, { onSuccess: (result) => { refetch(); }, onError: () => { refetch(); } } ); } } } catch {} }, [token, event]); if (!event) return ; const handleShare = async () => { if (!event) return; try { // TO DO const uri = `${API_HOST}/event/${eventUrl}`; if (uri) { await Share.open({ url: uri }); } } catch (error) { console.error('Error sharing the event url:', error); } }; const handleJoinEvent = async () => { if (event.settings.type !== 1) { setModalInfo({ visible: true, type: 'success', title: 'Success', buttonTitle: 'OK', message: `Thank you for joing, we’ll get back to you soon.`, action: () => {} }); } await joinEvent( { token, id: event.id }, { onSuccess: () => { setJoined(1); refetch(); } } ); }; const handleUnjoinEvent = async () => { await unjoinEvent( { token, id: event.id }, { onSuccess: () => { setJoined(0); refetch(); } } ); }; const handleDeleteFile = async (file: EventAttachments) => { setModalInfo({ visible: true, type: 'delete', title: 'Delete file', buttonTitle: 'Delete', message: `Are you sure you want to delete this file?`, action: async () => { await deleteFile( { token, id: file.id, event_id: event.id }, { onSuccess: () => { setMyFiles(myFiles.filter((f) => f.id !== file.id)); } } ); } }); }; const renderItem = ({ item, index }: { item: EventAttachments; index: number }) => { const totalItems = event.attachments.length; if (!isExpanded && index === 7 && totalItems > 8) { return ( { setIsExpanded(true); }} > ); } return ( handlePreviewDocument( API_HOST + '/webapi/events/get-attachment/' + event.id + '/' + item.id, event.id + '-' + item.filename ) } > {item.filename} ); }; const renderItemFile = ({ item, index }: { item: EventAttachments; index: number }) => { return ( { handlePreviewDocument( `${API_HOST}/webapi/events/get-file/${event.id}/${item.id}/?token=${token}`, item.filename ); }} > {item.filename} {item.type === 1 ? 'passport' : item.type === 2 ? 'disclaimer' : 'other'} {item.description ? ( {item.description} ) : null} handleDeleteFile(item)} > Delete ); }; const formatEventDate = (event: EventData) => { if (event.settings.date_from && event.settings.date_to) { return `${moment(event.settings.date_from, 'YYYY-MM-DD').format('DD MMMM YYYY')} - ${moment(event.settings.date_to, 'YYYY-MM-DD').format('DD MMMM YYYY')}`; } else { let date = moment(event.settings.date, 'YYYY-MM-DD').format('DD MMMM YYYY'); if (event.time) { date += `, ${event.time}`; } return date; } }; const handleSaveFile = async (file: TempFile) => { setMyTempFiles(() => myTempFiles.map((f) => (f.temp_name === file.temp_name ? { ...f, isSending: true } : f)) ); await saveFile( { token, event_id: event.id, type: file.type, description: file.description, filetype: file.filetype, filename: file.name, temp_filename: file.temp_name }, { onSuccess: () => { setMyTempFiles(myTempFiles.filter((f) => f.temp_name !== file.temp_name)); refetch(); }, onError: () => { setMyTempFiles(() => myTempFiles.map((f) => f.temp_name === file.temp_name ? { ...f, isSending: false } : f ) ); } } ); }; const staticImgUrl = event.type === 2 ? '/static/img/events/trip.webp' : event.type === 3 ? '/static/img/events/conference.webp' : '/static/img/events/meeting.webp'; const photoUrl = event.photo_available ? API_HOST + '/webapi/events/get-main-photo/' + event.id : API_HOST + staticImgUrl; return ( { navigation.goBack(); }} style={styles.backButton} > {/* { // navigation.dispatch( // CommonActions.reset({ // index: 1, // routes: [ // { // name: NAVIGATION_PAGES.IN_APP_MAP_TAB, // state: { // routes: [ // { // name: NAVIGATION_PAGES.MAP_TAB, // params: { id: regionId, type: type === 'nm' ? 'regions' : 'places' } // } // ] // } // } // ] // }) // ) }} style={styles.goToMapBtn} > */} {registrationInfo && ( {registrationInfo.name} )} {event.settings.name} {formatEventDate(event)} {event.settings.address1} {event.settings.address2 && ( {event.settings.address2} )} {event.settings.registrations_info !== 1 && ( {renderSpotsText(event)} )} {event.settings.host_data ? ( Host navigation.navigate( ...([ NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: event.settings.host_profile } ] as never) ) } disabled={!event.settings.host_profile} > {event.settings.host_data.avatar ? ( ) : null} {event.settings.host_data.first_name} {event.settings.host_data.last_name} NM:{' '} {event.settings.host_data.nm} {' '} / UN:{' '} {event.settings.host_data.un} ) : ( )} {filteredParticipants.length > 0 ? ( Participants {(filteredParticipants.length > maxVisibleParticipants ? filteredParticipants.slice(0, maxVisibleParticipants - 1) : filteredParticipants ).map((user, index) => ( { setTooltipUser(null); navigation.navigate( ...([ NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: user.uid } ] as never) ); }} > {user.name} } contentStyle={{ backgroundColor: Colors.FILL_LIGHT }} placement="top" onClose={() => setTooltipUser(null)} key={index} backgroundColor="transparent" > setTooltipUser(index)}> {user.avatar ? ( maxVisibleParticipantsWithGap || filteredParticipants.length < (event.participants ?? 0)) && index !== 0 ? { marginLeft: -10 } : {} ]} /> ) : ( )} ))} {maxVisibleParticipants < filteredParticipants.length || filteredParticipants.length < (event.participants ?? 0) ? ( {event.participants} ) : null} ) : ( )} {joined ? ( Cancel ) : !event.full ? ( {event.settings.free ? ( <> Join - free ) : ( <> Interested )} ) : null} {event.settings.details && event.settings.details.length ? ( Details ) : null} {event.attachments.length > 0 ? ( Attachments item.id.toString()} numColumns={4} columnWrapperStyle={{ justifyContent: 'flex-start', gap: 12 }} contentContainerStyle={{ gap: 8, alignSelf: 'center' }} showsVerticalScrollIndicator={false} scrollEnabled={false} /> ) : null} {(photos && photos.length) || (event.joined && event.settings.participants_can_add_photos) ? ( Photos {event.settings.participants_can_add_photos && event.joined ? ( Add ) : null} {photos && photos.length > 0 ? ( ) : null} ) : null} {(event.files && event.files.length) || (event.joined && event.settings.participants_can_add_files) ? ( My files {event.settings.participants_can_add_files && event.joined ? ( Add ) : null} {isUploading && ( )} {myTempFiles && myTempFiles.length ? myTempFiles.map((file) => { return ( {file.name} { setMyTempFiles(() => myTempFiles.map((f) => f.temp_name === file.temp_name ? { ...f, description: text } : f ) ); }} /> { setMyTempFiles(() => myTempFiles.map((f) => f.temp_name === file.temp_name ? { ...f, type: item.value } : f ) ); }} containerStyle={{ borderRadius: 4 }} renderItem={(item) => ( {item.label} )} /> handleSaveFile(file)} disabled={file.isSending} > {file.isSending ? ( ) : ( Save )} ); }) : null} item.id.toString()} contentContainerStyle={{ gap: 8 }} showsVerticalScrollIndicator={false} scrollEnabled={false} /> ) : null} setModalInfo({ ...modalInfo, visible: false })} title={modalInfo.title} /> ); }; const iframeModel = HTMLElementModel.fromCustomModel({ tagName: 'iframe', contentModel: 'block' as any }); const WebDisplay = React.memo(function WebDisplay({ html }: { html: string }) { const { width: windowWidth } = useWindowDimensions(); const contentWidth = windowWidth * 0.9; const token = storage.get('token', StoreType.STRING) as string; const processedHtml = React.useMemo(() => { let updatedHtml = html; const hrefRegex = /href="((?!http)[^"]+)"/g; const imgSrcRegex = /src="((?:\.{0,2}\/)*img\/[^"]*)"/g; const normalizePath = (path: string): string => { const segments = path.split('/').filter(Boolean); const resolved: string[] = []; for (const segment of segments) { if (segment === '..') resolved.pop(); else if (segment !== '.') resolved.push(segment); } return '/' + resolved.join('/'); }; updatedHtml = updatedHtml .replace(hrefRegex, (match, rawPath) => { const normalizedPath = normalizePath(rawPath); const fullUrl = `${API_HOST}${normalizedPath}`; if (normalizedPath.includes('shop')) { const separator = fullUrl.includes('?') ? '&' : '?'; return `href="${fullUrl}${separator}token=${encodeURIComponent(token)}"`; } return `href="${fullUrl}"`; }) .replace(imgSrcRegex, (_match, rawSrc) => { const normalizedImg = normalizePath(rawSrc); return `src="${API_HOST}${normalizedImg}"`; }); return updatedHtml; }, [html, token]); const renderers = { iframe: ({ tnode }: { tnode: TNode }) => { const src = tnode.attributes.src || ''; const width = contentWidth; const height = Number(tnode.attributes.height) || 250; return ( ); } }; const customHTMLElementModels = { iframe: iframeModel }; return ( ); }); export default EventScreen;