From 63e9b7c939e67784283702e3f9f1d294360aac0c Mon Sep 17 00:00:00 2001 From: Bishwajeet Parhi <62933155+2002Bishwajeet@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:22:27 +0530 Subject: [PATCH] Add Link Payloads in feed composer (#189) * fix image postioning * allow link preview uploads * memoised to avoid multiple re renders * update deps * websockets upgradation to get event updates from all channel and post drives * fix too much re rendering * cleanups * convert to 1 hour * cleanups * remove logs * final * fix bad audio cache fetching * conversation page animated list on new conversation --- package-lock.json | 8 +- package.json | 2 +- packages/mobile/ios/Podfile.lock | 2 +- .../src/components/Chat/Link-Preview-Bar.tsx | 29 ++++++- .../src/components/Chat/MediaMessage.tsx | 86 +++++++++++++------ .../src/components/ui/Media/MediaGallery.tsx | 2 - .../ui/OdinAudio/hooks/useAudio.tsx | 6 +- .../src/hooks/chat/useLiveChatProcessor.ts | 12 ++- .../src/hooks/drive/useDriveSubscriber.ts | 23 +++++ .../mobile/src/hooks/feed/post/usePost.ts | 39 +++++---- .../src/hooks/feed/post/usePostComposer.ts | 25 +++--- .../mobile/src/hooks/feed/useSocialFeed.ts | 21 +++-- .../mobile/src/hooks/links/useLinkPreview.ts | 2 +- .../mobile/src/pages/conversations-page.tsx | 4 +- packages/mobile/src/pages/feed/feed-page.tsx | 79 ++++++++--------- .../mobile/src/pages/feed/post-composer.tsx | 39 ++++++--- .../src/provider/feed/RNPostUploadProvider.ts | 70 +++++++++++---- 17 files changed, 294 insertions(+), 155 deletions(-) create mode 100644 packages/mobile/src/hooks/drive/useDriveSubscriber.ts diff --git a/package-lock.json b/package-lock.json index af4bc148..63b403a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "hasInstallScript": true, "dependencies": { - "@homebase-id/js-lib": "0.0.4-alpha.22", + "@homebase-id/js-lib": "0.0.4-alpha.23", "axios": "1.7.5", "patch-package": "8.0.0", "react": "18.2.0", @@ -2741,9 +2741,9 @@ } }, "node_modules/@homebase-id/js-lib": { - "version": "0.0.4-alpha.22", - "resolved": "https://npm.pkg.github.com/download/@homebase-id/js-lib/0.0.4-alpha.22/5bcbc3de7c06a30cf731075e6b3c00a30ab1aa53", - "integrity": "sha512-4l39pk6Dn9LRk88I3yLtYu2Lt3q4ojhjY1LDlgyl/LI0opr7vXqojiuuO7Q4HxfDxKee3Kxqbt2HmGqqAJg++A==", + "version": "0.0.4-alpha.23", + "resolved": "https://npm.pkg.github.com/download/@homebase-id/js-lib/0.0.4-alpha.23/2ede5b4b7c60cd9a51c3a85d16b16a220bd2c325", + "integrity": "sha512-yRKGP7FTnmYJf7ztDK7//uTD/NW62KtaW5+ZuWxKHlXwuHATPPDFXb6Qw3y3no50/XLtcSoP9TTL06TO5qWMfA==", "dependencies": { "guid-typescript": "^1.0.9" }, diff --git a/package.json b/package.json index 007458e2..2cf14d3a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:libs": "npm run build --w packages/react-native-gifted-chat" }, "dependencies": { - "@homebase-id/js-lib": "0.0.4-alpha.22", + "@homebase-id/js-lib": "0.0.4-alpha.23", "axios": "1.7.5", "patch-package": "8.0.0", "react": "18.2.0", diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 64239deb..bc686340 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -1681,7 +1681,7 @@ SPEC CHECKSUMS: RNSVG: 50cf2c7018e57cf5d3522d98d0a3a4dd6bf9d093 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9 - Yoga: d17d2cc8105eed528474683b42e2ea310e1daf61 + Yoga: 805bf71192903b20fc14babe48080582fee65a80 ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c PODFILE CHECKSUM: f32ae1d81504ba80ec01963a7150115ccbbf36fb diff --git a/packages/mobile/src/components/Chat/Link-Preview-Bar.tsx b/packages/mobile/src/components/Chat/Link-Preview-Bar.tsx index a80e114e..88a882ed 100644 --- a/packages/mobile/src/components/Chat/Link-Preview-Bar.tsx +++ b/packages/mobile/src/components/Chat/Link-Preview-Bar.tsx @@ -3,7 +3,7 @@ import { useLinkPreview } from '../../hooks/links/useLinkPreview'; import { Text } from '../ui/Text/Text'; import { memo, useCallback, useEffect, useState } from 'react'; -import { View } from 'react-native'; +import { ActivityIndicator, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { Close } from '../ui/Icons/icons'; import { LinkPreview } from '@homebase-id/js-lib/media'; @@ -17,7 +17,7 @@ export type LinkPreviewProps = { export const LinkPreviewBar = memo( ({ textToSearchIn, onDismiss, onLinkData }: LinkPreviewProps) => { const link = textToSearchIn.match(/(https?:\/\/[^\s]+)/g)?.[0]; - const { data } = useLinkPreview(link).get; + const { data, isLoading } = useLinkPreview(link).get; const [isVisible, setIsVisible] = useState(false); useEffect(() => { @@ -36,10 +36,31 @@ export const LinkPreviewBar = memo( onDismiss?.(); }, [onDismiss]); + if (isLoading) { + return ( + + + + ); + } + if (!isVisible || !data) { return null; } - const { title, description, imageUrl } = data; return ( )} diff --git a/packages/mobile/src/components/Chat/MediaMessage.tsx b/packages/mobile/src/components/Chat/MediaMessage.tsx index 78c51e40..ec591ab9 100644 --- a/packages/mobile/src/components/Chat/MediaMessage.tsx +++ b/packages/mobile/src/components/Chat/MediaMessage.tsx @@ -1,5 +1,5 @@ -import { Dimensions, GestureResponderEvent } from 'react-native'; -import { memo } from 'react'; +import { Dimensions, GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; +import { memo, useCallback, useMemo } from 'react'; import { MessageImageProps } from 'react-native-gifted-chat'; import { ChatDrive } from '../../provider/chat/ConversationProvider'; @@ -18,49 +18,83 @@ const MediaMessage = memo( props: MessageImageProps; onLongPress: (e: GestureResponderEvent, message: ChatMessageIMessage) => void; }) => { - const navigation = useNavigation>(); + const longPress = useCallback( + (e: GestureResponderEvent, message: ChatMessageIMessage) => onLongPress?.(e, message), + [onLongPress] + ); if (!props.currentMessage || !props.currentMessage.fileMetadata.payloads?.length) return null; + return ( + + ); + } +); + +const InnerMediaMessage = memo( + ({ + currentMessage, + containerStyle, + onLongPress, + }: { + currentMessage: ChatMessageIMessage; + containerStyle?: StyleProp; + onLongPress: (e: GestureResponderEvent, message: ChatMessageIMessage) => void; + }) => { + const navigation = useNavigation>(); const { width, height } = Dimensions.get('screen'); - const { currentMessage } = props; const payloads = currentMessage.fileMetadata.payloads; const isMe = currentMessage.fileMetadata.senderOdinId === ''; - const onClick = (currIndex?: number) => { - navigation.navigate('PreviewMedia', { - fileId: currentMessage.fileId, - payloads: payloads, - senderOdinId: currentMessage.fileMetadata.senderOdinId, - createdAt: currentMessage.fileMetadata.created, - previewThumbnail: currentMessage.fileMetadata.appData.previewThumbnail, - currIndex: currIndex || 0, - targetDrive: ChatDrive, - }); - }; - if (payloads.length === 1) { - const previewThumbnail = currentMessage.fileMetadata.appData.previewThumbnail; + const onClick = useCallback( + (currIndex?: number) => { + navigation.navigate('PreviewMedia', { + fileId: currentMessage.fileId, + payloads: payloads, + senderOdinId: currentMessage.fileMetadata.senderOdinId, + createdAt: currentMessage.fileMetadata.created, + previewThumbnail: currentMessage.fileMetadata.appData.previewThumbnail, + currIndex: currIndex || 0, + targetDrive: ChatDrive, + }); + }, + [currentMessage, navigation, payloads] + ); + const previewThumbnail = currentMessage.fileMetadata.appData.previewThumbnail; - const aspectRatio = - (previewThumbnail?.pixelWidth || 1) / (previewThumbnail?.pixelHeight || 1); + const aspectRatio = useMemo( + () => (previewThumbnail?.pixelWidth || 1) / (previewThumbnail?.pixelHeight || 1), + [previewThumbnail] + ); - const { width: newWidth, height: newHeight } = calculateScaledDimensions( - previewThumbnail?.pixelWidth || 300, - previewThumbnail?.pixelHeight || 300, - { width: width * 0.8, height: height * 0.68 } - ); + const { width: newWidth, height: newHeight } = useMemo( + () => + calculateScaledDimensions( + previewThumbnail?.pixelWidth || 300, + previewThumbnail?.pixelHeight || 300, + { width: width * 0.8, height: height * 0.68 } + ), + [previewThumbnail, width, height] + ); + if (payloads.length === 1) { return ( onLongPress(e, currentMessage)} style={{ borderRadius: 10, diff --git a/packages/mobile/src/components/ui/Media/MediaGallery.tsx b/packages/mobile/src/components/ui/Media/MediaGallery.tsx index 391fc548..794fe71f 100644 --- a/packages/mobile/src/components/ui/Media/MediaGallery.tsx +++ b/packages/mobile/src/components/ui/Media/MediaGallery.tsx @@ -33,7 +33,6 @@ export const MediaGallery = memo( ({ fileId, payloads, - previewThumbnail, onLongPress, onClick, targetDrive, @@ -164,7 +163,6 @@ export const MediaItem = memo( const isAudio = payload.contentType?.startsWith('audio'); const isImage = payload.contentType?.startsWith('image'); const isLink = payload.key === CHAT_LINKS_PAYLOAD_KEY || payload.key === POST_LINKS_PAYLOAD_KEY; - if (!payload.contentType || !payload.key || !fileId) { if (isImage && (payload as NewPayloadDescriptor).pendingFile) { return ( diff --git a/packages/mobile/src/components/ui/OdinAudio/hooks/useAudio.tsx b/packages/mobile/src/components/ui/OdinAudio/hooks/useAudio.tsx index 6162e153..e2b9f0dc 100644 --- a/packages/mobile/src/components/ui/OdinAudio/hooks/useAudio.tsx +++ b/packages/mobile/src/components/ui/OdinAudio/hooks/useAudio.tsx @@ -29,14 +29,12 @@ export const useAudio = (props?: OdinAudioProps) => { if (fileId === undefined || fileId === '' || !drive || !payloadKey) { return null; } - const cachedAudio = getFromCache(fileId, payloadKey, drive); if (cachedAudio) return cachedAudio; const audioBlob = await getPayloadBytes(dotYouClient, drive, fileId, payloadKey, { lastModified, }); - if (!audioBlob) return null; return { url: audioBlob.uri, @@ -44,11 +42,11 @@ export const useAudio = (props?: OdinAudioProps) => { }; }; - const getFromCache = async ( + const getFromCache = ( fileId: string, payloadKey: string, drive: TargetDrive - ): Promise => { + ): AudioData | null => { const queryKey = ['audio', drive?.alias, fileId, payloadKey]; const cachedEntries = queryClient.getQueryCache().find({ queryKey, diff --git a/packages/mobile/src/hooks/chat/useLiveChatProcessor.ts b/packages/mobile/src/hooks/chat/useLiveChatProcessor.ts index 83e8d642..8a81308f 100644 --- a/packages/mobile/src/hooks/chat/useLiveChatProcessor.ts +++ b/packages/mobile/src/hooks/chat/useLiveChatProcessor.ts @@ -18,7 +18,7 @@ import { import { processInbox } from '@homebase-id/js-lib/peer'; import { useNotificationSubscriber } from '../useNotificationSubscriber'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { stringGuidsEqual } from '@homebase-id/js-lib/helpers'; import { getConversationQueryOptions, useConversation } from './useConversation'; @@ -46,7 +46,7 @@ import { } from '../notifications/usePushNotifications'; import { insertNewReaction, removeReaction } from './useChatReaction'; import { useNotification } from '../notifications/useNotification'; -import { BlogConfig } from '@homebase-id/js-lib/public'; +import { useDriveSubscriber } from '../drive/useDriveSubscriber'; const MINUTE_IN_MS = 60000; const isDebug = false; // The babel plugin to remove console logs would remove any if they get to production @@ -146,6 +146,8 @@ const useChatWebsocket = (isEnabled: boolean) => { } = useConversation(); const { add } = useNotification(); const queryClient = useQueryClient(); + const { data: subscribedDrives, isFetched } = useDriveSubscriber(); + const [chatMessagesQueue, setChatMessagesQueue] = useState[]>([]); @@ -308,8 +310,10 @@ const useChatWebsocket = (isEnabled: boolean) => { } }, [processQueue, chatMessagesQueue]); + + return useNotificationSubscriber( - isEnabled ? handler : undefined, + isEnabled && isFetched ? handler : undefined, [ 'fileAdded', 'fileModified', @@ -318,7 +322,7 @@ const useChatWebsocket = (isEnabled: boolean) => { 'statisticsChanged', 'appNotificationAdded', ], - [ChatDrive, BlogConfig.FeedDrive], + subscribedDrives || [], () => { queryClient.invalidateQueries({ queryKey: ['process-inbox'] }); } diff --git a/packages/mobile/src/hooks/drive/useDriveSubscriber.ts b/packages/mobile/src/hooks/drive/useDriveSubscriber.ts new file mode 100644 index 00000000..54650a79 --- /dev/null +++ b/packages/mobile/src/hooks/drive/useDriveSubscriber.ts @@ -0,0 +1,23 @@ +import { getDrivesByType } from '@homebase-id/js-lib/core'; +import { BlogConfig } from '@homebase-id/js-lib/public'; +import { useQuery } from '@tanstack/react-query'; +import { useDotYouClientContext } from 'feed-app-common'; +import { ChatDrive } from '../../provider/chat/ConversationProvider'; + +const PAGE_SIZE = 100; + +export const useDriveSubscriber = () => { + const dotyouClient = useDotYouClientContext(); + const fetchPostsDrives = async () => { + const pagedDrives = await getDrivesByType(dotyouClient, BlogConfig.DriveType, 1, PAGE_SIZE); + return pagedDrives.results.map((drive) => drive.targetDriveInfo); + }; + return useQuery({ + queryKey: ['drive-subscriber'], + select: (data) => [ChatDrive, BlogConfig.FeedDrive, BlogConfig.PublicChannelDrive, ...data], + queryFn: fetchPostsDrives, + refetchOnMount: false, + refetchOnWindowFocus: true, + staleTime: 1000 * 60 * 60, + }); +}; diff --git a/packages/mobile/src/hooks/feed/post/usePost.ts b/packages/mobile/src/hooks/feed/post/usePost.ts index 8b857bac..761e67d5 100644 --- a/packages/mobile/src/hooks/feed/post/usePost.ts +++ b/packages/mobile/src/hooks/feed/post/usePost.ts @@ -19,16 +19,19 @@ import { getRichTextFromString, t, useDotYouClientContext } from 'feed-app-commo import { ImageSource } from '../../../provider/image/RNImageProvider'; import { getSynchronousDotYouClient } from '../../chat/getSynchronousDotYouClient'; import { addError } from '../../errors/useErrors'; +import { LinkPreview } from '@homebase-id/js-lib/media'; const savePost = async ({ postFile, channelId, mediaFiles, + linkPreviews, onUpdate, }: { postFile: NewHomebaseFile | HomebaseFile; channelId: string; mediaFiles?: (ImageSource | MediaFile)[]; + linkPreviews?: LinkPreview[]; onUpdate?: (phase: string, progress: number) => void; }) => { const dotYouClient = await getSynchronousDotYouClient(); @@ -54,13 +57,13 @@ const savePost = async ({ }, }, }; - return savePostFile(dotYouClient, newPost, channelId, mediaFiles, onVersionConflict); + return savePostFile(dotYouClient, newPost, channelId, mediaFiles, linkPreviews, onVersionConflict); }; postFile.fileMetadata.appData.content.captionAsRichText = getRichTextFromString( postFile.fileMetadata.appData.content.caption.trim() ); - return savePostFile(dotYouClient, postFile, channelId, mediaFiles, onVersionConflict, onUpdate); + return savePostFile(dotYouClient, postFile, channelId, mediaFiles, linkPreviews, onVersionConflict, onUpdate); }; export const getSavePostMutationOptions: (queryClient: QueryClient) => MutationOptions< @@ -70,6 +73,7 @@ export const getSavePostMutationOptions: (queryClient: QueryClient) => MutationO postFile: NewHomebaseFile | HomebaseFile; channelId: string; mediaFiles?: (ImageSource | MediaFile)[]; + linkPreviews?: LinkPreview[]; onUpdate?: (phase: string, progress: number) => void; }, { @@ -77,11 +81,12 @@ export const getSavePostMutationOptions: (queryClient: QueryClient) => MutationO postFile: NewHomebaseFile | HomebaseFile; channelId: string; mediaFiles?: (ImageSource | MediaFile)[] | undefined; + linkPreviews?: LinkPreview[] | undefined; onUpdate?: ((phase: string, progress: number) => void) | undefined; }; previousFeed: - | InfiniteData[]>, unknown> - | undefined; + | InfiniteData[]>, unknown> + | undefined; } > = (queryClient) => ({ mutationKey: ['save-post'], @@ -121,12 +126,12 @@ export const getSavePostMutationOptions: (queryClient: QueryClient) => MutationO newFeed.pages[0].results = newFeed.pages[0].results.map((post) => post.fileMetadata.appData.content.id === variables.postFile.fileMetadata.appData.content.id ? { - ...post, - fileMetadata: { - ...post.fileMetadata, - versionTag: _data.newVersionTag, - }, - } + ...post, + fileMetadata: { + ...post.fileMetadata, + versionTag: _data.newVersionTag, + }, + } : post ); @@ -247,14 +252,14 @@ export const usePost = () => { const newFeed = { ...previousFeed }; newFeed.pages[0].results = newFeed.pages[0].results.map((post) => post.fileMetadata.appData.content.id === - variables.postFile.fileMetadata.appData.content.id + variables.postFile.fileMetadata.appData.content.id ? { - ...post, - fileMetadata: { - ...post.fileMetadata, - versionTag: _data.newVersionTag, - }, - } + ...post, + fileMetadata: { + ...post.fileMetadata, + versionTag: _data.newVersionTag, + }, + } : post ); diff --git a/packages/mobile/src/hooks/feed/post/usePostComposer.ts b/packages/mobile/src/hooks/feed/post/usePostComposer.ts index f7dc9be9..717d8381 100644 --- a/packages/mobile/src/hooks/feed/post/usePostComposer.ts +++ b/packages/mobile/src/hooks/feed/post/usePostComposer.ts @@ -17,6 +17,7 @@ import { useState } from 'react'; import { usePost } from './usePost'; import { useDotYouClientContext } from 'feed-app-common'; import { ImageSource } from '../../../provider/image/RNImageProvider'; +import { LinkPreview } from '@homebase-id/js-lib/media'; type CollaborativeChannelDefinition = ChannelDefinition & { acl: AccessControlList }; @@ -32,6 +33,7 @@ export const usePostComposer = () => { const savePost = async ( caption: string | undefined, mediaFiles: ImageSource[] | undefined, + linkPreviews: LinkPreview[] | undefined, embeddedPost: EmbeddedPost | undefined, channel: HomebaseFile | NewHomebaseFile, reactAccess: ReactAccess | undefined, @@ -67,24 +69,25 @@ export const usePostComposer = () => { }, serverMetadata: overrideAcl ? { - accessControlList: overrideAcl, - } + accessControlList: overrideAcl, + } : channel.serverMetadata || - ((channel.fileMetadata.appData.content as CollaborativeChannelDefinition).acl - ? { - accessControlList: ( - channel.fileMetadata.appData.content as CollaborativeChannelDefinition - ).acl, - } - : undefined) || { - accessControlList: { requiredSecurityGroup: SecurityGroupType.Owner }, - }, + ((channel.fileMetadata.appData.content as CollaborativeChannelDefinition).acl + ? { + accessControlList: ( + channel.fileMetadata.appData.content as CollaborativeChannelDefinition + ).acl, + } + : undefined) || { + accessControlList: { requiredSecurityGroup: SecurityGroupType.Owner }, + }, }; await savePostFile({ postFile: postFile, channelId: channel.fileMetadata.appData.uniqueId as string, mediaFiles: mediaFiles, + linkPreviews: linkPreviews, onUpdate: (phase, progress) => setProcessingProgress({ phase, progress }), }); } catch (ex) { diff --git a/packages/mobile/src/hooks/feed/useSocialFeed.ts b/packages/mobile/src/hooks/feed/useSocialFeed.ts index e0408114..69cad1d7 100644 --- a/packages/mobile/src/hooks/feed/useSocialFeed.ts +++ b/packages/mobile/src/hooks/feed/useSocialFeed.ts @@ -5,11 +5,10 @@ import { getSocialFeed, processInbox } from '@homebase-id/js-lib/peer'; import { useCallback } from 'react'; import { stringGuidsEqual } from '@homebase-id/js-lib/helpers'; import { useChannels } from './channels/useChannels'; - import { useChannelDrives } from './channels/useChannelDrives'; import { useDotYouClientContext } from 'feed-app-common'; import { useNotificationSubscriber } from '../useNotificationSubscriber'; -import { ChatDrive } from '../../provider/chat/ConversationProvider'; +import { useDriveSubscriber } from '../drive/useDriveSubscriber'; const MINUTE_IN_MS = 60000; @@ -43,26 +42,26 @@ const useInboxProcessor = (isEnabled?: boolean) => { const useFeedWebsocket = (isEnabled: boolean) => { const queryClient = useQueryClient(); + const { data: subscribedDrives, isFetched } = useDriveSubscriber(); + const handler = useCallback( (notification: TypedConnectionNotification) => { if ( (notification.notificationType === 'fileAdded' || - notification.notificationType === 'fileModified') && - stringGuidsEqual(notification.targetDrive?.alias, BlogConfig.FeedDrive.alias) && - stringGuidsEqual(notification.targetDrive?.type, BlogConfig.FeedDrive.type) + notification.notificationType === 'fileModified') ) { - console.log('Invalidating social feeds'); - queryClient.invalidateQueries({ queryKey: ['social-feeds'] }); + if (subscribedDrives && subscribedDrives.slice(1).some((drive) => stringGuidsEqual(drive.alias, notification.targetDrive?.alias) && stringGuidsEqual(drive.type, notification.targetDrive?.type))) { + queryClient.invalidateQueries({ queryKey: ['social-feeds'] }); + } } }, - [queryClient] + [queryClient, subscribedDrives] ); - return useNotificationSubscriber( - isEnabled ? handler : undefined, + isEnabled && isFetched ? handler : undefined, ['fileAdded', 'fileModified'], - [BlogConfig.FeedDrive, ChatDrive], + subscribedDrives || [], () => { queryClient.invalidateQueries({ queryKey: ['process-inbox-feed'] }); } diff --git a/packages/mobile/src/hooks/links/useLinkPreview.ts b/packages/mobile/src/hooks/links/useLinkPreview.ts index 75cd79a6..f4517623 100644 --- a/packages/mobile/src/hooks/links/useLinkPreview.ts +++ b/packages/mobile/src/hooks/links/useLinkPreview.ts @@ -59,7 +59,7 @@ export const useLinkMetadata = ({ return getPayloadAsJsonOverPeer(dotYouClient, odinId, targetDrive, fileId, payloadKey); } return getPayloadAsJson(dotYouClient, targetDrive, fileId, payloadKey); - } + }; return useQuery({ queryKey: ['link-metadata', targetDrive.alias, fileId, payloadKey], diff --git a/packages/mobile/src/pages/conversations-page.tsx b/packages/mobile/src/pages/conversations-page.tsx index 2c121209..0a4519c8 100644 --- a/packages/mobile/src/pages/conversations-page.tsx +++ b/packages/mobile/src/pages/conversations-page.tsx @@ -37,6 +37,7 @@ import { SafeAreaView } from '../components/ui/SafeAreaView/SafeAreaView'; import { OfflineState } from '../components/Platform/OfflineState'; import { ConversationTileWithYourself } from '../components/Conversation/ConversationTileWithYourself'; import { EmptyConversation } from '../components/Conversation/EmptyConversation'; +import Animated, { LinearTransition } from 'react-native-reanimated'; type ConversationProp = NativeStackScreenProps; @@ -139,8 +140,9 @@ export const ConversationsPage = memo(({ navigation }: ConversationProp) => { {conversations && conversations?.length ? ( - ; -export const FeedPage = memo(({ navigation }: FeedProps) => { +export const FeedPage = memo((_: FeedProps) => { const isFocused = useIsFocused(); useRemoveNotifications({ disabled: !isFocused, appId: FEED_APP_ID }); - const [keyboardVisible, setKeyboardVisible] = useState(false); - - useEffect(() => { - const showSubscription = Keyboard.addListener('keyboardDidShow', () => { - setKeyboardVisible(true); - }); - const hideSubscription = Keyboard.addListener('keyboardDidHide', () => { - setKeyboardVisible(false); - }); - - return () => { - showSubscription.remove(); - hideSubscription.remove(); - }; - }, []); - return ( {/* */} - {!keyboardVisible && ( - navigation.navigate('Compose')} - > - - - )} + ); }); -export const FeedHeader = () => { +const FloatingActionButton = memo(() => { + const { isDarkMode } = useDarkMode(); + const backgroundColor = useMemo( + () => (isDarkMode ? Colors.indigo[500] : Colors.indigo[200]), + [isDarkMode] + ); + const navigation = useNavigation>(); + const onPress = useCallback(() => { + navigation.navigate('Compose'); + }, [navigation]); + + return ( + + + + ); +}); + +export const FeedHeader = memo(() => { const isOnline = useLiveFeedProcessor(); const headerTitle = useCallback( @@ -97,4 +94,4 @@ export const FeedHeader = () => { headerStatusBarHeight={0} /> ); -}; +}); diff --git a/packages/mobile/src/pages/feed/post-composer.tsx b/packages/mobile/src/pages/feed/post-composer.tsx index 09ba0544..d198a327 100644 --- a/packages/mobile/src/pages/feed/post-composer.tsx +++ b/packages/mobile/src/pages/feed/post-composer.tsx @@ -32,13 +32,14 @@ import React from 'react'; import Animated, { SlideInDown, SlideOutDown } from 'react-native-reanimated'; import { FeedStackParamList } from '../../app/FeedStack'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { LinkPreviewBar } from '../../components/Chat/Link-Preview-Bar'; +import { LinkPreview } from '@homebase-id/js-lib/media'; type PostComposerProps = NativeStackScreenProps; export const PostComposer = memo(({ navigation }: PostComposerProps) => { const { isDarkMode } = useDarkMode(); const insets = useSafeAreaInsets(); - const [stateIndex, setStateIndex] = useState(0); // Used to force a re-render of the component, to reset the input const identity = useDotYouClientContext().getIdentity(); @@ -52,15 +53,16 @@ export const PostComposer = memo(({ navigation }: PostComposerProps) => { const [assets, setAssets] = useState([]); const [reactAccess, setReactAccess] = useState(undefined); + const [linkPreviews, setLinkPreviews] = useState(null); + const onDismissLinkPreview = useCallback(() => { + setLinkPreviews(null); + }, []); + const onLinkData = useCallback((link: LinkPreview) => { + setLinkPreviews(link); + }, []); const isPosting = useMemo(() => !!processingProgress?.phase, [processingProgress]); - const resetUi = useCallback(() => { - setCaption(''); - setAssets([]); - setStateIndex((i) => i + 1); - }, []); - const doPost = useCallback(async () => { if (isPosting) return; await savePost( @@ -79,14 +81,25 @@ export const PostComposer = memo(({ navigation }: PostComposerProps) => { fileSize: value.fileSize, }; }), + linkPreviews ? [linkPreviews] : undefined, undefined, channel, reactAccess, customAcl ); - resetUi(); + navigation.goBack(); - }, [isPosting, savePost, caption, assets, channel, reactAccess, customAcl, resetUi, navigation]); + }, [ + isPosting, + savePost, + caption, + assets, + linkPreviews, + channel, + reactAccess, + customAcl, + navigation, + ]); const handleImageIconPress = useCallback(async () => { const imagePickerResult = await launchImageLibrary({ @@ -187,14 +200,18 @@ export const PostComposer = memo(({ navigation }: PostComposerProps) => { fontSize: 16, minHeight: 124, color: isDarkMode ? Colors.white : Colors.black, + textAlignVertical: 'top', }} multiline={true} onChange={(event) => setCaption(event.nativeEvent.text)} - key={stateIndex} /> - + ( file: HomebaseFile | NewHomebaseFile, channelId: string, toSaveFiles?: (ImageSource | MediaFile)[] | ImageSource[], + linkPreviews?: LinkPreview[], onVersionConflict?: () => void, onUpdate?: (phase: string, progress: number) => void ): Promise => { @@ -92,7 +95,7 @@ export const savePost = async ( const encrypt = !( file.serverMetadata?.accessControlList?.requiredSecurityGroup === SecurityGroupType.Anonymous || file.serverMetadata?.accessControlList?.requiredSecurityGroup === - SecurityGroupType.Authenticated + SecurityGroupType.Authenticated ); const targetDrive = GetTargetDriveFromChannelId(channelId); @@ -102,11 +105,48 @@ export const savePost = async ( const previewThumbnails: EmbeddedThumb[] = []; const keyHeader: KeyHeader | undefined = encrypt ? { - iv: getRandom16ByteArray(), - aesKey: getRandom16ByteArray(), - } + iv: getRandom16ByteArray(), + aesKey: getRandom16ByteArray(), + } : undefined; + if (!newMediaFiles?.length && linkPreviews?.length) { + // We only support link previews when there is no media + const descriptorContent = JSON.stringify( + linkPreviews.map((preview) => { + return { + url: preview.url, + hasImage: !!preview.imageUrl, + imageWidth: preview.imageWidth, + imageHeight: preview.imageHeight, + } as LinkPreviewDescriptor; + }) + ); + + const linkPreviewWithImage = linkPreviews.find((preview) => preview.imageUrl); + + const imageSource: ImageSource | undefined = linkPreviewWithImage + ? { + height: linkPreviewWithImage.imageHeight || 0, + width: linkPreviewWithImage.imageWidth || 0, + uri: linkPreviewWithImage.imageUrl, + } + : undefined; + + const { tinyThumb } = imageSource + ? await createThumbnails(imageSource, '') + : { tinyThumb: undefined }; + + payloads.push({ + key: POST_LINKS_PAYLOAD_KEY, + payload: new OdinBlob([stringToUint8Array(JSON.stringify(linkPreviews))], { + type: 'application/json', + }) as unknown as Blob, + descriptorContent, + previewThumbnail: tinyThumb, + }); + } + // Handle image files: for (let i = 0; newMediaFiles && i < newMediaFiles?.length; i++) { const newMediaFile = newMediaFiles[i]; @@ -164,10 +204,10 @@ export const savePost = async ( if (file.fileMetadata.appData.content.type !== 'Article') { file.fileMetadata.appData.content.primaryMediaFile = payloads[0] ? { - fileId: undefined, - fileKey: payloads[0].key, - type: payloads[0].payload.type, - } + fileId: undefined, + fileKey: payloads[0].key, + type: payloads[0].payload.type, + } : undefined; } @@ -213,7 +253,7 @@ const uploadPost = async ( const encrypt = !( file.serverMetadata?.accessControlList?.requiredSecurityGroup === SecurityGroupType.Anonymous || file.serverMetadata?.accessControlList?.requiredSecurityGroup === - SecurityGroupType.Authenticated + SecurityGroupType.Authenticated ); const instructionSet: UploadInstructionSet = { @@ -241,9 +281,8 @@ const uploadPost = async ( !stringGuidsEqual(existingPostWithThisSlug?.fileId, file.fileId) ) { // There is clash with an existing slug - file.fileMetadata.appData.content.slug = `${ - file.fileMetadata.appData.content.slug - }-${new Date().getTime()}`; + file.fileMetadata.appData.content.slug = `${file.fileMetadata.appData.content.slug + }-${new Date().getTime()}`; } const uniqueId = file.fileMetadata.appData.content.slug @@ -360,12 +399,11 @@ const uploadPostHeader = async ( if ( existingPostWithThisSlug && existingPostWithThisSlug?.fileMetadata.appData.content.id !== - file.fileMetadata.appData.content.id + file.fileMetadata.appData.content.id ) { // There is clash with an existing slug - file.fileMetadata.appData.content.slug = `${ - file.fileMetadata.appData.content.slug - }-${new Date().getTime()}`; + file.fileMetadata.appData.content.slug = `${file.fileMetadata.appData.content.slug + }-${new Date().getTime()}`; } const uniqueId = file.fileMetadata.appData.content.slug