mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Translated Timeline components
This commit is contained in:
		@@ -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,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user