import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { View, Text, Image, TouchableOpacity, Linking, Dimensions, FlatList, Platform, ActivityIndicator, Animated } 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 ImageView from 'better-react-native-image-viewing'; 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, useGetEventForEditingMutation, useGetEventQuery, useGetPhotosForRegionQuery, usePostDeleteFileMutation, usePostEventAddFileMutation, usePostGetPhotosForRegionMutation, 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'; import { PhotosData } from '../../MapScreen/RegionViewScreen/types'; import ImageCarousel from '../../MapScreen/RegionViewScreen/ImageCarousel'; import EditSvg from 'assets/icons/events/edit.svg'; import ChatIcon from 'assets/icons/bottom-navigation/messages.svg'; import InfoIcon from 'assets/icons/info-solid.svg'; 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.STRING) as string) ?? 0; const navigation = useNavigation(); const { width: windowWidth } = useWindowDimensions(); const contentWidth = windowWidth * 0.9; const scrollViewRef = useRef(null); const keyboardAwareScrollViewRef = 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 { mutateAsync: getForEditing } = useGetEventForEditingMutation(); const [isExpanded, setIsExpanded] = useState(false); const [tooltipUser, setTooltipUser] = useState(null); const [tooltipInterested, setTooltipInterested] = useState(null); const [event, setEvent] = useState(null); const [registrationInfo, setRegistrationInfo] = useState<{ color: string; name: string } | null>( null ); const [filteredParticipants, setFilteredParticipants] = useState( [] ); const [filteredInterested, setFilteredInterested] = 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 [photosForRegion, setPhotosForRegion] = useState<{ uriSmall: string; uri: string }[]>([]); const [activeIndex, setActiveIndex] = useState(0); const [isImageModalVisible, setIsImageModalVisible] = useState(false); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [nmId, setNmId] = useState(null); const [tooltipVisible, setTooltipVisible] = useState(false); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const { data: photosData } = useGetPhotosForRegionQuery(nmId ?? 0, nmId !== null); const { mutateAsync: getPhotosForRegion } = usePostGetPhotosForRegionMutation(); const [regions, setRegions] = useState([]); const [modalInfo, setModalInfo] = useState({ visible: false, type: 'success', title: '', message: '', buttonTitle: 'OK', action: () => {} }); const [showScrollToTop, setShowScrollToTop] = useState(false); const scrollToTopOpacity = useRef(new Animated.Value(0)).current; const [toolTipVisible, setToolTipVisible] = useState(false); useEffect(() => { if (regions && regions.length > 0 && event && event.type === 4) { handleGetPhotosForAllRegions(regions); } else { setPhotosForRegion([]); } }, [regions, event]); const handleGetPhotosForAllRegions = useCallback( async (regionsArray: any[]) => { if (!regionsArray || regionsArray.length === 0) { setPhotosForRegion([]); return; } const allPhotos: any[] = []; try { for (const region of regionsArray) { await getPhotosForRegion( { region_id: region }, { onSuccess: (res) => { if (res.photos && res.photos.length > 0) { allPhotos.push(...res.photos); } }, onError: (error) => { console.log(`Error loading photos for region ${region}:`, error); } } ); } setPhotosForRegion( allPhotos.map((item) => ({ uriSmall: `${API_HOST}/ajax/pic/${item}/small`, uri: `${API_HOST}/ajax/pic/${item}/full` })) ); } catch (error) { setPhotosForRegion([]); } }, [getPhotosForRegion, token] ); useEffect(() => { photosData && setPhotosForRegion( photosData?.photos?.map((item) => ({ uriSmall: `${API_HOST}/ajax/pic/${item}/small`, uri: `${API_HOST}/ajax/pic/${item}/full` })) ?? [] ); }, [photosData]); useEffect(() => { if (data && data.data) { setEvent(data.data); setJoined(data.data.joined); setNmId(data.data.settings.nm_region ?? null); if (data.data.settings.nm_regions && data.data.type === 4) { const ids = JSON.parse(data.data.settings.nm_regions); setRegions(ids); } setMyFiles(data.data.files ?? []); setPhotos(data.data.photos); const partisipantsWidth = contentWidth / 2; setMaxVisibleParticipants(Math.floor(partisipantsWidth / 24)); setMaxVisibleParticipantsWithGap(Math.floor(partisipantsWidth / 32)); setFilteredParticipants( data.data.type === 4 ? data.data.participants_approved_data : data.data.participants_data ); setFilteredInterested(data.data.participants_data); setRegistrationInfo(() => { if (data.data.full) { return { color: Colors.LIGHT_GRAY, name: 'FULL' }; } else if (data.data.closed) { return { color: Colors.LIGHT_GRAY, name: 'CLOSED' }; } 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 handleScroll = (event: any) => { const currentScrollY = event.nativeEvent.contentOffset.y; const shouldShow = currentScrollY > 350; if (shouldShow !== showScrollToTop) { setShowScrollToTop(shouldShow); Animated.timing(scrollToTopOpacity, { toValue: shouldShow ? 0.8 : 0, duration: 300, useNativeDriver: true }).start(); } }; const scrollToTop = () => { keyboardAwareScrollViewRef.current?.scrollToPosition(0, 0, true); }; const openModal = (index: number) => { setCurrentImageIndex(index); setIsImageModalVisible(true); }; 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 (!token) { setIsWarningModalVisible(true); return; } if (event.settings.type !== 1 && event.settings.type !== 4) { setModalInfo({ visible: true, type: 'success', title: 'Success', buttonTitle: 'OK', message: `Thank you for joining, 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.date_from && event.date_to) { if (event.date_tentative) { const dateFrom = moment(event.date_from, 'YYYY-MM').format('MMM YYYY'); const dateTo = moment(event.date_to, 'YYYY-MM').format('MMM YYYY'); if (dateFrom === dateTo) { return ( {dateFrom} ); } return ( {dateFrom}{' '} {dateTo} ); } return ( {moment(event.settings.date_from, 'YYYY-MM-DD').format('DD MMM YYYY')}{' '} {moment(event.settings.date_to, 'YYYY-MM-DD').format('DD MMM YYYY')} ); } else { let date = moment(event.date, 'YYYY-MM-DD').format('DD MMM YYYY'); if (event.date_tentative) { return ( {moment(event.date, 'YYYY-MM').format('MMM YYYY')} ); } return ( {date} {event.time_from && event.time_to ? ( {event.time_from} - {event.time_to} ) : event.time ? ( {event.time} ) : null} ); } }; 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 openMap = () => { const rawCoords = event.settings.location; const coords = typeof rawCoords === 'string' ? JSON.parse(rawCoords.replace(/'/g, '"')) : rawCoords; const lat = coords.lat; const lon = coords.lon; const url = Platform.OS === 'ios' ? `http://maps.apple.com/?ll=${lat},${lon}` : `geo:${lat},${lon}?q=${lat},${lon}`; const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`; Linking.canOpenURL(googleMapsUrl) .then((supported) => { if (supported) { Linking.openURL(googleMapsUrl); } else { return Linking.canOpenURL(url).then((supportedFallback) => { if (supportedFallback) { Linking.openURL(url); } else { navigation.dispatch( CommonActions.reset({ index: 1, routes: [ { name: NAVIGATION_PAGES.IN_APP_MAP_TAB, state: { routes: [ { name: NAVIGATION_PAGES.MAP_TAB, params: { lat, lon } } ] } } ] }) ); } }); } }) .catch((err) => console.error('Error opening event location', err)); }; const photoUrl = API_HOST + '/webapi/events/get-main-photo/' + event.id; return ( { navigation.goBack(); }} style={styles.backButton} > {(event.settings.type === 1 || event.settings.type === 4) && photosForRegion.length > 0 ? ( ) : ( )} {registrationInfo && ( {registrationInfo.name} )} {event.settings.name} {event.settings.type === 1 && event?.flag ? ( {event.country}} contentStyle={{ backgroundColor: Colors.FILL_LIGHT }} placement="top" onClose={() => setTooltipVisible(false)} backgroundColor="transparent" allowChildInteraction={false} > setTooltipVisible(true)}> ) : ( )} {event.address1} {formatEventDate(event)} {event.settings.address2 && ( {event.settings.address2} )} {event.settings.registrations_info !== 1 && event.type !== 4 && ( {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 ? ( ) : ( )} {event.settings.host_data.first_name} {event.settings.host_data.last_name} ) : ( )} {(filteredParticipants.length > 0 && event.type !== 4) || (filteredInterested.length === 0 && event.type === 4 && filteredParticipants.length > 0) ? ( Going{` (${filteredParticipants.length})`} {(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 ? ( ) : ( )} ))} navigation.navigate( ...([ NAVIGATION_PAGES.PARTICIPANTS_LIST, { participants: event.type === 4 ? event.participants_approved_full_data : event.participants_full_data, eventId: event.id, isHost: event.settings.host_profile === +currentUserId, isTrip: event.type === 4 } ] as never) ) } > ) : null} {filteredParticipants.length === 0 && event.type === 4 && filteredInterested.length > 0 ? ( Interested{` (${filteredInterested.length})`} {(filteredInterested.length > maxVisibleParticipants ? filteredInterested.slice(0, maxVisibleParticipants - 1) : filteredInterested ).map((user, index) => ( { setTooltipInterested(null); navigation.navigate( ...([ NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: user.uid } ] as never) ); }} > {user.name} } contentStyle={{ backgroundColor: Colors.FILL_LIGHT }} placement="top" onClose={() => setTooltipInterested(null)} key={index} backgroundColor="transparent" > setTooltipInterested(index)} > {user.avatar ? ( ) : ( )} ))} navigation.navigate( ...([ NAVIGATION_PAGES.PARTICIPANTS_LIST, { participants: event.participants_full_data, eventId: event.id, isHost: event.settings.host_profile === +currentUserId, interested: true, isTrip: event.type === 4 } ] as never) ) } > ) : null} {filteredInterested.length > 0 && filteredParticipants.length > 0 && event.type === 4 && ( {filteredInterested.length > 0 ? ( Interested{` (${filteredInterested.length})`} {(filteredInterested.length > maxVisibleParticipants ? filteredInterested.slice(0, maxVisibleParticipants - 1) : filteredInterested ).map((user, index) => ( { setTooltipInterested(null); navigation.navigate( ...([ NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: user.uid } ] as never) ); }} > {user.name} } contentStyle={{ backgroundColor: Colors.FILL_LIGHT }} placement="top" onClose={() => setTooltipInterested(null)} key={index} backgroundColor="transparent" > setTooltipInterested(index)} > {user.avatar ? ( ) : ( )} ))} navigation.navigate( ...([ NAVIGATION_PAGES.PARTICIPANTS_LIST, { participants: event.participants_full_data, eventId: event.id, isHost: event.settings.host_profile === +currentUserId, interested: true, isTrip: event.type === 4 } ] as never) ) } > ) : ( )} {filteredParticipants.length > 0 ? ( Going{` (${filteredParticipants.length})`} {(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 ? ( ) : ( )} ))} navigation.navigate( ...([ NAVIGATION_PAGES.PARTICIPANTS_LIST, { participants: event.type === 4 ? event.participants_approved_full_data : event.participants_full_data, eventId: event.id, isHost: event.settings.host_profile === +currentUserId, isTrip: event.type === 4 } ] as never) ) } > ) : ( )} )} {/* TO DO */} {event.settings.host_profile === +currentUserId && (event.settings.type === 1 || event.settings.type === 4) && !event.archived ? ( getForEditing( { token, event_id: event.id }, { onSuccess: (res) => { event.settings.type === 4 ? navigation.navigate( ...([ NAVIGATION_PAGES.CREATE_SHARED_TRIP, { eventId: event.id, event: res, full: event.full } ] as never) ) : navigation.navigate( ...([ NAVIGATION_PAGES.CREATE_EVENT, { eventId: event.id, event: res } ] as never) ); } } ) } > Edit ) : null} {event.settings.host_profile === +currentUserId || event.archived === 1 ? null : joined ? ( Cancel {event.settings?.chat_token && ( navigation.dispatch( CommonActions.reset({ index: 1, routes: [ { name: 'DrawerApp', state: { routes: [ { name: NAVIGATION_PAGES.IN_APP_MESSAGES_TAB, state: { routes: [ { name: NAVIGATION_PAGES.CHATS_LIST }, { name: NAVIGATION_PAGES.GROUP_CHAT, params: { group_token: event.settings?.chat_token } } ] } } ] } } ] }) ) } > Chat )} ) : !event.full && !event.closed ? ( {event.settings.type === 1 ? ( <> Going ) : ( <> Interested )} ) : null} {event.type === 4 ? ( setToolTipVisible(true)} style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }} > Disclaimer: This trip has been created and published by a community member. NomadMania is not the organizer and is not involved in planning, managing, or overseeing this trip. We take no responsibility for its content, safety, or execution. Please exercise your own judgment when joining. } contentStyle={{ backgroundColor: Colors.WHITE }} placement="bottom" onClose={() => setToolTipVisible(false)} backgroundColor="transparent" allowChildInteraction={false} > setToolTipVisible(true)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > Disclaimer [read more] ) : 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} /> setIsImageModalVisible(false)} backgroundColor={Colors.DARK_BLUE} onImageIndexChange={setActiveIndex} /> setIsWarningModalVisible(false)} /> {showScrollToTop && ( )} ); }; 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}\/)*[^":]+)"/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;