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
This commit is contained in:
Hailey 2024-02-27 15:22:03 -08:00 committed by GitHub
parent ac726497a4
commit d451f82f54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 860 additions and 12 deletions

View file

@ -6,6 +6,8 @@ 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()
@ -29,7 +31,7 @@ export function useIntentHandler() {
case 'compose': {
composeIntent({
text: params.get('text'),
imageUris: params.get('imageUris'),
imageUrisStr: params.get('imageUris'),
})
}
}
@ -45,18 +47,39 @@ function useComposeIntent() {
return React.useCallback(
({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
text,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
imageUris,
imageUrisStr,
}: {
text: string | null
imageUris: string | null // unused for right now, will be used later with intents
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({}) // will pass in values to the composer here in the share extension
openComposer({
text: text ?? undefined,
imageUris: isNative ? imageUris : undefined,
})
}, 500)
},
[openComposer, hasSession],

View file

@ -4,11 +4,21 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
import {openPicker} from 'lib/media/picker'
import {getImageDim} from 'lib/media/manip'
interface InitialImageUri {
uri: string
width: number
height: number
}
export class GalleryModel {
images: ImageModel[] = []
constructor() {
constructor(uris?: {uri: string; width: number; height: number}[]) {
makeAutoObservable(this)
if (uris) {
this.addFromUris(uris)
}
}
get isEmpty() {
@ -23,7 +33,7 @@ export class GalleryModel {
return this.images.some(image => image.altText.trim() === '')
}
async add(image_: Omit<RNImage, 'size'>) {
*add(image_: Omit<RNImage, 'size'>) {
if (this.size >= 4) {
return
}
@ -86,4 +96,15 @@ export class GalleryModel {
}),
)
}
async addFromUris(uris: InitialImageUri[]) {
for (const uriObj of uris) {
this.add({
mime: 'image/jpeg',
height: uriObj.height,
width: uriObj.width,
path: uriObj.uri,
})
}
}
}

View file

@ -38,6 +38,8 @@ export interface ComposerOpts {
quote?: ComposerOptsQuote
mention?: string // handle of user to mention
openPicker?: (pos: DOMRect | undefined) => void
text?: string
imageUris?: {uri: string; width: number; height: number}[]
}
type StateContext = ComposerOpts | undefined

View file

@ -71,6 +71,8 @@ export const ComposePost = observer(function ComposePost({
quote: initQuote,
mention: initMention,
openPicker,
text: initText,
imageUris: initImageUris,
}: Props) {
const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@ -91,7 +93,9 @@ export const ComposePost = observer(function ComposePost({
const [error, setError] = useState('')
const [richtext, setRichText] = useState(
new RichText({
text: initMention
text: initText
? initText
: initMention
? insertMentionAt(
`@${initMention}`,
initMention.length + 1,
@ -110,7 +114,10 @@ export const ComposePost = observer(function ComposePost({
const [labels, setLabels] = useState<string[]>([])
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
const gallery = useMemo(() => new GalleryModel(), [])
const gallery = useMemo(
() => new GalleryModel(initImageUris),
[initImageUris],
)
const onClose = useCallback(() => {
closeComposer()
}, [closeComposer])

View file

@ -55,6 +55,8 @@ export const Composer = observer(function ComposerImpl({
onPost={state.onPost}
quote={state.quote}
mention={state.mention}
text={state.text}
imageUris={state.imageUris}
/>
</Animated.View>
)

View file

@ -9,7 +9,7 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {
EmojiPicker,
EmojiPickerState,
} from 'view/com/composer/text-input/web/EmojiPicker.web.tsx'
} from 'view/com/composer/text-input/web/EmojiPicker.web'
const BOTTOM_BAR_HEIGHT = 61
@ -69,6 +69,7 @@ export function Composer({}: {winHeight: number}) {
onPost={state.onPost}
mention={state.mention}
openPicker={onOpenPicker}
text={state.text}
/>
</Animated.View>
<EmojiPicker state={pickerState} close={onClosePicker} />