diff --git a/app.config.js b/app.config.js index e710420b..5bbe864a 100644 --- a/app.config.js +++ b/app.config.js @@ -89,6 +89,10 @@ module.exports = function (config) { scheme: 'https', host: 'bsky.app', }, + { + scheme: 'http', + host: 'localhost:19006', + }, ], category: ['BROWSABLE', 'DEFAULT'], }, diff --git a/package.json b/package.json index 3d151603..4051849b 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "expo-image": "~1.10.3", "expo-image-manipulator": "^11.8.0", "expo-image-picker": "~14.7.1", + "expo-linking": "^6.2.2", "expo-localization": "~14.8.2", "expo-media-library": "~15.9.1", "expo-notifications": "~0.27.3", diff --git a/src/App.native.tsx b/src/App.native.tsx index 1284154f..f08a6235 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -45,6 +45,7 @@ import {Splash} from '#/Splash' import {Provider as PortalProvider} from '#/components/Portal' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useIntentHandler} from 'lib/hooks/useIntentHandler' SplashScreen.preventAutoHideAsync() @@ -53,6 +54,7 @@ function InnerApp() { const {resumeSession} = useSessionApi() const theme = useColorModeTheme() const {_} = useLingui() + useIntentHandler() // init useEffect(() => { diff --git a/src/App.web.tsx b/src/App.web.tsx index f10bb194..6ac32a01 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -32,11 +32,13 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Provider as PortalProvider} from '#/components/Portal' +import {useIntentHandler} from 'lib/hooks/useIntentHandler' function InnerApp() { const {isInitialLoad, currentAccount} = useSession() const {resumeSession} = useSessionApi() const theme = useColorModeTheme() + useIntentHandler() // init useEffect(() => { diff --git a/src/Navigation.tsx b/src/Navigation.tsx index dfbe816f..0aeeeb6a 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -460,7 +460,8 @@ const FlatNavigator = () => { */ const LINKING = { - prefixes: ['bsky://', 'https://bsky.app'], + // TODO figure out what we are going to use + prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'], getPathFromState(state: State) { // find the current node in the navigation tree @@ -478,6 +479,11 @@ const LINKING = { }, getStateFromPath(path: string) { + // Any time we receive a url that starts with `intent/` we want to ignore it here. It will be handled in the + // intent handler hook. We should check for the trailing slash, because if there isn't one then it isn't a valid + // intent + if (path.includes('intent/')) return + const [name, params] = router.matchPath(path) if (isNative) { if (name === 'Search') { diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts new file mode 100644 index 00000000..249e6898 --- /dev/null +++ b/src/lib/hooks/useIntentHandler.ts @@ -0,0 +1,64 @@ +import React from 'react' +import * as Linking from 'expo-linking' +import {isNative} from 'platform/detection' +import {useComposerControls} from 'state/shell' +import {useSession} from 'state/session' + +type IntentType = 'compose' + +export function useIntentHandler() { + const incomingUrl = Linking.useURL() + const composeIntent = useComposeIntent() + + React.useEffect(() => { + const handleIncomingURL = (url: string) => { + const urlp = new URL(url) + const [_, intentTypeNative, intentTypeWeb] = urlp.pathname.split('/') + + // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the + // intent check. On web, we have to check the first part of the path since we have an actual hostname + const intentType = isNative ? intentTypeNative : intentTypeWeb + const isIntent = isNative + ? urlp.hostname === 'intent' + : intentTypeNative === 'intent' + const params = urlp.searchParams + + if (!isIntent) return + + switch (intentType as IntentType) { + case 'compose': { + composeIntent({ + text: params.get('text'), + imageUris: params.get('imageUris'), + }) + } + } + } + + if (incomingUrl) handleIncomingURL(incomingUrl) + }, [incomingUrl, composeIntent]) +} + +function useComposeIntent() { + const {openComposer} = useComposerControls() + const {hasSession} = useSession() + + return React.useCallback( + ({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + text, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + imageUris, + }: { + text: string | null + imageUris: string | null // unused for right now, will be used later with intents + }) => { + if (!hasSession) return + + setTimeout(() => { + openComposer({}) // will pass in values to the composer here in the share extension + }, 500) + }, + [openComposer, hasSession], + ) +} diff --git a/yarn.lock b/yarn.lock index 3cec585b..a62ff2f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11739,6 +11739,14 @@ expo-keep-awake@~12.8.1: resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-12.8.1.tgz#3c8df9d86c265741b5e7bdd36965aa0c6fc17df0" integrity sha512-P/VZFV02Rzgj13skMwH+ceGOGZSEdaUu5n7pCS3wThh2LppZjPJ7sBxUwyzeLa3DXEVUtwLZi+BiQ91wPwy9Gg== +expo-linking@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.2.2.tgz#b7e148068ae49fd9ad814428c16fdf7a236e8aca" + integrity sha512-FEe6lP4f7xFT/vjoHRG+tt6EPVtkEGaWNK1smpaUevmNdyCJKqW0PDB8o8sfG6y7fly8ULe8qg3HhKh5J7aqUQ== + dependencies: + expo-constants "~15.4.3" + invariant "^2.2.4" + expo-localization@~14.8.2: version "14.8.2" resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-14.8.2.tgz#e0bbed2293265834d21a1c58d3a5f8d265bd04ae"