mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Fixed #336
This commit is contained in:
@ -1,9 +1,11 @@
|
||||
import analytics from '@components/analytics'
|
||||
import { HeaderLeft } from '@components/Header'
|
||||
import { displayMessage, Message } from '@components/Message'
|
||||
import CustomText from '@components/Text'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { NavigationContainer } from '@react-navigation/native'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import ScreenAccountSelection from '@screens/AccountSelection'
|
||||
import ScreenActions from '@screens/Actions'
|
||||
import ScreenAnnouncements from '@screens/Announcements'
|
||||
import ScreenCompose from '@screens/Compose'
|
||||
@ -170,7 +172,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
}
|
||||
| { data: string | string[]; mimeType: string }
|
||||
) => {
|
||||
console.log('item', item)
|
||||
if (instanceActive < 0) {
|
||||
return
|
||||
}
|
||||
@ -239,12 +240,6 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
if (!item.mimeType) {
|
||||
return
|
||||
}
|
||||
let tempData: string[]
|
||||
if (!Array.isArray(item.data)) {
|
||||
tempData = [item.data]
|
||||
} else {
|
||||
tempData = item.data
|
||||
}
|
||||
for (const d of item.data) {
|
||||
filterMedia({ uri: d, mime: item.mimeType })
|
||||
}
|
||||
@ -254,11 +249,20 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
if (!text && !media.length) {
|
||||
return
|
||||
} else {
|
||||
console.log('media', media)
|
||||
navigationRef.navigate('Screen-Compose', { type: 'share', text, media })
|
||||
if (instances.length > 1) {
|
||||
navigationRef.navigate('Screen-AccountSelection', {
|
||||
share: { text, media }
|
||||
})
|
||||
} else {
|
||||
navigationRef.navigate('Screen-Compose', {
|
||||
type: 'share',
|
||||
text,
|
||||
media
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[instanceActive]
|
||||
[instances.length]
|
||||
)
|
||||
useEffect(() => {
|
||||
ShareMenu.getInitialShare(handleShare)
|
||||
@ -333,6 +337,23 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => {
|
||||
animation: 'fade'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-AccountSelection'
|
||||
component={ScreenAccountSelection}
|
||||
options={({ navigation }) => ({
|
||||
title: t('screenAccountSelection:heading'),
|
||||
headerShadowVisible: false,
|
||||
presentation: 'modal',
|
||||
gestureEnabled: false,
|
||||
headerLeft: () => (
|
||||
<HeaderLeft
|
||||
type='text'
|
||||
content={t('common:buttons.cancel')}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
|
||||
<Message />
|
||||
|
@ -1,8 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import Constants from 'expo-constants'
|
||||
|
||||
const ctx = new chalk.Instance({ level: 3 })
|
||||
import handleError, { ctx } from './handleError'
|
||||
|
||||
export type Params = {
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
@ -57,40 +55,7 @@ const apiGeneral = async <T = unknown>({
|
||||
body: response.data
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API general '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message: error?.response.data.error
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(
|
||||
ctx.bold(' API general '),
|
||||
ctx.bold('request'),
|
||||
error.request
|
||||
)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
ctx.bold(' API general '),
|
||||
ctx.bold('internal'),
|
||||
error?.message,
|
||||
url
|
||||
)
|
||||
return Promise.reject()
|
||||
}
|
||||
})
|
||||
.catch(handleError)
|
||||
}
|
||||
|
||||
export default apiGeneral
|
||||
|
38
src/api/handleError.ts
Normal file
38
src/api/handleError.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import chalk from 'chalk'
|
||||
|
||||
export const ctx = new chalk.Instance({ level: 3 })
|
||||
|
||||
const handleError = (error: any) => {
|
||||
if (error?.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API instance '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||
)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message:
|
||||
error?.response.data?.error ||
|
||||
error?.response.message ||
|
||||
'Unknown error'
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
ctx.bold(' API instance '),
|
||||
ctx.bold('internal'),
|
||||
error?.message
|
||||
)
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
export default handleError
|
@ -1,10 +1,8 @@
|
||||
import { RootState } from '@root/store'
|
||||
import axios, { AxiosRequestConfig } from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import Constants from 'expo-constants'
|
||||
import li from 'li'
|
||||
|
||||
const ctx = new chalk.Instance({ level: 3 })
|
||||
import handleError, { ctx } from './handleError'
|
||||
|
||||
export type Params = {
|
||||
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
||||
@ -99,36 +97,7 @@ const apiInstance = async <T = unknown>({
|
||||
links: { prev, next }
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API instance '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message: error?.response.data.error
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
ctx.bold(' API instance '),
|
||||
ctx.bold('internal'),
|
||||
error?.message,
|
||||
url
|
||||
)
|
||||
return Promise.reject()
|
||||
}
|
||||
})
|
||||
.catch(handleError)
|
||||
}
|
||||
|
||||
export default apiInstance
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { mapEnvironment } from '@utils/checkEnvironment'
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import Constants from 'expo-constants'
|
||||
import * as Sentry from 'sentry-expo'
|
||||
|
||||
const ctx = new chalk.Instance({ level: 3 })
|
||||
import handleError, { ctx } from './handleError'
|
||||
|
||||
export type Params = {
|
||||
method: 'get' | 'post' | 'put' | 'delete'
|
||||
@ -75,38 +73,7 @@ const apiTooot = async <T = unknown>({
|
||||
Sentry.Native.captureException(error)
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.error(
|
||||
ctx.bold(' API tooot '),
|
||||
ctx.bold('response'),
|
||||
error.response.status,
|
||||
error.response.data.error
|
||||
)
|
||||
return Promise.reject({
|
||||
status: error?.response.status,
|
||||
message: error?.response.data.error
|
||||
})
|
||||
} else if (error?.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.error(
|
||||
ctx.bold(' API tooot '),
|
||||
ctx.bold('request'),
|
||||
error.request
|
||||
)
|
||||
return Promise.reject()
|
||||
} else {
|
||||
console.error(
|
||||
ctx.bold(' API tooot '),
|
||||
ctx.bold('internal'),
|
||||
error?.message,
|
||||
url
|
||||
)
|
||||
return Promise.reject()
|
||||
}
|
||||
return handleError(error)
|
||||
})
|
||||
}
|
||||
|
||||
|
45
src/components/AccountButton.tsx
Normal file
45
src/components/AccountButton.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import initQuery from '@utils/initQuery'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React from 'react'
|
||||
import Button from './Button'
|
||||
import haptics from './haptics'
|
||||
|
||||
interface Props {
|
||||
instance: InstanceLatest
|
||||
selected?: boolean
|
||||
additionalActions?: () => void
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({
|
||||
instance,
|
||||
selected = false,
|
||||
additionalActions
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
initQuery({ instance, prefetch: { enabled: true } })
|
||||
navigation.goBack()
|
||||
if (additionalActions) {
|
||||
additionalActions()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AccountButton
|
@ -92,7 +92,6 @@ const TimelineHeaderDefault = ({ queryKey, status, highlighted }: Props) => {
|
||||
dropdownMenuMode
|
||||
actions={actions}
|
||||
onPress={({ nativeEvent: { index } }) => {
|
||||
console.log('index', index)
|
||||
for (const on of [
|
||||
shareOnPress,
|
||||
statusOnPress,
|
||||
|
@ -72,7 +72,6 @@ const openLink = async (url: string, navigation?: any) => {
|
||||
|
||||
// If an account can be found
|
||||
const matchedAccount = url.match(matcherAccount)
|
||||
console.log(matchedAccount)
|
||||
if (matchedAccount) {
|
||||
// If the link in current instance
|
||||
const instanceUrl = getInstanceUrl(store.getState())
|
||||
|
@ -2,6 +2,7 @@ export default {
|
||||
common: require('./common'),
|
||||
|
||||
screens: require('./screens'),
|
||||
screenAccountSelection: require('./screens/accountSelection.json'),
|
||||
screenActions: require('./screens/actions'),
|
||||
screenAnnouncements: require('./screens/announcements'),
|
||||
screenCompose: require('./screens/compose'),
|
||||
|
6
src/i18n/en/screens/accountSelection.json
Normal file
6
src/i18n/en/screens/accountSelection.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"heading": "Share to ...",
|
||||
"content": {
|
||||
"select_account": "Select account"
|
||||
}
|
||||
}
|
163
src/screens/AccountSelection.tsx
Normal file
163
src/screens/AccountSelection.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
import AccountButton from '@components/AccountButton'
|
||||
import CustomText from '@components/Text'
|
||||
import navigationRef from '@helpers/navigationRef'
|
||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||
import { getInstances } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import * as VideoThumbnails from 'expo-video-thumbnails'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FlatList, Image, ScrollView, View } from 'react-native'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const Share = ({
|
||||
text,
|
||||
media
|
||||
}: {
|
||||
text?: string | undefined
|
||||
media?:
|
||||
| {
|
||||
uri: string
|
||||
mime: string
|
||||
}[]
|
||||
| undefined
|
||||
}) => {
|
||||
const { colors } = useTheme()
|
||||
|
||||
const [images, setImages] = useState<string[]>([])
|
||||
useEffect(() => {
|
||||
const prepareThumbs = async (media: { uri: string; mime: string }[]) => {
|
||||
const thumbs: string[] = []
|
||||
for (const m of media) {
|
||||
if (m.mime.startsWith('image/')) {
|
||||
thumbs.push(m.uri)
|
||||
} else if (m.mime.startsWith('video/')) {
|
||||
const { uri } = await VideoThumbnails.getThumbnailAsync(m.uri)
|
||||
thumbs.push(uri)
|
||||
}
|
||||
}
|
||||
setImages(thumbs)
|
||||
}
|
||||
if (media) {
|
||||
prepareThumbs(media)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (text) {
|
||||
return (
|
||||
<CustomText
|
||||
fontSize='S'
|
||||
style={{
|
||||
color: colors.primaryDefault,
|
||||
padding: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.shimmerHighlight,
|
||||
borderRadius: 8
|
||||
}}
|
||||
children={text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (media) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
padding: StyleConstants.Spacing.M,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.shimmerHighlight,
|
||||
borderRadius: 8
|
||||
}}
|
||||
>
|
||||
<FlatList
|
||||
horizontal
|
||||
data={images}
|
||||
renderItem={({ item }) => (
|
||||
<Image source={{ uri: item }} style={{ width: 88, height: 88 }} />
|
||||
)}
|
||||
ItemSeparatorComponent={() => (
|
||||
<View style={{ width: StyleConstants.Spacing.S }} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Only needed when data incoming into the app when there are multiple accounts
|
||||
const ScreenAccountSelection = ({
|
||||
route: {
|
||||
params: { share }
|
||||
}
|
||||
}: RootStackScreenProps<'Screen-AccountSelection'>) => {
|
||||
const { colors } = useTheme()
|
||||
const { t } = useTranslation('screenAccountSelection')
|
||||
|
||||
const instances = useSelector(getInstances, () => true)
|
||||
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<ScrollView
|
||||
style={{ marginBottom: StyleConstants.Spacing.L * 2 }}
|
||||
keyboardShouldPersistTaps='always'
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
|
||||
}}
|
||||
>
|
||||
{share ? <Share {...share} /> : null}
|
||||
<CustomText
|
||||
fontStyle='M'
|
||||
fontWeight='Bold'
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
marginTop: StyleConstants.Spacing.L,
|
||||
marginBottom: StyleConstants.Spacing.S,
|
||||
color: colors.primaryDefault
|
||||
}}
|
||||
>
|
||||
{t('content.select_account')}
|
||||
</CustomText>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
marginTop: StyleConstants.Spacing.M
|
||||
}}
|
||||
>
|
||||
{instances.length
|
||||
? instances
|
||||
.slice()
|
||||
.sort((a, b) =>
|
||||
`${a.uri}${a.account.acct}`.localeCompare(
|
||||
`${b.uri}${b.account.acct}`
|
||||
)
|
||||
)
|
||||
.map((instance, index) => {
|
||||
return (
|
||||
<AccountButton
|
||||
key={index}
|
||||
instance={instance}
|
||||
additionalActions={() => {
|
||||
navigationRef.navigate('Screen-Compose', {
|
||||
type: 'share',
|
||||
...share
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScreenAccountSelection
|
@ -42,7 +42,6 @@ export const uploadAttachment = async ({
|
||||
case 'video':
|
||||
VideoThumbnails.getThumbnailAsync(media.uri)
|
||||
.then(({ uri, width, height }) => {
|
||||
console.log('new', uri, width, height)
|
||||
composeDispatch({
|
||||
type: 'attachment/upload/start',
|
||||
payload: {
|
||||
|
@ -1,11 +1,6 @@
|
||||
import analytics from '@components/analytics'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import AccountButton from '@components/AccountButton'
|
||||
import ComponentInstance from '@components/Instance'
|
||||
import CustomText from '@components/Text'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import initQuery from '@utils/initQuery'
|
||||
import { InstanceLatest } from '@utils/migrations/instances/migration'
|
||||
import { getInstanceActive, getInstances } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
@ -15,35 +10,6 @@ import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
interface Props {
|
||||
instance: InstanceLatest
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
const AccountButton: React.FC<Props> = ({ instance, selected = false }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type='text'
|
||||
selected={selected}
|
||||
style={{
|
||||
marginBottom: StyleConstants.Spacing.M,
|
||||
marginRight: StyleConstants.Spacing.M
|
||||
}}
|
||||
content={`@${instance.account.acct}@${instance.uri}${
|
||||
selected ? ' ✓' : ''
|
||||
}`}
|
||||
onPress={() => {
|
||||
haptics('Light')
|
||||
analytics('switch_existing_press')
|
||||
initQuery({ instance, prefetch: { enabled: true } })
|
||||
navigation.goBack()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TabMeSwitch: React.FC = () => {
|
||||
const { t } = useTranslation('screenTabs')
|
||||
const { colors } = useTheme()
|
||||
|
@ -6,7 +6,6 @@ const sentry = () => {
|
||||
Sentry.init({
|
||||
dsn: 'https://53348b60ff844d52886e90251b3a5f41@o917354.ingest.sentry.io/6410576',
|
||||
enableInExpoDevelopment: false,
|
||||
// debug: !isRelease,
|
||||
autoSessionTracking: true
|
||||
})
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { NavigatorScreenParams } from '@react-navigation/native'
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
||||
import { StackNavigationProp } from '@react-navigation/stack'
|
||||
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
|
||||
import React from 'react'
|
||||
|
||||
export type RootStackParamList = {
|
||||
'Screen-Tabs': NavigatorScreenParams<ScreenTabsStackParamList>
|
||||
@ -57,6 +58,10 @@ export type RootStackParamList = {
|
||||
}[]
|
||||
id: Mastodon.Attachment['id']
|
||||
}
|
||||
'Screen-AccountSelection': {
|
||||
component?: () => JSX.Element | undefined
|
||||
share?: { text?: string; media?: { uri: string; mime: string }[] }
|
||||
}
|
||||
}
|
||||
export type RootStackScreenProps<T extends keyof RootStackParamList> =
|
||||
NativeStackScreenProps<RootStackParamList, T>
|
||||
|
Reference in New Issue
Block a user