diff --git a/.gitignore b/.gitignore index aadb65919..206a57023 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,8 @@ builds # iOS ios/Converse.xcworkspace/xcshareddata/swiftpm/Package.resolved - /.idea +.aider* # Reassure output directory .reassure diff --git a/data/db/index.ts b/data/db/index.ts index 2876f8dfc..f533ae2cf 100644 --- a/data/db/index.ts +++ b/data/db/index.ts @@ -85,7 +85,7 @@ export const initDb = async (account: string): Promise => { await waitUntilAppActive(1500); const migrationsResult = await dataSource.runMigrations(); logger.debug(`Migrations done for ${account}`); - console.log(migrationsResult); + logger.debug(migrationsResult); repositories[account] = { conversation: dataSource.getRepository(Conversation), message: dataSource.getRepository(Message), diff --git a/data/helpers/messages/handleGroupUpdatedMessage.test.ts b/data/helpers/messages/handleGroupUpdatedMessage.test.ts index 1759c8483..7c9582ff0 100644 --- a/data/helpers/messages/handleGroupUpdatedMessage.test.ts +++ b/data/helpers/messages/handleGroupUpdatedMessage.test.ts @@ -1,9 +1,9 @@ import { invalidateGroupMembersQuery } from "@queries/useGroupMembersQuery"; import { invalidateGroupNameQuery } from "@queries/useGroupNameQuery"; import { invalidateGroupPhotoQuery } from "@queries/useGroupPhotoQuery"; +import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client.types"; import { handleGroupUpdatedMessage } from "./handleGroupUpdatedMessage"; -import { DecodedMessageWithCodecsType } from "../../../utils/xmtpRN/client"; jest.mock("@queries/useGroupMembersQuery", () => ({ invalidateGroupMembersQuery: jest.fn(), diff --git a/data/helpers/messages/handleGroupUpdatedMessage.ts b/data/helpers/messages/handleGroupUpdatedMessage.ts index f383fa8e3..6a5214607 100644 --- a/data/helpers/messages/handleGroupUpdatedMessage.ts +++ b/data/helpers/messages/handleGroupUpdatedMessage.ts @@ -3,7 +3,7 @@ import { invalidateGroupIsActiveQuery } from "@queries/useGroupIsActive"; import { invalidateGroupMembersQuery } from "@queries/useGroupMembersQuery"; import { invalidateGroupNameQuery } from "@queries/useGroupNameQuery"; import { invalidateGroupPhotoQuery } from "@queries/useGroupPhotoQuery"; -import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client"; +import { DecodedMessageWithCodecsType } from "@utils/xmtpRN/client.types"; import { refreshGroup } from "@utils/xmtpRN/conversations"; import { GroupUpdatedContent } from "@xmtp/react-native-sdk"; diff --git a/utils/events.ts b/utils/events.ts index cbd938f86..08e8e75e5 100644 --- a/utils/events.ts +++ b/utils/events.ts @@ -1,10 +1,9 @@ import { MessageToDisplay } from "@components/Chat/Message/Message"; import { MediaPreview } from "@data/store/chatStore"; +import { GroupWithCodecsType } from "@utils/xmtpRN/client.types"; import EventEmitter from "eventemitter3"; import { Account, Wallet } from "thirdweb/wallets"; -import { GroupWithCodecsType } from "./xmtpRN/client"; - type ShowActionSheetEvent = `showActionSheetForTxRef-${T}`; type OpenAttachmentMessage = `openAttachmentForMessage-${T}`; type AttachmentMessageProcessed = diff --git a/utils/evm/xmtp.ts b/utils/evm/xmtp.ts index 7100fc0d4..db25ec76e 100644 --- a/utils/evm/xmtp.ts +++ b/utils/evm/xmtp.ts @@ -1,6 +1,6 @@ import { useCurrentAccount } from "@data/store/accountsStore"; import { translate } from "@i18n"; -import { ConverseXmtpClientType } from "@utils/xmtpRN/client"; +import { ConverseXmtpClientType } from "@utils/xmtpRN/client.types"; import { getXmtpClient } from "@utils/xmtpRN/sync"; import { useCallback } from "react"; import { Alert } from "react-native"; diff --git a/utils/logout/index.tsx b/utils/logout/index.tsx index 07bf9c707..86ebc1c35 100644 --- a/utils/logout/index.tsx +++ b/utils/logout/index.tsx @@ -1,6 +1,7 @@ import { deleteLibXmtpDatabaseForInboxId } from "@utils/fileSystem"; import logger from "@utils/logger"; -import { ConverseXmtpClientType, dropXmtpClient } from "@utils/xmtpRN/client"; +import { dropXmtpClient } from "@utils/xmtpRN/client"; +import { ConverseXmtpClientType } from "@utils/xmtpRN/client.types"; import { getInboxId } from "@utils/xmtpRN/signIn"; import { useCallback } from "react"; diff --git a/utils/notifications.ts b/utils/notifications.ts index 0995ff399..0833eb244 100644 --- a/utils/notifications.ts +++ b/utils/notifications.ts @@ -1,4 +1,20 @@ +import { + saveConversations, + saveConversationsLastNotificationSubscribePeriod, +} from "@data/helpers/conversations/upsertConversations"; +import { saveMessages } from "@data/helpers/messages"; +import { + currentAccount, + getAccountsList, + getChatStore, + getProfilesStore, + getSettingsStore, + useAccountsStore, +} from "@data/store/accountsStore"; +import { useAppStore } from "@data/store/appStore"; +import { XmtpMessage } from "@data/store/chatStore"; import { createHash } from "@mfellner/react-native-fast-create-hash"; +import { ConverseXmtpClientType } from "@utils/xmtpRN/client.types"; import { keystore } from "@xmtp/proto"; import { buildUserInviteTopic } from "@xmtp/xmtp-js"; import * as Notifications from "expo-notifications"; @@ -29,24 +45,8 @@ import { loadSavedNotificationsConversations, loadSavedNotificationsMessages, } from "./sharedData"; -import { ConverseXmtpClientType } from "./xmtpRN/client"; import { loadConversationsHmacKeys } from "./xmtpRN/conversations"; import { getXmtpClient } from "./xmtpRN/sync"; -import { - saveConversations, - saveConversationsLastNotificationSubscribePeriod, -} from "../data/helpers/conversations/upsertConversations"; -import { saveMessages } from "../data/helpers/messages"; -import { - currentAccount, - getAccountsList, - getChatStore, - getProfilesStore, - getSettingsStore, - useAccountsStore, -} from "../data/store/accountsStore"; -import { useAppStore } from "../data/store/appStore"; -import { XmtpMessage } from "../data/store/chatStore"; let nativePushToken: string | null; diff --git a/utils/result.type.ts b/utils/result.type.ts new file mode 100644 index 000000000..798eb8090 --- /dev/null +++ b/utils/result.type.ts @@ -0,0 +1,203 @@ +/** + * A generic Result class that represents either a success value or an error. + * This class is useful for handling operations that can fail, providing a type-safe + * way to represent and handle both successful and failed outcomes. + * + * @typeparam T The type of the success value. + */ +// TODO: add an idea of asyncResult where +// we have "unasked", error, success to more accurately +// represent that data isnt "null", we just +// haven't tried to get it yet +export class Result { + private constructor( + private readonly value: T | null, + private readonly error: Error | null + ) {} + + /** + * Creates a successful Result containing the given value. + * + * @param value The value to be wrapped in a successful Result. + * @returns A new Result instance representing a success. + * + * @example + * const successResult = Result.success(42); + * console.log(successResult.isSuccess()); // true + * console.log(successResult.getValue()); // 42 + */ + static success(value: U): Result { + return new Result(value, null); + } + + /** + * Creates a failed Result containing the given error. + * + * @param error The error to be wrapped in a failed Result. + * @returns A new Result instance representing a failure. + * + * @example + * const failureResult = Result.failure(new Error("Something went wrong")); + * console.log(failureResult.isFailure()); // true + * console.log(failureResult.getError().message); // "Something went wrong" + */ + static failure(error: Error): Result { + return new Result(null, error); + } + + /** + * Checks if the Result is successful. + * + * @returns true if the Result represents a success, false otherwise. + * + * @example + * const result = Result.success("Hello"); + * console.log(result.isSuccess()); // true + */ + isSuccess(): boolean { + return this.error === null; + } + + /** + * Checks if the Result is a failure. + * + * @returns true if the Result represents a failure, false otherwise. + * + * @example + * const result = Result.failure(new Error("Oops")); + * console.log(result.isFailure()); // true + */ + isFailure(): boolean { + return this.error !== null; + } + + /** + * Gets the success value if the Result is successful. + * + * @returns The success value. + * @throws Error if the Result represents a failure. + * + * @example + * const result = Result.success("Hello, World!"); + * if (result.isSuccess()) { + * console.log(result.getValue()); // "Hello, World!" + * } + */ + getValue(): T { + if (this.isFailure()) { + throw new Error("Cannot get value from a failed Result"); + } + return this.value as T; + } + + /** + * Gets the error if the Result is a failure. + * + * @returns The error. + * @throws Error if the Result represents a success. + * + * @example + * const result = Result.failure(new Error("File not found")); + * if (result.isFailure()) { + * console.log(result.getError().message); // "File not found" + * } + */ + getError(): Error { + if (this.isSuccess()) { + throw new Error("Cannot get error from a successful Result"); + } + return this.error as Error; + } + + /** + * Applies a function to the contained value if the Result is a success. + * If the Result is a failure, it returns a new Result with the same error. + * Use map when your transformation always succeeds and returns a regular value. + * + * @param fn The function to apply to the success value. + * @returns A new Result containing the result of the function application, or the original error. + * + * @example + * // Success case + * const successResult = Result.success(5); + * const doubled = successResult.map(x => x * 2); + * console.log(doubled.getValue()); // 10 + * + * // Failure case + * const failureResult = Result.failure(new Error("Invalid input")); + * const shouldNotDouble = failureResult.map(x => x * 2); + * console.log(shouldNotDouble.isFailure()); // true + * console.log(shouldNotDouble.getError().message); // "Invalid input" + */ + map(fn: (value: T) => U): Result { + if (this.isSuccess()) { + return Result.success(fn(this.getValue())); + } + return Result.failure(this.getError()); + } + + /** + * Applies a function that returns a Result to the contained value if this Result is a success. + * If this Result is a failure, it returns a new Result with the same error. + * Use flatMap when your transformation might fail or when it returns another Result. + * + * The key difference between map and flatMap: + * - map is for simple transformations that always succeed. + * - flatMap is for operations that might fail or that return a Result themselves. + * + * @param fn The function to apply to the success value. + * @returns The Result returned by the function, or this Result if it's a failure. + * + * @example + * function divide(a: number, b: number): Result { + * if (b === 0) return Result.failure(new Error("Division by zero")); + * return Result.success(a / b); + * } + * + * // Success case + * const successResult = Result.success(10) + * .flatMap(x => divide(x, 2)) + * .flatMap(x => divide(x, 2)); + * console.log(successResult.getValue()); // 2.5 + * + * // Failure case: division by zero + * const failureResult = Result.success(10) + * .flatMap(x => divide(x, 2)) + * .flatMap(x => divide(x, 0)); + * console.log(failureResult.isFailure()); // true + * console.log(failureResult.getError().message); // "Division by zero" + * + * // Failure case: starting with a failure + * const initialFailure = Result.failure(new Error("Initial error")) + * .flatMap(x => divide(x, 2)); + * console.log(initialFailure.isFailure()); // true + * console.log(initialFailure.getError().message); // "Initial error" + * + * // Example demonstrating the need for flatMap: + * function getUser(id: number): Result { + * // Imagine this fetches a user from a database + * // It might fail, so it returns a Result + * } + * + * function getUserEmail(user: User): Result { + * // This might fail if the user doesn't have an email + * // So it also returns a Result + * } + * + * // Using flatMap to chain operations that return Results + * const userEmailResult = getUser(123) + * .flatMap(user => getUserEmail(user)); + * + * // If we tried to use map, it wouldn't work correctly: + * const incorrectResult = getUser(123) + * .map(user => getUserEmail(user)); // This would be Result>, not what we want! + * + * // flatMap "flattens" the nested Result, giving us a Result as desired. + */ + flatMap(fn: (value: T) => Result): Result { + if (this.isSuccess()) { + return fn(this.getValue()); + } + return Result.failure(this.getError()); + } +} diff --git a/utils/xmtpRN/attachments.ts b/utils/xmtpRN/attachments.ts index aed3052a0..48394aab0 100644 --- a/utils/xmtpRN/attachments.ts +++ b/utils/xmtpRN/attachments.ts @@ -1,12 +1,12 @@ +import { XmtpMessage } from "@data/store/chatStore"; +import { ConverseXmtpClientType } from "@utils/xmtpRN/client.types"; import { DecryptedLocalAttachment, RemoteAttachmentContent, } from "@xmtp/react-native-sdk"; import RNFS from "react-native-fs"; -import { ConverseXmtpClientType } from "./client"; import { getXmtpClient } from "./sync"; -import { XmtpMessage } from "../../data/store/chatStore"; export const encryptRemoteAttachment = async ( account: string, diff --git a/utils/xmtpRN/client.ts b/utils/xmtpRN/client.ts index e429e28cf..f60c4785e 100644 --- a/utils/xmtpRN/client.ts +++ b/utils/xmtpRN/client.ts @@ -4,6 +4,7 @@ import { awaitableAlert } from "@utils/alert"; import { getDbEncryptionKey } from "@utils/keychain/helpers"; import logger from "@utils/logger"; import { useLogoutFromConverse } from "@utils/logout"; +import { XmtpClientByAccount } from "@utils/xmtpRN/client.types"; import { TransactionReferenceCodec } from "@xmtp/content-type-transaction-reference"; import { Client, @@ -48,30 +49,12 @@ export const getXmtpClientFromBase64Key = async (base64Key: string) => { }); }; -export type ConverseXmtpClientType = Awaited< - ReturnType ->; - -export type ConversationWithCodecsType = Awaited< - ReturnType ->; - -export type GroupWithCodecsType = Awaited< - ReturnType ->; - -export type DecodedMessageWithCodecsType = Awaited< - ReturnType ->[number]; - export const isOnXmtp = async (address: string) => Client.canMessage(getCleanAddress(address), { env, }); -export const xmtpClientByAccount: { - [account: string]: ConverseXmtpClientType; -} = {}; +export const xmtpClientByAccount: XmtpClientByAccount = {}; // On iOS, it's important to stop writing to SQLite database // when the app is going from BACKGROUNDED to SUSPENDED diff --git a/utils/xmtpRN/conversations.ts b/utils/xmtpRN/conversations.ts index 2c731c0f2..7f53cf23f 100644 --- a/utils/xmtpRN/conversations.ts +++ b/utils/xmtpRN/conversations.ts @@ -1,11 +1,28 @@ +import { getPendingConversationsToCreate } from "@data/helpers/conversations/pendingConversations"; +import { saveConversations } from "@data/helpers/conversations/upsertConversations"; +import { saveMemberInboxIds } from "@data/helpers/inboxId/saveInboxIds"; +import { getChatStore, getSettingsStore } from "@data/store/accountsStore"; +import { XmtpConversation, XmtpGroupConversation } from "@data/store/chatStore"; +import { SettingsStoreType } from "@data/store/settingsStore"; import { entifyWithAddress } from "@queries/entify"; import { setGroupDescriptionQueryData } from "@queries/useGroupDescriptionQuery"; import { setGroupMembersQueryData } from "@queries/useGroupMembersQuery"; +import { setGroupNameQueryData } from "@queries/useGroupNameQuery"; +import { setGroupPhotoQueryData } from "@queries/useGroupPhotoQuery"; import { setGroupQueryData } from "@queries/useGroupQuery"; +import { + addGroupToGroupsQuery, + fetchGroupsQuery, +} from "@queries/useGroupsQuery"; import { converseEventEmitter } from "@utils/events"; import { getGroupIdFromTopic, isGroupTopic } from "@utils/groupUtils/groupId"; import logger from "@utils/logger"; import { areSetsEqual } from "@utils/set"; +import { + ConversationWithCodecsType, + ConverseXmtpClientType, + GroupWithCodecsType, +} from "@utils/xmtpRN/client.types"; import { ConsentListEntry, ConversationContext, @@ -15,29 +32,9 @@ import { } from "@xmtp/react-native-sdk"; import { PermissionPolicySet } from "@xmtp/react-native-sdk/build/lib/types/PermissionPolicySet"; -import { - ConversationWithCodecsType, - ConverseXmtpClientType, - GroupWithCodecsType, -} from "./client"; import { syncConversationsMessages, syncGroupsMessages } from "./messages"; import { getXmtpClient } from "./sync"; import { Conversation as DbConversation } from "../../data/db/entities/conversationEntity"; -import { getPendingConversationsToCreate } from "../../data/helpers/conversations/pendingConversations"; -import { saveConversations } from "../../data/helpers/conversations/upsertConversations"; -import { saveMemberInboxIds } from "../../data/helpers/inboxId/saveInboxIds"; -import { getChatStore, getSettingsStore } from "../../data/store/accountsStore"; -import { - XmtpConversation, - XmtpGroupConversation, -} from "../../data/store/chatStore"; -import { SettingsStoreType } from "../../data/store/settingsStore"; -import { setGroupNameQueryData } from "../../queries/useGroupNameQuery"; -import { setGroupPhotoQueryData } from "../../queries/useGroupPhotoQuery"; -import { - addGroupToGroupsQuery, - fetchGroupsQuery, -} from "../../queries/useGroupsQuery"; import { ConversationWithLastMessagePreview } from "../conversation"; import { getCleanAddress } from "../evm/address"; import { getTopicDataFromKeychain } from "../keychain/helpers"; @@ -686,18 +683,22 @@ export const createGroup = async ( }; export const refreshGroup = async (account: string, topic: string) => { + logger.debug(`[refreshGroup] Refreshing group ${topic}`); const client = (await getXmtpClient(account)) as ConverseXmtpClientType; await client.conversations.syncGroups(); const group = await client.conversations.findGroup( getGroupIdFromTopic(topic) ); if (!group) throw new Error(`Group ${topic} not found, cannot refresh`); + await group.sync(); + logger.debug(`[refreshGroup] Group ${topic} synced`); saveConversations( client.address, [protocolGroupToStateConversation(account, group)], true ); + logger.debug(`[refreshGroup] Conversations saved`); const updatedMembers = await group.membersList(); saveMemberInboxIds(account, updatedMembers); }; diff --git a/utils/xmtpRN/messages.ts b/utils/xmtpRN/messages.ts index 82429ef12..e7cfac793 100644 --- a/utils/xmtpRN/messages.ts +++ b/utils/xmtpRN/messages.ts @@ -2,6 +2,11 @@ import { entifyWithAddress } from "@queries/entify"; import { setGroupMembersQueryData } from "@queries/useGroupMembersQuery"; import { getCleanAddress } from "@utils/evm/address"; import logger from "@utils/logger"; +import { + ConverseXmtpClientType, + DecodedMessageWithCodecsType, + GroupWithCodecsType, +} from "@utils/xmtpRN/client.types"; import { TransactionReference } from "@xmtp/content-type-transaction-reference"; import { DecodedMessage, @@ -14,11 +19,6 @@ import { } from "@xmtp/react-native-sdk"; import { serializeRemoteAttachmentMessageContent } from "./attachments"; -import { - ConverseXmtpClientType, - DecodedMessageWithCodecsType, - GroupWithCodecsType, -} from "./client"; import { getMessageContentType, isContentType } from "./contentTypes"; import { CoinbaseMessagingPaymentContent } from "./contentTypes/coinbasePayment"; import { getXmtpClient } from "./sync"; diff --git a/utils/xmtpRN/send.ts b/utils/xmtpRN/send.ts index 865e03416..cb21dccad 100644 --- a/utils/xmtpRN/send.ts +++ b/utils/xmtpRN/send.ts @@ -1,4 +1,15 @@ +import { + markMessageAsPrepared, + markMessageAsSent, + updateMessagesIds, +} from "@data/helpers/messages"; +import { getMessagesToSend } from "@data/helpers/messages/getMessagesToSend"; import logger from "@utils/logger"; +import { + ConversationWithCodecsType, + ConverseXmtpClientType, + GroupWithCodecsType, +} from "@utils/xmtpRN/client.types"; import { ContentTypeTransactionReference, TransactionReference, @@ -12,21 +23,10 @@ import { ConversationSendPayload } from "@xmtp/react-native-sdk/build/lib/types" import { DefaultContentTypes } from "@xmtp/react-native-sdk/build/lib/types/DefaultContentType"; import { deserializeRemoteAttachmentMessageContent } from "./attachments"; -import { - ConversationWithCodecsType, - ConverseXmtpClientType, - GroupWithCodecsType, -} from "./client"; import { isContentType } from "./contentTypes"; import { getConversationWithTopic } from "./conversations"; import { getXmtpClient } from "./sync"; import { Message as MessageEntity } from "../../data/db/entities/messageEntity"; -import { - markMessageAsPrepared, - markMessageAsSent, - updateMessagesIds, -} from "../../data/helpers/messages"; -import { getMessagesToSend } from "../../data/helpers/messages/getMessagesToSend"; let sendingPendingMessages = false; const sendingMessages: { [messageId: string]: boolean } = {}; diff --git a/utils/xmtpRN/sync.ts b/utils/xmtpRN/sync.ts index 69939af82..1359898c4 100644 --- a/utils/xmtpRN/sync.ts +++ b/utils/xmtpRN/sync.ts @@ -1,12 +1,12 @@ import logger from "@utils/logger"; import { retryWithBackoff } from "@utils/retryWithBackoff"; +import { ConverseXmtpClientType } from "@utils/xmtpRN/client.types"; import { Client } from "@xmtp/xmtp-js"; import intersect from "fast_array_intersect"; import { AppState } from "react-native"; import { xmtpSignatureByAccount } from "./api"; import { - ConverseXmtpClientType, getXmtpClientFromBase64Key, reconnectXmtpClientsDbConnections, xmtpClientByAccount,