Skip to content

Commit

Permalink
[native] Handle invite links in chats
Browse files Browse the repository at this point in the history
Summary:
Currently, clicking the link results in opening a popup and then a page in Safari. This seems to be a feature of iOS https://linear.app/comm/issue/ENG-4408#comment-a667ee51.

This diff fixes the issue by checking if a link is an invite link and overriding the default behavior. Instead of showing a popup, we're calling the same function that is called when deep links are clicked. But in order to do that we need to replace our handler with a context so that this function becomes accessible outside it.

https://linear.app/comm/issue/ENG-4408/opening-invite-link-from-ios-app-opens-safari

Test Plan:
Send a message with `https://comm.app/invite/secret` and check if clicking the link results in link modal being shown.
Check if links with other urls, e.g. `https://comm.app` still work as previously.

Reviewers: kamil, inka

Reviewed By: inka

Subscribers: ashoat

Differential Revision: https://phab.comm.dev/D8637
  • Loading branch information
palys-swm committed Jul 28, 2023
1 parent 4ba08e6 commit 123bc62
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,32 @@ import {
verifyInviteLinkActionTypes,
} from 'lib/actions/link-actions.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';
import type { SetState } from 'lib/types/hook-types.js';
import {
useDispatchActionPromise,
useServerCall,
} from 'lib/utils/action-utils.js';

import { InviteLinkModalRouteName } from './route-names.js';
import { InviteLinkModalRouteName } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { useOnFirstLaunchEffect } from '../utils/hooks.js';

function InviteLinkHandler(): null {
type InviteLinksContextType = {
+setCurrentLinkUrl: SetState<?string>,
};

const defaultContext = {
setCurrentLinkUrl: () => {},
};

const InviteLinksContext: React.Context<InviteLinksContextType> =
React.createContext<InviteLinksContextType>(defaultContext);

type Props = {
+children: React.Node,
};
function InviteLinksContextProvider(props: Props): React.Node {
const { children } = props;
const [currentLink, setCurrentLink] = React.useState(null);

React.useEffect(() => {
Expand Down Expand Up @@ -93,7 +109,18 @@ function InviteLinkHandler(): null {
})();
}, [currentLink, dispatchActionPromise, loggedIn, navigation, validateLink]);

return null;
const contextValue = React.useMemo(
() => ({
setCurrentLinkUrl: setCurrentLink,
}),
[],
);

return (
<InviteLinksContext.Provider value={contextValue}>
{children}
</InviteLinksContext.Provider>
);
}

const urlRegex = /invite\/(\S+)$/;
Expand All @@ -108,4 +135,4 @@ function parseInstallReferrer(referrer: string) {
return match?.[1];
}

export default InviteLinkHandler;
export { InviteLinksContext, InviteLinksContextProvider };
23 changes: 20 additions & 3 deletions native/markdown/markdown-link.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import invariant from 'invariant';
import * as React from 'react';
import { Text, Linking, Alert } from 'react-native';

import { inviteLinkUrl } from 'lib/facts/links.js';

import {
MarkdownContext,
type MarkdownContextType,
} from './markdown-context.js';
import { MarkdownSpoilerContext } from './markdown-spoiler-context.js';
import { MessagePressResponderContext } from '../chat/message-press-responder-context.js';
import { TextMessageMarkdownContext } from '../chat/text-message-markdown-context.js';
import { InviteLinksContext } from '../invite-links/invite-links-context-provider.react.js';
import { normalizeURL } from '../utils/url-utils.js';

function useDisplayLinkPrompt(
function useHandleLinkClick(
inputURL: string,
markdownContext: MarkdownContextType,
messageKey: ?string,
Expand All @@ -33,7 +36,13 @@ function useDisplayLinkPrompt(
if (url.length > displayURL.length) {
displayURL += '…';
}

const inviteLinksContext = React.useContext(InviteLinksContext);
return React.useCallback(() => {
if (url.startsWith(inviteLinkUrl(''))) {
inviteLinksContext?.setCurrentLinkUrl(url);
return;
}
messageKey && setLinkModalActive({ [messageKey]: true });
Alert.alert(
'External link',
Expand All @@ -44,7 +53,15 @@ function useDisplayLinkPrompt(
],
{ cancelable: true, onDismiss },
);
}, [setLinkModalActive, messageKey, displayURL, onConfirm, onDismiss]);
}, [
url,
messageKey,
setLinkModalActive,
displayURL,
onDismiss,
onConfirm,
inviteLinksContext,
]);
}

type TextProps = React.ElementConfig<typeof Text>;
Expand Down Expand Up @@ -76,7 +93,7 @@ function MarkdownLink(props: Props): React.Node {

const onPressMessage = messagePressResponderContext?.onPressMessage;

const onPressLink = useDisplayLinkPrompt(target, markdownContext, messageKey);
const onPressLink = useHandleLinkClick(target, markdownContext, messageKey);

return (
<Text
Expand Down
2 changes: 0 additions & 2 deletions native/navigation/navigation-handler.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { cookieSelector } from 'lib/selectors/keyserver-selectors.js';
import { isLoggedIn } from 'lib/selectors/user-selectors.js';

import { logInActionType, logOutActionType } from './action-types.js';
import InviteLinkHandler from './invite-link-handler.react.js';
import ModalPruner from './modal-pruner.react.js';
import NavFromReduxHandler from './nav-from-redux-handler.react.js';
import { useIsAppLoggedIn } from './nav-selectors.js';
Expand Down Expand Up @@ -44,7 +43,6 @@ const NavigationHandler: React.ComponentType<{}> = React.memo<{}>(
<ThreadScreenTracker />
<ModalPruner navContext={navContext} />
<PolicyAcknowledgmentHandler />
<InviteLinkHandler />
{devTools}
</>
);
Expand Down
5 changes: 4 additions & 1 deletion native/root.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import ConnectedStatusBar from './connected-status-bar.react.js';
import { SQLiteDataHandler } from './data/sqlite-data-handler.js';
import ErrorBoundary from './error-boundary.react.js';
import InputStateContainer from './input/input-state-container.react.js';
import { InviteLinksContextProvider } from './invite-links/invite-links-context-provider.react.js';
import LifecycleHandler from './lifecycle/lifecycle-handler.react.js';
import MarkdownContextProvider from './markdown/markdown-context-provider.react.js';
import { filesystemMediaCache } from './media/media-cache.js';
Expand Down Expand Up @@ -249,7 +250,9 @@ function Root() {
theme={theme}
ref={containerRef}
>
<RootNavigator />
<InviteLinksContextProvider>
<RootNavigator />
</InviteLinksContextProvider>
<NavigationHandler />
</NavigationContainer>
);
Expand Down

0 comments on commit 123bc62

Please sign in to comment.