diff --git a/android/app/src/main/java/com/converse/dev/Profile.kt b/android/app/src/main/java/com/converse/dev/Profile.kt index 1bb32692a..4700776d6 100644 --- a/android/app/src/main/java/com/converse/dev/Profile.kt +++ b/android/app/src/main/java/com/converse/dev/Profile.kt @@ -7,6 +7,7 @@ import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley import com.beust.klaxon.Klaxon import kotlinx.coroutines.suspendCancellableCoroutine +import org.web3j.crypto.Keys import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -14,18 +15,22 @@ import kotlin.coroutines.resumeWithException suspend fun getProfile(appContext: Context, account: String, address: String): Profile? { var profileState = getProfilesStore(appContext, account)?.state + var lowercasedAddress = address.lowercase() + var formattedAddress = Keys.toChecksumAddress(address) profileState?.profiles?.get(address)?.let { return it } + profileState?.profiles?.get(formattedAddress)?.let { return it } + profileState?.profiles?.get(lowercasedAddress)?.let { return it } // If profile is nil, let's refresh it try { - refreshProfileFromBackend(appContext, account, address) + refreshProfileFromBackend(appContext, account, formattedAddress) } catch (e: Exception) { // Handle exception if needed } profileState = getProfilesStore(appContext, account)?.state - return profileState?.profiles?.get(address) + return profileState?.profiles?.get(formattedAddress) } suspend fun refreshProfileFromBackend(appContext: Context, account: String, address: String) { diff --git a/android/app/src/main/java/com/converse/dev/PushNotificationsService.kt b/android/app/src/main/java/com/converse/dev/PushNotificationsService.kt index cd88d7a11..0bb73b3d7 100644 --- a/android/app/src/main/java/com/converse/dev/PushNotificationsService.kt +++ b/android/app/src/main/java/com/converse/dev/PushNotificationsService.kt @@ -70,6 +70,8 @@ import java.util.* import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaField +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ProcessLifecycleOwner class PushNotificationsService : FirebaseMessagingService() { companion object { @@ -111,6 +113,12 @@ class PushNotificationsService : FirebaseMessagingService() { return } + val appIsInForeground = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) + val currentAccount = getCurrentAccount(this) + if (appIsInForeground && currentAccount !== null && currentAccount.lowercase() == notificationData.account.lowercase()) { + Log.d(TAG, "Preventing notification for ${notificationData.account} because user is on it") + return + } Log.d(TAG, "INSTANTIATED XMTP CLIENT FOR ${notificationData.contentTopic}") val encryptedMessageData = Base64.decode(notificationData.message, Base64.NO_WRAP) diff --git a/android/app/src/main/java/com/converse/dev/xmtp/Conversations.kt b/android/app/src/main/java/com/converse/dev/xmtp/Conversations.kt index 22560bd01..280d20a51 100644 --- a/android/app/src/main/java/com/converse/dev/xmtp/Conversations.kt +++ b/android/app/src/main/java/com/converse/dev/xmtp/Conversations.kt @@ -135,18 +135,6 @@ fun persistNewConversation(appContext:Context, account: String, conversation: Co } } -fun getSavedConversationTitle(appContext: Context, topic: String): String { - try { - Log.d("PushNotificationsService", "Getting data conversation-$topic") - val mmkv = getMmkv(appContext) - val savedConversationDict = mmkv?.decodeString("conversation-$topic") ?: return "" - val parsedConversationDict = Klaxon().parse(savedConversationDict) - return parsedConversationDict?.title ?: parsedConversationDict?.shortAddress ?: "" - } catch (e: Exception) { - return "" - } -} - suspend fun getNewGroup(xmtpClient: Client, contentTopic: String): Group? { return try { if (isGroupWelcomeTopic(contentTopic)) { diff --git a/android/app/src/main/java/com/converse/dev/xmtp/Messages.kt b/android/app/src/main/java/com/converse/dev/xmtp/Messages.kt index ce10658cf..827156172 100644 --- a/android/app/src/main/java/com/converse/dev/xmtp/Messages.kt +++ b/android/app/src/main/java/com/converse/dev/xmtp/Messages.kt @@ -192,8 +192,7 @@ suspend fun handleOngoingConversationMessage( } val message = conversation.decode(envelope) - val contentTopic = envelope.contentTopic - var conversationTitle = getSavedConversationTitle(appContext, contentTopic) + var conversationTitle = "" val decodedMessageResult = handleMessageByContentType( appContext, @@ -201,6 +200,10 @@ suspend fun handleOngoingConversationMessage( xmtpClient, ) + decodedMessageResult.senderAddress?.let { + conversationTitle = shortAddress(it) + } + val senderProfile = decodedMessageResult.senderAddress?.let { getProfile(appContext, xmtpClient.address, it) } var senderAvatar: String? = null @@ -210,7 +213,7 @@ suspend fun handleOngoingConversationMessage( } val shouldShowNotification = if (decodedMessageResult.senderAddress != xmtpClient.address && !decodedMessageResult.forceIgnore && decodedMessageResult.content != null) { - if (conversationTitle.isEmpty() && decodedMessageResult.senderAddress != null) { + if ((conversationTitle == null || conversationTitle!!.isEmpty()) && decodedMessageResult.senderAddress != null) { conversationTitle = shortAddress(decodedMessageResult.senderAddress) } true diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 2ba740ede..4cb6a6ec2 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -4,6 +4,7 @@ import { itemSeparatorColor, tertiaryBackgroundColor, } from "@styles/colors"; +import { getCleanAddress } from "@utils/eth"; import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { @@ -47,7 +48,7 @@ import { useKeyboardAnimation } from "../../utils/animations/keyboardAnimation"; import { isAttachmentMessage } from "../../utils/attachment/helpers"; import { useConversationContext } from "../../utils/conversation"; import { converseEventEmitter } from "../../utils/events"; -import { getProfileData } from "../../utils/profile"; +import { getProfile, getProfileData } from "../../utils/profile"; import { UUID_REGEX } from "../../utils/regex"; import { isContentType } from "../../utils/xmtpRN/contentTypes"; import { Recommendation } from "../Recommendations/Recommendation"; @@ -183,7 +184,7 @@ export default function Chat() { const peerSocials = useProfilesStore( useShallow((s) => conversation?.peerAddress - ? s.profiles[conversation.peerAddress]?.socials + ? getProfile(conversation.peerAddress, s.profiles)?.socials : undefined ) ); @@ -228,7 +229,7 @@ export default function Chat() { conversation && !isBlockedPeer && (!conversation.isGroup || - conversation.groupMembers.includes(xmtpAddress.toLowerCase())) + conversation.groupMembers.includes(getCleanAddress(xmtpAddress))) ); const textInputStyle = useAnimatedStyle( diff --git a/components/Chat/ChatGroupUpdatedMessage.tsx b/components/Chat/ChatGroupUpdatedMessage.tsx index d30b8d2f6..1dd1b49ba 100644 --- a/components/Chat/ChatGroupUpdatedMessage.tsx +++ b/components/Chat/ChatGroupUpdatedMessage.tsx @@ -8,7 +8,7 @@ import { useInboxIdStore, useProfilesStore, } from "../../data/store/accountsStore"; -import { getPreferredName } from "../../utils/profile"; +import { getPreferredName, getProfile } from "../../utils/profile"; export default function ChatGroupUpdatedMessage({ message, @@ -27,7 +27,7 @@ export default function ChatGroupUpdatedMessage({ // TODO: Feat: handle multiple members const initiatedByAddress = byInboxId[parsedContent.initiatedByInboxId]?.[0]; const initiatedByReadableName = getPreferredName( - profiles[initiatedByAddress]?.socials, + getProfile(initiatedByAddress, profiles)?.socials, initiatedByAddress ); const membersActions: string[] = []; @@ -37,7 +37,7 @@ export default function ChatGroupUpdatedMessage({ // We haven't synced yet the members if (!firstAddress) return; const readableName = getPreferredName( - profiles[firstAddress]?.socials, + getProfile(firstAddress, profiles)?.socials, firstAddress ); membersActions.push(`${readableName} joined the conversation`); @@ -48,7 +48,7 @@ export default function ChatGroupUpdatedMessage({ // We haven't synced yet the members if (!firstAddress) return; const readableName = getPreferredName( - profiles[firstAddress]?.socials, + getProfile(firstAddress, profiles)?.socials, firstAddress ); membersActions.push(`${readableName} left the conversation`); diff --git a/components/Chat/ChatNullState.tsx b/components/Chat/ChatNullState.tsx index cca41fafd..9839cdc0a 100644 --- a/components/Chat/ChatNullState.tsx +++ b/components/Chat/ChatNullState.tsx @@ -31,6 +31,7 @@ import { getPreferredUsername, getPreferredName, getPreferredAvatar, + getProfile, } from "../../utils/profile"; import NewConversationButton from "../ConversationList/NewConversationButton"; @@ -48,7 +49,9 @@ const ChatNullState: React.FC = ({ const colorScheme = useColorScheme(); const styles = useStyles(); - const socials = useProfilesStore((s) => s.profiles[currentAccount]?.socials); + const socials = useProfilesStore( + (s) => getProfile(currentAccount, s.profiles)?.socials + ); const username = getPreferredUsername(socials); const displayName = getPreferredName(socials, currentAccount); const profileUrl = `https://${config.websiteDomain}/dm/${ diff --git a/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx b/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx index 92974fd39..37326f6bd 100644 --- a/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx +++ b/components/Chat/ChatPlaceholder/ChatPlaceholder.tsx @@ -18,7 +18,7 @@ import { } from "../../../data/store/accountsStore"; import { useConversationContext } from "../../../utils/conversation"; import { sendMessage } from "../../../utils/message"; -import { getProfileData } from "../../../utils/profile"; +import { getProfile, getProfileData } from "../../../utils/profile"; import { conversationName } from "../../../utils/str"; import { consentToPeersOnProtocol } from "../../../utils/xmtpRN/conversations"; import ActivityIndicator from "../../ActivityIndicator/ActivityIndicator"; @@ -42,7 +42,7 @@ export default function ChatPlaceholder({ messagesCount }: Props) { ); const peerSocials = useProfilesStore((s) => conversation?.peerAddress - ? s.profiles[conversation.peerAddress]?.socials + ? getProfile(conversation.peerAddress, s.profiles)?.socials : undefined ); const profileData = getProfileData(recommendationData, peerSocials); diff --git a/components/Chat/Message/Message.tsx b/components/Chat/Message/Message.tsx index 1bac4243b..48a680df2 100644 --- a/components/Chat/Message/Message.tsx +++ b/components/Chat/Message/Message.tsx @@ -47,7 +47,11 @@ import { } from "../../../utils/messageContent"; import { navigate } from "../../../utils/navigation"; import { LimitedMap } from "../../../utils/objects"; -import { getPreferredAvatar, getPreferredName } from "../../../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../../../utils/profile"; import { getMessageReactions } from "../../../utils/reactions"; import { getReadableProfile } from "../../../utils/str"; import { isTransactionMessage } from "../../../utils/transaction"; @@ -101,7 +105,9 @@ const MessageSender = ({ message }: { message: MessageToDisplay }) => { const address = useInboxIdStore( (s) => s.byInboxId[message.senderAddress]?.[0] ?? message.senderAddress ); - const senderSocials = useProfilesStore((s) => s.profiles[address]?.socials); + const senderSocials = useProfilesStore( + (s) => getProfile(address, s.profiles)?.socials + ); const styles = useStyles(); return ( @@ -116,7 +122,9 @@ const MessageSenderAvatar = ({ message }: { message: MessageToDisplay }) => { const address = useInboxIdStore( (s) => s.byInboxId[message.senderAddress]?.[0] ?? message.senderAddress ); - const senderSocials = useProfilesStore((s) => s.profiles[address]?.socials); + const senderSocials = useProfilesStore( + (s) => getProfile(address, s.profiles)?.socials + ); const styles = useStyles(); const openProfile = useCallback(() => { navigate("Profile", { address: message.senderAddress }); diff --git a/components/Chat/Message/MessageReactions.tsx b/components/Chat/Message/MessageReactions.tsx index 9baf76abe..df04b1123 100644 --- a/components/Chat/Message/MessageReactions.tsx +++ b/components/Chat/Message/MessageReactions.tsx @@ -20,7 +20,11 @@ import { useCurrentAccount, useProfilesStore, } from "../../../data/store/accountsStore"; -import { getPreferredAvatar, getPreferredName } from "../../../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../../../utils/profile"; import { MessageReaction, addReactionToMessage, @@ -145,9 +149,11 @@ function ChatMessageReactions({ message, reactions }: Props) { .map((reactor, index) => ( = ({ content, addresses, index }) => { const animatedValue = useSharedValue(0); const membersSocials = useProfilesStore((s) => addresses.map((address) => { - const socials = s.profiles[address]?.socials; + const socials = getProfile(address, s.profiles)?.socials; return { address, uri: getPreferredAvatar(socials), diff --git a/components/Conversation/ConversationTitle.tsx b/components/Conversation/ConversationTitle.tsx index b5fd35b38..cc7bb4df7 100644 --- a/components/Conversation/ConversationTitle.tsx +++ b/components/Conversation/ConversationTitle.tsx @@ -26,7 +26,7 @@ import { XmtpConversation } from "../../data/store/chatStore"; import { useGroupName } from "../../hooks/useGroupName"; import { useGroupPhoto } from "../../hooks/useGroupPhoto"; import { NavigationParamList } from "../../screens/Navigation/Navigation"; -import { getPreferredAvatar } from "../../utils/profile"; +import { getPreferredAvatar, getProfile } from "../../utils/profile"; import { conversationName, getTitleFontScale } from "../../utils/str"; import Avatar from "../Avatar"; import { useDebugEnabled } from "../DebugButton"; @@ -59,7 +59,7 @@ export default function ConversationTitle({ ? groupPhoto : getPreferredAvatar( conversation?.peerAddress - ? profiles[conversation.peerAddress]?.socials + ? getProfile(conversation.peerAddress, profiles)?.socials : undefined ) ); @@ -77,7 +77,7 @@ export default function ConversationTitle({ conversation.peerAddress !== previousConversation.peerAddress || conversation.context?.conversationId !== previousConversation.context?.conversationId || - conversation.conversationTitle || + conversationName(conversation) || (previousConversation.isGroup && conversation.isGroup && previousConversation.groupName !== conversation.groupName) @@ -87,7 +87,7 @@ export default function ConversationTitle({ setTitle(conversationName(conversation)); } if (!conversation.peerAddress) return; - const socials = profiles[conversation.peerAddress]?.socials; + const socials = getProfile(conversation.peerAddress, profiles)?.socials; setAvatar(getPreferredAvatar(socials)); } conversationRef.current = conversation; diff --git a/components/ConversationFlashList.tsx b/components/ConversationFlashList.tsx index 632299ad2..c15c05e5c 100644 --- a/components/ConversationFlashList.tsx +++ b/components/ConversationFlashList.tsx @@ -21,7 +21,7 @@ import { ConversationFlatListItem, ConversationWithLastMessagePreview, } from "../utils/conversation"; -import { getPreferredAvatar } from "../utils/profile"; +import { getPreferredAvatar, getProfile } from "../utils/profile"; import { conversationName } from "../utils/str"; type Props = { @@ -105,7 +105,7 @@ export default function ConversationFlashList({ const conversation = item as ConversationWithLastMessagePreview; const lastMessagePreview = conversation.lastMessagePreview; const socials = conversation.peerAddress - ? profiles[conversation.peerAddress]?.socials + ? getProfile(conversation.peerAddress, profiles)?.socials : undefined; if (conversation.isGroup) { return ; diff --git a/components/ConversationList/ProfileSettingsButton.tsx b/components/ConversationList/ProfileSettingsButton.tsx index d6b951e52..5a0d65457 100644 --- a/components/ConversationList/ProfileSettingsButton.tsx +++ b/components/ConversationList/ProfileSettingsButton.tsx @@ -11,7 +11,11 @@ import { } from "../../data/store/accountsStore"; import { evmHelpers } from "../../utils/evm/helpers"; import { navigate } from "../../utils/navigation"; -import { getPreferredAvatar, getPreferredName } from "../../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../../utils/profile"; import Avatar from "../Avatar"; import Button from "../Button/Button"; @@ -22,7 +26,7 @@ export default function ProfileSettingsButton() { const [stringSize, setStringSize] = useState(0); const account = currentAccount(); const profiles = useProfilesStore((state) => state.profiles); - const socials = profiles[account]?.socials; + const socials = getProfile(account, profiles)?.socials; const openProfile = useCallback(() => { navigate("Profile", { address: currentAccount() }); diff --git a/components/GroupAvatar.tsx b/components/GroupAvatar.tsx index 1e1e7a621..83420c010 100644 --- a/components/GroupAvatar.tsx +++ b/components/GroupAvatar.tsx @@ -23,7 +23,11 @@ import { useCurrentAccount, } from "../data/store/accountsStore"; import { useGroupMembers } from "../hooks/useGroupMembers"; -import { getPreferredAvatar, getPreferredName } from "../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../utils/profile"; const MAIN_CIRCLE_RADIUS = 50; const MAX_VISIBLE_MEMBERS = 4; @@ -156,7 +160,7 @@ const GroupAvatar: React.FC = ({ (acc: { address: string; uri?: string; name?: string }[], id) => { const member = groupMembers.byId[id]; const address = member.addresses[0].toLowerCase(); - const senderSocials = profiles[address]?.socials; + const senderSocials = getProfile(address, profiles)?.socials; const shouldExclude = excludeSelf && account && address === account.toLowerCase(); if (shouldExclude) return acc; diff --git a/components/Onboarding/UserProfile.tsx b/components/Onboarding/UserProfile.tsx index 66ef6a503..f115d6261 100644 --- a/components/Onboarding/UserProfile.tsx +++ b/components/Onboarding/UserProfile.tsx @@ -8,6 +8,7 @@ import { textSecondaryColor, } from "@styles/colors"; import logger from "@utils/logger"; +import { getProfile } from "@utils/profile"; import React, { useCallback, useRef, useState } from "react"; import { Platform, @@ -60,9 +61,10 @@ const LOADING_SENTENCES = Object.values( export const UserProfile = ({ onboarding, navigation }: Props) => { const address = useCurrentAccount() as string; const profiles = useProfilesStore((state) => state.profiles); - const currentUserUsername = profiles[address]?.socials?.userNames?.find( - (u) => u.isPrimary - ); + const currentUserUsername = getProfile( + address, + profiles + )?.socials?.userNames?.find((u) => u.isPrimary); const [errorMessage, setErrorMessage] = useState(""); const colorScheme = useColorScheme(); diff --git a/components/PinnedConversations/PinnedConversation.tsx b/components/PinnedConversations/PinnedConversation.tsx index 116ecb950..8166f8321 100644 --- a/components/PinnedConversations/PinnedConversation.tsx +++ b/components/PinnedConversations/PinnedConversation.tsx @@ -5,6 +5,7 @@ import { backgroundColor, textSecondaryColor } from "@styles/colors"; import { AvatarSizes } from "@styles/sizes"; import { ConversationWithLastMessagePreview } from "@utils/conversation"; import { showUnreadOnConversation } from "@utils/conversation/showUnreadOnConversation"; +import { conversationName } from "@utils/str"; import { FC, useCallback, useMemo } from "react"; import { StyleSheet, @@ -20,7 +21,11 @@ import { useProfilesStore, } from "../../data/store/accountsStore"; import { navigate } from "../../utils/navigation"; -import { getPreferredAvatar, getPreferredName } from "../../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../../utils/profile"; import GroupAvatar from "../GroupAvatar"; interface Props { @@ -37,8 +42,8 @@ export const PinnedConversation: FC = ({ conversation }) => { const { data: groupPhoto } = useGroupPhotoQuery(account, topic, { refetchOnMount: false, }); - const title = isGroup ? groupName : conversation.conversationTitle; - const socials = profiles[conversation.peerAddress as string]?.socials; + const title = isGroup ? groupName : conversationName(conversation); + const socials = getProfile(conversation.peerAddress, profiles)?.socials; const avatar = isGroup ? groupPhoto : getPreferredAvatar(socials); const setPinnedConversations = useChatStore((s) => s.setPinnedConversations); const styles = useStyles(); diff --git a/components/Recommendations/Recommendations.tsx b/components/Recommendations/Recommendations.tsx index d6fd8cd05..39db5d0d0 100644 --- a/components/Recommendations/Recommendations.tsx +++ b/components/Recommendations/Recommendations.tsx @@ -7,6 +7,7 @@ import { textPrimaryColor, textSecondaryColor, } from "@styles/colors"; +import { getProfile } from "@utils/profile"; import * as Linking from "expo-linking"; import { useCallback, useEffect, useState } from "react"; import { @@ -158,7 +159,7 @@ export default function Recommendations({ recommendationData={frens[item]} navigation={navigation} isVisible={!!viewableItems[item]} - socials={profiles?.[item]?.socials} + socials={getProfile(item, profiles)?.socials} groupMode={groupMode} addToGroup={addToGroup} /> diff --git a/containers/GroupPendingRequestsTable.tsx b/containers/GroupPendingRequestsTable.tsx index 94d94b50b..08e4c3183 100644 --- a/containers/GroupPendingRequestsTable.tsx +++ b/containers/GroupPendingRequestsTable.tsx @@ -7,7 +7,7 @@ import { useAddToGroupMutation } from "@queries/useAddToGroupMutation"; import { invalidatePendingJoinRequestsQuery } from "@queries/usePendingRequestsQuery"; import { actionSheetColors, textSecondaryColor } from "@styles/colors"; import { updateGroupJoinRequestStatus } from "@utils/api"; -import { getPreferredName } from "@utils/profile"; +import { getPreferredName, getProfile } from "@utils/profile"; import { FC, useMemo } from "react"; import { StyleSheet, Text, useColorScheme, View } from "react-native"; @@ -37,7 +37,7 @@ export const GroupPendingRequestsTable: FC = ({ const address = a[0]; const request = a[1]; const preferredName = getPreferredName( - profiles[address]?.socials, + getProfile(address, profiles)?.socials, address ); items.push({ diff --git a/containers/GroupScreenMembersTable.tsx b/containers/GroupScreenMembersTable.tsx index abcfe6b52..607f58353 100644 --- a/containers/GroupScreenMembersTable.tsx +++ b/containers/GroupScreenMembersTable.tsx @@ -14,7 +14,7 @@ import { getGroupMemberActions } from "@utils/groupUtils/getGroupMemberActions"; import { sortGroupMembersByAdminStatus } from "@utils/groupUtils/sortGroupMembersByAdminStatus"; import logger from "@utils/logger"; import { navigate } from "@utils/navigation"; -import { getPreferredName } from "@utils/profile"; +import { getPreferredName, getProfile } from "@utils/profile"; import { FC, useMemo } from "react"; import { Alert, StyleSheet, Text, useColorScheme, View } from "react-native"; @@ -62,7 +62,7 @@ export const GroupScreenMembersTable: FC = ({ const isCurrentUser = a.address.toLowerCase() === currentAccount.toLowerCase(); const preferredName = getPreferredName( - profiles[a.address]?.socials, + getProfile(a.address, profiles)?.socials, a.address ); items.push({ diff --git a/data/db/datasource.ts b/data/db/datasource.ts index 14fffa28d..825e22298 100644 --- a/data/db/datasource.ts +++ b/data/db/datasource.ts @@ -10,7 +10,6 @@ import { getDbDirectory, getDbFileName } from "."; import { typeORMDriver } from "./driver"; import { Conversation } from "./entities/conversationEntity"; import { Message } from "./entities/messageEntity"; -import { Profile } from "./entities/profileEntity"; import { TypeORMLogger } from "./logger"; import { init1671623489366 } from "./migrations/1671623489366-init"; import { addLensHandle1671788934503 } from "./migrations/1671788934503-addLensHandle"; @@ -39,6 +38,7 @@ import { AddIndexToSent1712656017130 } from "./migrations/1712656017130-addIndex import { RemoveSentViaConverse1717625558678 } from "./migrations/1717625558678-RemoveSentViaConverse"; import { AddSuperAdmin1717631723249 } from "./migrations/1717631723249-AddSuperAdmin"; import { AddIsActive1721143963530 } from "./migrations/1721143963530-addIsActive"; +import { RemoveProfile1726828413530 } from "./migrations/1726828413530-removeProfileDb"; // We now use built in SQLite v3.45.1 from op-sqlite @@ -68,7 +68,7 @@ export const getDataSource = async (account: string) => { Buffer.from(dbEncryptionKey).toString("base64"), dbEncryptionSalt ), - entities: [Conversation, Message, Profile], + entities: [Conversation, Message], synchronize: false, migrationsRun: false, migrations: [ @@ -99,6 +99,7 @@ export const getDataSource = async (account: string) => { RemoveSentViaConverse1717625558678, AddSuperAdmin1717631723249, AddIsActive1721143963530, + RemoveProfile1726828413530, ], type: "react-native", location: await getDbDirectory(), diff --git a/data/db/entities/profileEntity.ts b/data/db/entities/profileEntity.ts deleted file mode 100644 index 60cf845ec..000000000 --- a/data/db/entities/profileEntity.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Entity, Column, PrimaryColumn } from "typeorm/browser"; - -@Entity() -export class Profile { - // @ts-ignore - public static name = "Profile"; - - @PrimaryColumn("text") - address!: string; - - @Column("text", { default: "{}" }) - socials!: string; - - @Column("int", { default: 0 }) - updatedAt!: number; -} diff --git a/data/db/index.ts b/data/db/index.ts index 23b2aca33..2876f8dfc 100644 --- a/data/db/index.ts +++ b/data/db/index.ts @@ -11,7 +11,6 @@ import { } from "./datasource"; import { Conversation } from "./entities/conversationEntity"; import { Message } from "./entities/messageEntity"; -import { Profile } from "./entities/profileEntity"; import config from "../../config"; import { sentryTrackError, sentryTrackMessage } from "../../utils/sentry"; import { @@ -24,7 +23,6 @@ const env = config.xmtpEnv as "dev" | "production" | "local"; type RepositoriesForAccount = { conversation: Repository; message: Repository; - profile: Repository; }; const repositories: { @@ -85,12 +83,12 @@ export const initDb = async (account: string): Promise => { try { logger.debug(`Running migrations for ${account}`); await waitUntilAppActive(1500); - await dataSource.runMigrations(); + const migrationsResult = await dataSource.runMigrations(); logger.debug(`Migrations done for ${account}`); + console.log(migrationsResult); repositories[account] = { conversation: dataSource.getRepository(Conversation), message: dataSource.getRepository(Message), - profile: dataSource.getRepository(Profile), }; } catch (e: any) { logger.error(e, { account, message: "Error running migrations" }); diff --git a/data/db/migrations/1726828413530-removeProfileDb.ts b/data/db/migrations/1726828413530-removeProfileDb.ts new file mode 100644 index 000000000..08fa3b407 --- /dev/null +++ b/data/db/migrations/1726828413530-removeProfileDb.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RemoveProfile1726828413530 implements MigrationInterface { + name = "RemoveProfile1726828413530"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "profile"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "profile" ("address" text PRIMARY KEY NOT NULL, "socials" text NOT NULL DEFAULT ('{}'), "updatedAt" integer NOT NULL DEFAULT (0))` + ); + } +} diff --git a/data/helpers/conversations/upsertConversations.ts b/data/helpers/conversations/upsertConversations.ts index c71c10dea..bf400f7e2 100644 --- a/data/helpers/conversations/upsertConversations.ts +++ b/data/helpers/conversations/upsertConversations.ts @@ -6,14 +6,12 @@ import { navigateToTopicWithRetry, topicToNavigateTo, } from "../../../utils/navigation"; -import { saveConversationIdentifiersForNotifications } from "../../../utils/notifications"; -import { getPreferredName } from "../../../utils/profile"; import { getRepository } from "../../db"; import { getExistingDataSource } from "../../db/datasource"; import { Conversation } from "../../db/entities/conversationEntity"; import { upsertRepository } from "../../db/upsert"; import { xmtpConversationToDb } from "../../mappers"; -import { getChatStore, getProfilesStore } from "../../store/accountsStore"; +import { getChatStore } from "../../store/accountsStore"; import { XmtpConversation } from "../../store/chatStore"; import { refreshProfilesIfNeeded } from "../profiles/profilesUpdate"; @@ -81,25 +79,12 @@ const setupAndSaveConversations = async ( const alreadyConversationInDbWithTopic = alreadyConversationsByTopic[conversation.topic]; - if (!conversation.isGroup) { - const profileSocials = - getProfilesStore(account).getState().profiles[conversation.peerAddress] - ?.socials; - - conversation.conversationTitle = getPreferredName( - profileSocials, - conversation.peerAddress, - conversation.context?.conversationId - ); - } - conversation.readUntil = conversation.readUntil || alreadyConversationInDbWithTopic?.readUntil || 0; conversationsToUpsert.push(xmtpConversationToDb(conversation)); - saveConversationIdentifiersForNotifications(conversation); }); // Let's save by batch to avoid hermes issues diff --git a/data/helpers/profiles/index.ts b/data/helpers/profiles/index.ts deleted file mode 100644 index dcd93e8aa..000000000 --- a/data/helpers/profiles/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import logger from "@utils/logger"; - -import { getRepository } from "../../db"; -import { Profile } from "../../db/entities/profileEntity"; -import { ProfileSocials } from "../../store/profilesStore"; - -export const loadProfilesByAddress = async (account: string) => { - const profileRepository = await getRepository(account, "profile"); - const profiles = await profileRepository.find(); - const profileByAddress: { - [address: string]: { socials: ProfileSocials; updatedAt: number }; - } = {}; - profiles.forEach( - (p) => - (profileByAddress[p.address] = { - socials: getSocials(p), - updatedAt: p.updatedAt, - }) - ); - return profileByAddress; -}; - -const getSocials = (profileEntity: Profile): ProfileSocials => { - try { - const parsed = JSON.parse(profileEntity.socials); - return parsed; - } catch (error) { - logger.error(error, { socials: profileEntity.socials }); - return {}; - } -}; diff --git a/data/helpers/profiles/profilesUpdate.ts b/data/helpers/profiles/profilesUpdate.ts index 44b0d6417..8f2c19d3f 100644 --- a/data/helpers/profiles/profilesUpdate.ts +++ b/data/helpers/profiles/profilesUpdate.ts @@ -1,10 +1,7 @@ -import logger from "@utils/logger"; +import { getCleanAddress } from "@utils/eth"; import { getProfilesForAddresses } from "../../../utils/api"; -import { saveConversationIdentifiersForNotifications } from "../../../utils/notifications"; -import { getPreferredName } from "../../../utils/profile"; -import { getRepository } from "../../db"; -import { upsertRepository } from "../../db/upsert"; +import { getProfile } from "../../../utils/profile"; import { getChatStore, getProfilesStore } from "../../store/accountsStore"; import { XmtpConversation } from "../../store/chatStore"; import { ProfileSocials } from "../../store/profilesStore"; @@ -18,8 +15,6 @@ export const updateProfilesForConvos = async ( account: string, profilesWithGroups: Map ) => { - const profileRepository = await getRepository(account, "profile"); - const updates: ConversationHandlesUpdate[] = []; let batch: string[] = []; let rest = Array.from(profilesWithGroups.keys()); @@ -31,17 +26,6 @@ export const updateProfilesForConvos = async ( Array.from(addressesSet) ); const now = new Date().getTime(); - // Save profiles to db - await upsertRepository( - profileRepository, - Object.keys(profilesByAddress).map((address) => ({ - socials: JSON.stringify(profilesByAddress[address]), - updatedAt: now, - address, - })), - ["address"], - false - ); // Dispatching the profile to state const socialsToDispatch: { [address: string]: { socials: ProfileSocials; updatedAt: number }; @@ -53,40 +37,7 @@ export const updateProfilesForConvos = async ( }; } getProfilesStore(account).getState().setProfiles(socialsToDispatch); - const handleConversation = async (conversation: XmtpConversation) => { - if (conversation.isGroup) return; - const currentTitle = conversation.conversationTitle; - let updated = false; - try { - const profileForConversation = - profilesByAddress[conversation.peerAddress]; - - const newTitle = getPreferredName( - profileForConversation, - conversation.peerAddress, - conversation.context?.conversationId - ); - - if (newTitle !== currentTitle) { - updated = true; - } - conversation.conversationTitle = newTitle; - } catch (e) { - // Error (probably rate limited) - logger.warn("Could not resolve handles:", conversation.peerAddress, e); - } - - updates.push({ conversation, updated }); - saveConversationIdentifiersForNotifications(conversation); - }; - const allGroups: XmtpConversation[] = []; - batch.forEach((address) => { - allGroups.push(...(profilesWithGroups.get(address) || [])); - }); - await Promise.all(allGroups.map(handleConversation)); } - - return updates; }; export const refreshProfileForAddress = async ( @@ -97,17 +48,6 @@ export const refreshProfileForAddress = async ( const profilesByAddress = await getProfilesForAddresses([address]); // Save profiles to db - const profileRepository = await getRepository(account, "profile"); - await upsertRepository( - profileRepository, - Object.keys(profilesByAddress).map((address) => ({ - socials: JSON.stringify(profilesByAddress[address]), - updatedAt: now, - address, - })), - ["address"], - false - ); getProfilesStore(account) .getState() .setProfiles({ @@ -128,7 +68,7 @@ export const refreshProfilesIfNeeded = async (account: string) => { new Map(); conversations.forEach((c) => { if (!c.isGroup) { - const existingProfile = knownProfiles[c.peerAddress]; + const existingProfile = getProfile(c.peerAddress, knownProfiles); const lastProfileUpdate = existingProfile?.updatedAt || 0; const shouldUpdateProfile = now - lastProfileUpdate >= 24 * 3600 * 1000; if (shouldUpdateProfile) { @@ -142,8 +82,9 @@ export const refreshProfilesIfNeeded = async (account: string) => { typeof c.groupMembers === "string" ? (c as any).groupMembers.split(",") : c.groupMembers; - groupMembers.forEach((memberAddress) => { - const existingProfile = knownProfiles[memberAddress]; + groupMembers.forEach((_memberAddress) => { + const memberAddress = getCleanAddress(_memberAddress); + const existingProfile = getProfile(memberAddress, knownProfiles); const lastProfileUpdate = existingProfile?.updatedAt || 0; const shouldUpdateProfile = now - lastProfileUpdate >= 24 * 3600 * 1000; if (shouldUpdateProfile) { @@ -157,17 +98,6 @@ export const refreshProfilesIfNeeded = async (account: string) => { }); if (staleProfilesWithConversations.size > 0) { - updateProfilesForConvos(account, staleProfilesWithConversations).then( - (resolveResult) => { - const updatedConversations = resolveResult - .filter((r) => r.updated) - .map((r) => r.conversation); - if (updatedConversations.length > 0) { - getChatStore(account) - .getState() - .setConversations(updatedConversations); - } - } - ); + updateProfilesForConvos(account, staleProfilesWithConversations); } }; diff --git a/data/index.ts b/data/index.ts index 84646ea62..2d42acecb 100644 --- a/data/index.ts +++ b/data/index.ts @@ -1,9 +1,11 @@ import "reflect-metadata"; +import logger from "@utils/logger"; +import { getProfile } from "@utils/profile"; + import { getRepository } from "./db"; import { Conversation } from "./db/entities/conversationEntity"; import { Message } from "./db/entities/messageEntity"; -import { loadProfilesByAddress } from "./helpers/profiles"; import { xmtpConversationFromDb } from "./mappers"; import { getChatStore, getProfilesStore } from "./store/accountsStore"; import { saveXmtpEnv, saveApiURI } from "../utils/sharedData"; @@ -32,6 +34,10 @@ export const loadDataToContext = async (account: string) => { isActive: getTypeormBoolValue(c.isActive), })); + logger.debug( + `[InitialData] ${account}: Loading ${conversationsWithMessages.length} conversations from local db` + ); + const conversationsMessages: Message[][] = await Promise.all( conversationsWithMessages.map((c) => messageRepository @@ -47,6 +53,15 @@ export const loadDataToContext = async (account: string) => { ) ); + const totalMessagesCount = conversationsMessages.reduce( + (count, conversation) => count + conversation.length, + 0 + ); + + logger.debug( + `[InitialData] ${account}: Loading ${totalMessagesCount} messages from local db` + ); + conversationsWithMessages.forEach((conversation, index) => { // If no limit => ASC then no reverse conversation.messages = conversationsMessages[index] @@ -59,8 +74,7 @@ export const loadDataToContext = async (account: string) => { .reverse(); }); - const profilesByAddress = await loadProfilesByAddress(account); - getProfilesStore(account).getState().setProfiles(profilesByAddress); + const profilesByAddress = getProfilesStore(account).getState().profiles; getChatStore(account) .getState() .setConversations( @@ -68,7 +82,9 @@ export const loadDataToContext = async (account: string) => { xmtpConversationFromDb( account, c, - c.peerAddress ? profilesByAddress[c.peerAddress]?.socials : undefined + c.peerAddress + ? getProfile(c.peerAddress, profilesByAddress)?.socials + : undefined ) ) ); diff --git a/data/mappers.ts b/data/mappers.ts index 6ccb38e27..29f4a6cbf 100644 --- a/data/mappers.ts +++ b/data/mappers.ts @@ -104,11 +104,7 @@ export const xmtpConversationFromDb = ( } const conversationTitle = dbConversation.peerAddress - ? getPreferredName( - socials, - dbConversation.peerAddress, - dbConversation.contextConversationId - ) + ? getPreferredName(socials, dbConversation.peerAddress) : undefined; const hasOneMessageFromMe = !!dbConversation.messages?.find( diff --git a/data/store/chatStore.ts b/data/store/chatStore.ts index 1c55078ed..fbc825c08 100644 --- a/data/store/chatStore.ts +++ b/data/store/chatStore.ts @@ -39,7 +39,6 @@ type XmtpConversationShared = { context?: XmtpConversationContext; messages: Map; messagesIds: string[]; - conversationTitle?: string | null; messageDraft?: string; mediaPreview?: MediaPreview; readUntil: number; // UNUSED diff --git a/data/store/inboxIdStore.ts b/data/store/inboxIdStore.ts index 721eb1a23..dbafeebdc 100644 --- a/data/store/inboxIdStore.ts +++ b/data/store/inboxIdStore.ts @@ -1,3 +1,4 @@ +import { getCleanAddress } from "@utils/eth"; import { Member } from "@xmtp/react-native-sdk"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -72,7 +73,8 @@ export const initInboxIdStore = (account: string) => { const addresses = new Set( newState.byInboxId[member.inboxId] ?? [] ); - for (const address of member.addresses) { + for (const _address of member.addresses) { + const address = getCleanAddress(_address); newState.byAddress[address] = member.inboxId; addresses.add(address); } diff --git a/ios/ConverseNotificationExtension/NotificationService.swift b/ios/ConverseNotificationExtension/NotificationService.swift index 60d9365da..ba8561b19 100644 --- a/ios/ConverseNotificationExtension/NotificationService.swift +++ b/ios/ConverseNotificationExtension/NotificationService.swift @@ -209,8 +209,6 @@ class NotificationService: UNNotificationServiceExtension { sentryTrackMessage(message: "NOTIFICATION_TIMEOUT", extras: ["body": bestAttemptContent?.userInfo["body"]]) if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { if let body = bestAttemptContent.userInfo["body"] as? [String: Any], let contentTopic = body["contentTopic"] as? String { - let conversationTitle = getSavedConversationTitle(contentTopic: contentTopic); - bestAttemptContent.title = conversationTitle; incrementBadge(for: bestAttemptContent) } contentHandler(bestAttemptContent) diff --git a/ios/ConverseNotificationExtension/Profile.swift b/ios/ConverseNotificationExtension/Profile.swift index 82829edce..9ce771428 100644 --- a/ios/ConverseNotificationExtension/Profile.swift +++ b/ios/ConverseNotificationExtension/Profile.swift @@ -7,18 +7,21 @@ import Foundation import Alamofire +import web3 func getProfile(account: String, address: String) async -> Profile? { var profileState = getProfilesStore(account: account)?.state - if let profile = profileState?.profiles?[address] { + let lowercasedAddress = address.lowercased() + let formattedAddress = EthereumAddress(lowercasedAddress).toChecksumAddress() + if let profile = profileState?.profiles?[address] ?? profileState?.profiles?[formattedAddress] ?? profileState?.profiles?[lowercasedAddress] { return profile } // If profile is nil, let's refresh it - try? await refreshProfileFromBackend(account: account, address: address) + try? await refreshProfileFromBackend(account: account, address: formattedAddress) profileState = getProfilesStore(account: account)?.state - if let profile = profileState?.profiles?[address] { + if let profile = profileState?.profiles?[formattedAddress] { return profile } return nil diff --git a/ios/ConverseNotificationExtension/Xmtp/Conversations.swift b/ios/ConverseNotificationExtension/Xmtp/Conversations.swift index 2a6923455..29609186a 100644 --- a/ios/ConverseNotificationExtension/Xmtp/Conversations.swift +++ b/ios/ConverseNotificationExtension/Xmtp/Conversations.swift @@ -72,19 +72,6 @@ func saveConversation(account: String, topic: String, peerAddress: String, creat mmkv?.set(encodedString!, forKey: "saved-notifications-conversations") } -func getSavedConversationTitle(contentTopic: String)-> String { - let mmkv = getMmkv() - let conversationDictString = mmkv?.string(forKey: "conversation-\(contentTopic)") - if let data = conversationDictString?.data(using: .utf8) { - if let conversationDict = try! JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] { - let shortAddress = conversationDict["shortAddress"] - let title = conversationDict["title"] - return "\(title ?? shortAddress ?? "")" - } - } - return ""; -} - func getPersistedConversation(xmtpClient: XMTP.Client, contentTopic: String) async -> XMTP.Conversation? { let secureMmkv = getSecureMmkvForAccount(account: xmtpClient.address) if let mmkv = secureMmkv { diff --git a/ios/ConverseNotificationExtension/Xmtp/Messages.swift b/ios/ConverseNotificationExtension/Xmtp/Messages.swift index ccd6f24b6..7b65736c2 100644 --- a/ios/ConverseNotificationExtension/Xmtp/Messages.swift +++ b/ios/ConverseNotificationExtension/Xmtp/Messages.swift @@ -58,7 +58,13 @@ func handleNewConversationFirstMessage(xmtpClient: XMTP.Client, apiURI: String?, print("[NotificationExtension] Not showing a notification") break } else if let content = decodedMessageResult.content { - bestAttemptContent.title = shortAddress(address: try conversation.peerAddress) + let senderAddress = try conversation.peerAddress + let conversationTitle: String? = nil + if let senderProfile = await getProfile(account: xmtpClient.address, address: senderAddress) { + bestAttemptContent.title = getPreferredName(address: senderAddress, socials: senderProfile.socials) + } else { + bestAttemptContent.title = shortAddress(address: senderAddress) + } bestAttemptContent.body = content shouldShowNotification = true messageId = decodedMessageResult.id // @todo probably remove this? @@ -198,7 +204,7 @@ func handleGroupMessage(xmtpClient: XMTP.Client, envelope: XMTP.Envelope, apiURI func handleOngoingConversationMessage(xmtpClient: XMTP.Client, envelope: XMTP.Envelope, bestAttemptContent: inout UNMutableNotificationContent, body: [String: Any]) async -> (shouldShowNotification: Bool, messageId: String?, messageIntent: INSendMessageIntent?) { var shouldShowNotification = false let contentTopic = envelope.contentTopic - var conversationTitle = getSavedConversationTitle(contentTopic: contentTopic) + var conversationTitle: String? = nil var messageId: String? = nil var messageIntent: INSendMessageIntent? = nil @@ -206,7 +212,7 @@ func handleOngoingConversationMessage(xmtpClient: XMTP.Client, envelope: XMTP.En // If couldn't decode the message, not showing if let message = decodedMessage { let decodedMessageResult = handleMessageByContentType(decodedMessage: message, xmtpClient: xmtpClient); - + if decodedMessageResult.senderAddress == xmtpClient.address || decodedMessageResult.forceIgnore { // Message is from me or a reaction removal, let's drop it print("[NotificationExtension] Not showing a notification") @@ -218,11 +224,14 @@ func handleOngoingConversationMessage(xmtpClient: XMTP.Client, envelope: XMTP.En conversationTitle = getPreferredName(address: senderAddress, socials: senderProfile.socials) senderAvatar = getPreferredAvatar(socials: senderProfile.socials) } - - if conversationTitle.isEmpty, let senderAddress = decodedMessageResult.senderAddress { + + if (conversationTitle == nil), let senderAddress = decodedMessageResult.senderAddress { conversationTitle = shortAddress(address: senderAddress) } - bestAttemptContent.title = conversationTitle + if let convoTitle = conversationTitle { + bestAttemptContent.title = convoTitle + } + shouldShowNotification = true messageId = decodedMessageResult.id messageIntent = getIncoming1v1MessageIntent(topic: envelope.contentTopic, senderId: decodedMessage?.senderAddress ?? "", senderName: bestAttemptContent.title, senderAvatar: senderAvatar, content: bestAttemptContent.body) diff --git a/queries/useGroupMembersQuery.ts b/queries/useGroupMembersQuery.ts index d7fdc5392..285c97340 100644 --- a/queries/useGroupMembersQuery.ts +++ b/queries/useGroupMembersQuery.ts @@ -3,6 +3,7 @@ import { SetDataOptions, useQuery, } from "@tanstack/react-query"; +import { getCleanAddress } from "@utils/eth"; import { Member } from "@xmtp/react-native-sdk"; import { InboxId } from "@xmtp/react-native-sdk/build/lib/Client"; @@ -18,7 +19,7 @@ export const useGroupMembersQuery = ( topic: string, queryOptions?: Partial> ) => { - const { data: group, dataUpdatedAt } = useGroupQuery(account, topic); + const { data: group } = useGroupQuery(account, topic); return useQuery({ queryKey: groupMembersQueryKey(account, topic), queryFn: async () => { @@ -34,7 +35,7 @@ export const useGroupMembersQuery = ( updatedMembers, (member) => member.inboxId, // TODO: Multiple addresses support - (member) => member.addresses[0] + (member) => getCleanAddress(member.addresses[0]) ); }, enabled: !!group, diff --git a/queries/useGroupQuery.ts b/queries/useGroupQuery.ts index c9e95fb9e..318104780 100644 --- a/queries/useGroupQuery.ts +++ b/queries/useGroupQuery.ts @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { getCleanAddress } from "@utils/eth"; import { getGroupIdFromTopic, isGroupTopic } from "@utils/groupUtils/groupId"; import { ConverseXmtpClientType, @@ -53,7 +54,7 @@ export const useGroupQuery = (account: string, topic: string) => { group.members, (member) => member.inboxId, // TODO: Multiple addresses support - (member) => member.addresses[0] + (member) => getCleanAddress(member.addresses[0]) ), { updatedAt: groupDataUpdatedAt, diff --git a/screens/Main.tsx b/screens/Main.tsx index 4177b4de2..a27063578 100644 --- a/screens/Main.tsx +++ b/screens/Main.tsx @@ -1,5 +1,6 @@ import UserProfile from "@components/Onboarding/UserProfile"; import { backgroundColor } from "@styles/colors"; +import { getProfile } from "@utils/profile"; import { useCheckCurrentInstallation } from "@utils/xmtpRN/client"; import { StatusBar } from "expo-status-bar"; import React, { useCallback, useEffect, useRef } from "react"; @@ -39,7 +40,7 @@ export default function Main() { const colorScheme = useColorScheme(); const userAddress = useCurrentAccount(); const socials = useProfilesStore((s) => - userAddress ? s.profiles[userAddress]?.socials : undefined + userAddress ? getProfile(userAddress, s.profiles)?.socials : undefined ); const currentUserName = socials?.userNames?.find((e) => e.isPrimary); // const currentFarcaster = socials?.farcasterUsernames?.find( diff --git a/screens/NewConversation/NewGroupSummary.tsx b/screens/NewConversation/NewGroupSummary.tsx index 78a5b1a15..047a7409d 100644 --- a/screens/NewConversation/NewGroupSummary.tsx +++ b/screens/NewConversation/NewGroupSummary.tsx @@ -35,7 +35,11 @@ import { import { usePhotoSelect } from "../../hooks/usePhotoSelect"; import { uploadFile } from "../../utils/attachment"; import { navigate } from "../../utils/navigation"; -import { getPreferredName, getPreferredAvatar } from "../../utils/profile"; +import { + getPreferredName, + getPreferredAvatar, + getProfile, +} from "../../utils/profile"; import { createGroup } from "../../utils/xmtpRN/conversations"; import { useIsSplitScreen } from "../Navigation/navHelpers"; @@ -53,7 +57,7 @@ const getPendingGroupMembers = ( const memberDetails = members.map((m) => ({ address: m.address, uri: getPreferredAvatar( - useProfilesStore((s) => s.profiles[m.address ?? ""]?.socials) + useProfilesStore((s) => getProfile(m.address, s.profiles)?.socials) ), name: getPreferredName(m, m.address), })); @@ -81,7 +85,7 @@ export default function NewGroupSummary({ }); const account = useCurrentAccount(); const currentAccountSocials = useProfilesStore( - (s) => s.profiles[account ?? ""]?.socials + (s) => getProfile(account, s.profiles)?.socials ); const { photo: groupPhoto, addPhoto: addGroupPhoto } = usePhotoSelect(); const [ diff --git a/screens/Profile.tsx b/screens/Profile.tsx index 03a22614c..0316dc44e 100644 --- a/screens/Profile.tsx +++ b/screens/Profile.tsx @@ -67,7 +67,11 @@ import { NotificationPermissionStatus, requestPushNotificationsPermissions, } from "../utils/notifications"; -import { getPreferredAvatar, getPreferredName } from "../utils/profile"; +import { + getPreferredAvatar, + getPreferredName, + getProfile, +} from "../utils/profile"; import { getIPFSAssetURI } from "../utils/thirdweb"; import { refreshBalanceForAccount } from "../utils/wallet"; import { consentToPeersOnProtocol } from "../utils/xmtpRN/conversations"; @@ -92,7 +96,7 @@ export default function ProfileScreen({ (s) => s.peersStatus[peerAddress.toLowerCase()] === "blocked" ); const setPeersStatus = useSettingsStore((s) => s.setPeersStatus); - const socials = profiles[peerAddress]?.socials; + const socials = getProfile(peerAddress, profiles)?.socials; const groupTopic = route.params.fromGroupTopic; const { members: groupMembers, diff --git a/screens/ShareProfile.tsx b/screens/ShareProfile.tsx index 86a8cb063..5924c978f 100644 --- a/screens/ShareProfile.tsx +++ b/screens/ShareProfile.tsx @@ -35,6 +35,7 @@ import { getPreferredUsername, getPreferredAvatar, getPreferredName, + getProfile, } from "../utils/profile"; import { shortAddress } from "../utils/str"; @@ -158,7 +159,9 @@ export default function ShareProfileScreen({ navigation, }: NativeStackScreenProps) { const userAddress = useCurrentAccount() as string; - const socials = useProfilesStore((s) => s.profiles[userAddress]?.socials); + const socials = useProfilesStore( + (s) => getProfile(userAddress, s.profiles)?.socials + ); const username = getPreferredUsername(socials); const displayName = getPreferredName(socials, userAddress); const avatar = getPreferredAvatar(socials); diff --git a/scripts/migrations/converse-sample.sqlite b/scripts/migrations/converse-sample.sqlite index cb9fa20f7..f4c81b10b 100644 Binary files a/scripts/migrations/converse-sample.sqlite and b/scripts/migrations/converse-sample.sqlite differ diff --git a/scripts/migrations/datasource.ts b/scripts/migrations/datasource.ts index 178a44a55..7862abc3a 100644 --- a/scripts/migrations/datasource.ts +++ b/scripts/migrations/datasource.ts @@ -5,7 +5,6 @@ import { DataSource } from "typeorm"; import { Conversation } from "./entities/conversationEntity"; import { Message } from "./entities/messageEntity"; -import { Profile } from "./entities/profileEntity"; import { init1671623489366 } from "../../data/db/migrations/1671623489366-init"; import { addLensHandle1671788934503 } from "../../data/db/migrations/1671788934503-addLensHandle"; import { addEnsName1673277126468 } from "../../data/db/migrations/1673277126468-addEnsName"; @@ -33,10 +32,11 @@ import { AddIndexToSent1712656017130 } from "../../data/db/migrations/1712656017 import { RemoveSentViaConverse1717625558678 } from "../../data/db/migrations/1717625558678-RemoveSentViaConverse"; import { AddSuperAdmin1717631723249 } from "../../data/db/migrations/1717631723249-AddSuperAdmin"; import { AddIsActive1721143963530 } from "../../data/db/migrations/1721143963530-addIsActive"; +import { RemoveProfile1726828413530 } from "../../data/db/migrations/1726828413530-removeProfileDb"; const dataSource = new DataSource({ database: path.join(__dirname, "converse-sample.sqlite"), - entities: [Conversation, Message, Profile], + entities: [Conversation, Message], synchronize: false, migrationsRun: false, migrations: [ @@ -67,6 +67,7 @@ const dataSource = new DataSource({ RemoveSentViaConverse1717625558678, AddSuperAdmin1717631723249, AddIsActive1721143963530, + RemoveProfile1726828413530, ], type: "sqlite", }); diff --git a/scripts/migrations/db.ts b/scripts/migrations/db.ts index 62c2b1653..09eb4d97a 100644 --- a/scripts/migrations/db.ts +++ b/scripts/migrations/db.ts @@ -10,7 +10,6 @@ import { argv } from "process"; import dataSource from "./datasource"; import { Conversation } from "./entities/conversationEntity"; import { Message } from "./entities/messageEntity"; -import { Profile } from "./entities/profileEntity"; const ethAddress = (): string => { const wallet = ethers.Wallet.createRandom(); @@ -112,24 +111,6 @@ const commands = { groupAdmins: [myAddress], groupSuperAdmins: [myAddress], }); - await dataSource.getRepository(Profile).insert({ - address: peerAddress, - socials: JSON.stringify({ - ensNames: [ - { - name: ensName(), - isPrimary: true, - }, - ], - lensHandles: [ - { - handle: lensHandle(), - isDefault: false, - }, - ], - farcasterUsernames: [username()], - }), - }); for (let messageIndex = 0; messageIndex < 10; messageIndex++) { await dataSource.getRepository(Message).insert([ diff --git a/scripts/migrations/entities/conversationEntity.ts b/scripts/migrations/entities/conversationEntity.ts index 728feee47..a1ce64913 100644 --- a/scripts/migrations/entities/conversationEntity.ts +++ b/scripts/migrations/entities/conversationEntity.ts @@ -2,6 +2,10 @@ import { Column, Entity, Index, OneToMany, PrimaryColumn } from "typeorm"; import { type Message } from "./messageEntity"; +// Caution, when adding booleans here, they're not mapped correctly when +// using createQueryBuilder directly (as they're actually integers in Sqlite) +// see https://github.com/Unshut-Labs/converse-app/commit/5e498c99c3c1928f0256d3461299e9e5a0386b12 + @Entity() export class Conversation { @PrimaryColumn("text") diff --git a/scripts/migrations/entities/profileEntity.ts b/scripts/migrations/entities/profileEntity.ts deleted file mode 100644 index bc77c3cfc..000000000 --- a/scripts/migrations/entities/profileEntity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Entity, Column, PrimaryColumn } from "typeorm"; - -@Entity() -export class Profile { - - @PrimaryColumn("text") - address!: string; - - @Column("text", { default: "{}" }) - socials!: string; - - @Column("int", { default: 0 }) - updatedAt!: number; -} diff --git a/utils/eth.ts b/utils/eth.ts index 9c375a635..504ae7927 100644 --- a/utils/eth.ts +++ b/utils/eth.ts @@ -76,5 +76,10 @@ export const getPrivateKeyFromMnemonic = (mnemonic: string): Promise => }); }); -export const getCleanAddress = (address: string) => - getAddress(address.toLowerCase()); +export const getCleanAddress = (address: string) => { + const lowercased = address.toLowerCase(); + if (isAddress(lowercased)) { + return getAddress(lowercased); + } + return address; +}; diff --git a/utils/groupUtils/adminUtils.ts b/utils/groupUtils/adminUtils.ts index f23c94570..a8224cbc1 100644 --- a/utils/groupUtils/adminUtils.ts +++ b/utils/groupUtils/adminUtils.ts @@ -1,3 +1,4 @@ +import { getCleanAddress } from "@utils/eth"; import { Member } from "@xmtp/react-native-sdk"; import { InboxId } from "@xmtp/react-native-sdk/build/lib/Client"; @@ -24,7 +25,10 @@ export const getAddressIsAdmin = ( members: EntityObjectWithAddress | undefined, address: string ) => { - const currentId = members?.byAddress[address.toLowerCase()]; + const currentId = + members?.byAddress[address] || + members?.byAddress[getCleanAddress(address)] || + members?.byAddress[address.toLowerCase()]; if (!currentId) { return false; } @@ -38,7 +42,10 @@ export const getAddressIsSuperAdmin = ( members: EntityObjectWithAddress | undefined, address: string ) => { - const currentId = members?.byAddress[address.toLowerCase()]; + const currentId = + members?.byAddress[address] || + members?.byAddress[getCleanAddress(address)] || + members?.byAddress[address.toLowerCase()]; if (!currentId) { return false; } diff --git a/utils/groupUtils/sortGroupMembersByAdminStatus.ts b/utils/groupUtils/sortGroupMembersByAdminStatus.ts index b60b633d3..d40de22a1 100644 --- a/utils/groupUtils/sortGroupMembersByAdminStatus.ts +++ b/utils/groupUtils/sortGroupMembersByAdminStatus.ts @@ -1,3 +1,4 @@ +import { getCleanAddress } from "@utils/eth"; import { Member } from "@xmtp/react-native-sdk"; import { InboxId } from "@xmtp/react-native-sdk/build/lib/Client"; @@ -49,6 +50,6 @@ export const sortGroupMembersByAdminStatus = ( return groupMembers.map((inboxId) => ({ inboxId, // TODO: Multiple address support - address: members.byId[inboxId].addresses[0], + address: getCleanAddress(members.byId[inboxId].addresses[0]), })); }; diff --git a/utils/notifications.ts b/utils/notifications.ts index ae6a0d2a1..d930226ad 100644 --- a/utils/notifications.ts +++ b/utils/notifications.ts @@ -24,9 +24,7 @@ import { emptySavedNotificationsMessages, loadSavedNotificationsConversations, loadSavedNotificationsMessages, - saveConversationDict, } from "./sharedData"; -import { conversationName, shortAddress } from "./str"; import { ConverseXmtpClientType } from "./xmtpRN/client"; import { loadConversationsHmacKeys } from "./xmtpRN/conversations"; import { getXmtpClient } from "./xmtpRN/sync"; @@ -44,7 +42,7 @@ import { useAccountsStore, } from "../data/store/accountsStore"; import { useAppStore } from "../data/store/appStore"; -import { XmtpConversation, XmtpMessage } from "../data/store/chatStore"; +import { XmtpMessage } from "../data/store/chatStore"; let nativePushToken: string | null; @@ -465,21 +463,6 @@ export const loadSavedNotificationMessagesToContext = async () => { } }; -export const saveConversationIdentifiersForNotifications = ( - conversation: XmtpConversation -) => { - const conversationDict: any = { - peerAddress: conversation.peerAddress, - shortAddress: conversation.peerAddress - ? shortAddress(conversation.peerAddress) - : undefined, - title: conversationName(conversation), - }; - - // Also save to shared preferences to be able to show notification - saveConversationDict(conversation.topic, conversationDict); -}; - export const onInteractWithNotification = ( event: Notifications.NotificationResponse ) => { diff --git a/utils/notifications.web.ts b/utils/notifications.web.ts index 7d59f3a31..3161242c4 100644 --- a/utils/notifications.web.ts +++ b/utils/notifications.web.ts @@ -1,7 +1,5 @@ import * as Notifications from "expo-notifications"; -import { XmtpConversation } from "../data/store/chatStore"; - export type NotificationPermissionStatus = | "granted" | "undetermined" @@ -25,10 +23,6 @@ export const requestPushNotificationsPermissions = async (): Promise< export const loadSavedNotificationMessagesToContext = async () => {}; -export const saveConversationIdentifiersForNotifications = ( - conversation: XmtpConversation -) => {}; - export const onInteractWithNotification = ( event: Notifications.NotificationResponse ) => {}; diff --git a/utils/profile.ts b/utils/profile.ts index c504b6c52..a7458da44 100644 --- a/utils/profile.ts +++ b/utils/profile.ts @@ -1,6 +1,6 @@ -import { getLensHandleFromConversationIdAndPeer } from "./lens"; +import { getCleanAddress } from "./eth"; import { shortAddress } from "./str"; -import { ProfileSocials } from "../data/store/profilesStore"; +import { ProfileByAddress, ProfileSocials } from "../data/store/profilesStore"; import { RecommendationData } from "../data/store/recommendationsStore"; export const getProfileData = ( @@ -20,25 +20,14 @@ export const getProfileData = ( export function getPreferredName( socials: ProfileSocials | undefined, - peerAddress: string, - conversationId?: string | null + peerAddress: string ): string { - const lensHandle = - conversationId && socials?.lensHandles - ? getLensHandleFromConversationIdAndPeer( - conversationId, - socials.lensHandles - ) || null - : null; - const userName = socials?.userNames?.find((e) => e.isPrimary) || null; const ensName = socials?.ensNames?.find((e) => e.isPrimary) || null; const unsDomain = socials?.unstoppableDomains?.find((d) => d.isPrimary) || null; - if (lensHandle) { - return lensHandle; - } else if (userName) { + if (userName) { return userName.displayName || userName.name; } else if (ensName) { return ensName.displayName || ensName.name; @@ -119,3 +108,16 @@ export function getPrimaryNames(socials: ProfileSocials | undefined): string[] { return primaryNames; } + +export const getProfile = ( + address: string | undefined, + profilesByAddress: ProfileByAddress | undefined +) => { + // We might have stored values in lowercase or formatted, let's check both + if (!profilesByAddress || !address) return undefined; + return ( + profilesByAddress[address] || + profilesByAddress[getCleanAddress(address)] || + profilesByAddress[address.toLowerCase()] + ); +}; diff --git a/utils/sharedData.tsx b/utils/sharedData.tsx index 5203a8cbe..2f2bba2cc 100644 --- a/utils/sharedData.tsx +++ b/utils/sharedData.tsx @@ -2,9 +2,6 @@ import logger from "./logger"; import mmkv from "./mmkv"; import config from "../config"; -export const saveConversationDict = (topic: string, conversationDict: any) => - mmkv.set(`conversation-${topic}`, JSON.stringify(conversationDict)); - export const saveXmtpEnv = () => mmkv.set("xmtp-env", config.xmtpEnv); export const loadSavedNotificationsMessages = () => { diff --git a/utils/str.test.ts b/utils/str.test.ts index 95debe7eb..aed2a27ef 100644 --- a/utils/str.test.ts +++ b/utils/str.test.ts @@ -1,5 +1,6 @@ import { PixelRatio } from "react-native"; +import * as profileModule from "./profile"; import { addressPrefix, capitalize, @@ -28,8 +29,21 @@ jest.mock("react-native", () => ({ })); jest.mock("../data/store/accountsStore", () => ({ - getProfilesStore: jest.fn(), + getProfilesStore: jest + .fn() + .mockReturnValue({ getState: jest.fn().mockReturnValue({ profiles: {} }) }), useAccountsList: jest.fn().mockReturnValue(["account1", "account2"]), + currentAccount: jest.fn().mockReturnValue("currentAccount"), +})); + +jest.mock("expo-crypto", () => ({ + getRandomBytesAsync: jest.fn().mockReturnValue([0, 1, 2, 3, 4]), +})); + +jest.mock("expo-secure-store", () => ({ + setItemAsync: jest.fn(), + deleteItemAsync: jest.fn(), + getItemAsync: jest.fn().mockReturnValue(""), })); jest.mock("../data/store/chatStore", () => ({ @@ -40,9 +54,9 @@ jest.mock("../data/store/profilesStore", () => ({ ProfilesStoreType: jest.fn(), })); -jest.mock("./profile", () => ({ - getPreferredName: jest.fn((socials, address) => address), -})); +jest + .spyOn(profileModule, "getPreferredName") + .mockImplementation((socials, address) => address); describe("shortAddress", () => { it("should shorten the address correctly", () => { @@ -120,17 +134,6 @@ describe("conversationName", () => { } as unknown as XmtpConversation; expect(conversationName(conversation)).toBe("0x1234...cdef"); }); - - it("should return the conversation title if provided", () => { - const conversation = { - isGroup: false, - groupName: "", - topic: "", - peerAddress: "0x1234567890abcdef", - conversationTitle: "Custom Title", - } as unknown as XmtpConversation; - expect(conversationName(conversation)).toBe("Custom Title"); - }); }); describe("getTitleFontScale", () => { diff --git a/utils/str.ts b/utils/str.ts index 519ea739a..c2e397f01 100644 --- a/utils/str.ts +++ b/utils/str.ts @@ -2,8 +2,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Dimensions, PixelRatio, Platform, TextInput } from "react-native"; import logger from "./logger"; -import { getPreferredName } from "./profile"; -import { getProfilesStore, useAccountsList } from "../data/store/accountsStore"; +import { getPreferredName, getProfile } from "./profile"; +import { + currentAccount, + getProfilesStore, + useAccountsList, +} from "../data/store/accountsStore"; import { XmtpConversation } from "../data/store/chatStore"; import { ProfileSocials, ProfilesStoreType } from "../data/store/profilesStore"; @@ -47,7 +51,7 @@ export const addressPrefix = (address: string) => export const conversationName = ( conversation: XmtpConversation, - socials?: ProfileSocials + _socials?: ProfileSocials ) => { if (conversation.isGroup) { return ( @@ -55,22 +59,20 @@ export const conversationName = ( capitalize(humanize(conversation.topic.slice(14, 46), 3, " ")) ); } - if (conversation.conversationTitle) { - return conversation.conversationTitle; - } - const short = shortAddress(conversation.peerAddress); + const socials = + _socials || + getProfile( + conversation.peerAddress, + getProfilesStore(currentAccount()).getState().profiles + )?.socials; if (socials) { const preferredName = getPreferredName(socials, conversation.peerAddress); - if (preferredName && preferredName !== short) { - logger.error( - `1:1 conversation with ${conversation.peerAddress} has empty conversationTitle but it should not` - ); - return preferredName; - } + return preferredName; } + const short = shortAddress(conversation.peerAddress); return short; }; @@ -89,8 +91,10 @@ export const getTitleFontScale = (): number => { export type TextInputWithValue = TextInput & { currentValue: string }; export const getReadableProfile = (account: string, address: string) => { - const socials = - getProfilesStore(account).getState().profiles[address]?.socials; + const socials = getProfile( + address, + getProfilesStore(account).getState().profiles + )?.socials; return getPreferredName(socials, address); }; @@ -102,7 +106,7 @@ export const useAccountsProfiles = () => { const handleAccount = useCallback( (account: string, state: ProfilesStoreType) => { - const socials = state.profiles[account]?.socials; + const socials = getProfile(account, state.profiles)?.socials; const readableProfile = getPreferredName(socials, account); if (accountsProfiles[account] !== readableProfile) { diff --git a/utils/xmtpRN/conversations.ts b/utils/xmtpRN/conversations.ts index fa24d0446..c08b47f0c 100644 --- a/utils/xmtpRN/conversations.ts +++ b/utils/xmtpRN/conversations.ts @@ -58,7 +58,6 @@ const protocolConversationToStateConversation = ( : undefined, messages: new Map(), messagesIds: [], - conversationTitle: undefined, messageDraft: undefined, mediaPreview: undefined, readUntil: 0, @@ -83,7 +82,7 @@ const protocolGroupToStateConversation = ( group.members, (member) => member.inboxId, // TODO: Multiple addresses support - (member) => member.addresses[0] + (member) => getCleanAddress(member.addresses[0]) ) ); const groupMembersAddresses: string[] = []; @@ -92,14 +91,15 @@ const protocolGroupToStateConversation = ( let groupAddedBy: string | undefined; group.members.forEach((m) => { - if (m.addresses[0]) { - groupMembersAddresses.push(m.addresses[0]); - } - if (m.inboxId === group.creatorInboxId) { - groupCreator = m.addresses[0]; - } - if (m.inboxId === groupAddedByInboxId) { - groupAddedBy = m.addresses[0]; + const firstAddress = getCleanAddress(m.addresses[0]); + if (firstAddress) { + groupMembersAddresses.push(firstAddress); + if (m.inboxId === group.creatorInboxId) { + groupCreator = firstAddress; + } + if (m.inboxId === groupAddedByInboxId) { + groupAddedBy = firstAddress; + } } }); return { @@ -108,7 +108,6 @@ const protocolGroupToStateConversation = ( createdAt: group.createdAt, messages: new Map(), messagesIds: [], - conversationTitle: undefined, messageDraft: undefined, mediaPreview: undefined, readUntil: 0, @@ -203,7 +202,7 @@ const handleNewConversation = async ( const shouldSkip = isGroup && !(conversation as GroupWithCodecsType).members.some( - (m) => m.addresses[0] === client.address + (m) => m.addresses[0].toLowerCase() === client.address.toLowerCase() ); if (shouldSkip) { logger.warn( @@ -393,7 +392,9 @@ export const loadConversations = async ( updatedGroups.push(g); } else { const currentMembersSet = new Set(existingGroup.groupMembers); - const newMembersSet = new Set(g.members.map((m) => m.addresses[0])); + const newMembersSet = new Set( + g.members.map((m) => getCleanAddress(m.addresses[0])) + ); if ( existingGroup.groupName !== g.name || diff --git a/utils/xmtpRN/conversations.web.ts b/utils/xmtpRN/conversations.web.ts index 9cf4bc40c..0395fa641 100644 --- a/utils/xmtpRN/conversations.web.ts +++ b/utils/xmtpRN/conversations.web.ts @@ -19,7 +19,6 @@ const protocolConversationToStateConversation = ( context: conversation.context || undefined, messages: new Map(), messagesIds: [], - conversationTitle: undefined, messageDraft: undefined, mediaPreview: undefined, readUntil: 0, diff --git a/utils/xmtpRN/messages.ts b/utils/xmtpRN/messages.ts index d188a05df..cdacbe363 100644 --- a/utils/xmtpRN/messages.ts +++ b/utils/xmtpRN/messages.ts @@ -1,5 +1,6 @@ import { entifyWithAddress } from "@queries/entify"; import { setGroupMembersQueryData } from "@queries/useGroupMembersQuery"; +import { getCleanAddress } from "@utils/eth"; import logger from "@utils/logger"; import { TransactionReference } from "@xmtp/content-type-transaction-reference"; import { @@ -129,7 +130,7 @@ const protocolMessageToStateMessage = ( (m) => m.inboxId === message.senderAddress ); if (groupMember) { - senderAddress = groupMember.addresses[0]; + senderAddress = getCleanAddress(groupMember.addresses[0]); } } @@ -230,7 +231,7 @@ export const syncGroupsMessages = async ( group.members, (member) => member.inboxId, // TODO: Multiple addresses support - (member) => member.addresses[0] + (member) => getCleanAddress(member.addresses[0]) ) ); groupMembers[group.topic] = group.members;