POC compose using the new emoji selector

This commit is contained in:
xmflsct 2022-09-18 23:54:50 +02:00
parent 7282434e69
commit 2df23a8a2e
10 changed files with 138 additions and 245 deletions

View File

@ -44,7 +44,7 @@ const EmojisList = () => {
const contentFront = value.slice(0, selection.start)
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]) ? '' : ' '
setValue(
@ -52,7 +52,6 @@ const EmojisList = () => {
)
const addedLength = spaceFront.length + shortcode.length + spaceRear.length
setSelection({ start: selection.start + addedLength })
ref?.current?.setNativeProps({
selection: { start: selection.start + addedLength }

View File

@ -7,6 +7,7 @@ type inputProps = {
isFocused: MutableRefObject<boolean>
ref?: RefObject<TextInput> // For controlling focus
maxLength?: number
addFunc?: (add: string) => void // For none default state update
}
export type EmojisState = {

View File

@ -1,4 +1,6 @@
import analytics from '@components/analytics'
import { ComponentEmojis } from '@components/Emojis'
import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import haptics from '@root/components/haptics'
@ -6,10 +8,7 @@ import { useAppDispatch } from '@root/store'
import formatText from '@screens/Compose/formatText'
import ComposeRoot from '@screens/Compose/Root'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline'
import { updateStoreReview } from '@utils/slices/contextsSlice'
import {
getInstanceAccount,
@ -20,22 +19,9 @@ import {
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { filter } from 'lodash'
import React, {
useCallback,
useEffect,
useMemo,
useReducer,
useState
} from 'react'
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Alert,
Keyboard,
KeyboardAvoidingView,
Platform,
StyleSheet
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Alert, Keyboard, Platform } from 'react-native'
import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux'
import * as Sentry from 'sentry-expo'
@ -60,12 +46,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
const keyboardShown = Keyboard.addListener('keyboardWillShow', () =>
setHasKeyboard(true)
)
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () =>
setHasKeyboard(false)
)
const keyboardShown = Keyboard.addListener('keyboardWillShow', () => setHasKeyboard(true))
const keyboardHidden = Keyboard.addListener('keyboardWillHide', () => setHasKeyboard(false))
return () => {
keyboardShown.remove()
@ -89,32 +71,23 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
attachments: {
...composeInitialState.attachments,
sensitive:
localAccount?.preferences &&
localAccount?.preferences['posting:default:sensitive']
localAccount?.preferences && localAccount?.preferences['posting:default:sensitive']
? localAccount?.preferences['posting:default:sensitive']
: false
},
visibility:
localAccount?.preferences &&
localAccount.preferences['posting:default:visibility']
localAccount?.preferences && localAccount.preferences['posting:default:visibility']
? localAccount.preferences['posting:default:visibility']
: 'public'
}
}
}, [])
const [composeState, composeDispatch] = useReducer(
composeReducer,
initialReducerState
)
const [composeState, composeDispatch] = useReducer(composeReducer, initialReducerState)
const maxTootChars = useSelector(
getInstanceConfigurationStatusMaxChars,
() => true
)
const maxTootChars = useSelector(getInstanceConfigurationStatusMaxChars, () => true)
const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) +
composeState.text.count
(composeState.spoiler.active ? composeState.spoiler.count : 0) + composeState.text.count
// If compose state is dirty, then disallow add back drafts
useEffect(() => {
@ -173,8 +146,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
})
break
case 'reply':
const actualStatus =
params.incomingStatus.reblog || params.incomingStatus
const actualStatus = params.incomingStatus.reblog || params.incomingStatus
if (actualStatus.spoiler_text) {
formatText({
textInput: 'spoiler',
@ -278,16 +250,10 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
if (totalTextCount > maxTootChars) {
return true
}
if (
composeState.attachments.uploads.filter(upload => upload.uploading)
.length > 0
) {
if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) {
return true
}
if (
composeState.attachments.uploads.length === 0 &&
composeState.text.raw.length === 0
) {
if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) {
return true
}
return false
@ -309,18 +275,12 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
composePost(params, composeState)
.then(res => {
haptics('Success')
if (
Platform.OS === 'ios' &&
Platform.constants.osVersion === '13.3'
) {
if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') {
// https://github.com/tooot-app/app/issues/59
} else {
dispatch(updateStoreReview(1))
}
const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Following' }]
queryClient.invalidateQueries(queryKey)
switch (params?.type) {
@ -392,54 +352,61 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
}`
}, [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 (
<KeyboardAvoidingView
style={styles.base}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
<ComponentEmojis
inputProps={inputProps}
customButton
customBehavior={Platform.OS === 'ios' ? 'padding' : undefined}
customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']}
>
<SafeAreaView
style={styles.base}
edges={hasKeyboard ? ['top'] : ['top', 'bottom']}
>
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
<Stack.Screen
name='Screen-Compose-Root'
component={ComposeRoot}
options={{
title: headerContent,
headerTitleStyle: {
fontWeight:
totalTextCount > maxTootChars
? StyleConstants.Font.Weight.Bold
: StyleConstants.Font.Weight.Normal,
fontSize: StyleConstants.Font.Size.M
},
headerTintColor:
totalTextCount > maxTootChars ? colors.red : colors.secondary,
headerLeft,
headerRight
}}
/>
<Stack.Screen
name='Screen-Compose-DraftsList'
component={ComposeDraftsList}
options={{ headerShown: false, presentation: 'modal' }}
/>
<Stack.Screen
name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment}
options={{ headerShown: false, presentation: 'modal' }}
/>
</Stack.Navigator>
</ComposeContext.Provider>
</SafeAreaView>
</KeyboardAvoidingView>
<ComposeContext.Provider value={{ composeState, composeDispatch }}>
<Stack.Navigator initialRouteName='Screen-Compose-Root'>
<Stack.Screen
name='Screen-Compose-Root'
component={ComposeRoot}
options={{
title: headerContent,
headerTitleStyle: {
fontWeight:
totalTextCount > maxTootChars
? StyleConstants.Font.Weight.Bold
: StyleConstants.Font.Weight.Normal,
fontSize: StyleConstants.Font.Size.M
},
headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary,
headerLeft,
headerRight
}}
/>
<Stack.Screen
name='Screen-Compose-DraftsList'
component={ComposeDraftsList}
options={{ headerShown: false, presentation: 'modal' }}
/>
<Stack.Screen
name='Screen-Compose-EditAttachment'
component={ComposeEditAttachment}
options={{ headerShown: false, presentation: 'modal' }}
/>
</Stack.Navigator>
</ComposeContext.Provider>
</ComponentEmojis>
)
}
const styles = StyleSheet.create({
base: { flex: 1 }
})
export default ScreenCompose

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { chunk, forEach, groupBy, sortBy } from 'lodash'
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 ComposeActions from './Root/Actions'
import ComposePosting from './Posting'
@ -14,9 +14,7 @@ import ComposeRootHeader from './Root/Header'
import ComposeRootSuggestion from './Root/Suggestion'
import ComposeContext from './utils/createContext'
import ComposeDrafts from './Root/Drafts'
import FastImage from 'react-native-fast-image'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { ComposeState } from './utils/types'
import { useSelector } from 'react-redux'
import {
getInstanceConfigurationStatusCharsURL,
@ -24,30 +22,6 @@ import {
} from '@utils/slices/instancesSlice'
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
const ComposeRoot = React.memo(
@ -62,7 +36,6 @@ const ComposeRoot = React.memo(
const accessibleRefDrafts = useRef(null)
const accessibleRefAttachments = useRef(null)
const accessibleRefEmojis = useRef(null)
useEffect(() => {
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])
const listEmpty = useMemo(() => {
if (isFetching) {
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} />
</View>
)
@ -129,17 +97,12 @@ const ComposeRoot = React.memo(
}, [isFetching])
const Footer = useMemo(
() => (
<ComposeRootFooter
accessibleRefAttachments={accessibleRefAttachments}
accessibleRefEmojis={accessibleRefEmojis}
/>
),
[accessibleRefAttachments.current, accessibleRefEmojis.current]
() => <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />,
[accessibleRefAttachments.current]
)
return (
<View style={styles.base}>
<View style={{ flex: 1 }}>
<FlatList
renderItem={({ item }) => (
<ComposeRootSuggestion
@ -166,15 +129,4 @@ const ComposeRoot = React.memo(
() => true
)
const styles = StyleSheet.create({
base: {
flex: 1
},
contentView: { flex: 1 },
loading: {
flex: 1,
alignItems: 'center'
}
})
export default ComposeRoot

View File

@ -1,12 +1,13 @@
import analytics from '@components/analytics'
import EmojisContext from '@components/Emojis/helpers/EmojisContext'
import Icon from '@components/Icon'
import { useActionSheet } from '@expo/react-native-action-sheet'
import { getInstanceConfigurationStatusMaxAttachments } from '@utils/slices/instancesSlice'
import layoutAnimation from '@utils/styles/layoutAnimation'
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 { Pressable, StyleSheet, View } from 'react-native'
import { Keyboard, Pressable, StyleSheet, View } from 'react-native'
import { useSelector } from 'react-redux'
import ComposeContext from '../utils/createContext'
import chooseAndUploadAttachment from './Footer/addAttachment'
@ -30,22 +31,19 @@ const ComposeActions: React.FC = () => {
return colors.secondary
}
}, [composeState.poll.active, composeState.attachments.uploads])
const attachmentOnPress = useCallback(async () => {
const attachmentOnPress = () => {
if (composeState.poll.active) return
if (
composeState.attachments.uploads.length <
instanceConfigurationStatusMaxAttachments
) {
if (composeState.attachments.uploads.length < instanceConfigurationStatusMaxAttachments) {
analytics('compose_actions_attachment_press', {
count: composeState.attachments.uploads.length
})
return await chooseAndUploadAttachment({
return chooseAndUploadAttachment({
composeDispatch,
showActionSheetWithOptions
})
}
}, [composeState.poll.active, composeState.attachments.uploads])
}
const pollColor = useMemo(() => {
if (composeState.attachments.uploads.length) return colors.disabled
@ -56,7 +54,7 @@ const ComposeActions: React.FC = () => {
return colors.secondary
}
}, [composeState.poll.active, composeState.attachments.uploads])
const pollOnPress = useCallback(() => {
const pollOnPress = () => {
if (!composeState.attachments.uploads.length) {
analytics('compose_actions_poll_press', {
current: composeState.poll.active
@ -70,7 +68,7 @@ const ComposeActions: React.FC = () => {
if (composeState.poll.active) {
composeState.textInputFocus.refs.text.current?.focus()
}
}, [composeState.poll.active, composeState.attachments.uploads])
}
const visibilityIcon = useMemo(() => {
switch (composeState.visibility) {
@ -84,7 +82,7 @@ const ComposeActions: React.FC = () => {
return 'Mail'
}
}, [composeState.visibility])
const visibilityOnPress = useCallback(() => {
const visibilityOnPress = () => {
if (!composeState.visibilityLock) {
showActionSheetWithOptions(
{
@ -133,9 +131,9 @@ const ComposeActions: React.FC = () => {
}
)
}
}, [composeState.visibility])
}
const spoilerOnPress = useCallback(() => {
const spoilerOnPress = () => {
analytics('compose_actions_spoiler_press', {
current: composeState.spoiler.active
})
@ -147,29 +145,45 @@ const ComposeActions: React.FC = () => {
type: 'spoiler',
payload: { active: !composeState.spoiler.active }
})
}, [composeState.spoiler.active, composeState.textInputFocus])
}
const { emojisState, emojisDispatch } = useContext(EmojisContext)
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
} else {
return colors.secondary
}
}, [composeState.emoji.active, composeState.emoji.emojis])
const emojiOnPress = useCallback(() => {
analytics('compose_actions_emojis_press', {
current: composeState.emoji.active
})
if (composeState.emoji.emojis) {
layoutAnimation()
composeDispatch({
type: 'emoji',
payload: { ...composeState.emoji, active: !composeState.emoji.active }
})
}, [emojisState.emojis.length, emojisState.targetIndex])
// useEffect(() => {
// const showSubscription = Keyboard.addListener('keyboardWillShow', () => {
// composeDispatch({ type: 'emoji/shown', payload: false })
// })
// return () => {
// showSubscription.remove()
// }
// }, [])
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 (
<View
@ -186,12 +200,8 @@ const ComposeActions: React.FC = () => {
>
<Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.attachment.accessibilityLabel'
)}
accessibilityHint={t(
'content.root.actions.attachment.accessibilityHint'
)}
accessibilityLabel={t('content.root.actions.attachment.accessibilityLabel')}
accessibilityHint={t('content.root.actions.attachment.accessibilityHint')}
accessibilityState={{
disabled: composeState.poll.active
}}
@ -213,10 +223,9 @@ const ComposeActions: React.FC = () => {
/>
<Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.visibility.accessibilityLabel',
{ visibility: composeState.visibility }
)}
accessibilityLabel={t('content.root.actions.visibility.accessibilityLabel', {
visibility: composeState.visibility
})}
accessibilityState={{ disabled: composeState.visibilityLock }}
style={styles.button}
onPress={visibilityOnPress}
@ -224,17 +233,13 @@ const ComposeActions: React.FC = () => {
<Icon
name={visibilityIcon}
size={24}
color={
composeState.visibilityLock ? colors.disabled : colors.secondary
}
color={composeState.visibilityLock ? colors.disabled : colors.secondary}
/>
}
/>
<Pressable
accessibilityRole='button'
accessibilityLabel={t(
'content.root.actions.spoiler.accessibilityLabel'
)}
accessibilityLabel={t('content.root.actions.spoiler.accessibilityLabel')}
accessibilityState={{ expanded: composeState.spoiler.active }}
style={styles.button}
onPress={spoilerOnPress}
@ -242,11 +247,7 @@ const ComposeActions: React.FC = () => {
<Icon
name='AlertTriangle'
size={24}
color={
composeState.spoiler.active
? colors.primaryDefault
: colors.secondary
}
color={composeState.spoiler.active ? colors.primaryDefault : colors.secondary}
/>
}
/>
@ -255,8 +256,8 @@ const ComposeActions: React.FC = () => {
accessibilityLabel={t('content.root.actions.emoji.accessibilityLabel')}
accessibilityHint={t('content.root.actions.emoji.accessibilityHint')}
accessibilityState={{
disabled: composeState.emoji.emojis ? false : true,
expanded: composeState.emoji.active
disabled: emojisState.emojis.length ? false : true,
expanded: emojisState.targetIndex !== -1
}}
style={styles.button}
onPress={emojiOnPress}

View File

@ -1,31 +1,21 @@
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 ComposeReply from '@screens/Compose/Root/Footer/Reply'
import ComposeContext from '@screens/Compose/utils/createContext'
import React, { RefObject, useContext } from 'react'
import { SectionList, View } from 'react-native'
import { View } from 'react-native'
export interface Props {
accessibleRefAttachments: RefObject<View>
accessibleRefEmojis: RefObject<SectionList>
}
const ComposeRootFooter: React.FC<Props> = ({
accessibleRefAttachments,
accessibleRefEmojis
}) => {
const ComposeRootFooter: React.FC<Props> = ({ accessibleRefAttachments }) => {
const { composeState } = useContext(ComposeContext)
return (
<>
{composeState.emoji.active ? (
<ComposeEmojis accessibleRefEmojis={accessibleRefEmojis} />
) : null}
{composeState.attachments.uploads.length ? (
<ComposeAttachments
accessibleRefAttachments={accessibleRefAttachments}
/>
<ComposeAttachments accessibleRefAttachments={accessibleRefAttachments} />
) : null}
{composeState.poll.active ? <ComposePoll /> : null}
{composeState.replyToStatus ? <ComposeReply /> : null}

View File

@ -26,9 +26,8 @@ const updateText = ({
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
const newTextWithSpace = `${
whiteSpaceFront || type === 'suggestion' ? '' : ' '
}${newText}${whiteSpaceRear ? '' : ' '}`
const newTextWithSpace = `${whiteSpaceFront || type === 'suggestion' ? '' : ' '
}${newText}${whiteSpaceRear ? '' : ' '}`
formatText({
textInput,

View File

@ -9,16 +9,15 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
count: 0,
raw: '',
formatted: undefined,
selection: { start: 0, end: 0 }
selection: { start: 0 }
},
text: {
count: 0,
raw: '',
formatted: undefined,
selection: { start: 0, end: 0 }
selection: { start: 0 }
},
tag: undefined,
emoji: { active: false, emojis: undefined },
poll: {
active: false,
total: 2,

View File

@ -35,8 +35,6 @@ const composeReducer = (
return { ...state, text: { ...state.text, ...action.payload } }
case 'tag':
return { ...state, tag: action.payload }
case 'emoji':
return { ...state, emoji: action.payload }
case 'poll':
return { ...state, poll: { ...state.poll, ...action.payload } }
case 'attachments/sensitive':

View File

@ -26,13 +26,13 @@ export type ComposeState = {
count: number
raw: string
formatted: ReactNode
selection: { start: number; end: number }
selection: { start: number; end?: number }
}
text: {
count: number
raw: string
formatted: ReactNode
selection: { start: number; end: number }
selection: { start: number; end?: number }
}
tag?: {
type: 'url' | 'accounts' | 'hashtags'
@ -40,15 +40,6 @@ export type ComposeState = {
offset: number
length: number
}
emoji: {
active: boolean
emojis:
| {
title: string
data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
}[]
| undefined
}
poll: {
active: boolean
total: number
@ -96,10 +87,6 @@ export type ComposeAction =
type: 'tag'
payload: ComposeState['tag']
}
| {
type: 'emoji'
payload: ComposeState['emoji']
}
| {
type: 'poll'
payload: Partial<ComposeState['poll']>