diff --git a/src/components/Emojis/List.tsx b/src/components/Emojis/List.tsx index 9f5aeb14..0360734c 100644 --- a/src/components/Emojis/List.tsx +++ b/src/components/Emojis/List.tsx @@ -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 } diff --git a/src/components/Emojis/helpers/EmojisContext.tsx b/src/components/Emojis/helpers/EmojisContext.tsx index f821ce03..52935d9a 100644 --- a/src/components/Emojis/helpers/EmojisContext.tsx +++ b/src/components/Emojis/helpers/EmojisContext.tsx @@ -7,6 +7,7 @@ type inputProps = { isFocused: MutableRefObject ref?: RefObject // For controlling focus maxLength?: number + addFunc?: (add: string) => void // For none default state update } export type EmojisState = { diff --git a/src/screens/Compose.tsx b/src/screens/Compose.tsx index 3a7aa3ab..4a130def 100644 --- a/src/screens/Compose.tsx +++ b/src/screens/Compose.tsx @@ -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> = ({ 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> = ({ 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> = ({ }) 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> = ({ 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> = ({ 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> = ({ }` }, [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(composeState.textInputFocus.current === 'text'), + maxLength: maxTootChars + } + ] + return ( - - - - - maxTootChars - ? StyleConstants.Font.Weight.Bold - : StyleConstants.Font.Weight.Normal, - fontSize: StyleConstants.Font.Size.M - }, - headerTintColor: - totalTextCount > maxTootChars ? colors.red : colors.secondary, - headerLeft, - headerRight - }} - /> - - - - - - + + + maxTootChars + ? StyleConstants.Font.Weight.Bold + : StyleConstants.Font.Weight.Normal, + fontSize: StyleConstants.Font.Size.M + }, + headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary, + headerLeft, + headerRight + }} + /> + + + + + ) } -const styles = StyleSheet.create({ - base: { flex: 1 } -}) - export default ScreenCompose diff --git a/src/screens/Compose/Root.tsx b/src/screens/Compose/Root.tsx index 9aeb08f9..22e21bc4 100644 --- a/src/screens/Compose/Root.tsx +++ b/src/screens/Compose/Root.tsx @@ -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, - 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 ( - + ) @@ -129,17 +97,12 @@ const ComposeRoot = React.memo( }, [isFetching]) const Footer = useMemo( - () => ( - - ), - [accessibleRefAttachments.current, accessibleRefEmojis.current] + () => , + [accessibleRefAttachments.current] ) return ( - + ( true ) -const styles = StyleSheet.create({ - base: { - flex: 1 - }, - contentView: { flex: 1 }, - loading: { - flex: 1, - alignItems: 'center' - } -}) - export default ComposeRoot diff --git a/src/screens/Compose/Root/Actions.tsx b/src/screens/Compose/Root/Actions.tsx index 347afbac..8ac5b960 100644 --- a/src/screens/Compose/Root/Actions.tsx +++ b/src/screens/Compose/Root/Actions.tsx @@ -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 ( { > { /> { } /> { } /> @@ -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} diff --git a/src/screens/Compose/Root/Footer.tsx b/src/screens/Compose/Root/Footer.tsx index 55ccbb6b..37871324 100644 --- a/src/screens/Compose/Root/Footer.tsx +++ b/src/screens/Compose/Root/Footer.tsx @@ -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 - accessibleRefEmojis: RefObject } -const ComposeRootFooter: React.FC = ({ - accessibleRefAttachments, - accessibleRefEmojis -}) => { +const ComposeRootFooter: React.FC = ({ accessibleRefAttachments }) => { const { composeState } = useContext(ComposeContext) return ( <> - {composeState.emoji.active ? ( - - ) : null} {composeState.attachments.uploads.length ? ( - + ) : null} {composeState.poll.active ? : null} {composeState.replyToStatus ? : null} diff --git a/src/screens/Compose/updateText.ts b/src/screens/Compose/updateText.ts index 4d09ad4b..ec6af88c 100644 --- a/src/screens/Compose/updateText.ts +++ b/src/screens/Compose/updateText.ts @@ -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, diff --git a/src/screens/Compose/utils/initialState.ts b/src/screens/Compose/utils/initialState.ts index 85306b3b..28fbd460 100644 --- a/src/screens/Compose/utils/initialState.ts +++ b/src/screens/Compose/utils/initialState.ts @@ -9,16 +9,15 @@ const composeInitialState: Omit = { 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, diff --git a/src/screens/Compose/utils/reducer.ts b/src/screens/Compose/utils/reducer.ts index 0480393b..921a9f0d 100644 --- a/src/screens/Compose/utils/reducer.ts +++ b/src/screens/Compose/utils/reducer.ts @@ -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': diff --git a/src/screens/Compose/utils/types.d.ts b/src/screens/Compose/utils/types.d.ts index 7f4102d8..40c31c3a 100644 --- a/src/screens/Compose/utils/types.d.ts +++ b/src/screens/Compose/utils/types.d.ts @@ -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[][] - }[] - | 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