bsky-app/src/lib/hooks/useIntentHandler.ts
Hailey d451f82f54
Share Extension/Intents (#2587)
* add native ios code outside of ios project

* helper script

* going to be a lot of these commits to squash...backing up

* save

* start of an expo plugin

* create info.plist

* copy the view controller

* maybe working

* working

* wait working now

* working plugin

* use current scheme

* update intent path

* use better params

* support text in uri

* build

* use better encoding

* handle images

* cleanup ios plugin

* android

* move bash script to /scripts

* handle cases where loaded data is uiimage rather than uri

* remove unnecessary logic, allow more than 4 images and just take first 4

* android build plugin

* limit images to four on android

* use js for plugins, no need to build

* revert changes to app config

* use correct scheme on android

* android readme

* move ios extension to /modules

* remove unnecessary event

* revert typo

* plugin readme

* scripts readme

* add configurable scheme to .env, default to `bluesky`

* remove debug

* revert .gitignore change

* add comment about updating .env to app.config.js for those modifying scheme

* modify .env

* update android module to use the proper url

* update ios extension

* remove comment

* parse and validate incoming image uris

* fix types

* rm oops

* fix a few typos
2024-02-27 15:22:03 -08:00

87 lines
2.7 KiB
TypeScript

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'
const VALID_IMAGE_REGEX = /^[\w.:\-_/]+\|\d+(\.\d+)?\|\d+(\.\d+)?$/
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'),
imageUrisStr: params.get('imageUris'),
})
}
}
}
if (incomingUrl) handleIncomingURL(incomingUrl)
}, [incomingUrl, composeIntent])
}
function useComposeIntent() {
const {openComposer} = useComposerControls()
const {hasSession} = useSession()
return React.useCallback(
({
text,
imageUrisStr,
}: {
text: string | null
imageUrisStr: string | null // unused for right now, will be used later with intents
}) => {
if (!hasSession) return
const imageUris = imageUrisStr
?.split(',')
.filter(part => {
// For some security, we're going to filter out any image uri that is external. We don't want someone to
// be able to provide some link like "bluesky://intent/compose?imageUris=https://IHaveYourIpNow.com/image.jpeg
// and we load that image
if (part.includes('https://') || part.includes('http://')) {
return false
}
// We also should just filter out cases that don't have all the info we need
if (!VALID_IMAGE_REGEX.test(part)) {
return false
}
return true
})
.map(part => {
const [uri, width, height] = part.split('|')
return {uri, width: Number(width), height: Number(height)}
})
setTimeout(() => {
openComposer({
text: text ?? undefined,
imageUris: isNative ? imageUris : undefined,
})
}, 500)
},
[openComposer, hasSession],
)
}