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