Skip to content

Commit

Permalink
Community optional default payload fetching (#491)
Browse files Browse the repository at this point in the history
* WIP;

* Added dynamic fetching of the payloads;

* Extend header limits for the content header;

* Added clear limits; Included on optimistic updates and ws updates;
stef-coenen authored Dec 19, 2024
1 parent ca214b4 commit 3e5eeca
Showing 16 changed files with 471 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -143,7 +143,7 @@ export const MessageComposer = ({
channel,
thread,
threadParticipants: extendedParticipants,
message: trimRichText(message) || '',
message: trimRichText(message),
files: newFiles,
chatId: getNewId(),
userDate: new Date().getTime(),
Original file line number Diff line number Diff line change
@@ -8,16 +8,20 @@ import {
COMMUNITY_ROOT_PATH,
ActionButton,
useLongPress,
useContentFromPayload,
} from '@homebase-id/common-app';
import { HomebaseFile, RichText } from '@homebase-id/js-lib/core';
import { DEFAULT_PAYLOAD_KEY, HomebaseFile, RichText } from '@homebase-id/js-lib/core';
import {
formatGuidId,
isTouchDevice,
stringGuidsEqual,
toGuidId,
} from '@homebase-id/js-lib/helpers';
import { CommunityMessage } from '../../../../providers/CommunityMessageProvider';
import { CommunityDefinition } from '../../../../providers/CommunityDefinitionProvider';
import {
CommunityDefinition,
getTargetDriveFromCommunityId,
} from '../../../../providers/CommunityDefinitionProvider';
import { CommunityActions, ContextMenu } from '../../channel/ContextMenu';
import { Link, useMatch, useNavigate, useParams } from 'react-router-dom';
import { CommunityDeliveryIndicator } from './CommunityDeliveryIndicator';
@@ -59,7 +63,8 @@ export const CommunityMessageItem = memo(
scrollRef,
} = props;

const hasMedia = !!msg.fileMetadata.payloads?.length;
const hasMedia = !!msg.fileMetadata.payloads?.filter((pyld) => pyld.key !== DEFAULT_PAYLOAD_KEY)
?.length;

const { chatMessageKey, mediaKey, channelKey, threadKey } = useParams();
const isDetail = stringGuidsEqual(msg.fileMetadata.appData.uniqueId, chatMessageKey);
@@ -222,22 +227,52 @@ const CommunityTextMessageBody = ({
msg: HomebaseFile<CommunityMessage>;
community?: HomebaseFile<CommunityDefinition>;
}) => {
const [loadMore, setLoadMore] = useState(false);

const content = msg.fileMetadata.appData.content;
const plainText = getTextRootsRecursive(content.message).join(' ');
const isEmojiOnly =
(plainText?.match(/^\p{Extended_Pictographic}/u) && !plainText?.match(/[0-9a-zA-Z]/)) ?? false;

const hasMoreContent = msg.fileMetadata.payloads?.some(
(payload) => payload.key === DEFAULT_PAYLOAD_KEY
);

const { data: fullContent } = useContentFromPayload<CommunityMessage>(
hasMoreContent && loadMore && community
? {
odinId: community?.fileMetadata.senderOdinId,
targetDrive: getTargetDriveFromCommunityId(
community.fileMetadata.appData.uniqueId as string
),
fileId: msg.fileId,
payloadKey: DEFAULT_PAYLOAD_KEY,
lastModified: msg.fileMetadata.payloads?.find((pyld) => pyld.key === DEFAULT_PAYLOAD_KEY)
?.lastModified,
systemFileType: msg.fileSystemType,
}
: undefined
);

return (
<div className={`relative w-auto rounded-lg`}>
<div className="flex flex-col md:flex-row md:flex-wrap md:gap-2">
<div className="flex w-full min-w-0 flex-col gap-1">
<MessageTextRenderer
community={community}
message={content.message}
message={((loadMore && fullContent) || content).message}
className={`copyable-content whitespace-pre-wrap break-words ${
isEmojiOnly ? 'text-7xl' : ''
}`}
/>
{hasMoreContent ? (
<a
className="mr-auto cursor-pointer text-primary hover:underline"
onClick={() => setLoadMore((old) => !old)}
>
{loadMore ? t('Less') : t('More')}
</a>
) : null}
</div>
</div>
<CommunityReactions msg={msg} community={community} />
@@ -251,7 +286,7 @@ const MessageTextRenderer = ({
className,
}: {
community?: HomebaseFile<CommunityDefinition>;
message: RichText | string;
message: RichText | undefined;
className?: string;
}) => {
const { data: channels } = useCommunityChannels({
@@ -345,14 +380,45 @@ const CommunityMediaMessageBody = ({
const content = msg.fileMetadata.appData.content;
const hasACaption = !!content.message;

const [loadMore, setLoadMore] = useState(false);
const hasMoreContent = msg.fileMetadata.payloads?.some(
(payload) => payload.key === DEFAULT_PAYLOAD_KEY
);

const { data: fullContent } = useContentFromPayload<CommunityMessage>(
hasMoreContent && loadMore && community
? {
odinId: community?.fileMetadata.senderOdinId,
targetDrive: getTargetDriveFromCommunityId(
community.fileMetadata.appData.uniqueId as string
),
fileId: msg.fileId,
payloadKey: DEFAULT_PAYLOAD_KEY,
lastModified: msg.fileMetadata.payloads?.find((pyld) => pyld.key === DEFAULT_PAYLOAD_KEY)
?.lastModified,
systemFileType: msg.fileSystemType,
}
: undefined
);

return (
<div className={`relative w-full max-w-[75vw] rounded-lg md:max-w-[90%]`}>
{hasACaption ? (
<MessageTextRenderer
community={community}
message={content.message}
className={`whitespace-pre-wrap break-words`}
/>
<>
<MessageTextRenderer
community={community}
message={((loadMore && fullContent) || content).message}
className={`whitespace-pre-wrap break-words`}
/>
{hasMoreContent ? (
<a
className="mr-auto cursor-pointer text-primary hover:underline"
onClick={() => setLoadMore((old) => !old)}
>
{loadMore ? t('Less') : t('More')}
</a>
) : null}
</>
) : null}
<CommunityMedia
msg={msg}
Original file line number Diff line number Diff line change
@@ -33,7 +33,6 @@ import {
invalidateCommunityMessages,
removeMessage,
} from '../messages/useCommunityMessages';
import { getPayloadAsJsonOverPeer } from '@homebase-id/js-lib/peer';
import { invalidateCommunityThreads } from '../threads/useCommunityThreads';

const isDebug = hasDebugFlag();
@@ -45,7 +44,6 @@ export const useCommunityPeerWebsocket = (
) => {
const queryClient = useQueryClient();
const targetDrive = getTargetDriveFromCommunityId(communityId || '');
const dotYouClient = useDotYouClientContext();

const handler = useCallback(
async (decryptionClient: DotYouClient, notification: TypedConnectionNotification) => {
@@ -65,13 +63,7 @@ export const useCommunityPeerWebsocket = (
const groupId = notification.header.fileMetadata.appData.groupId;

// This skips the invalidation of all chat messages, as we only need to add/update this specific message
const updatedChatMessage = await wsDsrToMessage(
decryptionClient,
dotYouClient,
notification.header,
odinId,
targetDrive
);
const updatedChatMessage = await wsDsrToMessage(decryptionClient, notification.header);

if (
!updatedChatMessage ||
@@ -138,10 +130,7 @@ export const useCommunityPeerWebsocket = (

const wsDsrToMessage = async (
websocketDotyouClient: DotYouClient,
dotYouClient: DotYouClient,
dsr: HomebaseFile,
odinId: string | undefined,
targetDrive: TargetDrive
dsr: HomebaseFile
): Promise<HomebaseFile<CommunityMessage> | null> => {
const { fileId, fileMetadata, sharedSecretEncryptedKeyHeader } = dsr;
if (!fileId || !fileMetadata) {
@@ -150,37 +139,14 @@ const wsDsrToMessage = async (
);
}

const contentIsComplete =
fileMetadata.payloads?.filter((payload) => payload.key === DEFAULT_PAYLOAD_KEY).length === 0;
if (fileMetadata.isEncrypted && !sharedSecretEncryptedKeyHeader) return null;

const keyHeader = fileMetadata.isEncrypted
? await decryptKeyHeader(
websocketDotyouClient,
sharedSecretEncryptedKeyHeader as EncryptedKeyHeader
)
: undefined;

let content: CommunityMessage | undefined;
if (contentIsComplete) {
content = tryJsonParse<CommunityMessage>(await decryptJsonContent(fileMetadata, keyHeader));
} else {
content =
(odinId && odinId !== dotYouClient.getHostIdentity()
? await getPayloadAsJsonOverPeer<CommunityMessage>(
dotYouClient,
odinId,
targetDrive,
fileId,
DEFAULT_PAYLOAD_KEY
)
: await getPayloadAsJson<CommunityMessage>(
dotYouClient,
targetDrive,
fileId,
DEFAULT_PAYLOAD_KEY
)) || undefined;
}
const content = tryJsonParse<CommunityMessage>(await decryptJsonContent(fileMetadata, keyHeader));

if (!content) return null;

Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import {
useQuery,
useQueryClient,
} from '@tanstack/react-query';
import { useDotYouClientContext } from '@homebase-id/common-app';
import { ellipsisAtMaxCharOfRichText, useDotYouClientContext } from '@homebase-id/common-app';
import {
DotYouClient,
HomebaseFile,
@@ -20,6 +20,7 @@ import {
CommunityDeliveryStatus,
CommunityMessage,
getCommunityMessage,
MESSAGE_CHARACTERS_LIMIT,
updateCommunityMessage,
uploadCommunityMessage,
} from '../../../providers/CommunityMessageProvider';
@@ -93,7 +94,7 @@ export const useCommunityMessage = (props?: {
thread?: HomebaseFile<CommunityMessage>;
threadParticipants?: string[];
files?: NewMediaFile[];
message: RichText | string;
message: RichText | undefined;
linkPreviews?: LinkPreview[];
chatId?: string;
userDate?: number;
@@ -192,7 +193,7 @@ export const useCommunityMessage = (props?: {
groupId:
thread?.fileMetadata.globalTransitId || channel?.fileMetadata.appData.uniqueId,
content: {
message: message,
message: ellipsisAtMaxCharOfRichText(message, MESSAGE_CHARACTERS_LIMIT),
deliveryStatus: CommunityDeliveryStatus.Sending,
channelId: channel.fileMetadata.appData.uniqueId as string,
},
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import {
FileQueryParams,
GetBatchQueryResultOptions,
TargetDrive,
getContentFromHeaderOrPayload,
getContentFromHeader,
queryBatch,
deleteFile,
RichText,
@@ -28,6 +28,7 @@ import {
patchFile,
UpdateInstructionSet,
UpdateResult,
getContentFromHeaderOrPayload,
} from '@homebase-id/js-lib/core';
import {
jsonStringify64,
@@ -46,20 +47,22 @@ import { CommunityDefinition, getTargetDriveFromCommunityId } from './CommunityD
import {
deleteFileOverPeer,
getContentFromHeaderOrPayloadOverPeer,
getContentFromHeaderOverPeer,
getFileHeaderOverPeerByUniqueId,
queryBatchOverPeer,
TransitInstructionSet,
TransitUploadResult,
uploadFileOverPeer,
} from '@homebase-id/js-lib/peer';
import { COMMUNITY_APP_ID } from '@homebase-id/common-app';
import { COMMUNITY_APP_ID, ellipsisAtMaxCharOfRichText } from '@homebase-id/common-app';

export const COMMUNITY_MESSAGE_FILE_TYPE = 7020;
export const CommunityDeletedArchivalStaus = 2;

const COMMUNITY_MESSAGE_PAYLOAD_KEY = 'comm_web';
export const COMMUNITY_LINKS_PAYLOAD_KEY = 'comm_links';
export const COMMUNITY_PINNED_TAG = toGuidId('pinned-message');
export const MESSAGE_CHARACTERS_LIMIT = 1600;

export enum CommunityDeliveryStatus {
Sending = 15, // When it's sending; Used for optimistic updates
@@ -72,7 +75,7 @@ export interface CommunityMessage {
collaborators?: string[];

/// Content of the message
message: string | RichText;
message: RichText | undefined;

/// DeliveryStatus of the message. Indicates if the message is sent and/or delivered
deliveryStatus: CommunityDeliveryStatus;
@@ -98,8 +101,8 @@ export const uploadCommunityMessage = async (
const payloadJson: string = jsonStringify64({ ...messageContent });
const payloadBytes = stringToUint8Array(payloadJson);

// Set max of 3kb for content so enough room is left for metedata
const shouldEmbedContent = payloadBytes.length < 3000;
// Set max of 3kb + content of 1600 chars for content so enough room is left for metedata
const shouldEmbedContent = payloadBytes.length < 300 + MESSAGE_CHARACTERS_LIMIT * 4;

const uploadMetadata: UploadFileMetadata = {
versionTag: message?.fileMetadata.versionTag,
@@ -111,7 +114,12 @@ export const uploadCommunityMessage = async (
userDate: message.fileMetadata.appData.userDate,
tags: message.fileMetadata.appData.tags,
fileType: COMMUNITY_MESSAGE_FILE_TYPE,
content: shouldEmbedContent ? payloadJson : undefined,
content: shouldEmbedContent
? payloadJson
: jsonStringify64({
...messageContent,
message: ellipsisAtMaxCharOfRichText(messageContent.message, MESSAGE_CHARACTERS_LIMIT),
}),
},
isEncrypted: true,
accessControlList: message.serverMetadata?.accessControlList ||
@@ -313,8 +321,8 @@ export const updateCommunityMessage = async (
const payloadJson: string = jsonStringify64({ ...messageContent });
const payloadBytes = stringToUint8Array(payloadJson);

// Set max of 3kb for content so enough room is left for metedata
const shouldEmbedContent = payloadBytes.length < 3000;
// Set max of 3kb + content of 1600 chars for content so enough room is left for metedata
const shouldEmbedContent = payloadBytes.length < 300 + MESSAGE_CHARACTERS_LIMIT * 4;

const uploadMetadata: UploadFileMetadata = {
versionTag: message?.fileMetadata.versionTag,
@@ -337,7 +345,12 @@ export const updateCommunityMessage = async (
.archivalStatus,
previewThumbnail: message.fileMetadata.appData.previewThumbnail,
fileType: COMMUNITY_MESSAGE_FILE_TYPE,
content: shouldEmbedContent ? payloadJson : undefined,
content: shouldEmbedContent
? payloadJson
: jsonStringify64({
...messageContent,
message: ellipsisAtMaxCharOfRichText(messageContent.message, MESSAGE_CHARACTERS_LIMIT),
}),
},
isEncrypted: true,
accessControlList: message.serverMetadata?.accessControlList ||
@@ -516,23 +529,45 @@ export const dsrToMessage = async (
includeMetadataHeader: boolean
): Promise<HomebaseFile<CommunityMessage> | null> => {
try {
const msgContent =
odinId && dotYouClient.getHostIdentity() !== odinId
? await getContentFromHeaderOrPayloadOverPeer<CommunityMessage>(
dotYouClient,
odinId,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
)
: await getContentFromHeaderOrPayload<CommunityMessage>(
dotYouClient,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
);
const msgContent = await (async () => {
// Only here for backwards compatibility; Can be removed once Community is pushed live for all on production
const hasPartialOrFullContent = !!dsr.fileMetadata.appData.content?.length;
if (hasPartialOrFullContent)
return odinId && dotYouClient.getHostIdentity() !== odinId
? await getContentFromHeaderOverPeer<CommunityMessage>(
dotYouClient,
odinId,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
)
: await getContentFromHeader<CommunityMessage>(
dotYouClient,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
);
else
return odinId && dotYouClient.getHostIdentity() !== odinId
? await getContentFromHeaderOrPayloadOverPeer<CommunityMessage>(
dotYouClient,
odinId,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
)
: await getContentFromHeaderOrPayload<CommunityMessage>(
dotYouClient,
targetDrive,
dsr,
includeMetadataHeader,
dsr.fileSystemType
);
})();

if (!msgContent) return null;

const chatMessage: HomebaseFile<CommunityMessage> = {
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ export const CommunityChannelDetail = () => {
}).fetch;

useMarkCommunityAsRead({ odinId: odinKey, communityId, channelId });
const isPins = !!useMatch(`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityId}/${channelId}/pins`);

if (!community && isFetched)
return (
@@ -61,7 +62,6 @@ export const CommunityChannelDetail = () => {
);
}

const isPins = !!useMatch(`${COMMUNITY_ROOT_PATH}/${odinKey}/${communityId}/${channelId}/pins`);
if (!community || !channelDsr) return <NotFound />;

return (
Original file line number Diff line number Diff line change
@@ -33,9 +33,7 @@ export const CommunityLater = () => {

const savedMessages = communityMeta?.fileMetadata.appData.content.savedMessages;

if (!community) {
return <NotFound />;
}
if (!community) return <NotFound />;

return (
<ErrorBoundary>
3 changes: 2 additions & 1 deletion packages/common/common-app/package.json
Original file line number Diff line number Diff line change
@@ -21,7 +21,8 @@
},
"sideEffects": false,
"scripts": {
"lint": "eslint src/*"
"lint": "eslint src/*",
"test": "vitest"
},
"repository": {
"type": "git",
121 changes: 121 additions & 0 deletions packages/common/common-app/src/helpers/richTextHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { expect, test } from 'vitest';
const longRichText: RichText = [
{
type: 'p',
children: [
{
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim volutpat ut.',
},
],
id: '1vjzm',
},
{
type: 'p',
children: [
{
text: 'In porta magna in massa tincidunt, nec accumsan sem posuere. Donec ultrices aliquam convallis. Etiam placerat ipsum sit amet laoreet ultrices. ',
},
],
id: 'tf68w',
},
{
type: 'p',
children: [
{
text: 'Etiam et leo turpis. Nulla convallis lectus enim, ac rutrum diam condimentum malesuada. ',
},
],
id: 'op2ev',
},
{
type: 'p',
children: [
{
text: 'Curabitur nec consequat velit. Curabitur ipsum justo, fermentum sed faucibus et, mattis ac ipsum.',
},
],
id: 'lk9dv',
},
{
type: 'p',
children: [
{
text: 'Pellentesque ultrices sem nec quam feugiat pharetra. Curabitur purus nisi, molestie sed iaculis a, vehicula ut tortor.',
},
],
id: 'q6ja1',
},
{
type: 'p',
children: [
{
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim volutpat ut. Vivamus congue enim sit amet odio placerat porta. ',
},
],
id: 'fjmih',
},
];
const regularRichText: RichText = [
{
type: 'p',
children: [
{
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim volutpat ut. Vivamus congue enim sit amet odio placerat porta. Mauris semper semper lobortis. Maecenas bibendum augue vel massa efficitur dignissim. Donec et mauris venenatis, consectetur arcu ac, iaculis mauris. Nulla non turpis in ligula',
},
],
},
];

import { getPlainTextFromRichText, ellipsisAtMaxCharOfRichText } from './richTextHelper';
import { RichText } from '@homebase-id/js-lib/core';
test('getPlainTextFromRichText', () => {
expect(getPlainTextFromRichText(regularRichText)).toBe(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim volutpat ut. Vivamus congue enim sit amet odio placerat porta. Mauris semper semper lobortis. Maecenas bibendum augue vel massa efficitur dignissim. Donec et mauris venenatis, consectetur arcu ac, iaculis mauris. Nulla non turpis in ligula'
);
});

test('ellipsisAtMaxCharOfRichText', () => {
expect(ellipsisAtMaxCharOfRichText(regularRichText, 10)).toStrictEqual([
{
type: 'p',
children: [
{
text: 'Lorem ipsu...',
},
],
},
]);

expect(ellipsisAtMaxCharOfRichText(longRichText, 100)).toStrictEqual([
{
type: 'p',
children: [
{
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim ...',
},
],
id: '1vjzm',
},
]);

expect(ellipsisAtMaxCharOfRichText(longRichText, 140)).toStrictEqual([
{
type: 'p',
children: [
{
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam aliquet arcu augue, a commodo enim volutpat ut.',
},
],
id: '1vjzm',
},
{
type: 'p',
children: [
{
text: 'In porta magna in massa tinc...',
},
],
id: 'tf68w',
},
]);
});
48 changes: 30 additions & 18 deletions packages/common/common-app/src/helpers/richTextHelper.ts
Original file line number Diff line number Diff line change
@@ -29,7 +29,8 @@ export const getRichTextFromString = (body: string): RichText | undefined => {
return richText.some((part) => part.type) ? richText : undefined;
};

export const getTextRootsRecursive = (children: RichText | string): string[] => {
export const getTextRootsRecursive = (children: RichText | string | undefined): string[] => {
if (!children) return [];
if (!Array.isArray(children)) return [children as string];

return children
@@ -107,27 +108,38 @@ export const trimRichText = (richText: RichText | undefined): RichText | undefin
export const ellipsisAtMaxCharOfRichText = (richText: RichText | undefined, maxChar: number) => {
if (richText === undefined) return [];

let charCount = 0;
const result: RichText = [];
const recursiveEllipsisAtMaxCharOfRichText = (richText: RichText, maxChar: number) => {
let charCount = 0;
const result: RichText = [];

for (let i = 0; i < richText.length; i++) {
const entry = richText[i];
if ('text' in entry && typeof entry.text === 'string') {
if (charCount + entry.text.length > maxChar) {
entry.text = entry.text.substring(0, maxChar - charCount) + '...';
return result;
for (let i = 0; i < richText.length; i++) {
if (charCount >= maxChar) return [result, charCount] as const;

const node = { ...richText[i] };
if ('text' in node && typeof node.text === 'string') {
if (charCount + node.text.length > maxChar) {
node.text = node.text.substring(0, maxChar - charCount) + '...';
result.push(node);
return [result, charCount + node.text.length] as const;
}

charCount +=
('text' in node && typeof node.text === 'string' ? node.text?.length : undefined) || 0;
}

charCount +=
('text' in entry && typeof entry.text === 'string' ? entry.text?.length : undefined) || 0;
}
if ('children' in node && node.children !== undefined && Array.isArray(node.children)) {
const [newChildren, childrenCharCount] = recursiveEllipsisAtMaxCharOfRichText(
node.children,
maxChar - charCount
);
charCount += childrenCharCount;
node.children = newChildren;
}

if ('children' in entry && entry.children !== undefined && Array.isArray(entry.children)) {
entry.children = ellipsisAtMaxCharOfRichText(entry.children, maxChar - charCount);
result.push(node);
}

result.push(entry);
}

return result;
return [result, charCount] as const;
};
return recursiveEllipsisAtMaxCharOfRichText(richText, maxChar)[0];
};
65 changes: 65 additions & 0 deletions packages/common/common-app/src/hooks/file/useContentFromPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useQuery } from '@tanstack/react-query';
import { useDotYouClientContext } from '../auth/useDotYouClientContext';
import { getPayloadAsJson, SystemFileType, TargetDrive } from '@homebase-id/js-lib/core';
import { getPayloadAsJsonOverPeer } from '@homebase-id/js-lib/peer';

export const useContentFromPayload = <T>(props?: {
odinId?: string;
targetDrive: TargetDrive;
fileId: string | undefined;
payloadKey: string | undefined;
lastModified?: number;
systemFileType?: SystemFileType;
}) => {
const { odinId, targetDrive, fileId, payloadKey, lastModified, systemFileType } = props || {};
const dotYouClient = useDotYouClientContext();

const fetchContentFromPayload = async (
odinId: string | undefined,
targetDrive: TargetDrive,
fileId: string,
payloadKey: string,
lastModified: number | undefined,
systemFileType: SystemFileType | undefined
) => {
if (!odinId || odinId === dotYouClient.getHostIdentity()) {
return await getPayloadAsJson<T>(dotYouClient, targetDrive, fileId, payloadKey, {
lastModified,
systemFileType,
});
}
return await getPayloadAsJsonOverPeer<T>(
dotYouClient,
odinId,
targetDrive,
fileId,
payloadKey,
{
lastModified,
systemFileType,
}
);
};

return useQuery({
queryKey: [
'payload-content',
odinId,
(targetDrive as TargetDrive)?.alias,
fileId,
payloadKey,
lastModified,
],
queryFn: () =>
fetchContentFromPayload(
odinId,
targetDrive as TargetDrive,
fileId as string,
payloadKey as string,
lastModified,
systemFileType
),
enabled: !!targetDrive && !!fileId && !!payloadKey,
staleTime: 1000 * 60 * 60 * 24 * 14, // 14 Days, the lastModified is used to invalidate the cache
});
};
1 change: 1 addition & 0 deletions packages/common/common-app/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ export * from './follow/useFollowers';
export * from './follow/useFollowing';
export * from './follow/useIdentityIFollow';
export * from './file/useFile';
export * from './file/useContentFromPayload';
export * from './image/useRawImage';
export * from './intersection/useIntersection';
export * from './intersection/useMostSpace';
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ const DEFAULT_QUERY_KEYS = [
'push-notifications',
'site-data',
'profiles',
'payload-content',
];

const APP_QUERY_KEYS = [
71 changes: 47 additions & 24 deletions packages/libs/js-lib/src/core/DriveData/File/DriveFileProvider.ts
Original file line number Diff line number Diff line change
@@ -230,7 +230,7 @@ export const getThumbBytes = async (
lastModified?: number;
axiosConfig?: AxiosRequestConfig;
}
): Promise<{ bytes: ArrayBuffer; contentType: ImageContentType } | null> => {
): Promise<{ bytes: Uint8Array; contentType: ImageContentType } | null> => {
assertIfDefined('DotYouClient', dotYouClient);
assertIfDefined('TargetDrive', targetDrive);
assertIfDefined('FileId', fileId);
@@ -270,6 +270,45 @@ export const getThumbBytes = async (
});
};

export const getContentFromHeader = async <T>(
dotYouClient: DotYouClient,
targetDrive: TargetDrive,
dsr: {
fileId: string;
fileMetadata: FileMetadata;
sharedSecretEncryptedKeyHeader: EncryptedKeyHeader | undefined;
fileSystemType?: SystemFileType;
},
includesJsonContent: boolean,
systemFileType?: SystemFileType
) => {
const { fileId, fileMetadata, sharedSecretEncryptedKeyHeader } = dsr;

const keyHeader = fileMetadata.isEncrypted
? await decryptKeyHeader(dotYouClient, sharedSecretEncryptedKeyHeader as EncryptedKeyHeader)
: undefined;

let decryptedJsonContent;
if (includesJsonContent) {
decryptedJsonContent = await decryptJsonContent(fileMetadata, keyHeader);
} else {
// When contentIsComplete but includesJsonContent == false the query before was done without including the content; So we just get and parse
const fileHeader = await getFileHeader(dotYouClient, targetDrive, fileId, {
systemFileType: dsr.fileSystemType || systemFileType,
});
if (!fileHeader) return null;
decryptedJsonContent = await decryptJsonContent(fileHeader.fileMetadata, keyHeader);
}
return tryJsonParse<T>(decryptedJsonContent, (ex) => {
console.error(
'[odin-js] getContentFromHeaderOrPayload: Error parsing JSON',
ex && typeof ex === 'object' && 'stack' in ex ? ex.stack : ex,
dsr.fileId,
targetDrive
);
});
};

export const getContentFromHeaderOrPayload = async <T>(
dotYouClient: DotYouClient,
targetDrive: TargetDrive,
@@ -291,30 +330,14 @@ export const getContentFromHeaderOrPayload = async <T>(
fileMetadata.payloads?.filter((payload) => payload.key === DEFAULT_PAYLOAD_KEY).length === 0;
if (fileMetadata.isEncrypted && !sharedSecretEncryptedKeyHeader) return null;

const keyHeader = fileMetadata.isEncrypted
? await decryptKeyHeader(dotYouClient, sharedSecretEncryptedKeyHeader as EncryptedKeyHeader)
: undefined;

if (contentIsComplete) {
let decryptedJsonContent;
if (includesJsonContent) {
decryptedJsonContent = await decryptJsonContent(fileMetadata, keyHeader);
} else {
// When contentIsComplete but includesJsonContent == false the query before was done without including the content; So we just get and parse
const fileHeader = await getFileHeader(dotYouClient, targetDrive, fileId, {
systemFileType: dsr.fileSystemType || systemFileType,
});
if (!fileHeader) return null;
decryptedJsonContent = await decryptJsonContent(fileHeader.fileMetadata, keyHeader);
}
return tryJsonParse<T>(decryptedJsonContent, (ex) => {
console.error(
'[odin-js] getContentFromHeaderOrPayload: Error parsing JSON',
ex && typeof ex === 'object' && 'stack' in ex ? ex.stack : ex,
dsr.fileId,
targetDrive
);
});
return getContentFromHeader<T>(
dotYouClient,
targetDrive,
dsr,
includesJsonContent,
systemFileType
);
} else {
const payloadDescriptor = dsr.fileMetadata.payloads?.find(
(payload) => payload.key === DEFAULT_PAYLOAD_KEY
Original file line number Diff line number Diff line change
@@ -286,4 +286,11 @@ export interface CommentReaction extends ReactionBase {

export type EmojiReaction = ReactionBase;

export type RichText = Record<string, unknown>[];
export type RichTextNode = {
type?: string;
id?: string;
value?: string;
text?: string;
children?: RichTextNode[];
};
export type RichText = RichTextNode[];
72 changes: 49 additions & 23 deletions packages/libs/js-lib/src/peer/peerData/File/PeerFileProvider.ts
Original file line number Diff line number Diff line change
@@ -282,6 +282,46 @@ export const getFileHeaderBytesOverPeer = async (
return promise;
};

export const getContentFromHeaderOverPeer = async <T>(
dotYouClient: DotYouClient,
odinId: string,
targetDrive: TargetDrive,
dsr: {
fileId: string;
fileMetadata: FileMetadata;
sharedSecretEncryptedKeyHeader: EncryptedKeyHeader;
fileSystemType?: SystemFileType;
},
includesJsonContent: boolean,
systemFileType?: SystemFileType
) => {
const { fileId, fileMetadata, sharedSecretEncryptedKeyHeader } = dsr;

const keyHeader = fileMetadata.isEncrypted
? await decryptKeyHeader(dotYouClient, sharedSecretEncryptedKeyHeader as EncryptedKeyHeader)
: undefined;

let decryptedJsonContent;
if (includesJsonContent) {
decryptedJsonContent = await decryptJsonContent(fileMetadata, keyHeader);
} else {
// When contentIsComplete but includesJsonContent == false the query before was done without including the content; So we just get and parse
const fileHeader = await getFileHeaderOverPeer(dotYouClient, odinId, targetDrive, fileId, {
systemFileType: dsr.fileSystemType || systemFileType,
});
if (!fileHeader) return null;
decryptedJsonContent = await decryptJsonContent(fileHeader.fileMetadata, keyHeader);
}
return tryJsonParse<T>(decryptedJsonContent, (ex) => {
console.error(
'[odin-js] getContentFromHeaderOrPayloadOverPeer: Error parsing JSON',
ex && typeof ex === 'object' && 'stack' in ex ? ex.stack : ex,
dsr.fileId,
targetDrive
);
});
};

export const getContentFromHeaderOrPayloadOverPeer = async <T>(
dotYouClient: DotYouClient,
odinId: string,
@@ -298,31 +338,17 @@ export const getContentFromHeaderOrPayloadOverPeer = async <T>(
const { fileId, fileMetadata, sharedSecretEncryptedKeyHeader } = dsr;
const contentIsComplete =
fileMetadata.payloads?.filter((payload) => payload.key === DEFAULT_PAYLOAD_KEY).length === 0;

const keyHeader = fileMetadata.isEncrypted
? await decryptKeyHeader(dotYouClient, sharedSecretEncryptedKeyHeader)
: undefined;
if (fileMetadata.isEncrypted && !sharedSecretEncryptedKeyHeader) return null;

if (contentIsComplete) {
let decryptedJsonContent;
if (includesJsonContent) {
decryptedJsonContent = await decryptJsonContent(fileMetadata, keyHeader);
} else {
// When contentIsComplete but includesJsonContent == false the query before was done without including the content; So we just get and parse
const fileHeader = await getFileHeaderOverPeer(dotYouClient, odinId, targetDrive, fileId, {
systemFileType: dsr.fileSystemType || systemFileType,
});
if (!fileHeader) return null;
decryptedJsonContent = await decryptJsonContent(fileHeader.fileMetadata, keyHeader);
}
return tryJsonParse<T>(decryptedJsonContent, (ex) => {
console.error(
'[odin-js] getContentFromHeaderOrPayloadOverPeer: Error parsing JSON',
ex && typeof ex === 'object' && 'stack' in ex ? ex.stack : ex,
dsr.fileId,
targetDrive
);
});
return getContentFromHeaderOverPeer<T>(
dotYouClient,
odinId,
targetDrive,
dsr,
includesJsonContent,
systemFileType
);
} else {
const payloadDescriptor = dsr.fileMetadata.payloads?.find(
(payload) => payload.key === DEFAULT_PAYLOAD_KEY

0 comments on commit 3e5eeca

Please sign in to comment.