From 0996c44f422a17178d433d1132021e9b7b023868 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 5 Dec 2024 16:23:27 +0100 Subject: [PATCH 1/7] Remove unused helper method `showUnreadOnConversation` --- .../conversation/showUnreadOnConversation.ts | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 utils/conversation/showUnreadOnConversation.ts diff --git a/utils/conversation/showUnreadOnConversation.ts b/utils/conversation/showUnreadOnConversation.ts deleted file mode 100644 index 64e490a06..000000000 --- a/utils/conversation/showUnreadOnConversation.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { TopicsData } from "@data/store/chatStore"; -import { - ConversationWithCodecsType, - DecodedMessageWithCodecsType, -} from "@utils/xmtpRN/client"; - -export const showUnreadOnConversation = ( - initialLoadDoneOnce: boolean, - lastMessagePreview: DecodedMessageWithCodecsType | undefined, - topicsData: TopicsData, - conversation: ConversationWithCodecsType, - userAddress: string -) => { - if (!initialLoadDoneOnce) return false; - if (!lastMessagePreview) return false; - // Manually marked as unread - if (topicsData[conversation.topic]?.status === "unread") return true; - // If not manually markes as unread, we only show badge if last message - // not from me - if ( - lastMessagePreview.senderAddress.toLowerCase() === userAddress.toLowerCase() - ) - return false; - const readUntil = topicsData[conversation.topic]?.readUntil || 0; - return readUntil < lastMessagePreview.sentNs; -}; From e681692f4cb3aa9a04a7ca50c15f4e6d3e80a1ae Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Thu, 5 Dec 2024 17:57:38 +0100 Subject: [PATCH 2/7] WIP read/unread --- components/Conversation/V3Conversation.tsx | 24 ++++++++++++ .../hooks/useMessageIsUnread.ts | 39 ++++++++++++------- .../hooks/useToggleReadStatus.ts | 3 ++ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/components/Conversation/V3Conversation.tsx b/components/Conversation/V3Conversation.tsx index 6f2e4df00..65dfbf20b 100644 --- a/components/Conversation/V3Conversation.tsx +++ b/components/Conversation/V3Conversation.tsx @@ -41,6 +41,8 @@ import { import { DmConversationTitle } from "../../features/conversations/components/DmConversationTitle"; import { GroupConversationTitle } from "../../features/conversations/components/GroupConversationTitle"; import { NewConversationTitle } from "../../features/conversations/components/NewConversationTitle"; +import { useConversationIsUnread } from "@/features/conversation-list/hooks/useMessageIsUnread"; +import { useToggleReadStatus } from "@/features/conversation-list/hooks/useToggleReadStatus"; const keyExtractor = (item: string) => item; @@ -138,6 +140,28 @@ const DmContent = memo(function DmContent() { const { data: peerInboxId } = useDmPeerInboxId(currentAccount, topic!); + // Check if conversation is unread + const isUnread = useConversationIsUnread({ + topic, + lastMessage: messages?.byId[messages?.ids[0]], // Get latest message + timestamp: messages?.byId[messages?.ids[0]]?.sentNs ?? 0, + }); + + // Add hook to toggle read status + const toggleReadStatus = useToggleReadStatus({ + topic, + isUnread, + currentAccount, + }); + + // Effect to mark as read when entering chat + useEffect(() => { + if (isUnread && !messagesLoading && messages?.ids.length) { + console.log("== Mark as read?"); + toggleReadStatus(); + } + }, [isUnread, messagesLoading, messages?.ids.length, toggleReadStatus]); + useDmHeader(); if (conversationNotFound) { diff --git a/features/conversation-list/hooks/useMessageIsUnread.ts b/features/conversation-list/hooks/useMessageIsUnread.ts index ec682d129..9bb7dcad5 100644 --- a/features/conversation-list/hooks/useMessageIsUnread.ts +++ b/features/conversation-list/hooks/useMessageIsUnread.ts @@ -4,15 +4,26 @@ import { ConversationWithCodecsType, DecodedMessageWithCodecsType, } from "@utils/xmtpRN/client"; -import { useChatStore } from "@data/store/accountsStore"; +import { + currentAccount, + useChatStore, + useCurrentAccount, +} from "@data/store/accountsStore"; import { useSelect } from "@data/store/storeHelpers"; import { normalizeTimestamp } from "@/utils/date"; +import { useDmPeerInboxId } from "@/queries/useDmPeerInbox"; +import { useConversationCurrentTopic } from "@/features/conversation/conversation-service"; +import { ConversationTopic } from "@xmtp/react-native-sdk"; type UseConversationIsUnreadProps = { topic: string; lastMessage: DecodedMessageWithCodecsType | undefined; - conversation: ConversationWithCodecsType; timestamp: number; + /** + * @deprecated This prop has been deprecated. + * for backward compatibility only + */ + conversation?: ConversationWithCodecsType; }; const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"]; @@ -20,26 +31,24 @@ const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"]; export const useConversationIsUnread = ({ topic, lastMessage, - conversation, timestamp: timestampNs, }: UseConversationIsUnreadProps) => { - const { topicsData } = useChatStore(useSelect(chatStoreSelectKeys)); + const currentAccount = useCurrentAccount()!; + + const validTopic: ConversationTopic = "valid-topic" as ConversationTopic; + + const topicsData = useChatStore((state: ChatStoreType) => state.topicsData); + const { data: peerInboxId } = useDmPeerInboxId(currentAccount, validTopic); return useMemo(() => { - if (topicsData[topic]?.status === "unread") { - return true; - } - if (lastMessage?.senderAddress === conversation?.client.inboxId) { + if (!lastMessage) return false; + if (topicsData[topic]?.status === "unread") return true; + // Don't mark as unread if message is from peer + if (lastMessage.senderAddress === peerInboxId) { return false; } const timestamp = normalizeTimestamp(timestampNs); const readUntil = topicsData[topic]?.readUntil || 0; return readUntil < (timestamp ?? 0); - }, [ - topicsData, - topic, - lastMessage?.senderAddress, - conversation?.client.inboxId, - timestampNs, - ]); + }, [topicsData, topic, timestampNs, lastMessage, peerInboxId]); }; diff --git a/features/conversation-list/hooks/useToggleReadStatus.ts b/features/conversation-list/hooks/useToggleReadStatus.ts index 82d8010b5..700b4fc59 100644 --- a/features/conversation-list/hooks/useToggleReadStatus.ts +++ b/features/conversation-list/hooks/useToggleReadStatus.ts @@ -16,8 +16,11 @@ export const useToggleReadStatus = ({ }: UseToggleReadStatusProps) => { const { setTopicsData } = useChatStore(useSelect(["setTopicsData"])); + console.log("useToggleReadStatus", topic, isUnread); + return useCallback(() => { const newStatus = isUnread ? "read" : "unread"; + console.log("newStatus:", newStatus); const timestamp = new Date().getTime(); setTopicsData({ [topic]: { From d02db884b4a13e172c3c6b04fcd3dac9e6136742 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Sat, 7 Dec 2024 08:38:58 +0100 Subject: [PATCH 3/7] Fix --- .../hooks/useMessageIsUnread.ts | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/features/conversation-list/hooks/useMessageIsUnread.ts b/features/conversation-list/hooks/useMessageIsUnread.ts index 9bb7dcad5..49898fff8 100644 --- a/features/conversation-list/hooks/useMessageIsUnread.ts +++ b/features/conversation-list/hooks/useMessageIsUnread.ts @@ -1,29 +1,18 @@ import { useMemo } from "react"; -import { ChatStoreType, TopicsData } from "@data/store/chatStore"; +import { ChatStoreType } from "@data/store/chatStore"; import { ConversationWithCodecsType, DecodedMessageWithCodecsType, } from "@utils/xmtpRN/client"; -import { - currentAccount, - useChatStore, - useCurrentAccount, -} from "@data/store/accountsStore"; +import { useChatStore } from "@data/store/accountsStore"; import { useSelect } from "@data/store/storeHelpers"; import { normalizeTimestamp } from "@/utils/date"; -import { useDmPeerInboxId } from "@/queries/useDmPeerInbox"; -import { useConversationCurrentTopic } from "@/features/conversation/conversation-service"; -import { ConversationTopic } from "@xmtp/react-native-sdk"; type UseConversationIsUnreadProps = { topic: string; lastMessage: DecodedMessageWithCodecsType | undefined; + conversation: ConversationWithCodecsType; timestamp: number; - /** - * @deprecated This prop has been deprecated. - * for backward compatibility only - */ - conversation?: ConversationWithCodecsType; }; const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"]; @@ -31,24 +20,29 @@ const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"]; export const useConversationIsUnread = ({ topic, lastMessage, + conversation, timestamp: timestampNs, }: UseConversationIsUnreadProps) => { - const currentAccount = useCurrentAccount()!; - - const validTopic: ConversationTopic = "valid-topic" as ConversationTopic; - - const topicsData = useChatStore((state: ChatStoreType) => state.topicsData); - const { data: peerInboxId } = useDmPeerInboxId(currentAccount, validTopic); + const { topicsData } = useChatStore(useSelect(chatStoreSelectKeys)); return useMemo(() => { - if (!lastMessage) return false; - if (topicsData[topic]?.status === "unread") return true; - // Don't mark as unread if message is from peer - if (lastMessage.senderAddress === peerInboxId) { + if (topicsData[topic]?.status === "unread") { + return true; + } + // Return false because current user sent the last message) + if (lastMessage?.senderAddress === conversation?.client.inboxId) { return false; } const timestamp = normalizeTimestamp(timestampNs); const readUntil = topicsData[topic]?.readUntil || 0; + + // Return true if the last message timestamp is greater than readUntil return readUntil < (timestamp ?? 0); - }, [topicsData, topic, timestampNs, lastMessage, peerInboxId]); + }, [ + topicsData, + topic, + lastMessage?.senderAddress, + conversation?.client.inboxId, + timestampNs, + ]); }; From d4ebb1c90ad8bbdea0a59fbeaf5a76572bededd7 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Sat, 7 Dec 2024 08:53:30 +0100 Subject: [PATCH 4/7] Remove dependency from conversation prop --- .../PinnedV3DMConversation.tsx | 1 - .../PinnedV3GroupConversation.tsx | 1 - components/V3DMListItem.tsx | 1 - components/V3GroupConversationListItem.tsx | 1 - .../hooks/useMessageIsUnread.ts | 33 ++++++++----------- .../hooks/useToggleReadStatus.ts | 4 --- 6 files changed, 14 insertions(+), 27 deletions(-) diff --git a/components/PinnedConversations/PinnedV3DMConversation.tsx b/components/PinnedConversations/PinnedV3DMConversation.tsx index 475e9637e..b4facf498 100644 --- a/components/PinnedConversations/PinnedV3DMConversation.tsx +++ b/components/PinnedConversations/PinnedV3DMConversation.tsx @@ -55,7 +55,6 @@ export const PinnedV3DMConversation = ({ const isUnread = useConversationIsUnread({ topic, lastMessage: conversation.lastMessage, - conversation: conversation, timestamp, }); diff --git a/components/PinnedConversations/PinnedV3GroupConversation.tsx b/components/PinnedConversations/PinnedV3GroupConversation.tsx index 61a64b875..4d6344244 100644 --- a/components/PinnedConversations/PinnedV3GroupConversation.tsx +++ b/components/PinnedConversations/PinnedV3GroupConversation.tsx @@ -52,7 +52,6 @@ export const PinnedV3GroupConversation = ({ const isUnread = useConversationIsUnread({ topic, lastMessage: group.lastMessage, - conversation: group, timestamp, }); diff --git a/components/V3DMListItem.tsx b/components/V3DMListItem.tsx index 7a096fc1c..0fc4f6ac9 100644 --- a/components/V3DMListItem.tsx +++ b/components/V3DMListItem.tsx @@ -69,7 +69,6 @@ export const V3DMListItem = ({ conversation }: V3DMListItemProps) => { const isUnread = useConversationIsUnread({ topic, lastMessage: conversation.lastMessage, - conversation: conversation, timestamp, }); diff --git a/components/V3GroupConversationListItem.tsx b/components/V3GroupConversationListItem.tsx index 202363e56..d5df3a82d 100644 --- a/components/V3GroupConversationListItem.tsx +++ b/components/V3GroupConversationListItem.tsx @@ -69,7 +69,6 @@ const useData = ({ group }: UseDataProps) => { const isUnread = useConversationIsUnread({ topic, lastMessage: group.lastMessage, - conversation: group, timestamp, }); diff --git a/features/conversation-list/hooks/useMessageIsUnread.ts b/features/conversation-list/hooks/useMessageIsUnread.ts index 49898fff8..11b337a56 100644 --- a/features/conversation-list/hooks/useMessageIsUnread.ts +++ b/features/conversation-list/hooks/useMessageIsUnread.ts @@ -1,17 +1,14 @@ import { useMemo } from "react"; import { ChatStoreType } from "@data/store/chatStore"; -import { - ConversationWithCodecsType, - DecodedMessageWithCodecsType, -} from "@utils/xmtpRN/client"; -import { useChatStore } from "@data/store/accountsStore"; +import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client"; +import { useChatStore, useCurrentAccount } from "@data/store/accountsStore"; import { useSelect } from "@data/store/storeHelpers"; import { normalizeTimestamp } from "@/utils/date"; +import { getCurrentUserAccountInboxId } from "@/components/Chat/Message/message-utils"; type UseConversationIsUnreadProps = { topic: string; lastMessage: DecodedMessageWithCodecsType | undefined; - conversation: ConversationWithCodecsType; timestamp: number; }; @@ -20,29 +17,27 @@ const chatStoreSelectKeys: (keyof ChatStoreType)[] = ["topicsData"]; export const useConversationIsUnread = ({ topic, lastMessage, - conversation, timestamp: timestampNs, }: UseConversationIsUnreadProps) => { const { topicsData } = useChatStore(useSelect(chatStoreSelectKeys)); + const currentInboxId = getCurrentUserAccountInboxId(); return useMemo(() => { - if (topicsData[topic]?.status === "unread") { - return true; - } - // Return false because current user sent the last message) - if (lastMessage?.senderAddress === conversation?.client.inboxId) { + // No message means no unread status + if (!lastMessage) return false; + + // Check if status is unread + if (topicsData[topic]?.status === "unread") return true; + + // Check if the last message was sent by the current user + if (lastMessage.senderAddress === currentInboxId) { return false; } + const timestamp = normalizeTimestamp(timestampNs); const readUntil = topicsData[topic]?.readUntil || 0; // Return true if the last message timestamp is greater than readUntil return readUntil < (timestamp ?? 0); - }, [ - topicsData, - topic, - lastMessage?.senderAddress, - conversation?.client.inboxId, - timestampNs, - ]); + }, [topicsData, topic, timestampNs, lastMessage, currentInboxId]); }; diff --git a/features/conversation-list/hooks/useToggleReadStatus.ts b/features/conversation-list/hooks/useToggleReadStatus.ts index 700b4fc59..796dbec6f 100644 --- a/features/conversation-list/hooks/useToggleReadStatus.ts +++ b/features/conversation-list/hooks/useToggleReadStatus.ts @@ -15,12 +15,8 @@ export const useToggleReadStatus = ({ currentAccount, }: UseToggleReadStatusProps) => { const { setTopicsData } = useChatStore(useSelect(["setTopicsData"])); - - console.log("useToggleReadStatus", topic, isUnread); - return useCallback(() => { const newStatus = isUnread ? "read" : "unread"; - console.log("newStatus:", newStatus); const timestamp = new Date().getTime(); setTopicsData({ [topic]: { From e5de05aef007d51419d741cacfbee70778c1e09b Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Sat, 7 Dec 2024 09:51:17 +0100 Subject: [PATCH 5/7] Fix --- components/Conversation/V3Conversation.tsx | 54 ++++++++++++++----- components/V3GroupConversationListItem.tsx | 5 +- .../hooks/useToggleReadStatus.ts | 2 + 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/components/Conversation/V3Conversation.tsx b/components/Conversation/V3Conversation.tsx index 65dfbf20b..971714edb 100644 --- a/components/Conversation/V3Conversation.tsx +++ b/components/Conversation/V3Conversation.tsx @@ -1,7 +1,7 @@ import { useCurrentAccount } from "@data/store/accountsStore"; import { useConversationMessages } from "@queries/useConversationMessages"; import { ConversationTopic, ConversationVersion } from "@xmtp/react-native-sdk"; -import { memo, useCallback, useEffect } from "react"; +import { memo, useCallback, useEffect, useRef } from "react"; import { FlatListProps, Platform } from "react-native"; // import { DmChatPlaceholder } from "@components/Chat/ChatPlaceholder/ChatPlaceholder"; import { DmConsentPopup } from "@/components/Chat/ConsentPopup/dm-consent-popup"; @@ -140,27 +140,21 @@ const DmContent = memo(function DmContent() { const { data: peerInboxId } = useDmPeerInboxId(currentAccount, topic!); - // Check if conversation is unread const isUnread = useConversationIsUnread({ topic, lastMessage: messages?.byId[messages?.ids[0]], // Get latest message timestamp: messages?.byId[messages?.ids[0]]?.sentNs ?? 0, }); - - // Add hook to toggle read status const toggleReadStatus = useToggleReadStatus({ topic, isUnread, currentAccount, }); - - // Effect to mark as read when entering chat - useEffect(() => { - if (isUnread && !messagesLoading && messages?.ids.length) { - console.log("== Mark as read?"); - toggleReadStatus(); - } - }, [isUnread, messagesLoading, messages?.ids.length, toggleReadStatus]); + useMarkAsReadOnEnter({ + messagesLoading, + isUnread, + toggleReadStatus, + }); useDmHeader(); @@ -213,6 +207,22 @@ const GroupContent = memo(function GroupContent() { refetch, } = useConversationMessages(currentAccount, topic!); + const isUnread = useConversationIsUnread({ + topic, + lastMessage: messages?.byId[messages?.ids[0]], // Get latest message + timestamp: messages?.byId[messages?.ids[0]]?.sentNs ?? 0, + }); + const toggleReadStatus = useToggleReadStatus({ + topic, + isUnread, + currentAccount, + }); + useMarkAsReadOnEnter({ + messagesLoading, + isUnread, + toggleReadStatus, + }); + useGroupHeader(); if (conversationNotFound) { @@ -295,6 +305,26 @@ export const KeyboardFiller = memo(function KeyboardFiller() { return ; }); +const useMarkAsReadOnEnter = ({ + messagesLoading, + isUnread, + toggleReadStatus, +}: { + messagesLoading: boolean; + isUnread: boolean; + toggleReadStatus: () => void; +}) => { + const hasMarkedAsRead = useRef(false); + + useEffect(() => { + if (isUnread && !messagesLoading && !hasMarkedAsRead.current) { + console.log("Mark as read?"); + toggleReadStatus(); + hasMarkedAsRead.current = true; + } + }, [isUnread, messagesLoading, toggleReadStatus]); +}; + function useNewConversationHeader() { const navigation = useRouter(); diff --git a/components/V3GroupConversationListItem.tsx b/components/V3GroupConversationListItem.tsx index d5df3a82d..210fd6292 100644 --- a/components/V3GroupConversationListItem.tsx +++ b/components/V3GroupConversationListItem.tsx @@ -10,7 +10,7 @@ import { saveTopicsData } from "@utils/api"; import { getMinimalDate } from "@utils/date"; import { Haptics } from "@utils/haptics"; import { navigate } from "@utils/navigation"; -import { RefObject, useCallback, useMemo, useRef } from "react"; +import { RefObject, useCallback, useEffect, useMemo, useRef } from "react"; import { useColorScheme } from "react-native"; import { Swipeable } from "react-native-gesture-handler"; import { runOnJS } from "react-native-reanimated"; @@ -370,7 +370,9 @@ export function V3GroupConversationListItem({ handleRestore, messageText, } = useData({ group }); + const ref = useRef(null); + const { onPress, onLongPress, @@ -389,6 +391,7 @@ export function V3GroupConversationListItem({ handleRestore, isBlockedChatView, }); + const { timeToShow, leftActionIcon } = useDisplayInfo({ timestamp, isUnread, diff --git a/features/conversation-list/hooks/useToggleReadStatus.ts b/features/conversation-list/hooks/useToggleReadStatus.ts index 796dbec6f..f59e3119d 100644 --- a/features/conversation-list/hooks/useToggleReadStatus.ts +++ b/features/conversation-list/hooks/useToggleReadStatus.ts @@ -21,12 +21,14 @@ export const useToggleReadStatus = ({ setTopicsData({ [topic]: { status: newStatus, + readUntil: isUnread ? timestamp : 0, timestamp, }, }); saveTopicsData(currentAccount, { [topic]: { status: newStatus, + readUntil: isUnread ? timestamp : 0, timestamp, }, }); From 2262ed71df76a7f43bd6568c0637395d6ba4dbc0 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Sat, 7 Dec 2024 10:04:51 +0100 Subject: [PATCH 6/7] Add null check for messages array --- components/Conversation/V3Conversation.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/Conversation/V3Conversation.tsx b/components/Conversation/V3Conversation.tsx index 971714edb..726cc3c4f 100644 --- a/components/Conversation/V3Conversation.tsx +++ b/components/Conversation/V3Conversation.tsx @@ -142,8 +142,12 @@ const DmContent = memo(function DmContent() { const isUnread = useConversationIsUnread({ topic, - lastMessage: messages?.byId[messages?.ids[0]], // Get latest message - timestamp: messages?.byId[messages?.ids[0]]?.sentNs ?? 0, + lastMessage: messages?.ids?.length + ? messages.byId[messages.ids[0]] + : undefined, + timestamp: messages?.ids?.length + ? (messages.byId[messages.ids[0]]?.sentNs ?? 0) + : 0, }); const toggleReadStatus = useToggleReadStatus({ topic, From 8cfa15b711a1eea0505cf759541afb8697400711 Mon Sep 17 00:00:00 2001 From: Louis Rouffineau Date: Sat, 7 Dec 2024 10:06:52 +0100 Subject: [PATCH 7/7] Remove log --- components/Conversation/V3Conversation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/Conversation/V3Conversation.tsx b/components/Conversation/V3Conversation.tsx index 726cc3c4f..846cfa7e2 100644 --- a/components/Conversation/V3Conversation.tsx +++ b/components/Conversation/V3Conversation.tsx @@ -322,7 +322,6 @@ const useMarkAsReadOnEnter = ({ useEffect(() => { if (isUnread && !messagesLoading && !hasMarkedAsRead.current) { - console.log("Mark as read?"); toggleReadStatus(); hasMarkedAsRead.current = true; }