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:
parent
ac726497a4
commit
d451f82f54
27 changed files with 860 additions and 12 deletions
|
@ -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],
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue