Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ml/fix deeplink to groupchat #1395

Merged
merged 11 commits into from
Dec 19, 2024
56 changes: 56 additions & 0 deletions data/store/appStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

import { zustandMMKVStorage } from "../../utils/mmkv";
import logger from "@/utils/logger";
import { wait } from "@/utils/wait";

// A app-wide store to store settings that don't depend on
// an account like if notifications are accepted
Expand Down Expand Up @@ -85,3 +87,57 @@ export const useAppStore = create<AppStoreType>()(
}
)
);

/**
* Utility function to wait for the app store to be hydrated.
* This can be used, for example, to ensure that the xmtp clients have been instantiated and the
* conversation list is fetched before navigating to a conversation.
*
* As of December 19, 2024, when we say XMTP client hydration, we mean that the following are true:
* 1) XMTP client for all accounts added to device have been instantiated and cached
* 2) Conversation list for all accounts added to device have been fetched from the network and cached
* 3) Inbox ID for all accounts added to device have been fetched from the network and cached
*
* You can observe that logic in the HydrationStateHandler, and that will likely be moved once
* we refactor accounts to be InboxID based in upcoming refactors.
*/
export const waitForXmtpClientHydration = (): Promise<void> => {
const hydrationPromise = new Promise<void>((resolve) => {
const { hydrationDone } = useAppStore.getState();
if (hydrationDone) {
logger.debug(
"[waitForXmtpClientHydrationWithTimeout] Already hydrated, resolving"
);
resolve();
return;
}

logger.debug(
"[waitForXmtpClientHydrationWithTimeout] Not hydrated, subscribing"
);
const unsubscribe = useAppStore.subscribe(async (state, prevState) => {
logger.debug(
`[waitForXmtpClientHydrationWithTimeout] Hydration state changed: ${prevState.hydrationDone} -> ${state.hydrationDone}`
);
if (state.hydrationDone && !prevState.hydrationDone) {
logger.debug(
`[waitForXmtpClientHydrationWithTimeout] waiting a split second before resolving to allow next render`
);

// Wait for the next render to complete
// note(lustig): this is a hack to ensure that the next render has completed
// as this is used to navigate to a conversation currently.
// We'll revisit this and make something that doesn't suck as much later.
await wait(1);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


logger.debug(
`[waitForXmtpClientHydrationWithTimeout] resolving promise`
);
resolve();
unsubscribe();
}
});
});

return hydrationPromise;
};
2 changes: 2 additions & 0 deletions dependencies/Environment/Flavors/determineFlavor.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { DependencyFlavor, DependencyFlavors } from "./flavors.type";

