diff --git a/components/ConversationFlashList/ConversationFlashList.tsx b/components/ConversationFlashList/ConversationFlashList.tsx index 722e15397..82ee9c1aa 100644 --- a/components/ConversationFlashList/ConversationFlashList.tsx +++ b/components/ConversationFlashList/ConversationFlashList.tsx @@ -19,14 +19,9 @@ import { useSelect } from "@data/store/storeHelpers"; import { NavigationParamList } from "@screens/Navigation/Navigation"; import { ConversationFlatListHiddenRequestItem } from "@utils/conversation"; import { FlatListItemType } from "@features/conversation-list/ConversationList.types"; -import { unwrapConversationContainer } from "@utils/groupUtils/conversationContainerHelpers"; -import { ConversationVersion } from "@xmtp/react-native-sdk"; -import { - DmWithCodecsType, - GroupWithCodecsType, -} from "@/utils/xmtpRN/client.types"; import { V3DMListItem } from "../V3DMListItem"; import { CONVERSATION_FLASH_LIST_REFRESH_THRESHOLD } from "./ConversationFlashList.constants"; +import { isConversationGroup } from "@/features/conversation/utils/is-conversation-group"; type Props = { onScroll?: () => void; @@ -73,15 +68,10 @@ export default function ConversationFlashList({ const renderItem = useCallback(({ item }: { item: FlatListItemType }) => { if ("lastMessage" in item) { - const conversation = unwrapConversationContainer(item); - if (conversation.version === ConversationVersion.GROUP) { - return ( - - ); + if (isConversationGroup(item)) { + return ; } else { - return ; + return ; } } if (item.topic === "hiddenRequestsButton") { diff --git a/components/PinnedConversations/PinnedV3Conversation.tsx b/components/PinnedConversations/PinnedV3Conversation.tsx index 8edc8e3ba..2b99e1066 100644 --- a/components/PinnedConversations/PinnedV3Conversation.tsx +++ b/components/PinnedConversations/PinnedV3Conversation.tsx @@ -1,13 +1,9 @@ import { useConversationListGroupItem } from "@hooks/useConversationListGroupItem"; -import { conversationIsGroup } from "@utils/groupUtils/conversationContainerHelpers"; -import { - DmWithCodecsType, - GroupWithCodecsType, -} from "@/utils/xmtpRN/client.types"; import React from "react"; import { PinnedV3GroupConversation } from "./PinnedV3GroupConversation"; import { PinnedV3DMConversation } from "./PinnedV3DMConversation"; import type { ConversationTopic } from "@xmtp/react-native-sdk"; +import { isConversationGroup } from "@/features/conversation/utils/is-conversation-group"; type PinnedV3ConversationProps = { topic: string; @@ -18,12 +14,8 @@ export const PinnedV3Conversation = ({ topic }: PinnedV3ConversationProps) => { if (!conversation) { return null; } - if (conversationIsGroup(conversation)) { - return ( - - ); + if (isConversationGroup(conversation)) { + return ; } - return ( - - ); + return ; }; diff --git a/features/notifications/utils/background/groupMessageNotification.ts b/features/notifications/utils/background/groupMessageNotification.ts index 089a30e28..70c5e38e9 100644 --- a/features/notifications/utils/background/groupMessageNotification.ts +++ b/features/notifications/utils/background/groupMessageNotification.ts @@ -11,25 +11,201 @@ import { } from "@utils/profile"; import { ConverseXmtpClientType, + DecodedMessageWithCodecsType, + DmWithCodecsType, GroupWithCodecsType, } from "@utils/xmtpRN/client.types"; -import { - ConversationVersion, - Group, - type ConversationTopic, -} from "@xmtp/react-native-sdk"; +import { Conversation, type ConversationTopic } from "@xmtp/react-native-sdk"; import { androidChannel } from "../setupAndroidNotificationChannel"; import { notificationAlreadyShown } from "./alreadyShown"; import { getNotificationContent } from "./notificationContent"; import { computeSpamScoreGroupMessage } from "./notificationSpamScore"; import { ProtocolNotification } from "./protocolNotification"; import logger from "@/utils/logger"; +import { isConversationGroup } from "@/features/conversation/utils/is-conversation-group"; + +const getSenderProfileInfo = async ( + xmtpClient: ConverseXmtpClientType, + conversation: Conversation, + message: DecodedMessageWithCodecsType +) => { + // For now, use the group member linked address as "senderAddress" + // @todo => make inboxId a first class citizen + const senderAddress = (await conversation.members()).find( + (m) => m.inboxId === message.senderInboxId + )?.addresses[0]; + if (!senderAddress) + return { + senderName: "", + }; + const senderSocials = await getProfile( + xmtpClient.address, + message.senderInboxId, + senderAddress + ); + const senderName = getPreferredName(senderSocials, senderAddress); + const senderImage = getPreferredAvatar(senderSocials); + + return { + senderName, + senderImage, + }; +}; + +type INotificationFunctionPayloads = { + notificationContent: string; + notification: ProtocolNotification; + conversation: GroupWithCodecsType | DmWithCodecsType; + message: DecodedMessageWithCodecsType; + senderName: string; + senderImage?: string; +}; + +type IHandleGroupMessageNotification = INotificationFunctionPayloads & { + conversation: GroupWithCodecsType; +}; + +const handleGroupMessageNotification = async ({ + notificationContent, + notification, + conversation, + message, + senderName, + senderImage, +}: IHandleGroupMessageNotification) => { + const groupName = await conversation.groupName(); + const groupImage = await conversation.groupImageUrlSquare(); + const person: AndroidPerson = { + name: senderName, + }; + if (senderImage) { + person.icon = senderImage; + } + + const withLargeIcon = groupImage + ? { + largeIcon: groupImage, + circularLargeIcon: true, + } + : {}; + const displayedNotifications = await notifee.getDisplayedNotifications(); + const previousGroupIdNotification = + conversation.topic && + displayedNotifications.find( + (n) => n.notification.android?.groupId === conversation.topic + ); + const previousMessages = + previousGroupIdNotification?.notification.android?.style?.type === + AndroidStyle.MESSAGING + ? previousGroupIdNotification.notification.android?.style?.messages + : []; + if (previousGroupIdNotification?.notification.id) { + notifee.cancelDisplayedNotification( + previousGroupIdNotification.notification.id + ); + } + await notifee.displayNotification({ + title: groupName, + subtitle: senderName, + body: notificationContent, + data: notification, + android: { + channelId: androidChannel.id, + groupId: conversation.topic, + timestamp: normalizeTimestamp(message.sentNs), + showTimestamp: true, + pressAction: { + id: "default", + }, + visibility: AndroidVisibility.PUBLIC, + ...withLargeIcon, + style: { + type: AndroidStyle.MESSAGING, + person: person, + messages: [ + ...previousMessages, + { + text: notificationContent, + timestamp: normalizeTimestamp(message.sentNs), + }, + ], + group: true, + }, + }, + }); +}; + +type IHandleDmMessageNotification = INotificationFunctionPayloads & { + conversation: DmWithCodecsType; +}; -export const isGroupMessageContentTopic = (contentTopic: string) => { - return contentTopic.startsWith("/xmtp/mls/1/g-"); +const handleDmMessageNotification = async ({ + notificationContent, + notification, + conversation, + message, + senderName, + senderImage, +}: IHandleDmMessageNotification) => { + const person: AndroidPerson = { + name: senderName, + }; + if (senderImage) { + person.icon = senderImage; + } + const withLargeIcon = senderImage + ? { + largeIcon: senderImage, + circularLargeIcon: true, + } + : {}; + const displayedNotifications = await notifee.getDisplayedNotifications(); + const previousGroupIdNotification = + conversation?.topic && + displayedNotifications.find( + (n) => n.notification.android?.groupId === conversation?.topic + ); + const previousMessages = + previousGroupIdNotification?.notification.android?.style?.type === + AndroidStyle.MESSAGING + ? previousGroupIdNotification.notification.android?.style?.messages + : []; + if (previousGroupIdNotification?.notification.id) { + notifee.cancelDisplayedNotification( + previousGroupIdNotification.notification.id + ); + } + await notifee.displayNotification({ + title: senderName, + body: notificationContent, + data: notification, + android: { + channelId: androidChannel.id, + groupId: conversation.topic, + timestamp: normalizeTimestamp(message.sentNs), + showTimestamp: true, + pressAction: { + id: "default", + }, + visibility: AndroidVisibility.PUBLIC, + ...withLargeIcon, + style: { + type: AndroidStyle.MESSAGING, + person, + messages: [ + ...previousMessages, + { + text: notificationContent, + timestamp: normalizeTimestamp(message.sentNs), + }, + ], + group: false, + }, + }, + }); }; -export const handleGroupMessageNotification = async ( +export const handleV3MessageNotification = async ( xmtpClient: ConverseXmtpClientType, notification: ProtocolNotification ) => { @@ -45,7 +221,6 @@ export const handleGroupMessageNotification = async ( if (!conversation) throw new Error("Conversation not found"); } await conversation.sync(); - const isGroup = conversation.version === ConversationVersion.GROUP; const message = await conversation.processMessage(notification.message); // Not displaying notifications for ourselves, syncing is enough @@ -59,144 +234,35 @@ export const handleGroupMessageNotification = async ( message ); if (spamScore >= 0) return; - // For now, use the group member linked address as "senderAddress" - // @todo => make inboxId a first class citizen - const senderAddress = (await conversation.members()).find( - (m) => m.inboxId === message.senderInboxId - )?.addresses[0]; - if (!senderAddress) return; - const senderSocials = await getProfile( - xmtpClient.address, - message.senderInboxId, - senderAddress - ); - const senderName = getPreferredName(senderSocials, senderAddress); - const senderImage = getPreferredAvatar(senderSocials); const notificationContent = await getNotificationContent( conversation as GroupWithCodecsType, message ); if (!notificationContent) return; + const { senderName, senderImage } = await getSenderProfileInfo( + xmtpClient, + conversation, + message + ); - if (isGroup) { - const groupName = await (conversation as Group).groupName(); - const groupImage = await (conversation as Group).groupImageUrlSquare(); - const person: AndroidPerson = { - name: senderName, - }; - if (senderImage) { - person.icon = senderImage; - } - - const withLargeIcon = groupImage - ? { - largeIcon: groupImage, - circularLargeIcon: true, - } - : {}; - const displayedNotifications = await notifee.getDisplayedNotifications(); - const previousGroupIdNotification = - conversation?.topic && - displayedNotifications.find( - (n) => n.notification.android?.groupId === conversation?.topic - ); - const previousMessages = - previousGroupIdNotification?.notification.android?.style?.type === - AndroidStyle.MESSAGING - ? previousGroupIdNotification.notification.android?.style?.messages - : []; - if (previousGroupIdNotification?.notification.id) { - notifee.cancelDisplayedNotification( - previousGroupIdNotification.notification.id - ); - } - await notifee.displayNotification({ - title: groupName, - subtitle: senderName, - body: notificationContent, - data: notification, - android: { - channelId: androidChannel.id, - groupId: conversation.topic, - timestamp: normalizeTimestamp(message.sentNs), - showTimestamp: true, - pressAction: { - id: "default", - }, - visibility: AndroidVisibility.PUBLIC, - ...withLargeIcon, - style: { - type: AndroidStyle.MESSAGING, - person: person, - messages: [ - ...previousMessages, - { - text: notificationContent, - timestamp: normalizeTimestamp(message.sentNs), - }, - ], - group: true, - }, - }, + if (isConversationGroup(conversation)) { + handleGroupMessageNotification({ + notificationContent, + notification, + conversation, + message, + senderName, + senderImage, }); } else { - const senderImage = getPreferredAvatar(senderSocials); - const person: AndroidPerson = { - name: senderName, - }; - if (senderImage) { - person.icon = senderImage; - } - const withLargeIcon = senderImage - ? { - largeIcon: senderImage, - circularLargeIcon: true, - } - : {}; - const displayedNotifications = await notifee.getDisplayedNotifications(); - const previousGroupIdNotification = - conversation?.topic && - displayedNotifications.find( - (n) => n.notification.android?.groupId === conversation?.topic - ); - const previousMessages = - previousGroupIdNotification?.notification.android?.style?.type === - AndroidStyle.MESSAGING - ? previousGroupIdNotification.notification.android?.style?.messages - : []; - if (previousGroupIdNotification?.notification.id) { - notifee.cancelDisplayedNotification( - previousGroupIdNotification.notification.id - ); - } - await notifee.displayNotification({ - title: senderName, - body: notificationContent, - data: notification, - android: { - channelId: androidChannel.id, - groupId: conversation.topic, - timestamp: normalizeTimestamp(message.sentNs), - showTimestamp: true, - pressAction: { - id: "default", - }, - visibility: AndroidVisibility.PUBLIC, - ...withLargeIcon, - style: { - type: AndroidStyle.MESSAGING, - person, - messages: [ - ...previousMessages, - { - text: notificationContent, - timestamp: normalizeTimestamp(message.sentNs), - }, - ], - group: false, - }, - }, + handleDmMessageNotification({ + notificationContent, + notification, + conversation, + message, + senderName, + senderImage, }); } } catch (e) { diff --git a/features/notifications/utils/background/protocolNotification.ts b/features/notifications/utils/background/protocolNotification.ts index b8eec943e..4c589a99f 100644 --- a/features/notifications/utils/background/protocolNotification.ts +++ b/features/notifications/utils/background/protocolNotification.ts @@ -3,14 +3,12 @@ import { ConverseXmtpClientType } from "@/utils/xmtpRN/client.types"; import { getXmtpClient } from "@utils/xmtpRN/sync"; import { z } from "zod"; import logger from "@utils/logger"; -import { - handleGroupMessageNotification, - isGroupMessageContentTopic, -} from "./groupMessageNotification"; +import { handleV3MessageNotification } from "./groupMessageNotification"; import { handleGroupWelcomeNotification, isGroupWelcomeContentTopic, } from "./groupWelcomeNotification"; +import { isV3Topic } from "@/utils/groupUtils/groupId"; export const ProtocolNotificationSchema = z.object({ account: z.string().regex(/^0x[a-fA-F0-9]{40}$/), @@ -44,8 +42,8 @@ export const handleProtocolNotification = async ( const xmtpClient = (await getXmtpClient( notification.account )) as ConverseXmtpClientType; - if (isGroupMessageContentTopic(notification.contentTopic)) { - handleGroupMessageNotification(xmtpClient, notification); + if (isV3Topic(notification.contentTopic)) { + handleV3MessageNotification(xmtpClient, notification); } else if (isGroupWelcomeContentTopic(notification.contentTopic)) { handleGroupWelcomeNotification(xmtpClient, notification); } diff --git a/utils/groupUtils/conversationContainerHelpers.ts b/utils/groupUtils/conversationContainerHelpers.ts deleted file mode 100644 index d08df1f66..000000000 --- a/utils/groupUtils/conversationContainerHelpers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - ConversationWithCodecsType, - DmWithCodecsType, - GroupWithCodecsType, -} from "../xmtpRN/client.types"; -import { ConversationVersion } from "@xmtp/react-native-sdk"; - -export const unwrapConversationContainer = ( - conversation: ConversationWithCodecsType -) => { - if (conversationIsGroup(conversation)) { - return conversation as GroupWithCodecsType; - } - return conversation as DmWithCodecsType; -}; - -export const conversationIsGroup = ( - conversation: ConversationWithCodecsType -) => { - return conversation.version === ConversationVersion.GROUP; -};