From a4e7c209f81eb2baef7b4ab12bf5e72afa9f87cf Mon Sep 17 00:00:00 2001 From: HuyDo Date: Tue, 16 Jul 2024 15:16:28 +0700 Subject: [PATCH] feat: [FC-15] preview link --- src/chat/GalleryScreen.tsx | 189 ++++++++++++++++++++++ src/chat/components/links/Links.tsx | 55 +++++++ src/chat/components/links/PreviewLink.tsx | 123 ++++++++++++++ src/chat/index.ts | 1 + src/services/firebase/firestore.ts | 43 ++++- src/utilities/Date.ts | 17 +- src/utilities/index.ts | 1 + src/utilities/misc.ts | 39 +++++ 8 files changed, 461 insertions(+), 7 deletions(-) create mode 100644 src/chat/GalleryScreen.tsx create mode 100644 src/chat/components/links/Links.tsx create mode 100644 src/chat/components/links/PreviewLink.tsx create mode 100644 src/utilities/misc.ts diff --git a/src/chat/GalleryScreen.tsx b/src/chat/GalleryScreen.tsx new file mode 100644 index 0000000..a2e7356 --- /dev/null +++ b/src/chat/GalleryScreen.tsx @@ -0,0 +1,189 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + View, + Text, + // FlatList, + StyleSheet, + TouchableOpacity, + Dimensions, +} from 'react-native'; +// import FastImage from 'react-native-fast-image'; +import { FirestoreServices } from '../services/firebase'; +// import { MessageTypes, type MediaFile } from '../interfaces'; +// import ThumbnailVideoPlayer from '../chat_obs/components/ThumbnailVideoPlayer'; +// import SelectedViewModal from '../chat_obs/components/SelectedViewModal'; +import { Links, SectionData } from './components/links/Links'; +import { transformLinksDataForSectionList } from '../utilities/misc'; +// type MediaItem = { +// item: MediaFile; +// index: number; +// }; + +const { width } = Dimensions.get('window'); + +interface GalleryModalProps { + renderCustomHeader?: () => JSX.Element; + // renderCustomMedia?: ({ item, index }: MediaItem) => JSX.Element | null; + renderCustomFile?: () => JSX.Element; + renderCustomLink?: () => JSX.Element; +} + +export const GalleryScreen: React.FC = ({ + renderCustomHeader, + // renderCustomMedia, + renderCustomFile, + renderCustomLink, +}) => { + const [activeTab, setActiveTab] = useState('Media'); + // const [media, setMedia] = useState([]); + const [links, setLinks] = useState([]); + const firebaseInstance = useRef(FirestoreServices.getInstance()).current; + // const [mediaSelected, setMediaSelected] = useState(); + + useEffect(() => { + switch (activeTab) { + case 'Media': + // { + // const loadImages = async () => { + // const urls = await firebaseInstance.getMediaFilesByConversationId(); + // setMedia(urls); + // }; + // loadImages(); + // } + break; + case 'File': + break; + case 'Link': + const loadLinks = async () => { + const getLinks = transformLinksDataForSectionList( + await firebaseInstance.getUserLinks() + ); + setLinks(getLinks); + }; + loadLinks(); + break; + } + }, [activeTab, firebaseInstance]); + + // const renderImage = ({ item, index }: MediaItem) => { + // if (renderCustomMedia) return renderCustomMedia({ item, index }); + // return ( + // setMediaSelected(item)}> + // {item.type === MessageTypes.video ? ( + // + // ) : ( + // + // )} + // + // ); + // }; + + const renderHeader = (): JSX.Element => { + if (renderCustomHeader) return renderCustomHeader(); + return ( + + {['Media', 'File', 'Link'].map((tab) => ( + setActiveTab(tab)} + > + + {tab.toUpperCase()} + + {activeTab === tab && } + + ))} + + ); + }; + + const renderContent = () => { + switch (activeTab) { + case 'Media': + return ( + + {/* item.id} + numColumns={3} + /> + setMediaSelected(undefined)} + /> */} + + ); + case 'File': + if (renderCustomFile) return renderCustomFile(); + return ( + + No Files Available + + ); + case 'Link': + if (renderCustomLink) return renderCustomLink(); + return ; + default: + return null; + } + }; + + return ( + + {renderHeader()} + {renderContent()} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + tabContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + marginVertical: 10, + borderBottomWidth: 1, + borderBottomColor: '#ddd', + }, + tab: { + flex: 1, + alignItems: 'center', + paddingVertical: 10, + position: 'relative', + }, + tabText: { + color: '#8888', + fontSize: 16, + }, + activeTabText: { + color: 'black', + }, + activeTab: {}, + tabIndicator: { + position: 'absolute', + bottom: -1, + height: 2, + width: '100%', + backgroundColor: 'gray', + }, + image: { + width: width / 3, + height: width / 3, + margin: 1, + }, + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/src/chat/components/links/Links.tsx b/src/chat/components/links/Links.tsx new file mode 100644 index 0000000..9d3e14e --- /dev/null +++ b/src/chat/components/links/Links.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { View, Text, StyleSheet, SectionList } from 'react-native'; +import { PreviewLink } from './PreviewLink'; + +export type LinksType = { + [date: string]: string[]; +}; + +export type SectionData = { + title: string; + data: readonly string[]; +}; + +interface LinkProps { + renderCustomLink?: () => JSX.Element; + links: SectionData[]; +} + +export const Links: React.FC = ({ renderCustomLink, links }) => { + const renderItemLink = ({ item, index }: { item: string; index: number }) => ( + + + + ); + if (!links) return null; + + if (renderCustomLink) return renderCustomLink(); + return ( + `${index}-${item[index]}`} + renderItem={renderItemLink} + renderSectionHeader={({ section: { title } }) => ( + {title} + )} + /> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + fontSize: 16, + color: 'black', + fontWeight: 'bold', + marginLeft: 10, + }, + itemContainer: { + padding: 10, + borderBottomWidth: 1, + borderBottomColor: '#ccc', + }, +}); diff --git a/src/chat/components/links/PreviewLink.tsx b/src/chat/components/links/PreviewLink.tsx new file mode 100644 index 0000000..64dbc3f --- /dev/null +++ b/src/chat/components/links/PreviewLink.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { Image, StyleSheet, Text, View } from 'react-native'; +import { MessageText } from 'react-native-gifted-chat'; +import type { MessageProps } from '../../../interfaces'; +import { LinkPreview } from '@flyerhq/react-native-link-preview'; + +type PreviewLinkProps = { + link?: string; + currentMessage?: MessageProps; + customPreviewLink?: (link: string) => JSX.Element; + iconDefault?: string; +}; + +export const PreviewLink: React.FC = (props) => { + const { currentMessage, link, customPreviewLink } = props; + const urls = currentMessage?.text.match(/(https?:\/\/[^\s]+)/g); + + const renderPreview = (url: string, index: number = 0) => { + return ( + { + return ( + + + + ); + }} + renderTitle={(title) => ( + + {title} + + )} + renderText={(text) => ( + + {text} + + )} + renderDescription={(description) => ( + + {description} + + )} + /> + ); + }; + + if (customPreviewLink) { + if (link) return customPreviewLink(link); + } + + if (link) { + return renderPreview(link); + } + + if (urls) { + return ( + + {currentMessage?.text} + {urls.map((item, index) => ( + ( + {text} + )} + /> + ))} + + ); + } + return ; +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + containerPreview: { + padding: 10, + }, + previewContainer: { + backgroundColor: 'white', + borderRadius: 20, + marginTop: 16, + }, + textPreview: { + color: 'blue', + textDecorationLine: 'underline', + }, + + renderImage: { + width: 100, + height: 100, + backgroundColor: 'green', + position: 'absolute', + left: 8, + top: 20, + }, + imageStyle: { + width: 100, + height: 100, + resizeMode: 'stretch', + }, + viewTitle: { + marginLeft: 100, + }, + title: { + color: 'black', + fontWeight: 'bold', + }, + viewDetail: { + marginLeft: 100, + marginTop: 10, + }, + textDetails: { + color: 'black', + }, +}); diff --git a/src/chat/index.ts b/src/chat/index.ts index 850df13..383ce31 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -1,3 +1,4 @@ export * from './ChatProvider'; export * from './ChatScreen'; export * from './ListConversationScreen'; +export * from './GalleryScreen'; diff --git a/src/services/firebase/firestore.ts b/src/services/firebase/firestore.ts index 58e5286..6605aac 100644 --- a/src/services/firebase/firestore.ts +++ b/src/services/firebase/firestore.ts @@ -11,7 +11,10 @@ import { formatSendMessage, generateBadWordsRegex, generateKey, + getCurrentFormattedDate, getCurrentTimestamp, + addLinkByDate, + extractLinks, } from '../../utilities'; import { ConversationProps, @@ -242,8 +245,10 @@ export class FirestoreServices { this.userId, messageData.text ); + + const links = extractLinks(message.text); this.memberIds?.forEach((memberId) => { - this.updateUserConversation(memberId, latestMessageData); + this.updateUserConversation(memberId, latestMessageData, links); }); } catch (e) { console.log(e); @@ -251,25 +256,51 @@ export class FirestoreServices { } }; - updateUserConversation = ( + getUserLinks = async () => { + if (!this.conversationId) { + throw new Error( + 'Please create conversation before send the first message!' + ); + } + + const conversationRef = firestore() + .collection( + `${FireStoreCollection.users}/${this.userId}/${FireStoreCollection.conversations}` + ) + .doc(this.conversationId); + const doc = await conversationRef.get(); + const existingLinks = doc.data()?.links || []; + return existingLinks; + }; + + updateUserConversation = async ( userId: string, - latestMessageData: LatestMessageProps + latestMessageData: LatestMessageProps, + links?: string[] | null ) => { if (!this.conversationId) { throw new Error( 'Please create conversation before send the first message!' ); } - /** Update latest message for each member */ - firestore() + + const conversationRef = firestore() .collection( `${FireStoreCollection.users}/${userId}/${FireStoreCollection.conversations}` ) - .doc(this.conversationId) + .doc(this.conversationId); + const doc = await conversationRef.get(); + + const getToday = getCurrentFormattedDate(); + const existingLinks = doc.data()?.links || []; + const currentLinks = addLinkByDate(existingLinks, links, getToday); + + conversationRef .set( { latestMessage: latestMessageData, updatedAt: getCurrentTimestamp(), + links: currentLinks, }, { merge: true } ) diff --git a/src/utilities/Date.ts b/src/utilities/Date.ts index 80cfbc3..3d032f7 100644 --- a/src/utilities/Date.ts +++ b/src/utilities/Date.ts @@ -19,4 +19,19 @@ const getCurrentTimestamp = () => { return Math.floor(msCurrentTime); }; -export { formatDate, timeFromNow, getCurrentTimestamp }; +const formatTime = (time: number) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; +}; + +export const getCurrentFormattedDate = () => { + const date = new Date(); + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based + const year = date.getFullYear(); + + return `${day}-${month}-${year}`; +}; + +export { formatDate, timeFromNow, formatTime, getCurrentTimestamp }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 65ec47a..14012d8 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -3,3 +3,4 @@ export * from './AESCrypto'; export * from './Color'; export * from './MessageFormatter'; export * from './Blacklist'; +export * from './misc'; diff --git a/src/utilities/misc.ts b/src/utilities/misc.ts new file mode 100644 index 0000000..73eaa85 --- /dev/null +++ b/src/utilities/misc.ts @@ -0,0 +1,39 @@ +import { LinksType, SectionData } from '../chat/components/links/Links'; + +const extractLinks = (message: string) => { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const links = message.match(urlRegex) || null; + return links; +}; + +export interface Links { + [date: string]: string[]; +} + +const addLinkByDate = ( + links: Links, + newLinks: string[] | null | undefined, + date: string +): Links => { + if (!newLinks) return links; + if (links[date]) { + return { + ...links, + [date]: [...(links[date] ?? []), ...newLinks], + }; + } else { + return { + ...links, + [date]: newLinks, + }; + } +}; + +const transformLinksDataForSectionList = (links: LinksType): SectionData[] => { + return Object.keys(links).map((date) => ({ + title: date, + data: links[date] ?? [], + })); +}; + +export { extractLinks, addLinkByDate, transformLinksDataForSectionList };