export function determineDependencyFlavor(): DependencyFlavor {
// todo(lustig): remove this once we have a better way to determine the flavor
// @ts-ignore
if (typeof jest !== "undefined" || process.env.JEST_WORKER_ID !== undefined) {
return DependencyFlavors.jest;
}
Expand Down
6 changes: 3 additions & 3 deletions features/GroupInvites/joinGroup/JoinGroup.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import { JoinGroupResult } from "./joinGroup.types";
import { ConversationListQueryData } from "@/queries/useConversationListQuery";
import { entify } from "@/queries/entify";
import { GroupWithCodecsType } from "@/utils/xmtpRN/client";
import { wait } from "@/utils/wait";
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change is because I realized we had a utility for this already


const GROUP_JOIN_REQUEST_POLL_MAX_ATTEMPTS = 10;
const GROUP_JOIN_REQUEST_POLL_INTERVAL_MS = 1000;

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* TODOs:
*
Expand Down Expand Up @@ -187,7 +187,7 @@ export class JoinGroupClient {
logger.debug(
`[liveAttemptToJoinGroup] Waiting ${GROUP_JOIN_REQUEST_POLL_INTERVAL_MS}ms before next poll`
);
await sleep(GROUP_JOIN_REQUEST_POLL_INTERVAL_MS);
await wait(GROUP_JOIN_REQUEST_POLL_INTERVAL_MS);
}

logger.warn(
Expand Down Expand Up @@ -522,7 +522,7 @@ export class JoinGroupClient {
account: string,
groupInviteId: string
): Promise<JoinGroupResult> => {
await sleep(5000);
await wait(5000);
return {
type: "group-join-request.timed-out",
} as const;
Expand Down
44 changes: 7 additions & 37 deletions features/notifications/utils/onInteractWithNotification.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import * as Notifications from "expo-notifications";
import {
navigate,
navigateToTopic,
setTopicToNavigateTo,
} from "@utils/navigation";
import { navigate, navigateToTopic } from "@utils/navigation";
import { getTopicFromV3Id } from "@utils/groupUtils/groupId";
import { useAccountsStore } from "@data/store/accountsStore";
import type { ConversationId, ConversationTopic } from "@xmtp/react-native-sdk";
import { fetchPersistedConversationListQuery } from "@/queries/useConversationListQuery";
import { resetNotifications } from "./resetNotifications";
import logger from "@/utils/logger";
import { getXmtpClient } from "@/utils/xmtpRN/sync";
import { ConverseXmtpClientType } from "@/utils/xmtpRN/client";
import { waitForXmtpClientHydration } from "@/data/store/appStore";
import logger from "@utils/logger";

export const onInteractWithNotification = async (
event: Notifications.NotificationResponse
) => {
logger.debug("[onInteractWithNotification]");
// todo(lustig): zod verification of external payloads such as those from
// notifications, deep links, etc
let notificationData = event.notification.request.content.data;
// Android returns the data in the body as a string
if (
Expand Down Expand Up @@ -58,40 +55,13 @@ export const onInteractWithNotification = async (
| undefined;

if (conversationTopic) {
// todo(lustig): zod verification of external payloads such as those from
// notifications, deep links, etc
await waitForXmtpClientHydration();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out all the crap I was doing before in the previous diff uh was done in client hydration so it's not needed here

const account: string =
notificationData["account"] || useAccountsStore.getState().currentAccount;

// Fetch the conversation list to ensure we have the latest conversation list
// before navigating to the conversation
try {
await fetchPersistedConversationListQuery({ account });
} catch (e) {
logger.error(
`[onInteractWithNotification] Error fetching conversation list from network`
);
if (
`${e}`.includes("storage error: Pool needs to reconnect before use")
) {
logger.info(
`[onInteractWithNotification] Reconnecting XMTP client for account ${account}`
);
const client = (await getXmtpClient(account)) as ConverseXmtpClientType;
await client.reconnectLocalDatabase();
logger.info(
`[onInteractWithNotification] XMTP client reconnected for account ${account}`
);
logger.info(
`[onInteractWithNotification] Fetching conversation list from network for account ${account}`
);
await fetchPersistedConversationListQuery({ account });
}
}
useAccountsStore.getState().setCurrentAccount(account, false);

navigateToTopic(conversationTopic as ConversationTopic);
setTopicToNavigateTo(undefined);
resetNotifications();
}
};
14 changes: 7 additions & 7 deletions screens/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { useThemeProvider } from "../theme/useAppTheme";
import { useAddressBookStateHandler } from "../utils/addressBook";
import { useAutoConnectExternalWallet } from "../utils/evm/external";
import { usePrivyAccessToken } from "../utils/evm/privy";
import { converseNavigations } from "../utils/navigation";
import { setConverseNavigatorRef } from "../utils/navigation";
import { ConversationScreenConfig } from "../features/conversation/conversation.nav";
import { GroupScreenConfig } from "./Navigation/GroupNav";
import {
Expand All @@ -39,6 +39,7 @@ import {
getConverseStateFromPath,
} from "./Navigation/navHelpers";
import { JoinGroupScreenConfig } from "@/features/GroupInvites/joinGroup/JoinGroupNavigation";
import logger from "@/utils/logger";

const prefix = Linking.createURL("/");

Expand Down Expand Up @@ -84,9 +85,10 @@ export default function Main() {
theme={navigationTheme}
linking={linking}
ref={(r) => {
if (r) {
converseNavigations["main"] = r;
}
logger.info(
`[Main] Setting navigation ref to ${r ? "not null" : "null"}`
);
setConverseNavigatorRef(r);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only have one navigation ref now that we are no longer supporting different layouts and stuff so I did some refactoring to reflect that

}}
onUnhandledAction={() => {
// Since we're handling multiple navigators,
Expand All @@ -105,9 +107,7 @@ export default function Main() {
const NavigationContent = () => {
const authStatus = useAuthStatus();

const { splashScreenHidden } = useAppStore(
useSelect(["notificationsPermissionStatus", "splashScreenHidden"])
);
const { splashScreenHidden } = useAppStore(useSelect(["splashScreenHidden"]));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused


// Uncomment to test design system components
// return (
Expand Down
44 changes: 32 additions & 12 deletions utils/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,52 @@ import { Linking as RNLinking } from "react-native";

import logger from "./logger";
import config from "../config";
import { currentAccount, getChatStore } from "../data/store/accountsStore";
import { NavigationParamList } from "../screens/Navigation/Navigation";
import type { ConversationWithCodecsType } from "./xmtpRN/client";
import type { ConversationTopic } from "@xmtp/react-native-sdk";
import { NavigationContainerRef } from "@react-navigation/native";

export const converseNavigations: { [navigationName: string]: any } = {};
let converseNavigatorRef: NavigationContainerRef<NavigationParamList> | null =
null;

export const setConverseNavigatorRef = (
ref: NavigationContainerRef<NavigationParamList> | null
) => {
if (converseNavigatorRef) {
logger.error("[Navigation] Conversation navigator ref already set");
return;
}
if (!ref) {
logger.error("[Navigation] Conversation navigator ref is null");
return;
}
converseNavigatorRef = ref;
};

export const navigate = async <T extends keyof NavigationParamList>(
screen: T,
params?: NavigationParamList[T]
) => {
if (!converseNavigatorRef) {
logger.error("[Navigation] Conversation navigator not found");
return;
}

if (!converseNavigatorRef.isReady()) {
logger.error(
"[Navigation] Conversation navigator is not ready (wait for appStore#hydrated to be true using waitForAppStoreHydration)"
);
return;
}

logger.debug(
`[Navigation] Navigating to ${screen} ${
params ? JSON.stringify(params) : ""
}`
);
// Navigate to a screen in all navigators
// like Linking.openURL but without redirect on web
Object.values(converseNavigations).forEach((navigation) => {
navigation.navigate(screen, params);
});
};

export let topicToNavigateTo: string | undefined = undefined;
export const setTopicToNavigateTo = (topic: string | undefined) => {
topicToNavigateTo = topic;
// todo(any): figure out proper typing here
// @ts-ignore
converseNavigatorRef.navigate(screen, params);
};

export const navigateToTopic = async (topic: ConversationTopic) => {
Expand Down