1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Sharing works in simulator

This commit is contained in:
Zhiyuan Zheng
2022-05-02 22:31:22 +02:00
parent 829a25b76b
commit 43c2297387
22 changed files with 1301 additions and 114 deletions

View File

@ -27,6 +27,7 @@ import { addScreenshotListener } from 'expo-screen-capture'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Platform, StatusBar } from 'react-native'
import ShareMenu from 'react-native-share-menu'
import { useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo'
import { useAppDispatch } from './store'
@ -158,6 +159,77 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
}
}, [instanceActive, instances, deeplinked])
// Share Extension
const handleShare = useCallback(
(item?: {
extraData?: { share: { mimeType: string; data: string }[] }
}) => {
if (instanceActive < 0) {
return
}
if (
!item ||
!item.extraData ||
!Array.isArray(item.extraData.share) ||
!item.extraData.share.length
) {
return
}
let text: string | undefined = undefined
let images: { type: string; uri: string }[] = []
let video: { type: string; uri: string } | undefined = undefined
item.extraData.share.forEach((d, i) => {
const typesImage = ['png', 'jpg', 'jpeg', 'gif']
const typesVideo = ['mp4', 'm4v', 'mov', 'webm']
const { mimeType, data } = d
console.log('mimeType', mimeType)
console.log('data', data)
if (mimeType.startsWith('image/')) {
if (!typesImage.includes(mimeType.split('/')[1])) {
console.warn('Image type not supported:', mimeType.split('/')[1])
return
}
images.push({ type: mimeType.split('/')[1], uri: data })
} else if (mimeType.startsWith('video/')) {
if (!typesVideo.includes(mimeType.split('/')[1])) {
console.warn('Video type not supported:', mimeType.split('/')[1])
return
}
video = { type: mimeType.split('/')[1], uri: data }
} else {
if (typesImage.includes(data.split('.').pop() || '')) {
images.push({ type: data.split('.').pop()!, uri: data })
return
}
if (typesVideo.includes(data.split('.').pop() || '')) {
video = { type: data.split('.').pop()!, uri: data }
return
}
text = !text ? data : text.concat(text, `\n${data}`)
}
})
navigationRef.navigate('Screen-Compose', {
type: 'share',
text,
images,
video
})
},
[instanceActive]
)
useEffect(() => {
console.log('getting intial share')
ShareMenu.getInitialShare(handleShare)
}, [])
useEffect(() => {
console.log('getting just share')
const listener = ShareMenu.addNewShareListener(handleShare)
return () => {
listener.remove()
}
}, [])
return (
<>
<StatusBar backgroundColor={colors.backgroundDefault} />

105
src/ShareExtension.tsx Normal file
View File

@ -0,0 +1,105 @@
import { useEffect, useState } from 'react'
import { Appearance, Platform, Pressable, Text } from 'react-native'
import { Circle } from 'react-native-animated-spinkit'
import RNFS from 'react-native-fs'
import { ShareMenuReactView } from 'react-native-share-menu'
import uuid from 'react-native-uuid'
// mimeType
// text/plain - text only, website URL, video?!
// image/jpeg - image
// video/mp4 - video
const colors = {
primary: {
light: 'rgb(18, 18, 18)',
dark: 'rgb(180, 180, 180)'
},
background: {
light: 'rgb(250, 250, 250)',
dark: 'rgb(18, 18, 18)'
}
}
const clearDir = async (dir: string) => {
try {
const files = await RNFS.readDir(dir)
for (const file of files) {
await RNFS.unlink(file.path)
}
} catch (err: any) {
console.warn(err.message)
}
}
const ShareExtension = () => {
const [errorMessage, setErrorMessage] = useState<string>()
useEffect(() => {
ShareMenuReactView.data().then(async ({ data }) => {
console.log('length', data.length)
const newData = []
switch (Platform.OS) {
case 'ios':
for (const d of data) {
if (d.data.startsWith('file:///')) {
const extension = d.data.split('.').pop()?.toLowerCase()
const filename = `${uuid.v4()}.${extension}`
const groupDirectory = await RNFS.pathForGroup(
'group.com.xmflsct.app.tooot'
)
await clearDir(groupDirectory)
const newFilepath = `file://${groupDirectory}/${filename}`
console.log('newFilepath', newFilepath)
try {
await RNFS.copyFile(d.data, newFilepath)
newData.push({ ...d, data: newFilepath })
} catch (err: any) {
setErrorMessage(err.message)
console.warn(err.message)
}
} else {
newData.push(d)
}
}
break
case 'android':
break
default:
return
}
console.log('new data', newData)
if (!errorMessage) {
ShareMenuReactView.continueInApp({ share: newData })
}
})
}, [])
const theme = Appearance.getColorScheme() || 'light'
return (
<Pressable
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background[theme]
}}
onPress={() => {
if (errorMessage) {
ShareMenuReactView.dismissExtension(errorMessage)
}
}}
>
{!errorMessage ? (
<Text style={{ fontSize: 16, color: colors.primary[theme] }}>
{errorMessage}
</Text>
) : (
<Circle size={18} color={colors.primary[theme]} />
)}
</Pressable>
)
}
export default ShareExtension

