Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add conversation caching #1908

Merged
merged 2 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/web/src/components/utils/hooks/useMessagePreviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { useProfilesLazyQuery } from 'lens';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useAppStore } from 'src/store/app';
import { useConversationCache } from 'src/store/conversation-cache';
import { useMessageStore } from 'src/store/message';
import { useAccount } from 'wagmi';

const MAX_PROFILES_PER_REQUEST = 50;

const useMessagePreviews = () => {
const router = useRouter();
const { address: walletAddress } = useAccount();
const currentProfile = useAppStore((state) => state.currentProfile);
const conversations = useMessageStore((state) => state.conversations);
const setConversations = useMessageStore((state) => state.setConversations);
Expand All @@ -38,6 +41,9 @@ const useMessagePreviews = () => {
const [profilesToShow, setProfilesToShow] = useState<Map<string, Profile>>(new Map());
const [requestedCount, setRequestedCount] = useState(0);

const setConversationCache = useConversationCache((state) => state.setConversations);
const addToConversationCache = useConversationCache((state) => state.addConversation);

const getProfileFromKey = (key: string): string | null => {
const parsed = parseConversationKey(key);
const userProfileId = currentProfile?.id;
Expand Down Expand Up @@ -161,6 +167,12 @@ const useMessagePreviews = () => {
if (newProfileIds.size > profileIds.size) {
setProfileIds(newProfileIds);
}

if (walletAddress) {
// Update the cache with the full conversation exports
const convoExports = await client.conversations.export();
setConversationCache(walletAddress, convoExports);
}
};

const closeConversationStream = async () => {
Expand Down Expand Up @@ -195,6 +207,11 @@ const useMessagePreviews = () => {
setProfileIds(newProfileIds);
}
setConversations(newConversations);

if (walletAddress) {
// Add the newly streamed conversation to the cache
addToConversationCache(walletAddress, convo.export());
}
}
};

Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/components/utils/hooks/useXmtpClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Client } from '@xmtp/xmtp-js';
import { APP_NAME, APP_VERSION, LS_KEYS, XMTP_ENV } from 'data/constants';
import { useCallback, useEffect, useState } from 'react';
import { useAppStore } from 'src/store/app';
import { useConversationCache } from 'src/store/conversation-cache';
import { useMessageStore } from 'src/store/message';
import { useSigner } from 'wagmi';
import { useAccount, useSigner } from 'wagmi';

const ENCODING = 'binary';

Expand All @@ -23,6 +24,9 @@ const storeKeys = (walletAddress: string, keys: Uint8Array) => {
localStorage.setItem(buildLocalStorageKey(walletAddress), Buffer.from(keys).toString(ENCODING));
};

/**
* This will clear the conversation cache + the private keys
*/
const wipeKeys = (walletAddress: string) => {
localStorage.removeItem(buildLocalStorageKey(walletAddress));
};
Expand All @@ -33,6 +37,9 @@ const useXmtpClient = (cacheOnly = false) => {
const setClient = useMessageStore((state) => state.setClient);
const [awaitingXmtpAuth, setAwaitingXmtpAuth] = useState<boolean>();
const { data: signer, isLoading } = useSigner();
const { address } = useAccount();

const conversationExports = useConversationCache((state) => state.conversations[address as `0x${string}`]);

useEffect(() => {
const initXmtpClient = async () => {
Expand All @@ -55,6 +62,10 @@ const useXmtpClient = (cacheOnly = false) => {
appVersion: APP_NAME + '/' + APP_VERSION,
privateKeyOverride: keys
});
if (conversationExports && conversationExports.length) {
// Preload the client with conversations from the cache
await xmtp.conversations.import(conversationExports);
}
setClient(xmtp);
setAwaitingXmtpAuth(false);
} else {
Expand Down
50 changes: 50 additions & 0 deletions apps/web/src/store/conversation-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ConversationExport } from '@xmtp/xmtp-js/dist/types/src/conversations/Conversation';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

// If any breaking changes to the ConversationExport schema occur, increment the cache version.
const CONVERSATION_CACHE_VERSION = 1;

/**
* The ConversationCache is a JSON serializable Zustand store that is persisted to LocalStorage
* Persisting conversations to the cache saves on both bandwidth and CPU cycles, as we don't have to re-fetch or re-decrypt conversations on subsequent page loads
*/
interface ConversationCache {
// Mapping of conversation exports, keyed by wallet address
conversations: { [walletAddress: string]: ConversationExport[] };
// Overwrite the cache for a given wallet address
setConversations: (walletAddress: string, conversations: ConversationExport[]) => void;
// Add a single conversation to the cache.
// Deduping only happens at the time the cache is loaded, so be careful to not overfill or you will use more LocalStorage space than necessary
addConversation: (walletAddress: string, conversation: ConversationExport) => void;
}

export const useConversationCache = create<ConversationCache>()(
persist(
(set, get) => ({
conversations: {},
setConversations: (walletAddress: string, convos: ConversationExport[]) =>
set({
conversations: { ...get().conversations, [walletAddress]: convos }
}),
addConversation: (walletAddress: string, convo: ConversationExport) => {
const existing = get().conversations;
const existingForWallet = existing[walletAddress] || [];
return set({
conversations: {
...existing,
[walletAddress]: [...existingForWallet, convo]
}
});
}
}),
{
// Ensure that the LocalStorage key includes the network and the cache version
// If any breaking changes to the ConversationExport schema occur, increment the cache version.
name: `lenster:conversations:${
process.env.NEXT_PUBLIC_LENS_NETWORK || 'unknown'
}:v${CONVERSATION_CACHE_VERSION}`,
partialize: (state) => ({ conversations: state.conversations })
}
)
);