Skip to content

Commit

Permalink
Initial refactor of the conversation metadata into the local content … (
Browse files Browse the repository at this point in the history
#324)

* Initial refactor of the conversation metadata into the local content field;

* Removed cache-key

* Added backwards compatibility;

* Fix tests
  • Loading branch information
stef-coenen authored Feb 3, 2025
1 parent 935c875 commit b2f97a9
Show file tree
Hide file tree
Showing 24 changed files with 359 additions and 476 deletions.
11 changes: 9 additions & 2 deletions packages/mobile/__tests__/ChatProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import {
ChatMessage,
} from '../src/provider/chat/ChatProvider';
import * as ChatProvider from '../src/provider/chat/ChatProvider';
import { ChatDrive, UnifiedConversation } from '../src/provider/chat/ConversationProvider';
import {
ChatDrive,
ConversationMetadata,
UnifiedConversation,
} from '../src/provider/chat/ConversationProvider';
import { sendReadReceipt } from '@homebase-id/js-lib/peer';
import { getRandom16ByteArray } from '@homebase-id/js-lib/helpers';

Expand Down Expand Up @@ -371,7 +375,10 @@ describe('ChatProvider', () => {
});

it('should request mark as read', async () => {
const conversation = { fileId: 'conversation-id' } as HomebaseFile<UnifiedConversation>;
const conversation = { fileId: 'conversation-id' } as HomebaseFile<
UnifiedConversation,
ConversationMetadata
>;
const messages = [
{
fileId: 'message-id',
Expand Down
106 changes: 6 additions & 100 deletions packages/mobile/__tests__/ConversationProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ describe('ConversationProvider', () => {
const queryBatchMock = jest.fn().mockResolvedValue({
searchResults: [
{
/* mock result */
fileId: 'file-id',
fileMetadata: { appData: { content: { recipients: [] } }, payloads: [] },
},
],
});
Expand Down Expand Up @@ -127,7 +128,8 @@ describe('ConversationProvider', () => {

it('should return a valid conversation when conversationId is valid', async () => {
const getFileHeaderByUniqueIdMock = jest.fn().mockResolvedValue({
fileMetadata: { appData: { content: { recipients: [] } } },
fileId: 'file-id',
fileMetadata: { appData: { content: { recipients: [] } }, payloads: [] },
});
(getFileHeaderByUniqueId as jest.Mock) = getFileHeaderByUniqueIdMock;

Expand All @@ -149,7 +151,7 @@ describe('ConversationProvider', () => {
});

describe('uploadConversation', () => {
const conversation: NewHomebaseFile<UnifiedConversation> = {
const conversation: NewHomebaseFile<UnifiedConversation, ConversationMetadata> = {
fileMetadata: {
appData: {
uniqueId: 'unique-id',
Expand Down Expand Up @@ -198,7 +200,7 @@ describe('ConversationProvider', () => {
});

describe('updateConversation', () => {
const conversation: HomebaseFile<UnifiedConversation> = {
const conversation: HomebaseFile<UnifiedConversation, ConversationMetadata> = {
fileId: 'file-id',
fileState: 'active',
fileSystemType: 'Standard',
Expand Down Expand Up @@ -262,100 +264,4 @@ describe('ConversationProvider', () => {
expect(getConversationMock).toHaveBeenCalled();
});
});

describe('getConversationMetadata', () => {
it('should return metadata when queryBatch returns a valid response', async () => {
const queryBatchMock = jest.fn().mockResolvedValue({
searchResults: [
{
fileId: 'file-id',
fileState: 'active',
fileSystemType: 'Standard',
fileMetadata: {
created: 1727354465999,
updated: 1727354465999,
isEncrypted: true,
senderOdinId: 'frodo',
},
sharedSecretEncryptedKeyHeader: {
encryptedAesKey: new Uint8Array(32),
encryptionVersion: 1,
iv: new Uint8Array(16),
type: 2,
},
},
] as HomebaseFile<string>[],
} as QueryBatchResponse);
(queryBatch as jest.Mock) = queryBatchMock;

const getContentFromHeaderOrPayloadMock = jest
.fn()
.mockResolvedValue({ conversationId: 'valid-id' } as ConversationMetadata);
(getContentFromHeaderOrPayload as jest.Mock) = getContentFromHeaderOrPayloadMock;

const result = await getConversationMetadata(dotYouClientMock, 'valid-id');

expect(queryBatchMock).toHaveBeenCalled();
expect(result).toHaveProperty('fileMetadata');
});

it('should return null when queryBatch returns no response', async () => {
const queryBatchMock = jest.fn().mockResolvedValue(null);
(queryBatch as jest.Mock) = queryBatchMock;

const result = await getConversationMetadata(dotYouClientMock, 'valid-id');

expect(queryBatchMock).toHaveBeenCalled();
expect(result).toBeNull();
});
});

describe('uploadConversationMetadata', () => {
it('should upload metadata with valid tags', async () => {
const uploadFileMock = jest.fn().mockResolvedValue({});
(uploadFile as jest.Mock) = uploadFileMock;

const conversation = {
fileMetadata: {
appData: {
tags: ['valid-tag'],
content: { conversationId: 'valid-tag' },
},
},
} as NewHomebaseFile<ConversationMetadata>;

await uploadConversationMetadata(dotYouClientMock, conversation);

expect(uploadFileMock).toHaveBeenCalled();
});

it('should throw error when tags are missing', async () => {
const conversation = {
fileMetadata: {
appData: {
content: { conversationId: 'valid-tag' },
},
},
} as NewHomebaseFile<ConversationMetadata>;

await expect(uploadConversationMetadata(dotYouClientMock, conversation)).rejects.toThrow(
'ConversationMetadata must have tags'
);
});

it('should throw error when tags do not match conversationId', async () => {
const conversation = {
fileMetadata: {
appData: {
tags: ['invalid-tag'],
content: { conversationId: 'valid-tag' },
},
},
} as NewHomebaseFile<ConversationMetadata>;

await expect(uploadConversationMetadata(dotYouClientMock, conversation)).rejects.toThrow(
'ConversationMetadata must have a tag that matches the conversationId'
);
});
});
});
8 changes: 4 additions & 4 deletions packages/mobile/src/app/ChatStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '@homebase-id/js-lib/core';
import { ChatMessage } from '../provider/chat/ChatProvider';
import { MessageInfoPage } from '../pages/chat/message-info-page';
import { UnifiedConversation } from '../provider/chat/ConversationProvider';
import { ConversationMetadata, UnifiedConversation } from '../provider/chat/ConversationProvider';
import { OwnerAvatar } from '../components/ui/Avatars/Avatar';
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import { SharedItem } from '../hooks/platform/useShareManager';
Expand All @@ -48,18 +48,18 @@ export type ChatStackParamList = {
ChatInfo: { convoId: string };
MessageInfo: {
message: HomebaseFile<ChatMessage>;
conversation: HomebaseFile<UnifiedConversation>;
conversation: HomebaseFile<UnifiedConversation, ConversationMetadata>;
};
ChatFileOverview: {
initialAssets: ImageSource[];
recipients: HomebaseFile<UnifiedConversation>[];
recipients: HomebaseFile<UnifiedConversation, ConversationMetadata>[];
title?: string;
};
EditGroup: { convoId: string };
ShareChat: SharedItem[];
ShareEditor: {
text: string;
recipients: HomebaseFile<UnifiedConversation>[];
recipients: HomebaseFile<UnifiedConversation, ConversationMetadata>[];
};
Archived: undefined;
PreviewMedia: {
Expand Down
3 changes: 1 addition & 2 deletions packages/mobile/src/app/OdinQueryClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const INCLUDED_QUERY_KEYS = [
'chat-message',
'chat-messages',
'conversations',
'conversation-metadata',
'chat-reaction',
'connection-details',
'contacts',
Expand Down Expand Up @@ -77,7 +76,7 @@ export const OdinQueryClient = ({ children }: { children: ReactNode }) => {
});

const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
buster: '20241110',
buster: '202501',
maxAge: Infinity,
persister: asyncPersist,
dehydrateOptions: {
Expand Down
9 changes: 7 additions & 2 deletions packages/mobile/src/components/Chat/Chat-Connected-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { useDarkMode } from '../../hooks/useDarkMode';
import { Colors } from '../../app/Colors';
import { ReactNode, useEffect, useState } from 'react';
import { openURL } from '../../utils/utils';
import { UnifiedConversation } from '../../provider/chat/ConversationProvider';
import {
ConversationMetadata,
UnifiedConversation,
} from '../../provider/chat/ConversationProvider';
import Animated, {
AnimatedStyle,
FadeOut,
Expand All @@ -18,7 +21,9 @@ import Animated, {
} from 'react-native-reanimated';
import TextButton from '../ui/Text/Text-Button';

export const ChatConnectedState = (conversation: HomebaseFile<UnifiedConversation> | undefined) => {
export const ChatConnectedState = (
conversation: HomebaseFile<UnifiedConversation, ConversationMetadata> | undefined
) => {
const { isDarkMode } = useDarkMode();
const identity = useDotYouClientContext().getLoggedInIdentity();
const [expanded, setExpanded] = useState(false);
Expand Down
19 changes: 10 additions & 9 deletions packages/mobile/src/components/Chat/Chat-Forward.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ErrorNotification } from '../ui/Alert/ErrorNotification';
import useImage from '../ui/OdinImage/hooks/useImage';
import {
ChatDrive,
ConversationMetadata,
ConversationWithYourself,
UnifiedConversation,
} from '../../provider/chat/ConversationProvider';
Expand Down Expand Up @@ -75,7 +76,7 @@ export const ChatForwardModal = memo(
const { mutate: sendMessage, error } = useChatMessage().send;
const [selectedContact, setselectedContact] = useState<DotYouProfile[]>([]);
const [selectedConversation, setSelectedConversation] = useState<
HomebaseFile<UnifiedConversation>[]
HomebaseFile<UnifiedConversation, ConversationMetadata>[]
>([]);
const navigation = useNavigation<NavigationProp<ChatStackParamList>>();
const { bottom } = useSafeAreaInsets();
Expand Down Expand Up @@ -104,7 +105,7 @@ export const ChatForwardModal = memo(
if ((selectedContact.length === 0 && selectedConversation.length === 0) || !message) return;
setIsLoading(true);
async function forwardMessages(
conversation: HomebaseFile<UnifiedConversation>,
conversation: HomebaseFile<UnifiedConversation, ConversationMetadata>,
message: ChatMessageIMessage
) {
const messagePayloads: ImageSource[] = [];
Expand Down Expand Up @@ -390,9 +391,9 @@ const InnerForwardListPage = memo(
allConversations: ConversationWithRecentMessage[] | undefined;
selectedContact: DotYouProfile[];
setSelectedContact: React.Dispatch<React.SetStateAction<DotYouProfile[]>>;
selectedConversation: HomebaseFile<UnifiedConversation>[];
selectedConversation: HomebaseFile<UnifiedConversation, ConversationMetadata>[];
setSelectedConversation: React.Dispatch<
React.SetStateAction<HomebaseFile<UnifiedConversation>[]>
React.SetStateAction<HomebaseFile<UnifiedConversation, ConversationMetadata>[]>
>;
}) => {
const {
Expand All @@ -405,7 +406,7 @@ const InnerForwardListPage = memo(
} = props;

const onSelectConversation = useCallback(
(conversation: HomebaseFile<UnifiedConversation>) => {
(conversation: HomebaseFile<UnifiedConversation, ConversationMetadata>) => {
setSelectedConversation((selectedConversation) => {
if (selectedConversation.includes(conversation)) {
return selectedConversation.filter((c) => c !== conversation);
Expand Down Expand Up @@ -616,8 +617,8 @@ export const GroupConversationsComponent = memo(
selectedGroup,
setselectedGroup,
}: {
selectedGroup: HomebaseFile<UnifiedConversation>[];
setselectedGroup: (group: HomebaseFile<UnifiedConversation>[]) => void;
selectedGroup: HomebaseFile<UnifiedConversation, ConversationMetadata>[];
setselectedGroup: (group: HomebaseFile<UnifiedConversation, ConversationMetadata>[]) => void;
}) => {
const { data: conversations } = useConversations().all;
const flatConversations = useMemo(
Expand All @@ -630,15 +631,15 @@ export const GroupConversationsComponent = memo(
convo &&
[0, undefined].includes(convo.fileMetadata.appData.archivalStatus) &&
convo.fileMetadata.appData.content.recipients.length > 2
) as HomebaseFile<UnifiedConversation>[]
) as HomebaseFile<UnifiedConversation, ConversationMetadata>[]
)?.sort((a, b) =>
a?.fileMetadata.appData.content.title.localeCompare(b?.fileMetadata.appData.content.title)
) || [],
[conversations]
);

const onSelect = useCallback(
(group: HomebaseFile<UnifiedConversation>) => {
(group: HomebaseFile<UnifiedConversation, ConversationMetadata>) => {
if (selectedGroup.includes(group)) {
setselectedGroup(selectedGroup.filter((grp) => grp !== group));
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/components/Chat/Conversation-tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ConversationTile = memo((props: ConversationTileProps) => {
const { data: conversationMetadata } = useConversationMetadata({
conversationId: props.conversationId,
}).single;
const lastReadTime = conversationMetadata?.fileMetadata.appData.content.lastReadTime;
const lastReadTime = conversationMetadata?.fileMetadata?.localAppData?.content?.lastReadTime;
const unreadCount = useMemo(
() =>
lastReadTime &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import { TouchableHighlight } from 'react-native-gesture-handler';
import { useChatMessage } from '../../../../hooks/chat/useChatMessage';
import { HomebaseFile, RecipientTransferHistory } from '@homebase-id/js-lib/core';
import { ChatMessage } from '../../../../provider/chat/ChatProvider';
import { ChatDrive, UnifiedConversation } from '../../../../provider/chat/ConversationProvider';
import {
ChatDrive,
ConversationMetadata,
UnifiedConversation,
} from '../../../../provider/chat/ConversationProvider';
import { ErrorNotification } from '../../../ui/Alert/ErrorNotification';
import { Backdrop } from '../../../ui/Modal/Backdrop';
import { useTransferHistory } from '../../../../hooks/file/useTransferHistory';
Expand All @@ -27,7 +31,7 @@ export const RetryModal = forwardRef(
onClose,
}: {
message: ChatMessageIMessage | undefined;
conversation: HomebaseFile<UnifiedConversation>;
conversation: HomebaseFile<UnifiedConversation, ConversationMetadata>;
onClose: () => void;
},
ref: React.Ref<BottomSheetModalMethods>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { Text } from '../ui/Text/Text';
import { SafeAreaView } from '../ui/SafeAreaView/SafeAreaView';
import { DotYouProfile } from '@homebase-id/js-lib/network';
import { HomebaseFile } from '@homebase-id/js-lib/core';
import { UnifiedConversation } from '../../provider/chat/ConversationProvider';
import {
ConversationMetadata,
UnifiedConversation,
} from '../../provider/chat/ConversationProvider';
import Toast from 'react-native-toast-message';
import { maxConnectionsForward } from './Chat-Forward';

Expand Down Expand Up @@ -164,9 +167,9 @@ export const SearchConversationWithSelectionResults = memo(
selectedContact: DotYouProfile[];
setSelectedContact: React.Dispatch<React.SetStateAction<DotYouProfile[]>>;
onContactSelect?: (contact: string) => void;
selectedConversation?: HomebaseFile<UnifiedConversation>[];
selectedConversation?: HomebaseFile<UnifiedConversation, ConversationMetadata>[];
setSelectedConversation?: React.Dispatch<
React.SetStateAction<HomebaseFile<UnifiedConversation>[]>
React.SetStateAction<HomebaseFile<UnifiedConversation, ConversationMetadata>[]>
>;
selectionLimit?: number;
}) => {
Expand All @@ -189,7 +192,7 @@ export const SearchConversationWithSelectionResults = memo(
);

const onSelectConversation = useCallback(
(conversation: HomebaseFile<UnifiedConversation>) => {
(conversation: HomebaseFile<UnifiedConversation, ConversationMetadata>) => {
setSelectedConversation?.((selectedConversation) => {
if (selectedConversation.includes(conversation)) {
return selectedConversation.filter((c) => c !== conversation);
Expand Down
Loading

0 comments on commit b2f97a9

Please sign in to comment.