tooot/src/screens/Compose.tsx

411 lines
13 KiB
TypeScript
Raw Normal View History

2021-01-24 02:25:43 +01:00
import analytics from '@components/analytics'
2021-03-16 23:15:37 +01:00
import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
2020-12-30 14:33:33 +01:00
import haptics from '@root/components/haptics'
2021-01-30 01:29:15 +01:00
import formatText from '@screens/Compose/formatText'
import ComposeRoot from '@screens/Compose/Root'
2021-08-29 15:25:38 +02:00
import { RootStackScreenProps } from '@utils/navigation/navigators'
2021-01-11 21:36:57 +01:00
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
2021-01-18 00:23:40 +01:00
import { updateStoreReview } from '@utils/slices/contextsSlice'
import {
2021-02-20 19:12:44 +01:00
getInstanceAccount,
2021-11-15 22:34:43 +01:00
getInstanceConfigurationStatusMaxChars,
2021-02-20 19:12:44 +01:00
removeInstanceDraft,
updateInstanceDraft
} from '@utils/slices/instancesSlice'
2020-12-29 16:19:04 +01:00
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
2021-02-07 00:39:11 +01:00
import { filter } from 'lodash'
import React, {
useCallback,
useEffect,
useMemo,
useReducer,
useState
} from 'react'
2021-01-19 01:13:45 +01:00
import { useTranslation } from 'react-i18next'
2020-12-06 23:51:13 +01:00
import {
Alert,
Keyboard,
KeyboardAvoidingView,
2021-01-14 00:43:35 +01:00
Platform,
2021-02-07 00:39:11 +01:00
StyleSheet
2020-12-06 23:51:13 +01:00
} from 'react-native'
2020-11-15 23:33:01 +01:00
import { SafeAreaView } from 'react-native-safe-area-context'
2020-12-20 17:53:24 +01:00
import { useQueryClient } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
2021-01-22 01:34:20 +01:00
import * as Sentry from 'sentry-expo'
2021-02-07 00:39:11 +01:00
import ComposeDraftsList from './Compose/DraftsList'
2020-12-30 00:56:25 +01:00
import ComposeEditAttachment from './Compose/EditAttachment'
2021-01-01 17:52:14 +01:00
import ComposeContext from './Compose/utils/createContext'
2020-12-30 00:56:25 +01:00
import composeInitialState from './Compose/utils/initialState'
import composeParseState from './Compose/utils/parseState'
2021-01-01 23:10:47 +01:00
import composePost from './Compose/utils/post'
2020-12-30 00:56:25 +01:00
import composeReducer from './Compose/utils/reducer'
2021-01-30 01:29:15 +01:00
2020-11-15 20:29:43 +01:00
const Stack = createNativeStackNavigator()
2021-08-29 15:25:38 +02:00
const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
2021-01-07 22:18:14 +01:00
route: { params },
navigation
}) => {
2021-03-28 23:31:10 +02:00
const { t } = useTranslation('screenCompose')
2020-12-07 12:31:40 +01:00
const { theme } = useTheme()
2020-12-20 17:53:24 +01:00
const queryClient = useQueryClient()
2020-12-06 23:51:13 +01:00
2020-11-15 23:33:01 +01:00
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
2021-10-10 21:58:36 +02:00
const keyboardShown = Keyboard.addListener('keyboardWillShow', () =>
setHasKeyboard(true)
)
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () =>
setHasKeyboard(false)
)
2020-11-15 23:33:01 +01:00
return () => {
2021-10-10 21:58:36 +02:00
keyboardShown.remove()
keyboardHidden.remove()
2020-11-15 23:33:01 +01:00
}
}, [])
2021-02-20 19:12:44 +01:00
const localAccount = useSelector(getInstanceAccount, (prev, next) =>
2021-02-10 00:40:44 +01:00
prev?.preferences && next?.preferences
? prev?.preferences['posting:default:visibility'] ===
next?.preferences['posting:default:visibility']
: true
)
2021-02-07 00:39:11 +01:00
const initialReducerState = useMemo(() => {
if (params) {
return composeParseState(params)
} else {
return {
...composeInitialState,
timestamp: Date.now(),
attachments: {
...composeInitialState.attachments,
sensitive:
localAccount?.preferences &&
localAccount?.preferences['posting:default:sensitive']
? localAccount?.preferences['posting:default:sensitive']
: false
},
2021-02-07 00:39:11 +01:00
visibility:
localAccount?.preferences &&
localAccount.preferences['posting:default:visibility']
? localAccount.preferences['posting:default:visibility']
: 'public'
}
}
}, [])
2020-12-07 12:31:40 +01:00
const [composeState, composeDispatch] = useReducer(
composeReducer,
2021-02-07 00:39:11 +01:00
initialReducerState
2020-12-07 12:31:40 +01:00
)
2021-11-15 22:34:43 +01:00
const maxTootChars = useSelector(
getInstanceConfigurationStatusMaxChars,
() => true
)
2021-02-07 00:39:11 +01:00
const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
composeState.text.count
// If compose state is dirty, then disallow add back drafts
useEffect(() => {
composeDispatch({
type: 'dirty',
payload:
totalTextCount !== 0 ||
composeState.attachments.uploads.length !== 0 ||
(composeState.poll.active === true &&
filter(composeState.poll.options, o => {
return o !== undefined && o.length > 0
}).length > 0)
})
}, [
totalTextCount,
composeState.attachments.uploads.length,
composeState.poll.active,
composeState.poll.options
])
2020-12-07 12:31:40 +01:00
useEffect(() => {
switch (params?.type) {
case 'edit':
if (params.incomingStatus.spoiler_text) {
formatText({
textInput: 'spoiler',
2020-12-07 12:31:40 +01:00
composeDispatch,
content: params.incomingStatus.spoiler_text,
disableDebounce: true
})
}
formatText({
textInput: 'text',
2020-12-07 12:31:40 +01:00
composeDispatch,
content: params.incomingStatus.text!,
disableDebounce: true
})
break
case 'reply':
const actualStatus =
params.incomingStatus.reblog || params.incomingStatus
if (actualStatus.spoiler_text) {
formatText({
textInput: 'spoiler',
composeDispatch,
content: actualStatus.spoiler_text,
disableDebounce: true
})
}
2021-03-11 22:27:38 +01:00
params.accts.length && // When replying to myself only, do not add space or even format text
formatText({
textInput: 'text',
composeDispatch,
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
disableDebounce: true
})
break
2020-12-21 21:47:15 +01:00
case 'conversation':
formatText({
textInput: 'text',
composeDispatch,
2021-01-24 02:25:43 +01:00
content: params.accts.map(acct => `@${acct}`).join(' ') + ' ',
2020-12-21 21:47:15 +01:00
disableDebounce: true
})
break
2020-12-07 12:31:40 +01:00
}
}, [params?.type])
2020-11-15 22:33:09 +01:00
2021-02-07 00:39:11 +01:00
const saveDraft = () => {
dispatch(
2021-02-20 19:12:44 +01:00
updateInstanceDraft({
2021-02-07 00:39:11 +01:00
timestamp: composeState.timestamp,
spoiler: composeState.spoiler.raw,
text: composeState.text.raw,
poll: composeState.poll,
attachments: composeState.attachments,
visibility: composeState.visibility,
visibilityLock: composeState.visibilityLock,
replyToStatus: composeState.replyToStatus
})
)
}
const removeDraft = useCallback(() => {
2021-02-20 19:12:44 +01:00
dispatch(removeInstanceDraft(composeState.timestamp))
2021-02-07 00:39:11 +01:00
}, [composeState.timestamp])
useEffect(() => {
const autoSave = composeState.dirty
? setInterval(() => {
saveDraft()
2021-04-01 18:39:53 +02:00
}, 1000)
2021-02-07 00:39:11 +01:00
: removeDraft()
return () => autoSave && clearInterval(autoSave)
}, [composeState])
2020-12-06 23:51:13 +01:00
2020-12-26 00:53:49 +01:00
const headerLeft = useCallback(
() => (
<HeaderLeft
2020-12-26 23:27:53 +01:00
type='text'
2021-01-19 01:13:45 +01:00
content={t('heading.left.button')}
2021-01-17 22:37:05 +01:00
onPress={() => {
2021-01-24 02:25:43 +01:00
analytics('compose_header_back_press')
2021-02-07 00:39:11 +01:00
if (!composeState.dirty) {
2021-01-24 02:25:43 +01:00
analytics('compose_header_back_empty')
2021-01-17 22:37:05 +01:00
navigation.goBack()
return
} else {
2021-01-24 02:25:43 +01:00
analytics('compose_header_back_state_occupied')
2021-01-19 01:13:45 +01:00
Alert.alert(t('heading.left.alert.title'), undefined, [
2021-01-17 22:37:05 +01:00
{
2021-02-07 00:39:11 +01:00
text: t('heading.left.alert.buttons.delete'),
2021-01-17 22:37:05 +01:00
style: 'destructive',
2021-01-24 02:25:43 +01:00
onPress: () => {
2021-02-07 00:39:11 +01:00
analytics('compose_header_back_occupied_save')
removeDraft()
navigation.goBack()
}
},
{
text: t('heading.left.alert.buttons.save'),
onPress: () => {
analytics('compose_header_back_occupied_delete')
saveDraft()
2021-01-24 02:25:43 +01:00
navigation.goBack()
}
2021-01-17 22:37:05 +01:00
},
2021-01-19 01:13:45 +01:00
{
2021-02-07 00:39:11 +01:00
text: t('heading.left.alert.buttons.cancel'),
2021-01-24 02:25:43 +01:00
style: 'cancel',
onPress: () => {
analytics('compose_header_back_occupied_cancel')
}
2021-01-19 01:13:45 +01:00
}
2021-01-17 22:37:05 +01:00
])
}
}}
2020-12-26 00:53:49 +01:00
/>
),
2021-02-07 00:39:11 +01:00
[composeState]
2020-12-26 00:53:49 +01:00
)
2021-01-18 00:23:40 +01:00
const dispatch = useDispatch()
2021-02-08 00:23:32 +01:00
const headerRightDisabled = useMemo(() => {
if (totalTextCount > maxTootChars) {
return true
}
if (
composeState.attachments.uploads.filter(upload => upload.uploading)
.length > 0
) {
return true
}
if (
composeState.attachments.uploads.length === 0 &&
composeState.text.raw.length === 0
) {
return true
}
return false
}, [totalTextCount, composeState.attachments.uploads, composeState.text.raw])
2020-12-26 00:53:49 +01:00
const headerRight = useCallback(
2020-12-26 23:27:53 +01:00
() => (
<HeaderRight
type='text'
2021-01-19 01:13:45 +01:00
content={
params?.type
? t(`heading.right.button.${params.type}`)
: t('heading.right.button.default')
}
2020-12-30 00:56:25 +01:00
onPress={() => {
2021-01-24 02:25:43 +01:00
analytics('compose_header_post_press')
2021-01-14 22:53:01 +01:00
composeDispatch({ type: 'posting', payload: true })
2021-01-01 23:10:47 +01:00
composePost(params, composeState)
2020-12-30 00:56:25 +01:00
.then(() => {
2020-12-30 14:33:33 +01:00
haptics('Success')
2021-03-19 21:33:03 +01:00
if (
Platform.OS === 'ios' &&
Platform.constants.osVersion === '13.3'
) {
// https://github.com/tooot-app/app/issues/59
} else {
dispatch(updateStoreReview(1))
}
2021-01-11 21:36:57 +01:00
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
queryClient.invalidateQueries(queryKey)
2021-01-14 22:53:01 +01:00
2021-01-24 02:25:43 +01:00
switch (params?.type) {
case 'edit':
case 'reply':
if (params?.queryKey && params.queryKey[1].page === 'Toot') {
queryClient.invalidateQueries(params.queryKey)
}
break
2021-01-04 14:55:34 +01:00
}
2021-02-08 00:13:35 +01:00
removeDraft()
2020-12-30 00:56:25 +01:00
navigation.goBack()
})
2021-01-22 01:34:20 +01:00
.catch(error => {
2021-03-09 00:47:40 +01:00
if (error.removeReply) {
Alert.alert(
t('heading.right.alert.removeReply.title'),
t('heading.right.alert.removeReply.description'),
[
{
text: t('heading.right.alert.removeReply.cancel'),
onPress: () => {
composeDispatch({ type: 'posting', payload: false })
},
style: 'destructive'
},
{
text: t('heading.right.alert.removeReply.confirm'),
onPress: () => {
composeDispatch({ type: 'removeReply' })
composeDispatch({ type: 'posting', payload: false })
},
style: 'default'
}
]
)
} else {
2022-01-31 00:31:57 +01:00
Sentry.Native.captureMessage('Compose posting', error)
2021-03-09 00:47:40 +01:00
haptics('Error')
composeDispatch({ type: 'posting', payload: false })
Alert.alert(t('heading.right.alert.default.title'), undefined, [
{
text: t('heading.right.alert.default.button')
}
])
}
2020-12-30 00:56:25 +01:00
})
}}
2021-01-14 22:53:01 +01:00
loading={composeState.posting}
2021-02-08 00:23:32 +01:00
disabled={headerRightDisabled}
2020-12-26 23:27:53 +01:00
/>
),
2021-02-07 00:39:11 +01:00
[totalTextCount, composeState]
2020-12-29 16:19:04 +01:00
)
2021-03-18 23:32:31 +01:00
const headerContent = useMemo(() => {
return `${totalTextCount} / ${maxTootChars}${
__DEV__ ? ` Dirty: ${composeState.dirty.toString()}` : ''
}`
}, [totalTextCount, maxTootChars, composeState.dirty])
return (
2021-01-14 00:43:35 +01:00
<KeyboardAvoidingView
2021-02-02 22:50:38 +01:00
style={styles.base}
2021-03-11 23:12:17 +01:00
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
2021-01-14 00:43:35 +01:00
>
2020-11-15 23:33:01 +01:00
<SafeAreaView
2021-01-30 01:29:15 +01:00
style={styles.base}
edges={hasKeyboard ? ['top'] : ['top', 'bottom']}
2020-11-15 23:33:01 +01:00
>
2020-12-30 00:56:25 +01:00
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
2020-12-30 00:56:25 +01:00
<Stack.Screen
2021-01-31 03:09:35 +01:00
name='Screen-Compose-Root'
2020-12-30 00:56:25 +01:00
component={ComposeRoot}
2021-02-07 00:39:11 +01:00
options={{
2021-12-18 23:44:08 +01:00
title: headerContent,
titleStyle: {
fontWeight:
totalTextCount > maxTootChars
? StyleConstants.Font.Weight.Bold
: StyleConstants.Font.Weight.Normal,
fontSize: StyleConstants.Font.Size.M
},
headerTintColor:
totalTextCount > maxTootChars ? theme.red : theme.secondary,
2021-02-07 00:39:11 +01:00
headerLeft,
headerRight
}}
/>
<Stack.Screen
name='Screen-Compose-DraftsList'
component={ComposeDraftsList}
options={{ headerShown: false, presentation: 'modal' }}
2020-12-30 00:56:25 +01:00
/>
<Stack.Screen
2021-01-31 03:09:35 +01:00
name='Screen-Compose-EditAttachment'
2020-12-30 00:56:25 +01:00
component={ComposeEditAttachment}
options={{ headerShown: false, presentation: 'modal' }}
2020-12-30 00:56:25 +01:00
/>
</Stack.Navigator>
</ComposeContext.Provider>
2020-11-15 23:33:01 +01:00
</SafeAreaView>
</KeyboardAvoidingView>
)
}
2020-12-06 23:51:13 +01:00
const styles = StyleSheet.create({
2021-02-07 00:39:11 +01:00
base: { flex: 1 }
2020-12-06 23:51:13 +01:00
})
2021-01-30 01:29:15 +01:00
export default ScreenCompose