mirror of
https://github.com/tooot-app/app
synced 2025-02-02 19:47:06 +01:00
POC compose using the new emoji selector
This commit is contained in:
parent
7282434e69
commit
2df23a8a2e
@ -44,7 +44,7 @@ const EmojisList = () => {
|
|||||||
const contentFront = value.slice(0, selection.start)
|
const contentFront = value.slice(0, selection.start)
|
||||||
const contentRear = value.slice(selection.end)
|
const contentRear = value.slice(selection.end)
|
||||||
|
|
||||||
const spaceFront = /\s/g.test(contentFront.slice(-1)) ? '' : ' '
|
const spaceFront = value.length === 0 || /\s/g.test(contentFront.slice(-1)) ? '' : ' '
|
||||||
const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' '
|
const spaceRear = /\s/g.test(contentRear[0]) ? '' : ' '
|
||||||
|
|
||||||
setValue(
|
setValue(
|
||||||
@ -52,7 +52,6 @@ const EmojisList = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const addedLength = spaceFront.length + shortcode.length + spaceRear.length
|
const addedLength = spaceFront.length + shortcode.length + spaceRear.length
|
||||||
|
|
||||||
setSelection({ start: selection.start + addedLength })
|
setSelection({ start: selection.start + addedLength })
|
||||||
ref?.current?.setNativeProps({
|
ref?.current?.setNativeProps({
|
||||||
selection: { start: selection.start + addedLength }
|
selection: { start: selection.start + addedLength }
|
||||||
|
@ -7,6 +7,7 @@ type inputProps = {
|
|||||||
isFocused: MutableRefObject<boolean>
|
isFocused: MutableRefObject<boolean>
|
||||||
ref?: RefObject<TextInput> // For controlling focus
|
ref?: RefObject<TextInput> // For controlling focus
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
|
addFunc?: (add: string) => void // For none default state update
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EmojisState = {
|
export type EmojisState = {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
|
import { ComponentEmojis } from '@components/Emojis'
|
||||||
|
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
|
||||||
import { HeaderLeft, HeaderRight } from '@components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||||
import haptics from '@root/components/haptics'
|
import haptics from '@root/components/haptics'
|
||||||
@ -6,10 +8,7 @@ import { useAppDispatch } from '@root/store'
|
|||||||
import formatText from '@screens/Compose/formatText'
|
import formatText from '@screens/Compose/formatText'
|
||||||
import ComposeRoot from '@screens/Compose/Root'
|
import ComposeRoot from '@screens/Compose/Root'
|
||||||
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
import { RootStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import {
|
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
|
||||||
QueryKeyTimeline,
|
|
||||||
useTimelineMutation
|
|
||||||
} from '@utils/queryHooks/timeline'
|
|
||||||
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
import { updateStoreReview } from '@utils/slices/contextsSlice'
|
||||||
import {
|
import {
|
||||||
getInstanceAccount,
|
getInstanceAccount,
|
||||||
@ -20,22 +19,9 @@ import {
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { filter } from 'lodash'
|
import { filter } from 'lodash'
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useReducer,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import { Alert, Keyboard, Platform } from 'react-native'
|
||||||
Alert,
|
|
||||||
Keyboard,
|
|
||||||
KeyboardAvoidingView,
|
|
||||||
Platform,
|
|
||||||
StyleSheet
|
|
||||||
} from 'react-native'
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
||||||
import { useQueryClient } from 'react-query'
|
import { useQueryClient } from 'react-query'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import * as Sentry from 'sentry-expo'
|
import * as Sentry from 'sentry-expo'
|
||||||
@ -60,12 +46,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
|
|
||||||
const [hasKeyboard, setHasKeyboard] = useState(false)
|
const [hasKeyboard, setHasKeyboard] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyboardShown = Keyboard.addListener('keyboardWillShow', () =>
|
const keyboardShown = Keyboard.addListener('keyboardWillShow', () => setHasKeyboard(true))
|
||||||
setHasKeyboard(true)
|
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () => setHasKeyboard(false))
|
||||||
)
|
|
||||||
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () =>
|
|
||||||
setHasKeyboard(false)
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
keyboardShown.remove()
|
keyboardShown.remove()
|
||||||
@ -89,32 +71,23 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
attachments: {
|
attachments: {
|
||||||
...composeInitialState.attachments,
|
...composeInitialState.attachments,
|
||||||
sensitive:
|
sensitive:
|
||||||
localAccount?.preferences &&
|
localAccount?.preferences && localAccount?.preferences['posting:default:sensitive']
|
||||||
localAccount?.preferences['posting:default:sensitive']
|
|
||||||
? localAccount?.preferences['posting:default:sensitive']
|
? localAccount?.preferences['posting:default:sensitive']
|
||||||
: false
|
: false
|
||||||
},
|
},
|
||||||
visibility:
|
visibility:
|
||||||
localAccount?.preferences &&
|
localAccount?.preferences && localAccount.preferences['posting:default:visibility']
|
||||||
localAccount.preferences['posting:default:visibility']
|
|
||||||
? localAccount.preferences['posting:default:visibility']
|
? localAccount.preferences['posting:default:visibility']
|
||||||
: 'public'
|
: 'public'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const [composeState, composeDispatch] = useReducer(
|
const [composeState, composeDispatch] = useReducer(composeReducer, initialReducerState)
|
||||||
composeReducer,
|
|
||||||
initialReducerState
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxTootChars = useSelector(
|
const maxTootChars = useSelector(getInstanceConfigurationStatusMaxChars, () => true)
|
||||||
getInstanceConfigurationStatusMaxChars,
|
|
||||||
() => true
|
|
||||||
)
|
|
||||||
const totalTextCount =
|
const totalTextCount =
|
||||||
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
|
(composeState.spoiler.active ? composeState.spoiler.count : 0) + composeState.text.count
|
||||||
composeState.text.count
|
|
||||||
|
|
||||||
// If compose state is dirty, then disallow add back drafts
|
// If compose state is dirty, then disallow add back drafts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -173,8 +146,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'reply':
|
case 'reply':
|
||||||
const actualStatus =
|
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
|
||||||
params.incomingStatus.reblog || params.incomingStatus
|
|
||||||
if (actualStatus.spoiler_text) {
|
if (actualStatus.spoiler_text) {
|
||||||
formatText({
|
formatText({
|
||||||
textInput: 'spoiler',
|
textInput: 'spoiler',
|
||||||
@ -278,16 +250,10 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
if (totalTextCount > maxTootChars) {
|
if (totalTextCount > maxTootChars) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (
|
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
|
||||||
composeState.attachments.uploads.filter(upload => upload.uploading)
|
|
||||||
.length > 0
|
|
||||||
) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (
|
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
|
||||||
composeState.attachments.uploads.length === 0 &&
|
|
||||||
composeState.text.raw.length === 0
|
|
||||||
) {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -309,18 +275,12 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
composePost(params, composeState)
|
composePost(params, composeState)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
if (
|
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
|
||||||
Platform.OS === 'ios' &&
|
|
||||||
Platform.constants.osVersion === '13.3'
|
|
||||||
) {
|
|
||||||
// https://github.com/tooot-app/app/issues/59
|
// https://github.com/tooot-app/app/issues/59
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateStoreReview(1))
|
dispatch(updateStoreReview(1))
|
||||||
}
|
}
|
||||||
const queryKey: QueryKeyTimeline = [
|
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
|
||||||
'Timeline',
|
|
||||||
{ page: 'Following' }
|
|
||||||
]
|
|
||||||
queryClient.invalidateQueries(queryKey)
|
queryClient.invalidateQueries(queryKey)
|
||||||
|
|
||||||
switch (params?.type) {
|
switch (params?.type) {
|
||||||
@ -392,54 +352,61 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
|
|||||||
}`
|
}`
|
||||||
}, [totalTextCount, maxTootChars, composeState.dirty])
|
}, [totalTextCount, maxTootChars, composeState.dirty])
|
||||||
|
|
||||||
|
const inputProps: EmojisState['inputProps'] = [
|
||||||
|
{
|
||||||
|
value: [
|
||||||
|
composeState.text.raw,
|
||||||
|
content => formatText({ textInput: 'text', composeDispatch, content })
|
||||||
|
],
|
||||||
|
selection: [
|
||||||
|
composeState.text.selection,
|
||||||
|
selection => composeDispatch({ type: 'text', payload: { selection } })
|
||||||
|
],
|
||||||
|
isFocused: useRef<boolean>(composeState.textInputFocus.current === 'text'),
|
||||||
|
maxLength: maxTootChars
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<ComponentEmojis
|
||||||
style={styles.base}
|
inputProps={inputProps}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
customButton
|
||||||
|
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
||||||
>
|
>
|
||||||
<SafeAreaView
|
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
||||||
style={styles.base}
|
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
||||||
edges={hasKeyboard ? ['top'] : ['top', 'bottom']}
|
<Stack.Screen
|
||||||
>
|
name='Screen-Compose-Root'
|
||||||
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
|
component={ComposeRoot}
|
||||||
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
|
options={{
|
||||||
<Stack.Screen
|
title: headerContent,
|
||||||
name='Screen-Compose-Root'
|
headerTitleStyle: {
|
||||||
component={ComposeRoot}
|
fontWeight:
|
||||||
options={{
|
totalTextCount > maxTootChars
|
||||||
title: headerContent,
|
? StyleConstants.Font.Weight.Bold
|
||||||
headerTitleStyle: {
|
: StyleConstants.Font.Weight.Normal,
|
||||||
fontWeight:
|
fontSize: StyleConstants.Font.Size.M
|
||||||
totalTextCount > maxTootChars
|
},
|
||||||
? StyleConstants.Font.Weight.Bold
|
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
||||||
: StyleConstants.Font.Weight.Normal,
|
headerLeft,
|
||||||
fontSize: StyleConstants.Font.Size.M
|
headerRight
|
||||||
},
|
}}
|
||||||
headerTintColor:
|
/>
|
||||||
totalTextCount > maxTootChars ? colors.red : colors.secondary,
|
<Stack.Screen
|
||||||
headerLeft,
|
name='Screen-Compose-DraftsList'
|
||||||
headerRight
|
component={ComposeDraftsList}
|
||||||
}}
|
options={{ headerShown: false, presentation: 'modal' }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Compose-DraftsList'
|
name='Screen-Compose-EditAttachment'
|
||||||
component={ComposeDraftsList}
|
component={ComposeEditAttachment}
|
||||||
options={{ headerShown: false, presentation: 'modal' }}
|
options={{ headerShown: false, presentation: 'modal' }}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
</Stack.Navigator>
|
||||||
name='Screen-Compose-EditAttachment'
|
</ComposeContext.Provider>
|
||||||
component={ComposeEditAttachment}
|
</ComponentEmojis>
|
||||||
options={{ headerShown: false, presentation: 'modal' }}
|
|
||||||
/>
|
|
||||||
</Stack.Navigator>
|
|
||||||
</ComposeContext.Provider>
|
|
||||||
</SafeAreaView>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
base: { flex: 1 }
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ScreenCompose
|
export default ScreenCompose
|
||||||
|
@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
import { chunk, forEach, groupBy, sortBy } from 'lodash'
|
||||||
import React, { useContext, useEffect, useMemo, useRef } from 'react'
|
import React, { useContext, useEffect, useMemo, useRef } from 'react'
|
||||||
import { AccessibilityInfo, findNodeHandle, FlatList, StyleSheet, View } from 'react-native'
|
import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native'
|
||||||
import { Circle } from 'react-native-animated-spinkit'
|
import { Circle } from 'react-native-animated-spinkit'
|
||||||
import ComposeActions from './Root/Actions'
|
import ComposeActions from './Root/Actions'
|
||||||
import ComposePosting from './Posting'
|
import ComposePosting from './Posting'
|
||||||
@ -14,9 +14,7 @@ import ComposeRootHeader from './Root/Header'
|
|||||||
import ComposeRootSuggestion from './Root/Suggestion'
|
import ComposeRootSuggestion from './Root/Suggestion'
|
||||||
import ComposeContext from './utils/createContext'
|
import ComposeContext from './utils/createContext'
|
||||||
import ComposeDrafts from './Root/Drafts'
|
import ComposeDrafts from './Root/Drafts'
|
||||||
import FastImage from 'react-native-fast-image'
|
|
||||||
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
|
||||||
import { ComposeState } from './utils/types'
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import {
|
import {
|
||||||
getInstanceConfigurationStatusCharsURL,
|
getInstanceConfigurationStatusCharsURL,
|
||||||
@ -24,30 +22,6 @@ import {
|
|||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const prefetchEmojis = (
|
|
||||||
sortedEmojis: NonNullable<ComposeState['emoji']['emojis']>,
|
|
||||||
reduceMotionEnabled: boolean
|
|
||||||
) => {
|
|
||||||
const prefetches: { uri: string }[] = []
|
|
||||||
let requestedIndex = 0
|
|
||||||
sortedEmojis.forEach(sorted => {
|
|
||||||
sorted.data.forEach(emojis =>
|
|
||||||
emojis.forEach(emoji => {
|
|
||||||
if (requestedIndex > 40) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
prefetches.push({
|
|
||||||
uri: reduceMotionEnabled ? emoji.static_url : emoji.url
|
|
||||||
})
|
|
||||||
requestedIndex++
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
FastImage.preload(prefetches)
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export let instanceConfigurationStatusCharsURL = 23
|
export let instanceConfigurationStatusCharsURL = 23
|
||||||
|
|
||||||
const ComposeRoot = React.memo(
|
const ComposeRoot = React.memo(
|
||||||
@ -62,7 +36,6 @@ const ComposeRoot = React.memo(
|
|||||||
|
|
||||||
const accessibleRefDrafts = useRef(null)
|
const accessibleRefDrafts = useRef(null)
|
||||||
const accessibleRefAttachments = useRef(null)
|
const accessibleRefAttachments = useRef(null)
|
||||||
const accessibleRefEmojis = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tagDrafts = findNodeHandle(accessibleRefDrafts.current)
|
const tagDrafts = findNodeHandle(accessibleRefDrafts.current)
|
||||||
@ -110,18 +83,13 @@ const ComposeRoot = React.memo(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
composeDispatch({
|
|
||||||
type: 'emoji',
|
|
||||||
payload: { ...composeState.emoji, emojis: sortedEmojis }
|
|
||||||
})
|
|
||||||
prefetchEmojis(sortedEmojis, reduceMotionEnabled)
|
|
||||||
}
|
}
|
||||||
}, [emojisData, reduceMotionEnabled])
|
}, [emojisData, reduceMotionEnabled])
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
const listEmpty = useMemo(() => {
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return (
|
return (
|
||||||
<View key='listEmpty' style={styles.loading}>
|
<View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}>
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@ -129,17 +97,12 @@ const ComposeRoot = React.memo(
|
|||||||
}, [isFetching])
|
}, [isFetching])
|
||||||
|
|
||||||
const Footer = useMemo(
|
const Footer = useMemo(
|
||||||
() => (
|
() => <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />,
|
||||||
<ComposeRootFooter
|
[accessibleRefAttachments.current]
|
||||||
accessibleRefAttachments={accessibleRefAttachments}
|
|
||||||
accessibleRefEmojis={accessibleRefEmojis}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[accessibleRefAttachments.current, accessibleRefEmojis.current]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<ComposeRootSuggestion
|
<ComposeRootSuggestion
|
||||||
@ -166,15 +129,4 @@ const ComposeRoot = React.memo(
|
|||||||
() => true
|
() => true
|
||||||
)
|
)
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
base: {
|
|
||||||
flex: 1
|
|
||||||
},
|
|
||||||
contentView: { flex: 1 },
|
|
||||||
loading: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default ComposeRoot
|
export default ComposeRoot
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import analytics from '@components/analytics'
|
import analytics from '@components/analytics'
|
||||||
|
import EmojisContext from '@components/Emojis/helpers/EmojisContext'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
import { useActionSheet } from '@expo/react-native-action-sheet'
|
import { useActionSheet } from '@expo/react-native-action-sheet'
|
||||||
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useContext, useMemo } from 'react'
|
import React, { useContext, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
import chooseAndUploadAttachment from './Footer/addAttachment'
|
import chooseAndUploadAttachment from './Footer/addAttachment'
|
||||||
@ -30,22 +31,19 @@ const ComposeActions: React.FC = () => {
|
|||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}, [composeState.poll.active, composeState.attachments.uploads])
|
||||||
const attachmentOnPress = useCallback(async () => {
|
const attachmentOnPress = () => {
|
||||||
if (composeState.poll.active) return
|
if (composeState.poll.active) return
|
||||||
|
|
||||||
if (
|
if (composeState.attachments.uploads.length < instanceConfigurationStatusMaxAttachments) {
|
||||||
composeState.attachments.uploads.length <
|
|
||||||
instanceConfigurationStatusMaxAttachments
|
|
||||||
) {
|
|
||||||
analytics('compose_actions_attachment_press', {
|
analytics('compose_actions_attachment_press', {
|
||||||
count: composeState.attachments.uploads.length
|
count: composeState.attachments.uploads.length
|
||||||
})
|
})
|
||||||
return await chooseAndUploadAttachment({
|
return chooseAndUploadAttachment({
|
||||||
composeDispatch,
|
composeDispatch,
|
||||||
showActionSheetWithOptions
|
showActionSheetWithOptions
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
|
|
||||||
const pollColor = useMemo(() => {
|
const pollColor = useMemo(() => {
|
||||||
if (composeState.attachments.uploads.length) return colors.disabled
|
if (composeState.attachments.uploads.length) return colors.disabled
|
||||||
@ -56,7 +54,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}, [composeState.poll.active, composeState.attachments.uploads])
|
||||||
const pollOnPress = useCallback(() => {
|
const pollOnPress = () => {
|
||||||
if (!composeState.attachments.uploads.length) {
|
if (!composeState.attachments.uploads.length) {
|
||||||
analytics('compose_actions_poll_press', {
|
analytics('compose_actions_poll_press', {
|
||||||
current: composeState.poll.active
|
current: composeState.poll.active
|
||||||
@ -70,7 +68,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
if (composeState.poll.active) {
|
if (composeState.poll.active) {
|
||||||
composeState.textInputFocus.refs.text.current?.focus()
|
composeState.textInputFocus.refs.text.current?.focus()
|
||||||
}
|
}
|
||||||
}, [composeState.poll.active, composeState.attachments.uploads])
|
}
|
||||||
|
|
||||||
const visibilityIcon = useMemo(() => {
|
const visibilityIcon = useMemo(() => {
|
||||||
switch (composeState.visibility) {
|
switch (composeState.visibility) {
|
||||||
@ -84,7 +82,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
return 'Mail'
|
return 'Mail'
|
||||||
}
|
}
|
||||||
}, [composeState.visibility])
|
}, [composeState.visibility])
|
||||||
const visibilityOnPress = useCallback(() => {
|
const visibilityOnPress = () => {
|
||||||
if (!composeState.visibilityLock) {
|
if (!composeState.visibilityLock) {
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
@ -133,9 +131,9 @@ const ComposeActions: React.FC = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [composeState.visibility])
|
}
|
||||||
|
|
||||||
const spoilerOnPress = useCallback(() => {
|
const spoilerOnPress = () => {
|
||||||
analytics('compose_actions_spoiler_press', {
|
analytics('compose_actions_spoiler_press', {
|
||||||
current: composeState.spoiler.active
|
current: composeState.spoiler.active
|
||||||
})
|
})
|
||||||
@ -147,29 +145,45 @@ const ComposeActions: React.FC = () => {
|
|||||||
type: 'spoiler',
|
type: 'spoiler',
|
||||||
payload: { active: !composeState.spoiler.active }
|
payload: { active: !composeState.spoiler.active }
|
||||||
})
|
})
|
||||||
}, [composeState.spoiler.active, composeState.textInputFocus])
|
}
|
||||||
|
|
||||||
|
const { emojisState, emojisDispatch } = useContext(EmojisContext)
|
||||||
const emojiColor = useMemo(() => {
|
const emojiColor = useMemo(() => {
|
||||||
if (!composeState.emoji.emojis) return colors.disabled
|
if (!emojisState.emojis.length) return colors.disabled
|
||||||
|
|
||||||
if (composeState.emoji.active) {
|
if (emojisState.targetIndex !== -1) {
|
||||||
return colors.primaryDefault
|
return colors.primaryDefault
|
||||||
} else {
|
} else {
|
||||||
return colors.secondary
|
return colors.secondary
|
||||||
}
|
}
|
||||||
}, [composeState.emoji.active, composeState.emoji.emojis])
|
}, [emojisState.emojis.length, emojisState.targetIndex])
|
||||||
const emojiOnPress = useCallback(() => {
|
// useEffect(() => {
|
||||||
analytics('compose_actions_emojis_press', {
|
// const showSubscription = Keyboard.addListener('keyboardWillShow', () => {
|
||||||
current: composeState.emoji.active
|
// composeDispatch({ type: 'emoji/shown', payload: false })
|
||||||
})
|
// })
|
||||||
if (composeState.emoji.emojis) {
|
|
||||||
layoutAnimation()
|
// return () => {
|
||||||
composeDispatch({
|
// showSubscription.remove()
|
||||||
type: 'emoji',
|
// }
|
||||||
payload: { ...composeState.emoji, active: !composeState.emoji.active }
|
// }, [])
|
||||||
})
|
const emojiOnPress = () => {
|
||||||
|
if (emojisState.targetIndex === -1) {
|
||||||
|
Keyboard.dismiss()
|
||||||
}
|
}
|
||||||
}, [composeState.emoji.active, composeState.emoji.emojis])
|
const focusedPropsIndex = emojisState.inputProps?.findIndex(props => props.isFocused.current)
|
||||||
|
if (focusedPropsIndex === -1) return
|
||||||
|
emojisDispatch({ type: 'target', payload: focusedPropsIndex })
|
||||||
|
// Keyboard.dismiss()
|
||||||
|
// analytics('compose_actions_emojis_press', {
|
||||||
|
// current: composeState.emoji.active
|
||||||
|
// })
|
||||||
|
// if (composeState.emoji.emojis) {
|
||||||
|
// composeDispatch({
|
||||||
|
// type: 'emoji',
|
||||||
|
// payload: { ...composeState.emoji, active: !composeState.emoji.active }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -186,12 +200,8 @@ const ComposeActions: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityLabel={t(
|
accessibilityLabel={t('content.root.actions.attachment.accessibilityLabel')}
|
||||||
'content.root.actions.attachment.accessibilityLabel'
|
accessibilityHint={t('content.root.actions.attachment.accessibilityHint')}
|
||||||
)}
|
|
||||||
accessibilityHint={t(
|
|
||||||
'content.root.actions.attachment.accessibilityHint'
|
|
||||||
)}
|
|
||||||
accessibilityState={{
|
accessibilityState={{
|
||||||
disabled: composeState.poll.active
|
disabled: composeState.poll.active
|
||||||
}}
|
}}
|
||||||
@ -213,10 +223,9 @@ const ComposeActions: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityLabel={t(
|
accessibilityLabel={t('content.root.actions.visibility.accessibilityLabel', {
|
||||||
'content.root.actions.visibility.accessibilityLabel',
|
visibility: composeState.visibility
|
||||||
{ visibility: composeState.visibility }
|
})}
|
||||||
)}
|
|
||||||
accessibilityState={{ disabled: composeState.visibilityLock }}
|
accessibilityState={{ disabled: composeState.visibilityLock }}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={visibilityOnPress}
|
onPress={visibilityOnPress}
|
||||||
@ -224,17 +233,13 @@ const ComposeActions: React.FC = () => {
|
|||||||
<Icon
|
<Icon
|
||||||
name={visibilityIcon}
|
name={visibilityIcon}
|
||||||
size={24}
|
size={24}
|
||||||
color={
|
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
|
||||||
composeState.visibilityLock ? colors.disabled : colors.secondary
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole='button'
|
accessibilityRole='button'
|
||||||
accessibilityLabel={t(
|
accessibilityLabel={t('content.root.actions.spoiler.accessibilityLabel')}
|
||||||
'content.root.actions.spoiler.accessibilityLabel'
|
|
||||||
)}
|
|
||||||
accessibilityState={{ expanded: composeState.spoiler.active }}
|
accessibilityState={{ expanded: composeState.spoiler.active }}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={spoilerOnPress}
|
onPress={spoilerOnPress}
|
||||||
@ -242,11 +247,7 @@ const ComposeActions: React.FC = () => {
|
|||||||
<Icon
|
<Icon
|
||||||
name='AlertTriangle'
|
name='AlertTriangle'
|
||||||
size={24}
|
size={24}
|
||||||
color={
|
color={composeState.spoiler.active ? colors.primaryDefault : colors.secondary}
|
||||||
composeState.spoiler.active
|
|
||||||
? colors.primaryDefault
|
|
||||||
: colors.secondary
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -255,8 +256,8 @@ const ComposeActions: React.FC = () => {
|
|||||||
accessibilityLabel={t('content.root.actions.emoji.accessibilityLabel')}
|
accessibilityLabel={t('content.root.actions.emoji.accessibilityLabel')}
|
||||||
accessibilityHint={t('content.root.actions.emoji.accessibilityHint')}
|
accessibilityHint={t('content.root.actions.emoji.accessibilityHint')}
|
||||||
accessibilityState={{
|
accessibilityState={{
|
||||||
disabled: composeState.emoji.emojis ? false : true,
|
disabled: emojisState.emojis.length ? false : true,
|
||||||
expanded: composeState.emoji.active
|
expanded: emojisState.targetIndex !== -1
|
||||||
}}
|
}}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={emojiOnPress}
|
onPress={emojiOnPress}
|
||||||
|
@ -1,31 +1,21 @@
|
|||||||
import ComposeAttachments from '@screens/Compose/Root/Footer/Attachments'
|
import ComposeAttachments from '@screens/Compose/Root/Footer/Attachments'
|
||||||
import ComposeEmojis from '@screens/Compose/Root/Footer/Emojis'
|
|
||||||
import ComposePoll from '@screens/Compose/Root/Footer/Poll'
|
import ComposePoll from '@screens/Compose/Root/Footer/Poll'
|
||||||
import ComposeReply from '@screens/Compose/Root/Footer/Reply'
|
import ComposeReply from '@screens/Compose/Root/Footer/Reply'
|
||||||
import ComposeContext from '@screens/Compose/utils/createContext'
|
import ComposeContext from '@screens/Compose/utils/createContext'
|
||||||
import React, { RefObject, useContext } from 'react'
|
import React, { RefObject, useContext } from 'react'
|
||||||
import { SectionList, View } from 'react-native'
|
import { View } from 'react-native'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
accessibleRefAttachments: RefObject<View>
|
accessibleRefAttachments: RefObject<View>
|
||||||
accessibleRefEmojis: RefObject<SectionList>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposeRootFooter: React.FC<Props> = ({
|
const ComposeRootFooter: React.FC<Props> = ({ accessibleRefAttachments }) => {
|
||||||
accessibleRefAttachments,
|
|
||||||
accessibleRefEmojis
|
|
||||||
}) => {
|
|
||||||
const { composeState } = useContext(ComposeContext)
|
const { composeState } = useContext(ComposeContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{composeState.emoji.active ? (
|
|
||||||
<ComposeEmojis accessibleRefEmojis={accessibleRefEmojis} />
|
|
||||||
) : null}
|
|
||||||
{composeState.attachments.uploads.length ? (
|
{composeState.attachments.uploads.length ? (
|
||||||
<ComposeAttachments
|
<ComposeAttachments accessibleRefAttachments={accessibleRefAttachments} />
|
||||||
accessibleRefAttachments={accessibleRefAttachments}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
{composeState.poll.active ? <ComposePoll /> : null}
|
{composeState.poll.active ? <ComposePoll /> : null}
|
||||||
{composeState.replyToStatus ? <ComposeReply /> : null}
|
{composeState.replyToStatus ? <ComposeReply /> : null}
|
||||||
|
@ -26,9 +26,8 @@ const updateText = ({
|
|||||||
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
|
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
|
||||||
const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
|
const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
|
||||||
|
|
||||||
const newTextWithSpace = `${
|
const newTextWithSpace = `${whiteSpaceFront || type === 'suggestion' ? '' : ' '
|
||||||
whiteSpaceFront || type === 'suggestion' ? '' : ' '
|
}${newText}${whiteSpaceRear ? '' : ' '}`
|
||||||
}${newText}${whiteSpaceRear ? '' : ' '}`
|
|
||||||
|
|
||||||
formatText({
|
formatText({
|
||||||
textInput,
|
textInput,
|
||||||
|
@ -9,16 +9,15 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
|
|||||||
count: 0,
|
count: 0,
|
||||||
raw: '',
|
raw: '',
|
||||||
formatted: undefined,
|
formatted: undefined,
|
||||||
selection: { start: 0, end: 0 }
|
selection: { start: 0 }
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
count: 0,
|
count: 0,
|
||||||
raw: '',
|
raw: '',
|
||||||
formatted: undefined,
|
formatted: undefined,
|
||||||
selection: { start: 0, end: 0 }
|
selection: { start: 0 }
|
||||||
},
|
},
|
||||||
tag: undefined,
|
tag: undefined,
|
||||||
emoji: { active: false, emojis: undefined },
|
|
||||||
poll: {
|
poll: {
|
||||||
active: false,
|
active: false,
|
||||||
total: 2,
|
total: 2,
|
||||||
|
@ -35,8 +35,6 @@ const composeReducer = (
|
|||||||
return { ...state, text: { ...state.text, ...action.payload } }
|
return { ...state, text: { ...state.text, ...action.payload } }
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return { ...state, tag: action.payload }
|
return { ...state, tag: action.payload }
|
||||||
case 'emoji':
|
|
||||||
return { ...state, emoji: action.payload }
|
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return { ...state, poll: { ...state.poll, ...action.payload } }
|
return { ...state, poll: { ...state.poll, ...action.payload } }
|
||||||
case 'attachments/sensitive':
|
case 'attachments/sensitive':
|
||||||
|
17
src/screens/Compose/utils/types.d.ts
vendored
17
src/screens/Compose/utils/types.d.ts
vendored
@ -26,13 +26,13 @@ export type ComposeState = {
|
|||||||
count: number
|
count: number
|
||||||
raw: string
|
raw: string
|
||||||
formatted: ReactNode
|
formatted: ReactNode
|
||||||
selection: { start: number; end: number }
|
selection: { start: number; end?: number }
|
||||||
}
|
}
|
||||||
text: {
|
text: {
|
||||||
count: number
|
count: number
|
||||||
raw: string
|
raw: string
|
||||||
formatted: ReactNode
|
formatted: ReactNode
|
||||||
selection: { start: number; end: number }
|
selection: { start: number; end?: number }
|
||||||
}
|
}
|
||||||
tag?: {
|
tag?: {
|
||||||
type: 'url' | 'accounts' | 'hashtags'
|
type: 'url' | 'accounts' | 'hashtags'
|
||||||
@ -40,15 +40,6 @@ export type ComposeState = {
|
|||||||
offset: number
|
offset: number
|
||||||
length: number
|
length: number
|
||||||
}
|
}
|
||||||
emoji: {
|
|
||||||
active: boolean
|
|
||||||
emojis:
|
|
||||||
| {
|
|
||||||
title: string
|
|
||||||
data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
|
|
||||||
}[]
|
|
||||||
| undefined
|
|
||||||
}
|
|
||||||
poll: {
|
poll: {
|
||||||
active: boolean
|
active: boolean
|
||||||
total: number
|
total: number
|
||||||
@ -96,10 +87,6 @@ export type ComposeAction =
|
|||||||
type: 'tag'
|
type: 'tag'
|
||||||
payload: ComposeState['tag']
|
payload: ComposeState['tag']
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: 'emoji'
|
|
||||||
payload: ComposeState['emoji']
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: 'poll'
|
type: 'poll'
|
||||||
payload: Partial<ComposeState['poll']>
|
payload: Partial<ComposeState['poll']>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user