Allow custom exit

This commit is contained in:
xmflsct 2022-09-23 00:21:41 +02:00
parent 8ef2b89a09
commit 7d9056b562
12 changed files with 38 additions and 71 deletions

View File

@ -1,20 +1,12 @@
import EmojisButton from '@components/Emojis/Button' import EmojisButton from '@components/Emojis/Button'
import EmojisList from '@components/Emojis/List' import EmojisList from '@components/Emojis/List'
import { PasteInputRef } from '@mattermost/react-native-paste-input'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager' import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useEmojisQuery } from '@utils/queryHooks/emojis' import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice' import { getInstanceFrequentEmojis } from '@utils/slices/instancesSlice'
import { chunk, forEach, groupBy, sortBy } from 'lodash' import { chunk, forEach, groupBy, sortBy } from 'lodash'
import React, { import React, { createRef, PropsWithChildren, useEffect, useReducer, useState } from 'react'
createRef,
PropsWithChildren,
RefObject,
useEffect,
useReducer,
useState
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Keyboard, KeyboardAvoidingView, TextInput, View } from 'react-native' import { Keyboard, KeyboardAvoidingView, View } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { Edge, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -49,7 +41,6 @@ const prefetchEmojis = (
export type Props = { export type Props = {
inputProps: EmojisState['inputProps'] inputProps: EmojisState['inputProps']
focusRef?: RefObject<TextInput | PasteInputRef>
customButton?: boolean customButton?: boolean
customEdges?: Edge[] customEdges?: Edge[]
customBehavior?: 'height' | 'padding' | 'position' customBehavior?: 'height' | 'padding' | 'position'
@ -60,7 +51,6 @@ export const emojis: Emojis = createRef()
const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
children, children,
inputProps, inputProps,
focusRef,
customButton = false, customButton = false,
customEdges = ['bottom'], customEdges = ['bottom'],
customBehavior customBehavior
@ -79,14 +69,14 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
if (data && data.length) { if (data && data.length) {
let sortedEmojis: NonNullable<Emojis['current']> = [] let sortedEmojis: NonNullable<Emojis['current']> = []
forEach(groupBy(sortBy(data, ['category', 'shortcode']), 'category'), (value, key) => forEach(groupBy(sortBy(data, ['category', 'shortcode']), 'category'), (value, key) =>
sortedEmojis.push({ title: key, data: chunk(value, 4) }) sortedEmojis.push({ title: key, data: chunk(value, 5) })
) )
if (frequentEmojis.length) { if (frequentEmojis.length) {
sortedEmojis.unshift({ sortedEmojis.unshift({
title: t('componentEmojis:frequentUsed'), title: t('componentEmojis:frequentUsed'),
data: chunk( data: chunk(
frequentEmojis.map(e => e.emoji), frequentEmojis.map(e => e.emoji),
4 5
), ),
type: 'frequent' type: 'frequent'
}) })
@ -115,11 +105,6 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
hideSubscription.remove() hideSubscription.remove()
} }
}, [inputProps]) }, [inputProps])
useEffect(() => {
if (focusRef) {
setTimeout(() => focusRef.current?.focus(), 500)
}
}, [])
return ( return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior={customBehavior}> <KeyboardAvoidingView style={{ flex: 1 }} behavior={customBehavior}>
@ -129,7 +114,7 @@ const ComponentEmojis: React.FC<Props & PropsWithChildren> = ({
{children} {children}
<View <View
style={[ style={[
keyboardShown ? { position: 'absolute', bottom: 0, width: '100%' } : null, { position: 'absolute', bottom: 0, width: '100%' },
{ {
marginBottom: keyboardShown && emojisState.targetIndex === -1 ? insets.bottom : 0 marginBottom: keyboardShown && emojisState.targetIndex === -1 ? insets.bottom : 0
} }

View File

@ -27,7 +27,7 @@ const EmojisList = () => {
const { reduceMotionEnabled } = useAccessibility() const { reduceMotionEnabled } = useAccessibility()
const { t } = useTranslation() const { t } = useTranslation()
const { emojisState } = useContext(EmojisContext) const { emojisState, emojisDispatch } = useContext(EmojisContext)
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
const addEmoji = (shortcode: string) => { const addEmoji = (shortcode: string) => {
@ -165,6 +165,17 @@ const EmojisList = () => {
autoCorrect={false} autoCorrect={false}
spellCheck={false} spellCheck={false}
/> />
<Pressable
style={{ paddingLeft: StyleConstants.Spacing.M }}
onPress={() => {
if (emojisState.targetIndex !== -1) {
emojisState.inputProps[emojisState.targetIndex].ref?.current?.focus()
}
emojisDispatch({ type: 'target', payload: -1 })
}}
>
<Icon name='ChevronDown' size={StyleConstants.Font.Size.L} color={colors.secondary} />
</Pressable>
</View> </View>
<SectionList <SectionList
accessible accessible

View File

@ -5,9 +5,8 @@ type inputProps = {
value: [string, (value: string) => void] value: [string, (value: string) => void]
selection: [{ start: number; end?: number }, (selection: { start: number; end?: number }) => void] selection: [{ start: number; end?: number }, (selection: { start: number; end?: number }) => void]
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 Emojis = MutableRefObject< export type Emojis = MutableRefObject<

View File

@ -365,7 +365,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
selection => composeDispatch({ type: 'text', payload: { selection } }) selection => composeDispatch({ type: 'text', payload: { selection } })
], ],
isFocused: composeState.textInputFocus.isFocused.text, isFocused: composeState.textInputFocus.isFocused.text,
maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0) maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0),
ref: composeState.textInputFocus.refs.text
}, },
{ {
value: [ value: [
@ -377,7 +378,8 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({
selection => composeDispatch({ type: 'spoiler', payload: { selection } }) selection => composeDispatch({ type: 'spoiler', payload: { selection } })
], ],
isFocused: composeState.textInputFocus.isFocused.spoiler, isFocused: composeState.textInputFocus.isFocused.spoiler,
maxLength: maxTootChars - composeState.text.count maxLength: maxTootChars - composeState.text.count,
ref: composeState.textInputFocus.refs.spoiler
} }
] ]

View File

@ -1,7 +1,7 @@
import { HeaderLeft } from '@components/Header' import { HeaderLeft } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators' import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import React, { useCallback } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { KeyboardAvoidingView, Platform } from 'react-native' import { KeyboardAvoidingView, Platform } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'

View File

@ -1,9 +1,7 @@
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import { useEmojisQuery } from '@utils/queryHooks/emojis'
import { useSearchQuery } from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
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 { 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, 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'
@ -14,19 +12,13 @@ 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 { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { import { getInstanceConfigurationStatusCharsURL } from '@utils/slices/instancesSlice'
getInstanceConfigurationStatusCharsURL,
getInstanceFrequentEmojis
} from '@utils/slices/instancesSlice'
import { useTranslation } from 'react-i18next'
export let instanceConfigurationStatusCharsURL = 23 export let instanceConfigurationStatusCharsURL = 23
const ComposeRoot = React.memo( const ComposeRoot = React.memo(
() => { () => {
const { reduceMotionEnabled } = useAccessibility()
const { colors } = useTheme() const { colors } = useTheme()
instanceConfigurationStatusCharsURL = useSelector( instanceConfigurationStatusCharsURL = useSelector(
@ -42,7 +34,7 @@ const ComposeRoot = React.memo(
tagDrafts && AccessibilityInfo.setAccessibilityFocus(tagDrafts) tagDrafts && AccessibilityInfo.setAccessibilityFocus(tagDrafts)
}, [accessibleRefDrafts.current]) }, [accessibleRefDrafts.current])
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState } = useContext(ComposeContext)
const mapSchemaToType = () => { const mapSchemaToType = () => {
if (composeState.tag) { if (composeState.tag) {
@ -71,30 +63,6 @@ const ComposeRoot = React.memo(
} }
}, [composeState.tag]) }, [composeState.tag])
const { t } = useTranslation()
const { data: emojisData } = useEmojisQuery({})
const frequentEmojis = useSelector(getInstanceFrequentEmojis, () => true)
useEffect(() => {
if (emojisData && emojisData.length) {
const sortedEmojis: {
title: string
data: Pick<Mastodon.Emoji, 'shortcode' | 'url' | 'static_url'>[][]
}[] = []
forEach(groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'), (value, key) =>
sortedEmojis.push({ title: key, data: chunk(value, 5) })
)
if (frequentEmojis.length) {
sortedEmojis.unshift({
title: t('componentEmojis:frequentUsed'),
data: chunk(
frequentEmojis.map(e => e.emoji),
5
)
})
}
}
}, [emojisData, reduceMotionEnabled])
const listEmpty = useMemo(() => { const listEmpty = useMemo(() => {
if (isFetching) { if (isFetching) {
return ( return (

View File

@ -21,6 +21,7 @@ const ComposeSpoilerInput: React.FC = () => {
return ( return (
<TextInput <TextInput
ref={composeState.textInputFocus.refs.spoiler}
keyboardAppearance={mode} keyboardAppearance={mode}
style={{ style={{
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,

View File

@ -39,7 +39,7 @@ const composeInitialState: Omit<ComposeState, 'timestamp'> = {
replyToStatus: undefined, replyToStatus: undefined,
textInputFocus: { textInputFocus: {
current: 'text', current: 'text',
refs: { text: createRef() }, refs: { text: createRef(), spoiler: createRef() },
isFocused: { text: createRef(), spoiler: createRef() } isFocused: { text: createRef(), spoiler: createRef() }
} }
} }

View File

@ -1,3 +1,4 @@
import { RefObject } from 'react';
import { Asset } from 'react-native-image-picker' import { Asset } from 'react-native-image-picker'
export type ExtendedAttachment = { export type ExtendedAttachment = {
@ -39,10 +40,6 @@ export type ComposeState = {
index: number index: number
lastIndex: number lastIndex: number
raw: string raw: string
// type: 'url' | 'accounts' | 'hashtags'
// text: string
// offset: number
// length: number
} }
poll: { poll: {
active: boolean active: boolean
@ -62,7 +59,7 @@ export type ComposeState = {
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
textInputFocus: { textInputFocus: {
current: 'text' | 'spoiler' current: 'text' | 'spoiler'
refs: { text: RefObject<TextInput> } refs: { text: RefObject<TextInput>, spoiler: RefObject<TextInput> }
isFocused: { text: MutableRefObject<boolean>, spoiler: MutableRefObject<boolean> } isFocused: { text: MutableRefObject<boolean>, spoiler: MutableRefObject<boolean> }
} }
} }

View File

@ -9,7 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react' import React, { Dispatch, RefObject, SetStateAction, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, ScrollView } from 'react-native' import { Alert, ScrollView, TextInput } from 'react-native'
import FlashMessage from 'react-native-flash-message' import FlashMessage from 'react-native-flash-message'
const Field: React.FC<{ const Field: React.FC<{
@ -21,19 +21,23 @@ const Field: React.FC<{
const { colors } = useTheme() const { colors } = useTheme()
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
const nameRef = useRef<TextInput>(null)
const valueRef = useRef<TextInput>(null)
const [name, setName] = useState(field?.name || '') const [name, setName] = useState(field?.name || '')
const [value, setValue] = useState(field?.value || '') const [value, setValue] = useState(field?.value || '')
allProps[index * 2] = { allProps[index * 2] = {
value: [name, setName], value: [name, setName],
selection: useState({ start: name.length }), selection: useState({ start: name.length }),
isFocused: useRef<boolean>(false), isFocused: useRef<boolean>(false),
maxLength: 255 maxLength: 255,
ref: nameRef
} }
allProps[index * 2 + 1] = { allProps[index * 2 + 1] = {
value: [value, setValue], value: [value, setValue],
selection: useState({ start: value.length }), selection: useState({ start: value.length }),
isFocused: useRef<boolean>(false), isFocused: useRef<boolean>(false),
maxLength: 255 maxLength: 255,
ref: valueRef
} }
useEffect(() => { useEffect(() => {

View File

@ -93,7 +93,7 @@ const TabMeProfileName: React.FC<
}, [theme, i18n.language, dirty, status, value]) }, [theme, i18n.language, dirty, status, value])
return ( return (
<ComponentEmojis inputProps={[displayNameProps]} focusRef={displayNameProps.ref}> <ComponentEmojis inputProps={[displayNameProps]}>
<ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}> <ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
<ComponentInput <ComponentInput
{...displayNameProps} {...displayNameProps}

View File

@ -93,7 +93,7 @@ const TabMeProfileNote: React.FC<
}, [theme, i18n.language, dirty, status, notes]) }, [theme, i18n.language, dirty, status, notes])
return ( return (
<ComponentEmojis inputProps={[notesProps]} focusRef={notesProps.ref}> <ComponentEmojis inputProps={[notesProps]}>
<ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}> <ScrollView style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
<ComponentInput {...notesProps} multiline /> <ComponentInput {...notesProps} multiline />
</ScrollView> </ScrollView>