mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
POC compose using the new emoji selector
This commit is contained in:
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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':
|
||||
|
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
|
||||
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']>
|
||||
|
Reference in New Issue
Block a user