1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Spoiler is done

This commit is contained in:
Zhiyuan Zheng
2020-12-06 23:51:13 +01:00
parent 37ad208f8b
commit 662fd08c3a
12 changed files with 409 additions and 162 deletions

View File

@ -21,7 +21,11 @@ const TimelineContent: React.FC<Props> = ({ status, numberOfLines }) => {
{status.spoiler_text ? (
<>
<Text style={{ fontSize: StyleConstants.Font.Size.M }}>
{status.spoiler_text}{' '}
<ParseContent
content={status.spoiler_text}
size={StyleConstants.Font.Size.M}
emojis={status.emojis}
/>{' '}
<Text
onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
style={{

View File

@ -119,7 +119,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
id: accountId
})
}}
iconFront='alert-triangle'
iconFront='flag'
title={`举报 @${account}`}
/>
</MenuContainer>

View File

@ -1,24 +1,41 @@
import React, { ReactNode, useEffect, useReducer, useState } from 'react'
import { Alert, Keyboard, KeyboardAvoidingView } from 'react-native'
import {
ActivityIndicator,
Alert,
Keyboard,
KeyboardAvoidingView,
StyleSheet,
Text
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native'
import sha256 from 'crypto-js/sha256'
import { store } from 'src/store'
import ComposeRoot from './Compose/Root'
import client from 'src/api/client'
import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
import { HeaderLeft, HeaderRight } from 'src/components/Header'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
const Stack = createNativeStackNavigator()
export type PostState = {
spoiler: {
active: boolean
count: number
raw: string
formatted: ReactNode
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'
@ -57,12 +74,12 @@ export type PostState = {
export type PostAction =
| {
type: 'text'
payload: Partial<PostState['text']>
type: 'spoiler'
payload: Partial<PostState['spoiler']>
}
| {
type: 'selection'
payload: PostState['selection']
type: 'text'
payload: Partial<PostState['text']>
}
| {
type: 'tag'
@ -94,22 +111,29 @@ export type PostAction =
}
const postInitialState: PostState = {
text: {
count: 500,
spoiler: {
active: false,
count: 0,
raw: '',
formatted: undefined
formatted: undefined,
selection: { start: 0, end: 0 }
},
text: {
count: 0,
raw: '',
formatted: undefined,
selection: { start: 0, end: 0 }
},
selection: { start: 0, end: 0 },
tag: undefined,
emoji: { active: false, emojis: undefined },
poll: {
active: false,
total: 2,
options: {
'0': undefined,
'1': undefined,
'2': undefined,
'3': undefined,
'4': undefined
'3': undefined
},
multiple: false,
expire: '86400'
@ -123,10 +147,10 @@ const postInitialState: PostState = {
}
const postReducer = (state: PostState, action: PostAction): PostState => {
switch (action.type) {
case 'spoiler':
return { ...state, spoiler: { ...state.spoiler, ...action.payload } }
case 'text':
return { ...state, text: { ...state.text, ...action.payload } }
case 'selection':
return { ...state, selection: action.payload }
case 'tag':
return { ...state, tag: action.payload }
case 'emoji':
@ -152,8 +176,11 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
}
const Compose: React.FC = () => {
const { theme } = useTheme()
const navigation = useNavigation()
const [isSubmitting, setIsSubmitting] = useState(false)
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
Keyboard.addListener('keyboardWillShow', _keyboardDidShow)
@ -175,6 +202,7 @@ const Compose: React.FC = () => {
const [postState, postDispatch] = useReducer(postReducer, postInitialState)
const tootPost = async () => {
setIsSubmitting(true)
if (postState.text.count < 0) {
Alert.alert('字数超限', '', [
{
@ -184,6 +212,10 @@ const Compose: React.FC = () => {
} else {
const formData = new FormData()
if (postState.spoiler.active) {
formData.append('spoiler_text', postState.spoiler.raw)
}
formData.append('status', postState.text.raw)
if (postState.poll.active) {
@ -207,13 +239,25 @@ const Compose: React.FC = () => {
instance: 'local',
url: 'statuses',
headers: {
'Idempotency-Key': Date.now().toString() + Math.random().toString()
'Idempotency-Key': sha256(
postState.spoiler.raw +
postState.text.raw +
postState.poll.options['0'] +
postState.poll.options['1'] +
postState.poll.options['2'] +
postState.poll.options['3'] +
postState.poll.multiple +
postState.poll.expire +
postState.attachments.map(attachment => attachment.id) +
postState.visibility
).toString()
},
body: formData
})
.then(
res => {
if (res.body.id) {
setIsSubmitting(false)
Alert.alert('发布成功', '', [
{
text: '好的',
@ -224,6 +268,7 @@ const Compose: React.FC = () => {
}
])
} else {
setIsSubmitting(false)
Alert.alert('发布失败', '', [
{
text: '返回重试'
@ -232,6 +277,7 @@ const Compose: React.FC = () => {
}
},
error => {
setIsSubmitting(false)
Alert.alert('发布失败', error.body, [
{
text: '返回重试'
@ -240,6 +286,7 @@ const Compose: React.FC = () => {
}
)
.catch(() => {
setIsSubmitting(false)
Alert.alert('发布失败', '', [
{
text: '返回重试'
@ -249,6 +296,10 @@ const Compose: React.FC = () => {
}
}
const totalTextCount =
(postState.spoiler.active ? postState.spoiler.count : 0) +
postState.text.count
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView
@ -274,16 +325,31 @@ const Compose: React.FC = () => {
text='退出编辑'
/>
),
headerCenter: () => <></>,
headerRight: () => (
<HeaderRight
onPress={async () => tootPost()}
text='发嘟嘟'
disabled={
postState.text.raw.length < 1 || postState.text.count < 0
}
/>
)
headerCenter: () => (
<Text
style={[
styles.count,
{
color:
totalTextCount > 500 ? theme.error : theme.secondary
}
]}
>
{totalTextCount} / 500
</Text>
),
headerRight: () =>
isSubmitting ? (
<ActivityIndicator />
) : (
<HeaderRight
onPress={async () => tootPost()}
text='发嘟嘟'
disabled={
postState.text.raw.length < 1 || totalTextCount > 500
}
/>
)
}}
>
{() => (
@ -296,4 +362,11 @@ const Compose: React.FC = () => {
)
}
const styles = StyleSheet.create({
count: {
textAlign: 'center',
fontSize: StyleConstants.Font.Size.M
}
})
export default Compose

View File

@ -169,20 +169,23 @@ const ComposeActions: React.FC<Props> = ({
)
}
/>
<Feather
name='alert-triangle'
size={24}
color={postState.spoiler.active ? theme.primary : theme.secondary}
onPress={() =>
postDispatch({
type: 'spoiler',
payload: { active: !postState.spoiler.active }
})
}
/>
<Feather
name='smile'
size={24}
color={emojiColor}
onPress={emojiOnPress}
/>
<Text
style={[
styles.count,
{ color: postState.text.count < 0 ? theme.error : theme.secondary }
]}
>
{postState.text.count}
</Text>
</Pressable>
)
}

View File

@ -48,12 +48,14 @@ const ComposeEmojis: React.FC<Props> = ({
key={emoji.shortcode}
onPress={() => {
updateText({
origin: textInputRef.current?.isFocused()
? 'text'
: 'spoiler',
postState,
postDispatch,
newText: `:${emoji.shortcode}:`,
type: 'emoji'
})
textInputRef.current?.focus()
postDispatch({
type: 'emoji',
payload: { ...postState.emoji, active: false }

View File

@ -22,6 +22,7 @@ import ComposeActions from './Actions'
import ComposeAttachments from './Attachments'
import ComposeEmojis from './Emojis'
import ComposePoll from './Poll'
import ComposeSpoilerInput from './SpoilerInput'
import ComposeTextInput from './TextInput'
import updateText from './updateText'
import * as Permissions from 'expo-permissions'
@ -94,11 +95,19 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
<FlatList
keyboardShouldPersistTaps='handled'
ListHeaderComponent={
<ComposeTextInput
postState={postState}
postDispatch={postDispatch}
textInputRef={textInputRef}
/>
<>
{postState.spoiler.active ? (
<ComposeSpoilerInput
postState={postState}
postDispatch={postDispatch}
/>
) : null}
<ComposeTextInput
postState={postState}
postDispatch={postDispatch}
textInputRef={textInputRef}
/>
</>
}
ListFooterComponent={
<>
@ -138,19 +147,26 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
<Pressable
key={index}
onPress={() => {
const focusedInput = textInputRef.current?.isFocused()
? 'text'
: 'spoiler'
updateText({
origin: focusedInput,
postState: {
...postState,
selection: {
start: postState.tag!.offset,
end: postState.tag!.offset + postState.tag!.text.length + 1
[focusedInput]: {
...postState[focusedInput],
selection: {
start: postState.tag!.offset,
end:
postState.tag!.offset + postState.tag!.text.length + 1
}
}
},
postDispatch,
newText: item.acct ? `@${item.acct}` : `#${item.name}`,
type: 'suggestion'
})
textInputRef.current?.focus()
}}
style={styles.suggestion}
>

View File

@ -0,0 +1,77 @@
import React, { Dispatch, RefObject } from 'react'
import { StyleSheet, Text, TextInput } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose'
import formatText from './formatText'
export interface Props {
postState: PostState
postDispatch: Dispatch<PostAction>
// textInputRef: RefObject<TextInput>
}
const ComposeSpoilerInput: React.FC<Props> = ({
postState,
postDispatch,
// textInputRef
}) => {
const { theme } = useTheme()
return (
<TextInput
style={[
styles.spoilerInput,
{
color: theme.primary,
borderBottomColor: theme.border
}
]}
autoCapitalize='none'
autoCorrect={false}
autoFocus
enablesReturnKeyAutomatically
multiline
placeholder='折叠部分的警告信息'
placeholderTextColor={theme.secondary}
onChangeText={content =>
formatText({
origin: 'spoiler',
postDispatch,
content
})
}
onSelectionChange={({
nativeEvent: {
selection: { start, end }
}
}) => {
postDispatch({
type: 'spoiler',
payload: { selection: { start, end } }
})
}}
// ref={textInputRef}
scrollEnabled
>
<Text>{postState.spoiler.formatted}</Text>
</TextInput>
)
}
const styles = StyleSheet.create({
spoilerInput: {
fontSize: StyleConstants.Font.Size.M,
marginTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.M,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding,
borderBottomWidth: 0.5
}
})
export default React.memo(
ComposeSpoilerInput,
(prev, next) =>
prev.postState.spoiler.formatted === next.postState.spoiler.formatted
)

View File

@ -36,6 +36,7 @@ const ComposeTextInput: React.FC<Props> = ({
placeholderTextColor={theme.secondary}
onChangeText={content =>
formatText({
origin: 'text',
postDispatch,
content
})
@ -45,7 +46,7 @@ const ComposeTextInput: React.FC<Props> = ({
selection: { start, end }
}
}) => {
postDispatch({ type: 'selection', payload: { start, end } })
postDispatch({ type: 'text', payload: { selection: { start, end } } })
}}
ref={textInputRef}
scrollEnabled
@ -61,7 +62,7 @@ const styles = StyleSheet.create({
marginTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.M,
marginLeft: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding,
marginRight: StyleConstants.Spacing.Global.PagePadding
// borderBottomWidth: 0.5
}
})

View File

@ -7,6 +7,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose'
export interface Params {
origin: 'text' | 'spoiler'
postDispatch: Dispatch<PostAction>
content: string
refetch?: (options?: RefetchOptions | undefined) => Promise<any>
@ -36,6 +37,7 @@ const debouncedSuggestions = debounce(
let prevTags: PostState['tag'][] = []
const formatText = ({
origin,
postDispatch,
content,
disableDebounce = false
@ -70,7 +72,6 @@ const formatText = ({
})
const changedTag = differenceWith(tags, prevTags, isEqual)
// quick delete causes flicking of suggestion box
if (changedTag.length && !disableDebounce) {
if (changedTag[0]!.type !== 'url') {
debouncedSuggestions(postDispatch, changedTag[0])
@ -107,9 +108,9 @@ const formatText = ({
contentLength = contentLength + _content.length
postDispatch({
type: 'text',
type: origin,
payload: {
count: 500 - contentLength,
count: contentLength,
raw: content,
formatted: createElement(Text, null, children)
}

View File

@ -3,19 +3,26 @@ import { PostAction, PostState } from '../Compose'
import formatText from './formatText'
const updateText = ({
origin,
postState,
postDispatch,
newText,
type
}: {
origin: 'text' | 'spoiler'
postState: PostState
postDispatch: Dispatch<PostAction>
newText: string
type: 'emoji' | 'suggestion'
}) => {
if (postState.text.raw.length) {
const contentFront = postState.text.raw.slice(0, postState.selection.start)
const contentRear = postState.text.raw.slice(postState.selection.end)
if (postState[origin].raw.length) {
const contentFront = postState[origin].raw.slice(
0,
postState[origin].selection.start
)
const contentRear = postState[origin].raw.slice(
postState[origin].selection.end
)
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
@ -25,12 +32,14 @@ const updateText = ({
}${newText}${whiteSpaceRear ? '' : ' '}`
formatText({
origin,
postDispatch,
content: [contentFront, newTextWithSpace, contentRear].join(''),
disableDebounce: true
})
} else {
formatText({
origin,
postDispatch,
content: `${newText} `,
disableDebounce: true