From 6b95e361922c3d97517bcff50fc0cdc904b48dfa Mon Sep 17 00:00:00 2001 From: Alex Risch Date: Mon, 28 Oct 2024 13:40:41 -0400 Subject: [PATCH] fix: Frames Renders on initial Send (#1113) * fix: Frames Renders on initial Send Updated to use zustand store for managing frames Updated to use hasFrames rather than isFrame * cleanup --- components/Chat/Chat.tsx | 24 ++++--- components/Chat/Frame/FramesPreviews.tsx | 46 ++++++-------- components/Chat/Message/Message.tsx | 81 +++++++++++++----------- data/store/framesStore.ts | 35 +++++++++- utils/frames.ts | 11 +++- 5 files changed, 120 insertions(+), 77 deletions(-) diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 102e57e81..f8d1b0ce1 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -6,7 +6,7 @@ import { tertiaryBackgroundColor, } from "@styles/colors"; import { getCleanAddress } from "@utils/evm/address"; -import { FrameWithType } from "@utils/frames"; +import { FrameWithType, messageHasFrames } from "@utils/frames"; import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { @@ -72,13 +72,13 @@ const usePeerSocials = () => { const useRenderItem = ({ xmtpAddress, conversation, - framesStore, + messageFramesMap, colorScheme, }: { xmtpAddress: string; conversation: XmtpConversationWithUpdate | undefined; - framesStore: { - [frameUrl: string]: FrameWithType; + messageFramesMap: { + [messageId: string]: FrameWithType[]; }; colorScheme: ColorSchemeName; }) => { @@ -90,11 +90,11 @@ const useRenderItem = ({ message={{ ...item }} colorScheme={colorScheme} isGroup={!!conversation?.isGroup} - isFrame={!!framesStore[item.content.toLowerCase().trim()]} + hasFrames={messageHasFrames(item.id, messageFramesMap)} /> ); }, - [colorScheme, xmtpAddress, conversation?.isGroup, framesStore] + [colorScheme, xmtpAddress, conversation?.isGroup, messageFramesMap] ); }; @@ -383,7 +383,9 @@ export function Chat() { styles.inChatRecommendations, ]); - const { frames: framesStore } = useFramesStore(useSelect(["frames"])); + const { messageFramesMap, frames: framesStore } = useFramesStore( + useSelect(["messageFramesMap", "frames"]) + ); const showPlaceholder = useIsShowingPlaceholder({ messages: listArray, @@ -394,7 +396,7 @@ export function Chat() { const renderItem = useRenderItem({ xmtpAddress, conversation, - framesStore, + messageFramesMap, colorScheme, }); @@ -539,7 +541,9 @@ export function ChatPreview() { ] ); - const { frames: framesStore } = useFramesStore(useSelect(["frames"])); + const { frames: framesStore, messageFramesMap } = useFramesStore( + useSelect(["frames", "messageFramesMap"]) + ); const showPlaceholder = useIsShowingPlaceholder({ messages: listArray, @@ -550,7 +554,7 @@ export function ChatPreview() { const renderItem = useRenderItem({ xmtpAddress, conversation, - framesStore, + messageFramesMap, colorScheme, }); diff --git a/components/Chat/Frame/FramesPreviews.tsx b/components/Chat/Frame/FramesPreviews.tsx index 16cf41cf5..d39294e77 100644 --- a/components/Chat/Frame/FramesPreviews.tsx +++ b/components/Chat/Frame/FramesPreviews.tsx @@ -1,58 +1,50 @@ +import { useSelect } from "@data/store/storeHelpers"; import { useConversationContext } from "@utils/conversation"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { View } from "react-native"; +import { useShallow } from "zustand/react/shallow"; import FramePreview from "./FramePreview"; import { useCurrentAccount } from "../../../data/store/accountsStore"; import { useFramesStore } from "../../../data/store/framesStore"; -import { - FrameWithType, - FramesForMessage, - fetchFramesForMessage, -} from "../../../utils/frames"; +import { FramesForMessage, fetchFramesForMessage } from "../../../utils/frames"; import { MessageToDisplay } from "../Message/Message"; type Props = { message: MessageToDisplay; }; -export default function FramesPreviews({ message }: Props) { +export function FramesPreviews({ message }: Props) { const messageId = useRef(undefined); const tagsFetchedOnceForMessage = useConversationContext( "tagsFetchedOnceForMessage" ); const account = useCurrentAccount() as string; - const [framesForMessage, setFramesForMessage] = useState<{ - [messageId: string]: FrameWithType[]; - }>({ - [message.id]: useFramesStore - .getState() - .getFramesForURLs(message.converseMetadata?.frames || []), - }); + const framesToDisplay = useFramesStore( + useShallow((s) => s.messageFramesMap[message.id] ?? []) + ); + const { setMessageFramesMap } = useFramesStore( + useSelect(["setMessageFramesMap"]) + ); const fetchTagsIfNeeded = useCallback(() => { if (!tagsFetchedOnceForMessage.current[message.id]) { tagsFetchedOnceForMessage.current[message.id] = true; fetchFramesForMessage(account, message).then( (frames: FramesForMessage) => { - setFramesForMessage({ [frames.messageId]: frames.frames }); + setMessageFramesMap(frames.messageId, frames.frames); } ); } - }, [account, message, tagsFetchedOnceForMessage]); + }, [account, message, tagsFetchedOnceForMessage, setMessageFramesMap]); // Components are recycled, let's fix when stuff changes - if (message.id !== messageId.current) { - messageId.current = message.id; - fetchTagsIfNeeded(); - setFramesForMessage({ - [message.id]: useFramesStore - .getState() - .getFramesForURLs(message.converseMetadata?.frames || []), - }); - } - - const framesToDisplay = framesForMessage[message.id] || []; + useEffect(() => { + if (message.id !== messageId.current) { + messageId.current = message.id; + fetchTagsIfNeeded(); + } + }, [message.id, fetchTagsIfNeeded]); return ( diff --git a/components/Chat/Message/Message.tsx b/components/Chat/Message/Message.tsx index eecbecfde..95c39fb8d 100644 --- a/components/Chat/Message/Message.tsx +++ b/components/Chat/Message/Message.tsx @@ -1,3 +1,4 @@ +import { useFramesStore } from "@data/store/framesStore"; import { inversePrimaryColor, messageInnerBubbleColor, @@ -26,6 +27,7 @@ import Animated, { useAnimatedStyle, withTiming, } from "react-native-reanimated"; +import { useShallow } from "zustand/react/shallow"; import ChatMessageActions from "./MessageActions"; import ChatMessageReactions from "./MessageReactions"; @@ -64,7 +66,7 @@ import ClickableText from "../../ClickableText"; import ActionButton from "../ActionButton"; import AttachmentMessagePreview from "../Attachment/AttachmentMessagePreview"; import ChatGroupUpdatedMessage from "../ChatGroupUpdatedMessage"; -import FramesPreviews from "../Frame/FramesPreviews"; +import { FramesPreviews } from "../Frame/FramesPreviews"; import ChatInputReplyBubble from "../Input/InputReplyBubble"; import TransactionPreview from "../Transaction/TransactionPreview"; @@ -84,7 +86,7 @@ type Props = { message: MessageToDisplay; colorScheme: ColorSchemeName; isGroup: boolean; - isFrame: boolean; + hasFrames: boolean; }; // On iOS, the native context menu view handles the long press, but could potentially trigger the onPress event @@ -151,7 +153,7 @@ const ChatMessage = ({ message, colorScheme, isGroup, - isFrame, + hasFrames, }: Props) => { const styles = useStyles(); @@ -163,6 +165,10 @@ const ChatMessage = ({ () => getLocalizedTime(message.sent), [message.sent] ); + // The content is completely a frame so a larger full width frame will be shown + const isFrame = useFramesStore( + useShallow((s) => !!s.frames[message.content.toLowerCase().trim()]) + ); // Reanimated shared values for time and date-time animations const timeHeight = useSharedValue(0); @@ -325,6 +331,37 @@ const ChatMessage = ({ const swipeableRef = useRef(null); + const renderLeftActions = useCallback( + ( + progressAnimatedValue: RNAnimated.AnimatedInterpolation + ) => { + return ( + + + + ); + }, + [] + ); + return ( - ) => { - return ( - - - - ); - }} + renderLeftActions={renderLeftActions} leftThreshold={10000} // Never trigger opening onSwipeableWillClose={() => { const translation = swipeableRef.current?.state.rowTranslation; @@ -543,7 +552,7 @@ type RenderedChatMessage = { message: MessageToDisplay; colorScheme: ColorSchemeName; isGroup: boolean; - isFrame: boolean; + hasFrames: boolean; }; const renderedMessages = new LimitedMap(50); @@ -567,7 +576,7 @@ export default function CachedChatMessage({ message, colorScheme, isGroup, - isFrame = false, + hasFrames = false, }: Props) { const alreadyRenderedMessage = renderedMessages.get( `${account}-${message.id}` @@ -584,14 +593,14 @@ export default function CachedChatMessage({ message, colorScheme, isGroup, - isFrame, + hasFrames, }); renderedMessages.set(`${account}-${message.id}`, { message, renderedMessage, colorScheme, isGroup, - isFrame, + hasFrames, }); return renderedMessage; } else { diff --git a/data/store/framesStore.ts b/data/store/framesStore.ts index 618e89a55..42add7925 100644 --- a/data/store/framesStore.ts +++ b/data/store/framesStore.ts @@ -10,19 +10,38 @@ type FramesStoreType = { frames: { [frameUrl: string]: FrameWithType; }; - setFrames: (framesToSet: { [frameUrl: string]: FrameWithType }) => void; + messageFramesMap: { + [messageId: string]: FrameWithType[]; + }; + setFrames: ( + messageId: string, + framesToSet: { [frameUrl: string]: FrameWithType } + ) => void; getFramesForURLs: (urls: string[]) => FrameWithType[]; + setMessageFramesMap: (messageId: string, framesUrls: FrameWithType[]) => void; }; export const useFramesStore = create()( persist( (set, get) => ({ frames: {}, - setFrames: (framesToSet: { [frameUrl: string]: FrameWithType }) => + messageFramesMap: {}, + setFrames: ( + messageId: string, + framesToSet: { [frameUrl: string]: FrameWithType } + ) => set((state) => { const existingFrames = pick(state.frames, Object.keys(framesToSet)); + if (isDeepEqual(existingFrames, framesToSet)) return {}; - return { frames: { ...state.frames, ...framesToSet } }; + return { + ...state, + frames: { ...state.frames, ...framesToSet }, + messageFramesMap: { + ...state.messageFramesMap, + [messageId]: Object.values(framesToSet), + }, + }; }), getFramesForURLs: (urls: string[]) => { const framesToReturn: FrameWithType[] = []; @@ -35,6 +54,16 @@ export const useFramesStore = create()( }); return framesToReturn; }, + setMessageFramesMap: (messageId: string, frames: FrameWithType[]) => + set((state) => { + return { + ...state, + messageFramesMap: { + ...state.messageFramesMap, + [messageId]: frames, + }, + }; + }), }), { name: `store-frames`, diff --git a/utils/frames.ts b/utils/frames.ts index 3320918ed..483268275 100644 --- a/utils/frames.ts +++ b/utils/frames.ts @@ -121,7 +121,7 @@ export const fetchFramesForMessage = async ( frames: fetchedFrames.map((f) => f.url), }; // Save frame to store - useFramesStore.getState().setFrames(framesToSave); + useFramesStore.getState().setFrames(message.id, framesToSave); // Then update message to reflect change saveMessageMetadata(account, message, messageMetadataToSave); @@ -282,3 +282,12 @@ export const isFrameMessage = ( !!framesStore[message.converseMetadata.frames[0].toLowerCase().trim()] ); }; + +export const messageHasFrames = ( + messageId: string, + messageFramesMap: { + [messageId: string]: FrameWithType[]; + } +) => { + return (messageFramesMap[messageId]?.length ?? 0) > 0; +};