Skip to content

Commit

Permalink
feat: add conversation caching (#1908)
Browse files Browse the repository at this point in the history
  • Loading branch information
rygine committed Mar 6, 2023
1 parent 9ca6341 commit 0ec8312
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 1 deletion.
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 })
}
)
);

3 comments on commit 0ec8312

@vercel
Copy link

@vercel vercel bot commented on 0ec8312 Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

prerender – ./apps/prerender

prerender-git-main-lenster.vercel.app
prerender-lenster.vercel.app
prerender.lenster.xyz

@vercel
Copy link

@vercel vercel bot commented on 0ec8312 Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web – ./apps/web

web-lenster.vercel.app
lenster.vercel.app
lenster.xyz
web-git-main-lenster.vercel.app

@hop-deploy
Copy link

@hop-deploy hop-deploy bot commented on 0ec8312 Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deployment Status Build Logs Updated At
lenster ✅ Deployed View Logs 2023-03-06T18:50:27.328Z

Please sign in to comment.