View File

@ -17,7 +17,8 @@
"conversation": "Toot DM",
"reply": "Toot reply",
"deleteEdit": "Toot",
"edit": "Toot"
"edit": "Toot",
"share": "Toot"
},
"alert": {
"default": {

View File

@ -1,3 +1,4 @@
import apiInstance from '@api/instance'
import analytics from '@components/analytics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
@ -41,6 +42,7 @@ import { useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo'
import ComposeDraftsList from './Compose/DraftsList'
import ComposeEditAttachment from './Compose/EditAttachment'
import { uploadAttachment } from './Compose/Root/Footer/addAttachment'
import ComposeContext from './Compose/utils/createContext'
import composeInitialState from './Compose/utils/initialState'
import composeParseState from './Compose/utils/parseState'
@ -135,7 +137,36 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
])
useEffect(() => {
const uploadImage = async ({
type,
uri
}: {
type: 'image' | 'video'
uri: string
}) => {
await uploadAttachment({
composeDispatch,
imageInfo: { type, uri, width: 100, height: 100 }
})
}
switch (params?.type) {
case 'share':
if (params.text) {
formatText({
textInput: 'text',
composeDispatch,
content: params.text,
disableDebounce: true
})
}
if (params.images?.length) {
params.images.forEach(image => {
uploadImage({ type: 'image', uri: image.uri })
})
} else if (params.video) {
uploadImage({ type: 'video', uri: params.video.uri })
}
break
case 'edit':
case 'deleteEdit':
if (params.incomingStatus.spoiler_text) {

View File

@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import addAttachment from './Footer/addAttachment'
import chooseAndUploadAttachment from './Footer/addAttachment'
const ComposeActions: React.FC = () => {
const { showActionSheetWithOptions } = useActionSheet()
@ -41,7 +41,7 @@ const ComposeActions: React.FC = () => {
analytics('compose_actions_attachment_press', {
count: composeState.attachments.uploads.length
})
return await addAttachment({
return await chooseAndUploadAttachment({
composeDispatch,
showActionSheetWithOptions
})

View File

@ -27,7 +27,7 @@ import {
import { Circle } from 'react-native-animated-spinkit'
import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types'
import addAttachment from './addAttachment'
import chooseAndUploadAttachment from './addAttachment'
export interface Props {
accessibleRefAttachments: RefObject<View>
@ -218,7 +218,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
]}
onPress={async () => {
analytics('compose_attachment_add_container_press')
await addAttachment({ composeDispatch, showActionSheetWithOptions })
await chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
}}
>
<Button
@ -229,7 +229,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
overlay
onPress={async () => {
analytics('compose_attachment_add_button_press')
await addAttachment({ composeDispatch, showActionSheetWithOptions })
await chooseAndUploadAttachment({ composeDispatch, showActionSheetWithOptions })
}}
style={{
position: 'absolute',

View File

@ -17,114 +17,127 @@ export interface Props {
) => void
}
const addAttachment = async ({
export const uploadAttachment = async ({
composeDispatch,
imageInfo
}: {
composeDispatch: Dispatch<ComposeAction>
imageInfo: Pick<ImageInfo, 'type' | 'uri' | 'width' | 'height'>
}) => {
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
imageInfo.uri + Math.random()
)
let attachmentType: string
switch (imageInfo.type) {
case 'image':
console.log('uri', imageInfo.uri)
attachmentType = `image/${imageInfo.uri.split('.')[1]}`
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
uploading: true
}
})
break
case 'video':
attachmentType = `video/${imageInfo.uri.split('.')[1]}`
VideoThumbnails.getThumbnailAsync(imageInfo.uri)
.then(({ uri, width, height }) =>
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: {
...imageInfo,
local_thumbnail: uri,
hash,
width,
height
},
uploading: true
}
})
)
.catch(() =>
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
uploading: true
}
})
)
break
default:
attachmentType = 'unknown'
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
uploading: true
}
})
break
}
const uploadFailed = () => {
composeDispatch({
type: 'attachment/upload/fail',
payload: hash
})
Alert.alert(
i18next.t(
'screenCompose:content.root.actions.attachment.failed.alert.title'
),
undefined,
[
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.failed.alert.button'
),
onPress: () => {}
}
]
)
}
const formData = new FormData()
formData.append('file', {
// @ts-ignore
uri: imageInfo.uri,
name: attachmentType,
type: attachmentType
})
return apiInstance<Mastodon.Attachment>({
method: 'post',
url: 'media',
body: formData
})
.then(res => {
if (res.body.id) {
composeDispatch({
type: 'attachment/upload/end',
payload: { remote: res.body, local: imageInfo }
})
} else {
uploadFailed()
}
})
.catch(() => {
uploadFailed()
})
}
const chooseAndUploadAttachment = async ({
composeDispatch,
showActionSheetWithOptions
}: Props): Promise<any> => {
const uploader = async (imageInfo: ImageInfo) => {
const hash = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
imageInfo.uri + Math.random()
)
let attachmentType: string
switch (imageInfo.type) {
case 'image':
attachmentType = `image/${imageInfo.uri.split('.')[1]}`
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, local_thumbnail: imageInfo.uri, hash },
uploading: true
}
})
break
case 'video':
attachmentType = `video/${imageInfo.uri.split('.')[1]}`
VideoThumbnails.getThumbnailAsync(imageInfo.uri)
.then(({ uri }) =>
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, local_thumbnail: uri, hash },
uploading: true
}
})
)
.catch(() =>
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
uploading: true
}
})
)
break
default:
attachmentType = 'unknown'
composeDispatch({
type: 'attachment/upload/start',
payload: {
local: { ...imageInfo, hash },
uploading: true
}
})
break
}
const uploadFailed = () => {
composeDispatch({
type: 'attachment/upload/fail',
payload: hash
})
Alert.alert(
i18next.t(
'screenCompose:content.root.actions.attachment.failed.alert.title'
),
undefined,
[
{
text: i18next.t(
'screenCompose:content.root.actions.attachment.failed.alert.button'
),
onPress: () => {}
}
]
)
}
const formData = new FormData()
formData.append('file', {
// @ts-ignore
uri: imageInfo.uri,
name: attachmentType,
type: attachmentType
})
return apiInstance<Mastodon.Attachment>({
method: 'post',
url: 'media',
body: formData
})
.then(res => {
if (res.body.id) {
composeDispatch({
type: 'attachment/upload/end',
payload: { remote: res.body, local: imageInfo }
})
} else {
uploadFailed()
}
})
.catch(() => {
uploadFailed()
})
}
const result = await mediaSelector({ showActionSheetWithOptions })
await uploader(result)
await uploadAttachment({ composeDispatch, imageInfo: result })
}
export default addAttachment
export default chooseAndUploadAttachment

View File

@ -38,6 +38,8 @@ const composeParseState = (
params: NonNullable<RootStackParamList['Screen-Compose']>
): ComposeState => {
switch (params.type) {
case 'share':
return { ...composeInitialState, dirty: true, timestamp: Date.now() }
case 'edit':
case 'deleteEdit':
return {

View File

@ -45,6 +45,12 @@ export type RootStackParamList = {
type: 'conversation'
accts: Mastodon.Account['acct'][]
}
| {
type: 'share'
text?: string
images?: { type: string; uri: string }[]
video?: { type: string; uri: string }
}
| undefined
'Screen-ImagesViewer': {
imageUrls: {