mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +01:00
Translated Timeline components
This commit is contained in:
parent
d5ccc95704
commit
ea465c828a
3
App.tsx
3
App.tsx
@ -1,4 +1,3 @@
|
||||
import 'react-native-gesture-handler'
|
||||
import NetInfo from '@react-native-community/netinfo'
|
||||
import client from '@root/api/client'
|
||||
import Index from '@root/Index'
|
||||
@ -173,4 +172,4 @@ const App: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
export default React.memo(App, () => true)
|
||||
|
@ -70,7 +70,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
const showLocalCorrect = localCorrupt
|
||||
? toast({
|
||||
type: 'error',
|
||||
content: '登录已过期',
|
||||
message: '登录已过期',
|
||||
description: '请重新登录',
|
||||
autoHide: false
|
||||
})
|
||||
@ -253,7 +253,6 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
||||
<NavigationContainer
|
||||
ref={navigationRef}
|
||||
theme={themes[mode]}
|
||||
// key={i18n.language}
|
||||
onReady={navigationContainerOnReady}
|
||||
onStateChange={navigationContainerOnStateChange}
|
||||
>
|
||||
|
@ -85,44 +85,48 @@ const MenuRow: React.FC<Props> = ({
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.back}>
|
||||
{content && content.length ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{loading && !iconBack && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<>
|
||||
<Feather
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.M + 2}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
{(content && content.length) ||
|
||||
switchValue !== undefined ||
|
||||
iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content && content.length ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{loading && !iconBack && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<>
|
||||
<Feather
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.M + 2}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
@ -139,17 +143,16 @@ const styles = StyleSheet.create({
|
||||
paddingRight: StyleConstants.Spacing.Global.PagePadding
|
||||
},
|
||||
front: {
|
||||
flex: 1,
|
||||
flexBasis: '70%',
|
||||
flex: 2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
back: {
|
||||
flex: 1,
|
||||
flexBasis: '30%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
marginLeft: StyleConstants.Spacing.M
|
||||
},
|
||||
iconFront: {
|
||||
marginRight: 8
|
||||
|
@ -1,11 +1,12 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { QueryStatus } from 'react-query'
|
||||
import Button from '@components/Button'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { QueryStatus } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
status: QueryStatus
|
||||
@ -13,7 +14,8 @@ export interface Props {
|
||||
}
|
||||
|
||||
const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
const { theme } = useTheme()
|
||||
const { mode, theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('timeline')
|
||||
|
||||
const children = useMemo(() => {
|
||||
switch (status) {
|
||||
@ -30,9 +32,13 @@ const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
加载错误
|
||||
{t('empty.error.message')}
|
||||
</Text>
|
||||
<Button type='text' content='重试' onPress={() => refetch()} />
|
||||
<Button
|
||||
type='text'
|
||||
content={t('empty.error.button')}
|
||||
onPress={() => refetch()}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
case 'success':
|
||||
@ -44,12 +50,12 @@ const TimelineEmpty: React.FC<Props> = ({ status, refetch }) => {
|
||||
color={theme.primary}
|
||||
/>
|
||||
<Text style={[styles.error, { color: theme.primary }]}>
|
||||
空无一物
|
||||
{t('empty.success.message')}
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [status])
|
||||
}, [mode, i18n.language, status])
|
||||
return <View style={styles.base} children={children} />
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { ParseEmojis } from '@root/components/Parse'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
@ -17,6 +18,7 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
action,
|
||||
notification = false
|
||||
}) => {
|
||||
const { t } = useTranslation('timeline')
|
||||
const { theme } = useTheme()
|
||||
const navigation = useNavigation()
|
||||
const name = account.display_name || account.username
|
||||
@ -41,7 +43,7 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content('置顶')}
|
||||
{content(t('shared.actioned.pinned'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
@ -55,7 +57,7 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(`${name} 喜欢了你的嘟嘟`)}
|
||||
{content(t('shared.actioned.favourite', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
@ -70,7 +72,7 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(`${name} 开始关注你`)}
|
||||
{content(t('shared.actioned.follow', { name }))}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
@ -84,7 +86,7 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
{content('你参与的投票已结束')}
|
||||
{content(t('shared.actioned.poll'))}
|
||||
</>
|
||||
)
|
||||
break
|
||||
@ -98,7 +100,11 @@ const TimelineActioned: React.FC<Props> = ({
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Pressable onPress={onPress}>
|
||||
{content(`${name} 转嘟了${notification ? '你的嘟嘟' : ''}`)}
|
||||
{content(
|
||||
notification
|
||||
? t('shared.actioned.reblog.notification', { name })
|
||||
: t('shared.actioned.reblog.default', { name })
|
||||
)}
|
||||
</Pressable>
|
||||
</>
|
||||
)
|
||||
|
@ -8,40 +8,10 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
type,
|
||||
stateKey,
|
||||
prevState
|
||||
}: {
|
||||
id: string
|
||||
type: 'favourite' | 'reblog' | 'bookmark'
|
||||
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
||||
prevState?: boolean
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'favourite':
|
||||
case 'reblog':
|
||||
case 'bookmark':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon
|
||||
|
||||
if (!res.body[stateKey] === prevState) {
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
status: Mastodon.Status
|
||||
@ -50,14 +20,32 @@ export interface Props {
|
||||
|
||||
const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
const navigation = useNavigation()
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const iconColor = theme.secondary
|
||||
const iconColorAction = (state: boolean) =>
|
||||
state ? theme.primary : theme.secondary
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(
|
||||
async ({
|
||||
type,
|
||||
state
|
||||
}: {
|
||||
type: 'favourite' | 'reblog' | 'bookmark'
|
||||
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
|
||||
state?: boolean
|
||||
}) => {
|
||||
return client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon
|
||||
},
|
||||
[]
|
||||
)
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||
onMutate: ({ type, stateKey, state }) => {
|
||||
queryClient.cancelQueries(queryKey)
|
||||
const oldData = queryClient.getQueryData(queryKey)
|
||||
|
||||
@ -75,7 +63,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
: reblog
|
||||
? 'reblog.id'
|
||||
: 'id',
|
||||
id
|
||||
status.id
|
||||
])
|
||||
if (tempIndex >= 0) {
|
||||
tootIndex = tempIndex
|
||||
@ -94,14 +82,14 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
} else {
|
||||
if (queryKey[0] === 'Notifications') {
|
||||
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
|
||||
typeof prevState === 'boolean' ? !prevState : true
|
||||
typeof state === 'boolean' ? !state : true
|
||||
} else {
|
||||
if (reblog) {
|
||||
old!.pages[pageIndex].toots[tootIndex].reblog![stateKey] =
|
||||
typeof prevState === 'boolean' ? !prevState : true
|
||||
typeof state === 'boolean' ? !state : true
|
||||
} else {
|
||||
old!.pages[pageIndex].toots[tootIndex][stateKey] =
|
||||
typeof prevState === 'boolean' ? !prevState : true
|
||||
typeof state === 'boolean' ? !state : true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -114,9 +102,14 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
onError: (_, { type }, oldData) => {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(`timeline:shared.actions.${type}.function`)
|
||||
})
|
||||
})
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
@ -133,30 +126,27 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
const onPressReblog = useCallback(
|
||||
() =>
|
||||
mutate({
|
||||
id: status.id,
|
||||
type: 'reblog',
|
||||
stateKey: 'reblogged',
|
||||
prevState: status.reblogged
|
||||
state: status.reblogged
|
||||
}),
|
||||
[status.reblogged]
|
||||
)
|
||||
const onPressFavourite = useCallback(
|
||||
() =>
|
||||
mutate({
|
||||
id: status.id,
|
||||
type: 'favourite',
|
||||
stateKey: 'favourited',
|
||||
prevState: status.favourited
|
||||
state: status.favourited
|
||||
}),
|
||||
[status.favourited]
|
||||
)
|
||||
const onPressBookmark = useCallback(
|
||||
() =>
|
||||
mutate({
|
||||
id: status.id,
|
||||
type: 'bookmark',
|
||||
stateKey: 'bookmarked',
|
||||
prevState: status.bookmarked
|
||||
state: status.bookmarked
|
||||
}),
|
||||
[status.bookmarked]
|
||||
)
|
||||
|
@ -1,13 +1,14 @@
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import AttachmentAudio from '@components/Timelines/Timeline/Shared/Attachment/Audio'
|
||||
import AttachmentImage from '@components/Timelines/Timeline/Shared/Attachment/Image'
|
||||
import AttachmentUnsupported from '@components/Timelines/Timeline/Shared/Attachment/Unsupported'
|
||||
import AttachmentVideo from '@components/Timelines/Timeline/Shared/Attachment/Video'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import haptics from '@root/components/haptics'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { IImageInfo } from 'react-native-image-zoom-viewer/built/image-viewer.type'
|
||||
|
||||
@ -16,6 +17,8 @@ export interface Props {
|
||||
}
|
||||
|
||||
const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
const { t } = useTranslation('timeline')
|
||||
|
||||
const [sensitiveShown, setSensitiveShown] = useState(status.sensitive)
|
||||
const onPressBlurView = useCallback(() => {
|
||||
layoutAnimation()
|
||||
@ -106,7 +109,7 @@ const TimelineAttachment: React.FC<Props> = ({ status }) => {
|
||||
<Pressable style={styles.sensitiveBlur}>
|
||||
<Button
|
||||
type='text'
|
||||
content='显示敏感内容'
|
||||
content={t('shared.attachment.sensitive.button')}
|
||||
overlay
|
||||
onPress={onPressBlurView}
|
||||
/>
|
||||
|
@ -2,9 +2,10 @@ import Button from '@components/Button'
|
||||
import openLink from '@components/openLink'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import { Blurhash } from 'gl-react-blurhash'
|
||||
import { Surface } from 'gl-react-expo'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
@ -16,7 +17,9 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
||||
sensitiveShown,
|
||||
attachment
|
||||
}) => {
|
||||
const { t } = useTranslation('timeline')
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
{attachment.blurhash ? (
|
||||
@ -38,12 +41,12 @@ const AttachmentUnsupported: React.FC<Props> = ({
|
||||
{ color: attachment.blurhash ? theme.background : theme.primary }
|
||||
]}
|
||||
>
|
||||
文件读取错误
|
||||
{t('shared.attachment.unsupported.text')}
|
||||
</Text>
|
||||
{attachment.remote_url ? (
|
||||
<Button
|
||||
type='text'
|
||||
content='尝试远程链接'
|
||||
content={t('shared.attachment.unsupported.button')}
|
||||
size='S'
|
||||
overlay
|
||||
onPress={async () => await openLink(attachment.remote_url!)}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
@ -14,6 +15,8 @@ const TimelineContent: React.FC<Props> = ({
|
||||
numberOfLines,
|
||||
highlighted = false
|
||||
}) => {
|
||||
const { t } = useTranslation('timeline')
|
||||
|
||||
return (
|
||||
<>
|
||||
{status.spoiler_text ? (
|
||||
@ -35,7 +38,7 @@ const TimelineContent: React.FC<Props> = ({
|
||||
mentions={status.mentions}
|
||||
tags={status.tags}
|
||||
numberOfLines={0}
|
||||
expandHint='隐藏内容'
|
||||
expandHint={t('shared.content.expandHint')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
|
||||
export interface Props {
|
||||
hasNextPage?: boolean
|
||||
@ -18,13 +19,16 @@ const TimelineEnd: React.FC<Props> = ({ hasNextPage }) => {
|
||||
<Chase size={StyleConstants.Font.Size.L} color={theme.secondary} />
|
||||
) : (
|
||||
<Text style={[styles.text, { color: theme.secondary }]}>
|
||||
居然刷到底了,喝杯{' '}
|
||||
<Feather
|
||||
name='coffee'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
/>{' '}
|
||||
吧
|
||||
<Trans
|
||||
i18nKey='timeline:shared.end.message' // optional -> fallbacks to defaults if not provided
|
||||
components={[
|
||||
<Feather
|
||||
name='coffee'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
@ -1,42 +1,41 @@
|
||||
import client from '@api/client'
|
||||
import haptics from '@components/haptics'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import { toast } from '@components/toast'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
conversation: Mastodon.Conversation
|
||||
}
|
||||
|
||||
const fireMutation = async ({ id }: { id: string }) => {
|
||||
const res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `conversations/${id}`
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '删除私信成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '删除私信失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(async () => {
|
||||
const res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `conversations/${conversation.id}`
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', message: '删除私信成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
message: '删除私信失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}, [])
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
queryClient.cancelQueries(queryKey)
|
||||
@ -56,14 +55,14 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
toast({ type: 'error', message: '请重试', autoHide: false })
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useTheme()
|
||||
|
||||
const actionOnPress = useCallback(() => mutate({ id: conversation.id }), [])
|
||||
const actionOnPress = useCallback(() => mutate(), [])
|
||||
|
||||
const actionChildren = useMemo(
|
||||
() => (
|
||||
@ -78,31 +77,14 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndDate}>
|
||||
<View style={styles.namdAndAccount}>
|
||||
<Text numberOfLines={1}>
|
||||
<ParseEmojis
|
||||
content={
|
||||
conversation.accounts[0].display_name ||
|
||||
conversation.accounts[0].username
|
||||
}
|
||||
emojis={conversation.accounts[0].emojis}
|
||||
fontBold
|
||||
/>
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{conversation.accounts[0].acct}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.nameAndMeta}>
|
||||
<HeaderSharedAccount account={conversation.accounts[0]} />
|
||||
<View style={styles.meta}>
|
||||
{conversation.last_status?.created_at && (
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{relativeTime(conversation.last_status?.created_at)}
|
||||
</Text>
|
||||
)}
|
||||
{conversation.last_status?.created_at ? (
|
||||
<HeaderSharedCreated
|
||||
created_at={conversation.last_status?.created_at}
|
||||
/>
|
||||
) : null}
|
||||
{conversation.unread && (
|
||||
<Feather name='circle' color={theme.blue} style={styles.unread} />
|
||||
)}
|
||||
@ -123,16 +105,8 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameAndDate: {
|
||||
width: '80%'
|
||||
},
|
||||
namdAndAccount: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
account: {
|
||||
flexShrink: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
nameAndMeta: {
|
||||
flex: 4
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
@ -147,7 +121,7 @@ const styles = StyleSheet.create({
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
},
|
||||
action: {
|
||||
flexBasis: '20%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
import BottomSheet from '@components/BottomSheet'
|
||||
import openLink from '@components/openLink'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { getLocalUrl } from '@utils/slices/instancesSlice'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
@ -9,9 +6,13 @@ import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
|
||||
import HeaderDefaultActionsDomain from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsDomain'
|
||||
import HeaderDefaultActionsStatus from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { useSelector } from 'react-redux'
|
||||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKey.Timeline
|
||||
@ -27,32 +28,12 @@ const TimelineHeaderDefault: React.FC<Props> = ({
|
||||
const domain = status.uri
|
||||
? status.uri.split(new RegExp(/\/\/(.*?)\//))[1]
|
||||
: ''
|
||||
const name = status.account.display_name || status.account.username
|
||||
const emojis = status.account.emojis
|
||||
const account = status.account.acct
|
||||
const { theme } = useTheme()
|
||||
|
||||
const localDomain = useSelector(getLocalUrl)
|
||||
const [since, setSince] = useState(relativeTime(status.created_at))
|
||||
const [modalVisible, setBottomSheetVisible] = useState(false)
|
||||
|
||||
// causing full re-render
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setSince(relativeTime(status.created_at))
|
||||
}, 1000)
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [since])
|
||||
|
||||
const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
|
||||
const onPressApplication = useCallback(
|
||||
async () =>
|
||||
status.application!.website &&
|
||||
(await openLink(status.application!.website)),
|
||||
[]
|
||||
)
|
||||
|
||||
const pressableAction = useMemo(
|
||||
() => (
|
||||
@ -67,42 +48,12 @@ const TimelineHeaderDefault: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={queryKey ? { flexBasis: '80%' } : { flexBasis: '100%' }}>
|
||||
<View style={styles.nameAndAccount}>
|
||||
<Text numberOfLines={1}>
|
||||
<ParseEmojis content={name} emojis={emojis} fontBold />
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.accountAndMeta}>
|
||||
<HeaderSharedAccount account={status.account} />
|
||||
<View style={styles.meta}>
|
||||
<View>
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
{status.visibility === 'private' && (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
style={styles.visibility}
|
||||
/>
|
||||
)}
|
||||
{status.application && status.application.name !== 'Web' && (
|
||||
<View>
|
||||
<Text
|
||||
onPress={onPressApplication}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {status.application.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<HeaderSharedCreated created_at={status.created_at} />
|
||||
<HeaderSharedVisibility visibility={status.visibility} />
|
||||
<HeaderSharedApplication application={status.application} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -154,16 +105,8 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline'
|
||||
},
|
||||
nameAndMeta: {
|
||||
flexBasis: '80%'
|
||||
},
|
||||
nameAndAccount: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
account: {
|
||||
flex: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
accountAndMeta: {
|
||||
flex: 4
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
@ -174,15 +117,8 @@ const styles = StyleSheet.create({
|
||||
created_at: {
|
||||
...StyleConstants.FontStyle.S
|
||||
},
|
||||
visibility: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
application: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
action: {
|
||||
flexBasis: '20%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
|
@ -1,64 +1,14 @@
|
||||
import React from 'react'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import client from '@api/client'
|
||||
import haptics from '@components/haptics'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { toast } from '@components/toast'
|
||||
import haptics from '@root/components/haptics'
|
||||
|
||||
const fireMutation = async ({
|
||||
type,
|
||||
id,
|
||||
stateKey
|
||||
}: {
|
||||
type: 'mute' | 'block' | 'reports'
|
||||
id: string
|
||||
stateKey?: 'muting' | 'blocking'
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'block':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `accounts/${id}/${type}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey!] === true) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({ type: 'error', content: '功能错误', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'reports':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `reports`,
|
||||
params: {
|
||||
account_id: id!
|
||||
}
|
||||
})
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '举报账户成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '举报账户失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey?: QueryKey.Timeline
|
||||
account: Pick<Mastodon.Account, 'id' | 'username' | 'acct' | 'url'>
|
||||
account: Pick<Mastodon.Account, 'id' | 'acct'>
|
||||
setBottomSheetVisible: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
@ -67,51 +17,103 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
||||
account,
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(
|
||||
async ({ type }: { type: 'mute' | 'block' | 'reports' }) => {
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'block':
|
||||
return client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `accounts/${account.id}/${type}`
|
||||
})
|
||||
break
|
||||
case 'reports':
|
||||
return client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `reports`,
|
||||
params: {
|
||||
account_id: account.id!
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
onSuccess: (_, { type }) => {
|
||||
haptics('Success')
|
||||
toast({
|
||||
type: 'success',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.account.${type}.function`,
|
||||
{ acct: account.acct }
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
onError: (_, { type }) => {
|
||||
haptics('Error')
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.error.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.account.${type}.function`,
|
||||
{ acct: account.acct }
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryKey && queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于账户' />
|
||||
<MenuHeader
|
||||
heading={t('timeline:shared.header.default.actions.account.heading')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'mute',
|
||||
id: account.id,
|
||||
stateKey: 'muting'
|
||||
})
|
||||
mutate({ type: 'mute' })
|
||||
}}
|
||||
iconFront='eye-off'
|
||||
title={`隐藏 @${account.acct} 的嘟嘟`}
|
||||
title={t('timeline:shared.header.default.actions.account.mute.button', {
|
||||
acct: account.acct
|
||||
})}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'block',
|
||||
id: account.id,
|
||||
stateKey: 'blocking'
|
||||
})
|
||||
mutate({ type: 'block' })
|
||||
}}
|
||||
iconFront='x-circle'
|
||||
title={`屏蔽用户 @${account.acct}`}
|
||||
title={t(
|
||||
'timeline:shared.header.default.actions.account.block.button',
|
||||
{
|
||||
acct: account.acct
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'reports',
|
||||
id: account.id
|
||||
})
|
||||
mutate({ type: 'reports' })
|
||||
}}
|
||||
iconFront='flag'
|
||||
title={`举报 @${account.acct}`}
|
||||
title={t(
|
||||
'timeline:shared.header.default.actions.account.report.button',
|
||||
{
|
||||
acct: account.acct
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
|
@ -1,33 +1,11 @@
|
||||
import React from 'react'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import client from '@api/client'
|
||||
import MenuContainer from '@components/Menu/Container'
|
||||
import MenuHeader from '@components/Menu/Header'
|
||||
import MenuRow from '@components/Menu/Row'
|
||||
import { toast } from '@components/toast'
|
||||
|
||||
const fireMutation = async ({ domain }: { domain: string }) => {
|
||||
const res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `domain_blocks`,
|
||||
params: {
|
||||
domain: domain!
|
||||
}
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
toast({ type: 'success', content: '隐藏域名成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
toast({
|
||||
type: 'error',
|
||||
content: '隐藏域名失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
@ -40,23 +18,46 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
|
||||
domain,
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(() => {
|
||||
return client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `domain_blocks`,
|
||||
params: {
|
||||
domain: domain!
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onSettled: () => {
|
||||
toast({
|
||||
type: 'success',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.domain.block.function`
|
||||
)
|
||||
})
|
||||
})
|
||||
queryClient.invalidateQueries(queryKey)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于域名' />
|
||||
<MenuHeader
|
||||
heading={t(`timeline:shared.header.default.actions.domain.heading`)}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({ domain })
|
||||
mutate()
|
||||
}}
|
||||
iconFront='cloud-off'
|
||||
title={`屏蔽域名 ${domain}`}
|
||||
title={t(`timeline:shared.header.default.actions.domain.block.button`, {
|
||||
domain
|
||||
})}
|
||||
/>
|
||||
</MenuContainer>
|
||||
)
|
||||
|
@ -1,59 +1,14 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
import client from '@api/client'
|
||||
import haptics from '@components/haptics'
|
||||
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
|
||||
import { TimelineData } from '@components/Timelines/Timeline'
|
||||
import { toast } from '@components/toast'
|
||||
import { TimelineData } from '@root/components/Timelines/Timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
type,
|
||||
stateKey,
|
||||
prevState
|
||||
}: {
|
||||
id: string
|
||||
type: 'mute' | 'pin' | 'delete'
|
||||
stateKey: 'muted' | 'pinned' | 'id'
|
||||
prevState?: boolean
|
||||
}) => {
|
||||
let res
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
res = await client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon
|
||||
|
||||
if (!res.body[stateKey] === prevState) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
toast({ type: 'error', content: '功能错误' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
case 'delete':
|
||||
res = await client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `statuses/${id}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey] === id) {
|
||||
toast({ type: 'success', content: '删除成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
toast({ type: 'error', content: '删除失败' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
export interface Props {
|
||||
queryKey: QueryKey.Timeline
|
||||
@ -67,19 +22,56 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
setBottomSheetVisible
|
||||
}) => {
|
||||
const navigation = useNavigation()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const fireMutation = useCallback(
|
||||
({ type, state }: { type: 'mute' | 'pin' | 'delete'; state?: boolean }) => {
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
return client({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
|
||||
}) // bug in response from Mastodon, but onMutate ignore the error in response
|
||||
break
|
||||
case 'delete':
|
||||
return client({
|
||||
method: 'delete',
|
||||
instance: 'local',
|
||||
url: `statuses/${status.id}`
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
enum mapTypeToProp {
|
||||
mute = 'muted',
|
||||
pin = 'pinned'
|
||||
}
|
||||
const { mutate } = useMutation(fireMutation, {
|
||||
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||
onMutate: ({ type, state }) => {
|
||||
queryClient.cancelQueries(queryKey)
|
||||
const oldData = queryClient.getQueryData(queryKey)
|
||||
|
||||
switch (type) {
|
||||
case 'mute':
|
||||
case 'pin':
|
||||
haptics('Success')
|
||||
toast({
|
||||
type: 'success',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.status.${type}.function`
|
||||
)
|
||||
})
|
||||
})
|
||||
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||
let tootIndex = -1
|
||||
const pageIndex = findIndex(old?.pages, page => {
|
||||
const tempIndex = findIndex(page.toots, ['id', id])
|
||||
const tempIndex = findIndex(page.toots, ['id', status.id])
|
||||
if (tempIndex >= 0) {
|
||||
tootIndex = tempIndex
|
||||
return true
|
||||
@ -89,9 +81,8 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
})
|
||||
|
||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||
old!.pages[pageIndex].toots[tootIndex][
|
||||
stateKey as 'muted' | 'pinned'
|
||||
] = typeof prevState === 'boolean' ? !prevState : true
|
||||
old!.pages[pageIndex].toots[tootIndex][mapTypeToProp[type]] =
|
||||
typeof state === 'boolean' ? !state : true // State could be null from response
|
||||
}
|
||||
|
||||
return old
|
||||
@ -105,7 +96,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
...old,
|
||||
pages: old?.pages.map(paging => ({
|
||||
...paging,
|
||||
toots: paging.toots.filter(toot => toot.id !== id)
|
||||
toots: paging.toots.filter(toot => toot.id !== status.id)
|
||||
}))
|
||||
}
|
||||
)
|
||||
@ -114,36 +105,45 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
onError: (_, { type }, oldData) => {
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.status.${type}.function`
|
||||
)
|
||||
})
|
||||
})
|
||||
queryClient.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='关于嘟嘟' />
|
||||
<MenuHeader
|
||||
heading={t('timeline:shared.header.default.actions.status.heading')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'delete',
|
||||
id: status.id,
|
||||
stateKey: 'id'
|
||||
})
|
||||
mutate({ type: 'delete' })
|
||||
}}
|
||||
iconFront='trash'
|
||||
title='删除嘟嘟'
|
||||
title={t('timeline:shared.header.default.actions.status.delete.button')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'确认删除嘟嘟?',
|
||||
'你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。',
|
||||
t('timeline:shared.header.default.actions.status.edit.alert.title'),
|
||||
t(
|
||||
'timeline:shared.header.default.actions.status.edit.alert.message'
|
||||
),
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: t('common:buttons.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: '删除并重新编辑',
|
||||
text: t(
|
||||
'timeline:shared.header.default.actions.status.edit.alert.confirm'
|
||||
),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await client({
|
||||
@ -160,7 +160,14 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
toast({ type: 'error', content: '删除失败' })
|
||||
toast({
|
||||
type: 'error',
|
||||
message: t('common:toastMessage.success.message', {
|
||||
function: t(
|
||||
`timeline:shared.header.default.actions.status.edit.function`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -168,35 +175,41 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
||||
)
|
||||
}}
|
||||
iconFront='trash'
|
||||
title='删除并重新编辑'
|
||||
title={t('timeline:shared.header.default.actions.status.edit.button')}
|
||||
/>
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'mute',
|
||||
id: status.id,
|
||||
stateKey: 'muted',
|
||||
prevState: status.muted
|
||||
})
|
||||
mutate({ type: 'mute', state: status.muted })
|
||||
}}
|
||||
iconFront='volume-x'
|
||||
title={status.muted ? '取消静音对话' : '静音对话'}
|
||||
title={
|
||||
status.muted
|
||||
? t(
|
||||
'timeline:shared.header.default.actions.status.mute.button.negative'
|
||||
)
|
||||
: t(
|
||||
'timeline:shared.header.default.actions.status.mute.button.positive'
|
||||
)
|
||||
}
|
||||
/>
|
||||
{/* Also note that reblogs cannot be pinned. */}
|
||||
{(status.visibility === 'public' || status.visibility === 'unlisted') && (
|
||||
<MenuRow
|
||||
onPress={() => {
|
||||
setBottomSheetVisible(false)
|
||||
mutate({
|
||||
type: 'pin',
|
||||
id: status.id,
|
||||
stateKey: 'pinned',
|
||||
prevState: status.pinned
|
||||
})
|
||||
mutate({ type: 'pin', state: status.pinned })
|
||||
}}
|
||||
iconFront='anchor'
|
||||
title={status.pinned ? '取消置顶' : '置顶'}
|
||||
title={
|
||||
status.pinned
|
||||
? t(
|
||||
'timeline:shared.header.default.actions.status.pin.button.negative'
|
||||
)
|
||||
: t(
|
||||
'timeline:shared.header.default.actions.status.pin.button.positive'
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</MenuContainer>
|
||||
|
@ -1,33 +1,26 @@
|
||||
import client from '@api/client'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import haptics from '@components/haptics'
|
||||
import openLink from '@components/openLink'
|
||||
import { ParseEmojis } from '@components/Parse'
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import { toast } from '@components/toast'
|
||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Pressable, StyleSheet, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { useQuery } from 'react-query'
|
||||
import HeaderSharedApplication from './HeaderShared/Application'
|
||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||
import HeaderSharedCreated from './HeaderShared/Created'
|
||||
import HeaderSharedAccount from './HeaderShared/Account'
|
||||
|
||||
export interface Props {
|
||||
notification: Mastodon.Notification
|
||||
}
|
||||
|
||||
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
const actualAccount = notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
const name = actualAccount.display_name || actualAccount.username
|
||||
const emojis = actualAccount.emojis
|
||||
const account = actualAccount.acct
|
||||
const { theme } = useTheme()
|
||||
|
||||
const [since, setSince] = useState(relativeTime(notification.created_at))
|
||||
|
||||
const { status, data, refetch } = useQuery(
|
||||
['Relationship', { id: notification.account.id }],
|
||||
relationshipFetch,
|
||||
@ -39,19 +32,6 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
Mastodon.Relationship | undefined
|
||||
>()
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setSince(relativeTime(notification.created_at))
|
||||
}, 1000)
|
||||
}, [since])
|
||||
|
||||
const applicationOnPress = useCallback(
|
||||
async () =>
|
||||
notification.status?.application.website &&
|
||||
(await openLink(notification.status.application.website)),
|
||||
[]
|
||||
)
|
||||
|
||||
const relationshipOnPress = useCallback(() => {
|
||||
client({
|
||||
method: 'post',
|
||||
@ -72,7 +52,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
haptics('Error')
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
toast({ type: 'error', message: '请重试', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
})
|
||||
@ -127,44 +107,22 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<View style={styles.nameAndMeta}>
|
||||
<View style={styles.nameAndAccount}>
|
||||
<Text numberOfLines={1}>
|
||||
<ParseEmojis content={name} emojis={emojis} fontBold />
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.account, { color: theme.secondary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.accountAndMeta}>
|
||||
<HeaderSharedAccount
|
||||
account={
|
||||
notification.status
|
||||
? notification.status.account
|
||||
: notification.account
|
||||
}
|
||||
/>
|
||||
<View style={styles.meta}>
|
||||
<View>
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>
|
||||
{since}
|
||||
</Text>
|
||||
</View>
|
||||
{notification.status?.visibility === 'private' && (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
style={styles.visibility}
|
||||
/>
|
||||
)}
|
||||
{notification.status?.application &&
|
||||
notification.status?.application.name !== 'Web' && (
|
||||
<View>
|
||||
<Text
|
||||
onPress={applicationOnPress}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {notification.status?.application.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<HeaderSharedCreated created_at={notification.created_at} />
|
||||
<HeaderSharedVisibility
|
||||
visibility={notification.status?.visibility}
|
||||
/>
|
||||
<HeaderSharedApplication
|
||||
application={notification.status?.application}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -180,16 +138,8 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
nameAndMeta: {
|
||||
width: '80%'
|
||||
},
|
||||
nameAndAccount: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
account: {
|
||||
flexShrink: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
accountAndMeta: {
|
||||
flex: 4
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
@ -197,18 +147,8 @@ const styles = StyleSheet.create({
|
||||
marginTop: StyleConstants.Spacing.XS,
|
||||
marginBottom: StyleConstants.Spacing.S
|
||||
},
|
||||
created_at: {
|
||||
...StyleConstants.FontStyle.S
|
||||
},
|
||||
visibility: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
application: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
},
|
||||
relationship: {
|
||||
flexBasis: '20%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { ParseEmojis } from '@root/components/Parse'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
account: Mastodon.Account
|
||||
}
|
||||
|
||||
const HeaderSharedAccount: React.FC<Props> = ({ account }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<View style={styles.base}>
|
||||
<Text numberOfLines={1}>
|
||||
<ParseEmojis
|
||||
content={account.display_name || account.username}
|
||||
emojis={account.emojis}
|
||||
fontBold
|
||||
/>
|
||||
</Text>
|
||||
<Text style={[styles.acct, { color: theme.secondary }]} numberOfLines={1}>
|
||||
@{account.acct}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
acct: {
|
||||
flexShrink: 1,
|
||||
marginLeft: StyleConstants.Spacing.XS
|
||||
}
|
||||
})
|
||||
|
||||
export default HeaderSharedAccount
|
@ -0,0 +1,35 @@
|
||||
import openLink from '@components/openLink'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
application?: Mastodon.Application
|
||||
}
|
||||
|
||||
const HeaderSharedApplication: React.FC<Props> = ({ application }) => {
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation('timeline')
|
||||
|
||||
return application && application.name !== 'Web' ? (
|
||||
<Text
|
||||
onPress={async () =>
|
||||
application.website && (await openLink(application.website))
|
||||
}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
{t('shared.header.shared.application', { application: application.name })}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
application: {
|
||||
...StyleConstants.FontStyle.S,
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
export default HeaderSharedApplication
|
@ -0,0 +1,35 @@
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyleSheet, Text } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
created_at: Mastodon.Status['created_at']
|
||||
}
|
||||
|
||||
const HeaderSharedCreated: React.FC<Props> = ({ created_at }) => {
|
||||
const { theme } = useTheme()
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const [since, setSince] = useState(relativeTime(created_at, i18n.language))
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setSince(relativeTime(created_at, i18n.language))
|
||||
}, 1000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [since])
|
||||
|
||||
return (
|
||||
<Text style={[styles.created_at, { color: theme.secondary }]}>{since}</Text>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
created_at: {
|
||||
...StyleConstants.FontStyle.S
|
||||
}
|
||||
})
|
||||
|
||||
export default React.memo(HeaderSharedCreated, () => true)
|
@ -0,0 +1,30 @@
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
export interface Props {
|
||||
visibility?: Mastodon.Status['visibility']
|
||||
}
|
||||
|
||||
const HeaderSharedVisibility: React.FC<Props> = ({ visibility }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return visibility && visibility === 'private' ? (
|
||||
<Feather
|
||||
name='lock'
|
||||
size={StyleConstants.Font.Size.S}
|
||||
color={theme.secondary}
|
||||
style={styles.visibility}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
visibility: {
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
}
|
||||
})
|
||||
|
||||
export default HeaderSharedVisibility
|
@ -9,6 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { findIndex } from 'lodash'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useMutation, useQueryClient } from 'react-query'
|
||||
|
||||
@ -55,6 +56,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
sameAccount
|
||||
}) => {
|
||||
const { mode, theme } = useTheme()
|
||||
const { t, i18n } = useTranslation('timeline')
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [allOptions, setAllOptions] = useState(
|
||||
@ -107,7 +109,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
mutation.mutate({ id: poll.id, options: allOptions })
|
||||
}
|
||||
type='text'
|
||||
content='投票'
|
||||
content={t('shared.poll.meta.button.vote')}
|
||||
loading={mutation.isLoading}
|
||||
disabled={allOptions.filter(o => o !== false).length === 0}
|
||||
/>
|
||||
@ -118,9 +120,8 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
<View style={styles.button}>
|
||||
<Button
|
||||
onPress={() => mutation.mutate({ id: poll.id })}
|
||||
{...(mutation.isLoading ? { icon: 'loader' } : { text: '刷新' })}
|
||||
type='text'
|
||||
content='刷新'
|
||||
content={t('shared.poll.meta.button.refresh')}
|
||||
loading={mutation.isLoading}
|
||||
/>
|
||||
</View>
|
||||
@ -133,17 +134,19 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
if (poll.expired) {
|
||||
return (
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
投票已结束
|
||||
{t('shared.poll.meta.expiration.expired')}
|
||||
</Text>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Text style={[styles.expiration, { color: theme.secondary }]}>
|
||||
{relativeTime(poll.expires_at)}截止
|
||||
{t('shared.poll.meta.expiration.until', {
|
||||
at: relativeTime(poll.expires_at, i18n.language)
|
||||
})}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
}, [mode])
|
||||
}, [mode, poll.expired, poll.expires_at])
|
||||
|
||||
const isSelected = useCallback(
|
||||
(index: number): any =>
|
||||
@ -241,7 +244,7 @@ const TimelinePoll: React.FC<Props> = ({
|
||||
<View style={styles.meta}>
|
||||
{pollButton}
|
||||
<Text style={[styles.votes, { color: theme.secondary }]}>
|
||||
已投{poll.voters_count || 0}人{' • '}
|
||||
{t('shared.poll.meta.voted', { count: poll.voters_count })}
|
||||
</Text>
|
||||
{pollExpiration}
|
||||
</View>
|
||||
@ -270,7 +273,9 @@ const styles = StyleSheet.create({
|
||||
optionPercentage: {
|
||||
...StyleConstants.FontStyle.M,
|
||||
alignSelf: 'center',
|
||||
marginLeft: StyleConstants.Spacing.S
|
||||
marginLeft: StyleConstants.Spacing.S,
|
||||
flexBasis: '20%',
|
||||
textAlign: 'center'
|
||||
},
|
||||
background: {
|
||||
height: StyleConstants.Spacing.XS,
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { store } from '@root/store'
|
||||
|
||||
const relativeTime = (date: string) => {
|
||||
const relativeTime = (date: string, language: string) => {
|
||||
const units = {
|
||||
year: 24 * 60 * 60 * 1000 * 365,
|
||||
month: (24 * 60 * 60 * 1000 * 365) / 12,
|
||||
@ -10,7 +8,7 @@ const relativeTime = (date: string) => {
|
||||
second: 1000
|
||||
}
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat(store.getState().settings.language, {
|
||||
const rtf = new Intl.RelativeTimeFormat(language, {
|
||||
numeric: 'auto'
|
||||
})
|
||||
|
||||
|
@ -9,7 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
export interface Params {
|
||||
type: 'success' | 'error' | 'warning'
|
||||
position?: 'top' | 'bottom'
|
||||
content: string
|
||||
message: string
|
||||
description?: string
|
||||
autoHide?: boolean
|
||||
onShow?: () => void
|
||||
@ -19,14 +19,14 @@ export interface Params {
|
||||
type Config = {
|
||||
type: Params['type']
|
||||
position: Params['position']
|
||||
text1: Params['content']
|
||||
text1: Params['message']
|
||||
text2: Params['description']
|
||||
}
|
||||
|
||||
const toast = ({
|
||||
type,
|
||||
position = 'top',
|
||||
content,
|
||||
message,
|
||||
description,
|
||||
autoHide = true,
|
||||
onShow,
|
||||
@ -35,7 +35,7 @@ const toast = ({
|
||||
Toast.show({
|
||||
type,
|
||||
position,
|
||||
text1: content,
|
||||
text1: message,
|
||||
text2: description,
|
||||
visibilityTime: 1500,
|
||||
autoHide,
|
||||
|
@ -17,5 +17,7 @@ export default {
|
||||
|
||||
sharedAccount: require('./screens/sharedAccount').default,
|
||||
sharedToot: require('./screens/sharedToot').default,
|
||||
sharedAnnouncements: require('./screens/sharedAnnouncements').default
|
||||
sharedAnnouncements: require('./screens/sharedAnnouncements').default,
|
||||
|
||||
timeline: require('./components/timeline').default
|
||||
}
|
||||
|
@ -1,5 +1,16 @@
|
||||
export default {
|
||||
buttons: {
|
||||
cancel: '取消'
|
||||
},
|
||||
toastMessage: {
|
||||
success: {
|
||||
message: '{{function}}成功'
|
||||
},
|
||||
warning: {
|
||||
message: ''
|
||||
},
|
||||
error: {
|
||||
message: '{{function}}失败,请重试'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
127
src/i18n/zh/components/timeline.ts
Normal file
127
src/i18n/zh/components/timeline.ts
Normal file
@ -0,0 +1,127 @@
|
||||
export default {
|
||||
empty: {
|
||||
error: {
|
||||
message: '加载错误',
|
||||
button: '重试'
|
||||
},
|
||||
success: {
|
||||
message: '🈳️🈚️1一物'
|
||||
}
|
||||
},
|
||||
shared: {
|
||||
actioned: {
|
||||
pinned: '置顶',
|
||||
favourite: '{{name}} 喜欢了你的嘟嘟',
|
||||
follow: '{{name}} 开始关注你',
|
||||
poll: '您参与的投票已结束',
|
||||
reblog: {
|
||||
default: '{{name}} 转嘟了',
|
||||
notification: '{{name}} 转嘟了您的嘟文'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
favourite: {
|
||||
function: '喜欢嘟文'
|
||||
// button: '隐藏 {{acct}} 的嘟文'
|
||||
},
|
||||
reblog: {
|
||||
function: '转嘟'
|
||||
// button: '屏蔽 {{acct}}'
|
||||
},
|
||||
bookmark: {
|
||||
function: '收藏嘟文'
|
||||
// button: '举报 {{acct}}'
|
||||
}
|
||||
},
|
||||
attachment: {
|
||||
sensitive: {
|
||||
button: '显示敏感内容'
|
||||
},
|
||||
unsupported: {
|
||||
text: '文件读取错误',
|
||||
button: '尝试远程链接'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
expandHint: '隐藏内容'
|
||||
},
|
||||
end: {
|
||||
message: '居然刷到底了,喝杯 <0 /> 吧'
|
||||
},
|
||||
header: {
|
||||
shared: {
|
||||
application: '发自于 {{application}}'
|
||||
},
|
||||
default: {
|
||||
actions: {
|
||||
account: {
|
||||
heading: '关于用户',
|
||||
mute: {
|
||||
function: '隐藏 {{acct}} 的嘟文',
|
||||
button: '隐藏 {{acct}} 的嘟文'
|
||||
},
|
||||
block: {
|
||||
function: '屏蔽 {{acct}}',
|
||||
button: '屏蔽 {{acct}}'
|
||||
},
|
||||
report: {
|
||||
function: '举报 {{acct}}',
|
||||
button: '举报 {{acct}}'
|
||||
}
|
||||
},
|
||||
domain: {
|
||||
heading: '关于域名',
|
||||
block: {
|
||||
function: '屏蔽域名',
|
||||
button: '屏蔽域名 {{domain}}'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
heading: '关于嘟嘟',
|
||||
delete: {
|
||||
function: '删除',
|
||||
button: '删除次条嘟文'
|
||||
},
|
||||
edit: {
|
||||
function: '删除',
|
||||
button: '删除并重新编辑次条嘟文',
|
||||
alert: {
|
||||
title: '确认删除嘟嘟?',
|
||||
message:
|
||||
'你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。',
|
||||
confirm: '删除并重新编辑'
|
||||
}
|
||||
},
|
||||
mute: {
|
||||
function: '静音',
|
||||
button: {
|
||||
positive: '静音此条嘟文及对话',
|
||||
negative: '取消静音此条嘟文及对话'
|
||||
}
|
||||
},
|
||||
pin: {
|
||||
function: '置顶',
|
||||
button: {
|
||||
positive: '置顶此条嘟文',
|
||||
negative: '取消置顶此条嘟文'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
poll: {
|
||||
meta: {
|
||||
button: {
|
||||
vote: '投票',
|
||||
refresh: '刷新'
|
||||
},
|
||||
expiration: {
|
||||
expired: '投票已结束',
|
||||
until: '{{at}}截止'
|
||||
},
|
||||
voted: '已投{{count}}人 • '
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ const styles = StyleSheet.create({
|
||||
paddingBottom: StyleConstants.Spacing.S
|
||||
},
|
||||
fieldLeft: {
|
||||
flexBasis: '30%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@ -72,7 +72,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
fieldCheck: { marginLeft: StyleConstants.Spacing.XS },
|
||||
fieldRight: {
|
||||
flexBasis: '70%',
|
||||
flex: 3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: StyleConstants.Spacing.S,
|
||||
|
@ -1,13 +1,14 @@
|
||||
import client from '@api/client'
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
||||
import Button from '@components/Button'
|
||||
import haptics from '@components/haptics'
|
||||
import { ParseHTML } from '@components/Parse'
|
||||
import relativeTime from '@components/relativeTime'
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
|
||||
import { announcementFetch } from '@utils/fetches/announcementsFetch'
|
||||
import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dimensions,
|
||||
Image,
|
||||
@ -56,6 +57,7 @@ const ScreenSharedAnnouncements: React.FC = ({
|
||||
const { theme } = useTheme()
|
||||
const bottomTabBarHeight = useBottomTabBarHeight()
|
||||
const [index, setIndex] = useState(0)
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const queryKey = ['Announcements', { showAll }]
|
||||
const { data, refetch } = useQuery(queryKey, announcementFetch, {
|
||||
@ -100,7 +102,7 @@ const ScreenSharedAnnouncements: React.FC = ({
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.published, { color: theme.secondary }]}>
|
||||
发布于{relativeTime(item.published_at)}
|
||||
发布于 {relativeTime(item.published_at, i18n.language)}
|
||||
</Text>
|
||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator>
|
||||
<ParseHTML
|
||||
|
@ -22,7 +22,7 @@ import ComposeEditAttachment from './Compose/EditAttachment'
|
||||
import ComposeContext from './Compose/utils/createContext'
|
||||
import composeInitialState from './Compose/utils/initialState'
|
||||
import composeParseState from './Compose/utils/parseState'
|
||||
import composeSend from './Compose/utils/post'
|
||||
import composePost from './Compose/utils/post'
|
||||
import composeReducer from './Compose/utils/reducer'
|
||||
import { ComposeState } from './Compose/utils/types'
|
||||
|
||||
@ -171,7 +171,7 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
||||
onPress={() => {
|
||||
layoutAnimation()
|
||||
setIsSubmitting(true)
|
||||
composeSend(params, composeState)
|
||||
composePost(params, composeState)
|
||||
.then(() => {
|
||||
haptics('Success')
|
||||
queryClient.invalidateQueries(['Following'])
|
||||
@ -191,7 +191,7 @@ const Compose: React.FC<Props> = ({ route: { params }, navigation }) => {
|
||||
disabled={composeState.text.raw.length < 1 || totalTextCount > 500}
|
||||
/>
|
||||
),
|
||||
[isSubmitting, composeState.text.raw, totalTextCount]
|
||||
[isSubmitting, totalTextCount, composeState]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,7 @@ import { Props } from '@screens/Shared/Compose'
|
||||
import { ComposeState } from '@screens/Shared/Compose/utils/types'
|
||||
import * as Crypto from 'expo-crypto'
|
||||
|
||||
const composeSend = async (
|
||||
const composePost = async (
|
||||
params: Props['route']['params'],
|
||||
composeState: ComposeState
|
||||
) => {
|
||||
@ -71,4 +71,4 @@ const composeSend = async (
|
||||
}
|
||||
}
|
||||
|
||||
export default composeSend
|
||||
export default composePost
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ComposeAction, ComposeState } from "./types"
|
||||
import { ComposeAction, ComposeState } from './types'
|
||||
|
||||
const composeReducer = (
|
||||
state: ComposeState,
|
||||
|
Loading…
x
Reference in New Issue
Block a user