mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Some basic styling
This commit is contained in:
		
							
								
								
									
										75
									
								
								src/components/Timelines/Timeline/Conversation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/components/Timelines/Timeline/Conversation.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import React, { useMemo } from 'react'
 | 
			
		||||
import { Pressable, StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
 | 
			
		||||
import Avatar from './Shared/Avatar'
 | 
			
		||||
import HeaderConversation from './Shared/HeaderConversation'
 | 
			
		||||
import Content from './Shared/Content'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  item: Mastodon.Conversation
 | 
			
		||||
}
 | 
			
		||||
// Unread and mark as unread
 | 
			
		||||
const TimelineConversation: React.FC<Props> = ({ item }) => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
 | 
			
		||||
  const statusView = useMemo(() => {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.statusView}>
 | 
			
		||||
        <View style={styles.status}>
 | 
			
		||||
          <Avatar uri={item.accounts[0].avatar} id={item.accounts[0].id} />
 | 
			
		||||
          <View style={styles.details}>
 | 
			
		||||
            <HeaderConversation
 | 
			
		||||
              account={item.accounts[0]}
 | 
			
		||||
              created_at={item.last_status?.created_at}
 | 
			
		||||
            />
 | 
			
		||||
            {/* Can pass toot info to next page to speed up performance */}
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() =>
 | 
			
		||||
                item.last_status &&
 | 
			
		||||
                navigation.navigate('Screen-Shared-Toot', {
 | 
			
		||||
                  toot: item.last_status.id
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {item.last_status ? (
 | 
			
		||||
                <Content
 | 
			
		||||
                  content={item.last_status.content}
 | 
			
		||||
                  emojis={item.last_status.emojis}
 | 
			
		||||
                  mentions={item.last_status.mentions}
 | 
			
		||||
                  spoiler_text={item.last_status.spoiler_text}
 | 
			
		||||
                  // tags={actualStatus.tags}
 | 
			
		||||
                  // style={{ flex: 1 }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
            </Pressable>
 | 
			
		||||
          </View>
 | 
			
		||||
        </View>
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  }, [item])
 | 
			
		||||
 | 
			
		||||
  return statusView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  statusView: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    padding: constants.GLOBAL_PAGE_PADDING
 | 
			
		||||
  },
 | 
			
		||||
  status: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  details: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexGrow: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default TimelineConversation
 | 
			
		||||
							
								
								
									
										117
									
								
								src/components/Timelines/Timeline/Default.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/components/Timelines/Timeline/Default.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
import React, { useMemo } from 'react'
 | 
			
		||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
 | 
			
		||||
import Actioned from './Shared/Actioned'
 | 
			
		||||
import Avatar from './Shared/Avatar'
 | 
			
		||||
import HeaderDefault from './Shared/HeaderDefault'
 | 
			
		||||
import Content from './Shared/Content'
 | 
			
		||||
import Poll from './Shared/Poll'
 | 
			
		||||
import Attachment from './Shared/Attachment'
 | 
			
		||||
import Card from './Shared/Card'
 | 
			
		||||
import ActionsStatus from './Shared/ActionsStatus'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  item: Mastodon.Status
 | 
			
		||||
  queryKey: App.QueryKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// When the poll is long
 | 
			
		||||
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
 | 
			
		||||
  let actualStatus = item.reblog ? item.reblog : item
 | 
			
		||||
 | 
			
		||||
  const statusView = useMemo(() => {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.statusView}>
 | 
			
		||||
        {item.reblog && (
 | 
			
		||||
          <Actioned
 | 
			
		||||
            action='reblog'
 | 
			
		||||
            name={item.account.display_name || item.account.username}
 | 
			
		||||
            emojis={item.account.emojis}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <View style={styles.status}>
 | 
			
		||||
          <Avatar
 | 
			
		||||
            uri={actualStatus.account.avatar}
 | 
			
		||||
            id={actualStatus.account.id}
 | 
			
		||||
          />
 | 
			
		||||
          <View style={styles.details}>
 | 
			
		||||
            <HeaderDefault
 | 
			
		||||
              queryKey={queryKey}
 | 
			
		||||
              accountId={actualStatus.account.id}
 | 
			
		||||
              domain={actualStatus.uri.split(new RegExp(/\/\/(.*?)\//))[1]}
 | 
			
		||||
              name={
 | 
			
		||||
                actualStatus.account.display_name ||
 | 
			
		||||
                actualStatus.account.username
 | 
			
		||||
              }
 | 
			
		||||
              emojis={actualStatus.account.emojis}
 | 
			
		||||
              account={actualStatus.account.acct}
 | 
			
		||||
              created_at={item.created_at}
 | 
			
		||||
              application={item.application}
 | 
			
		||||
            />
 | 
			
		||||
            {/* Can pass toot info to next page to speed up performance */}
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() =>
 | 
			
		||||
                navigation.navigate('Screen-Shared-Toot', {
 | 
			
		||||
                  toot: actualStatus.id
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {actualStatus.content ? (
 | 
			
		||||
                <Content
 | 
			
		||||
                  content={actualStatus.content}
 | 
			
		||||
                  emojis={actualStatus.emojis}
 | 
			
		||||
                  mentions={actualStatus.mentions}
 | 
			
		||||
                  spoiler_text={actualStatus.spoiler_text}
 | 
			
		||||
                  // tags={actualStatus.tags}
 | 
			
		||||
                  // style={{ flex: 1 }}
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
              {actualStatus.poll && <Poll poll={actualStatus.poll} />}
 | 
			
		||||
              {actualStatus.media_attachments.length > 0 && (
 | 
			
		||||
                <Attachment
 | 
			
		||||
                  media_attachments={actualStatus.media_attachments}
 | 
			
		||||
                  sensitive={actualStatus.sensitive}
 | 
			
		||||
                  width={
 | 
			
		||||
                    Dimensions.get('window').width -
 | 
			
		||||
                    constants.SPACING_M * 2 -
 | 
			
		||||
                    50 -
 | 
			
		||||
                    8
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              {actualStatus.card && <Card card={actualStatus.card} />}
 | 
			
		||||
            </Pressable>
 | 
			
		||||
            <ActionsStatus queryKey={queryKey} status={actualStatus} />
 | 
			
		||||
          </View>
 | 
			
		||||
        </View>
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  }, [item])
 | 
			
		||||
 | 
			
		||||
  return statusView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  statusView: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    padding: constants.GLOBAL_PAGE_PADDING
 | 
			
		||||
  },
 | 
			
		||||
  status: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  details: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexGrow: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default TimelineDefault
 | 
			
		||||
							
								
								
									
										113
									
								
								src/components/Timelines/Timeline/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/components/Timelines/Timeline/Notifications.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
import React, { useMemo } from 'react'
 | 
			
		||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
 | 
			
		||||
import Actioned from './Shared/Actioned'
 | 
			
		||||
import Avatar from './Shared/Avatar'
 | 
			
		||||
import HeaderDefault from './Shared/HeaderDefault'
 | 
			
		||||
import Content from './Shared/Content'
 | 
			
		||||
import Poll from './Shared/Poll'
 | 
			
		||||
import Attachment from './Shared/Attachment'
 | 
			
		||||
import Card from './Shared/Card'
 | 
			
		||||
import ActionsStatus from './Shared/ActionsStatus'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  notification: Mastodon.Notification
 | 
			
		||||
  queryKey: App.QueryKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
  const actualAccount = notification.status
 | 
			
		||||
    ? notification.status.account
 | 
			
		||||
    : notification.account
 | 
			
		||||
 | 
			
		||||
  const statusView = useMemo(() => {
 | 
			
		||||
    return (
 | 
			
		||||
      <View style={styles.notificationView}>
 | 
			
		||||
        <Actioned
 | 
			
		||||
          action={notification.type}
 | 
			
		||||
          name={
 | 
			
		||||
            notification.account.display_name || notification.account.username
 | 
			
		||||
          }
 | 
			
		||||
          emojis={notification.account.emojis}
 | 
			
		||||
          notification
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <View style={styles.notification}>
 | 
			
		||||
          <Avatar uri={actualAccount.avatar} id={actualAccount.id} />
 | 
			
		||||
          <View style={styles.details}>
 | 
			
		||||
            <HeaderDefault
 | 
			
		||||
              name={actualAccount.display_name || actualAccount.username}
 | 
			
		||||
              emojis={actualAccount.emojis}
 | 
			
		||||
              account={actualAccount.acct}
 | 
			
		||||
              created_at={notification.created_at}
 | 
			
		||||
            />
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() =>
 | 
			
		||||
                navigation.navigate('Screen-Shared-Toot', {
 | 
			
		||||
                  toot: notification.id
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              {notification.status ? (
 | 
			
		||||
                <>
 | 
			
		||||
                  {notification.status.content && (
 | 
			
		||||
                    <Content
 | 
			
		||||
                      content={notification.status.content}
 | 
			
		||||
                      emojis={notification.status.emojis}
 | 
			
		||||
                      mentions={notification.status.mentions}
 | 
			
		||||
                      spoiler_text={notification.status.spoiler_text}
 | 
			
		||||
                      // tags={notification.notification.tags}
 | 
			
		||||
                      // style={{ flex: 1 }}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {notification.status.poll && (
 | 
			
		||||
                    <Poll poll={notification.status.poll} />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {notification.status.media_attachments.length > 0 && (
 | 
			
		||||
                    <Attachment
 | 
			
		||||
                      media_attachments={notification.status.media_attachments}
 | 
			
		||||
                      sensitive={notification.status.sensitive}
 | 
			
		||||
                      width={Dimensions.get('window').width - 24 - 50 - 8}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {notification.status.card && (
 | 
			
		||||
                    <Card card={notification.status.card} />
 | 
			
		||||
                  )}
 | 
			
		||||
                </>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
            </Pressable>
 | 
			
		||||
            {notification.status && (
 | 
			
		||||
              <ActionsStatus queryKey={queryKey} status={notification.status} />
 | 
			
		||||
            )}
 | 
			
		||||
          </View>
 | 
			
		||||
        </View>
 | 
			
		||||
      </View>
 | 
			
		||||
    )
 | 
			
		||||
  }, [notification])
 | 
			
		||||
 | 
			
		||||
  return statusView
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  notificationView: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    padding: constants.GLOBAL_PAGE_PADDING
 | 
			
		||||
  },
 | 
			
		||||
  notification: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  details: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexGrow: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default TimelineNotifications
 | 
			
		||||
							
								
								
									
										20
									
								
								src/components/Timelines/Timeline/Separator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/Timelines/Timeline/Separator.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { StyleSheet, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
 | 
			
		||||
const TimelineSeparator = () => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  return <View style={[styles.base, { borderTopColor: theme.separator }]} />
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  base: {
 | 
			
		||||
    borderTopWidth: 1,
 | 
			
		||||
    marginLeft: constants.SPACING_M + constants.AVATAR_S + constants.SPACING_S
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default TimelineSeparator
 | 
			
		||||
							
								
								
									
										111
									
								
								src/components/Timelines/Timeline/Shared/Actioned.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/components/Timelines/Timeline/Shared/Actioned.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import { Feather } from '@expo/vector-icons'
 | 
			
		||||
 | 
			
		||||
import Emojis from './Emojis'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
 | 
			
		||||
  name?: string
 | 
			
		||||
  emojis?: Mastodon.Emoji[]
 | 
			
		||||
  notification?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Actioned: React.FC<Props> = ({
 | 
			
		||||
  action,
 | 
			
		||||
  name,
 | 
			
		||||
  emojis,
 | 
			
		||||
  notification = false
 | 
			
		||||
}) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const iconColor = theme.primary
 | 
			
		||||
 | 
			
		||||
  let icon
 | 
			
		||||
  let content
 | 
			
		||||
  switch (action) {
 | 
			
		||||
    case 'favourite':
 | 
			
		||||
      icon = (
 | 
			
		||||
        <Feather
 | 
			
		||||
          name='heart'
 | 
			
		||||
          size={constants.FONT_SIZE_S}
 | 
			
		||||
          color={iconColor}
 | 
			
		||||
          style={styles.icon}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      content = `${name} 喜欢了你的嘟嘟`
 | 
			
		||||
      break
 | 
			
		||||
    case 'follow':
 | 
			
		||||
      icon = (
 | 
			
		||||
        <Feather
 | 
			
		||||
          name='user-plus'
 | 
			
		||||
          size={constants.FONT_SIZE_S}
 | 
			
		||||
          color={iconColor}
 | 
			
		||||
          style={styles.icon}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      content = `${name} 开始关注你`
 | 
			
		||||
      break
 | 
			
		||||
    case 'poll':
 | 
			
		||||
      icon = (
 | 
			
		||||
        <Feather
 | 
			
		||||
          name='bar-chart-2'
 | 
			
		||||
          size={constants.FONT_SIZE_S}
 | 
			
		||||
          color='black'
 | 
			
		||||
          style={styles.icon}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      content = `你参与的投票已结束`
 | 
			
		||||
      break
 | 
			
		||||
    case 'reblog':
 | 
			
		||||
      icon = (
 | 
			
		||||
        <Feather
 | 
			
		||||
          name='repeat'
 | 
			
		||||
          size={constants.FONT_SIZE_S}
 | 
			
		||||
          color={iconColor}
 | 
			
		||||
          style={styles.icon}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      content = `${name} 转嘟了${notification ? '你的嘟嘟' : ''}`
 | 
			
		||||
      break
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={styles.actioned}>
 | 
			
		||||
      {icon}
 | 
			
		||||
      {content ? (
 | 
			
		||||
        <View style={styles.content}>
 | 
			
		||||
          {emojis ? (
 | 
			
		||||
            <Emojis
 | 
			
		||||
              content={content}
 | 
			
		||||
              emojis={emojis}
 | 
			
		||||
              size={constants.FONT_SIZE_S}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text>{content}</Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </View>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <></>
 | 
			
		||||
      )}
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  actioned: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    marginBottom: constants.SPACING_S
 | 
			
		||||
  },
 | 
			
		||||
  icon: {
 | 
			
		||||
    marginLeft: constants.AVATAR_S - constants.FONT_SIZE_S,
 | 
			
		||||
    marginRight: constants.SPACING_S
 | 
			
		||||
  },
 | 
			
		||||
  content: {
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Actioned
 | 
			
		||||
							
								
								
									
										380
									
								
								src/components/Timelines/Timeline/Shared/ActionsStatus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										380
									
								
								src/components/Timelines/Timeline/Shared/ActionsStatus.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,380 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  ActionSheetIOS,
 | 
			
		||||
  Clipboard,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Pressable,
 | 
			
		||||
  StyleSheet,
 | 
			
		||||
  Text,
 | 
			
		||||
  View
 | 
			
		||||
} from 'react-native'
 | 
			
		||||
import Toast from 'react-native-toast-message'
 | 
			
		||||
import { useMutation, useQueryCache } from 'react-query'
 | 
			
		||||
import { Feather } from '@expo/vector-icons'
 | 
			
		||||
 | 
			
		||||
import client from 'src/api/client'
 | 
			
		||||
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
 | 
			
		||||
import { store } from 'src/store'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
const fireMutation = async ({
 | 
			
		||||
  id,
 | 
			
		||||
  type,
 | 
			
		||||
  stateKey,
 | 
			
		||||
  prevState
 | 
			
		||||
}: {
 | 
			
		||||
  id: string
 | 
			
		||||
  type:
 | 
			
		||||
    | 'favourite'
 | 
			
		||||
    | 'reblog'
 | 
			
		||||
    | 'bookmark'
 | 
			
		||||
    | 'mute'
 | 
			
		||||
    | 'pin'
 | 
			
		||||
    | 'delete'
 | 
			
		||||
    | 'account/mute'
 | 
			
		||||
  stateKey:
 | 
			
		||||
    | 'favourited'
 | 
			
		||||
    | 'reblogged'
 | 
			
		||||
    | 'bookmarked'
 | 
			
		||||
    | 'muted'
 | 
			
		||||
    | 'pinned'
 | 
			
		||||
    | 'id'
 | 
			
		||||
  prevState?: boolean
 | 
			
		||||
}) => {
 | 
			
		||||
  let res
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'favourite':
 | 
			
		||||
    case 'reblog':
 | 
			
		||||
    case 'bookmark':
 | 
			
		||||
    case 'mute':
 | 
			
		||||
    case 'pin':
 | 
			
		||||
      res = await client({
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        instance: 'local',
 | 
			
		||||
        endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!res.body[stateKey] === prevState) {
 | 
			
		||||
        if (type === 'bookmark' || 'mute' || 'pin')
 | 
			
		||||
          Toast.show({
 | 
			
		||||
            type: 'success',
 | 
			
		||||
            position: 'bottom',
 | 
			
		||||
            text1: '功能成功',
 | 
			
		||||
            visibilityTime: 2000,
 | 
			
		||||
            autoHide: true,
 | 
			
		||||
            bottomOffset: 65
 | 
			
		||||
          })
 | 
			
		||||
        return Promise.resolve(res.body)
 | 
			
		||||
      } else {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '请重试',
 | 
			
		||||
          autoHide: false,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.reject()
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
    case 'delete':
 | 
			
		||||
      res = await client({
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        instance: 'local',
 | 
			
		||||
        endpoint: `statuses/${id}`
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (res.body[stateKey] === id) {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '删除成功',
 | 
			
		||||
          visibilityTime: 2000,
 | 
			
		||||
          autoHide: true,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.resolve(res.body)
 | 
			
		||||
      } else {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '请重试',
 | 
			
		||||
          autoHide: false,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.reject()
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  queryKey: App.QueryKey
 | 
			
		||||
  status: Mastodon.Status
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const iconColor = theme.secondary
 | 
			
		||||
  const iconColorAction = (state: boolean) =>
 | 
			
		||||
    state ? theme.primary : theme.secondary
 | 
			
		||||
 | 
			
		||||
  const localAccountId = getLocalAccountId(store.getState())
 | 
			
		||||
  const [modalVisible, setModalVisible] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const queryCache = useQueryCache()
 | 
			
		||||
  const [mutateAction] = useMutation(fireMutation, {
 | 
			
		||||
    onMutate: () => {
 | 
			
		||||
      queryCache.cancelQueries(queryKey)
 | 
			
		||||
      const prevData = queryCache.getQueryData(queryKey)
 | 
			
		||||
      return prevData
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: (newData, params) => {
 | 
			
		||||
      if (params.type === 'reblog') {
 | 
			
		||||
        queryCache.invalidateQueries(['Following', { page: 'Following' }])
 | 
			
		||||
      }
 | 
			
		||||
      // queryCache.setQueryData(queryKey, (oldData: any) => {
 | 
			
		||||
      //   oldData &&
 | 
			
		||||
      //     oldData.map((paging: any) => {
 | 
			
		||||
      //       paging.toots.map(
 | 
			
		||||
      //         (status: Mastodon.Status | Mastodon.Notification, i: number) => {
 | 
			
		||||
      //           if (status.id === newData.id) {
 | 
			
		||||
      //             paging.toots[i] = newData
 | 
			
		||||
      //           }
 | 
			
		||||
      //         }
 | 
			
		||||
      //       )
 | 
			
		||||
      //     })
 | 
			
		||||
      //   return oldData
 | 
			
		||||
      // })
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    },
 | 
			
		||||
    onError: (err, variables, prevData) => {
 | 
			
		||||
      queryCache.setQueryData(queryKey, prevData)
 | 
			
		||||
    },
 | 
			
		||||
    onSettled: () => {
 | 
			
		||||
      queryCache.invalidateQueries(queryKey)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <View style={styles.actions}>
 | 
			
		||||
        <Pressable style={styles.action}>
 | 
			
		||||
          <Feather
 | 
			
		||||
            name='message-circle'
 | 
			
		||||
            color={iconColor}
 | 
			
		||||
            size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
          />
 | 
			
		||||
          {status.replies_count > 0 && (
 | 
			
		||||
            <Text
 | 
			
		||||
              style={{
 | 
			
		||||
                color: theme.secondary,
 | 
			
		||||
                fontSize: constants.FONT_SIZE_M,
 | 
			
		||||
                marginLeft: constants.SPACING_XS
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {status.replies_count}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </Pressable>
 | 
			
		||||
 | 
			
		||||
        <Pressable
 | 
			
		||||
          style={styles.action}
 | 
			
		||||
          onPress={() =>
 | 
			
		||||
            mutateAction({
 | 
			
		||||
              id: status.id,
 | 
			
		||||
              type: 'reblog',
 | 
			
		||||
              stateKey: 'reblogged',
 | 
			
		||||
              prevState: status.reblogged
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Feather
 | 
			
		||||
            name='repeat'
 | 
			
		||||
            color={iconColorAction(status.reblogged)}
 | 
			
		||||
            size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
          />
 | 
			
		||||
        </Pressable>
 | 
			
		||||
 | 
			
		||||
        <Pressable
 | 
			
		||||
          style={styles.action}
 | 
			
		||||
          onPress={() =>
 | 
			
		||||
            mutateAction({
 | 
			
		||||
              id: status.id,
 | 
			
		||||
              type: 'favourite',
 | 
			
		||||
              stateKey: 'favourited',
 | 
			
		||||
              prevState: status.favourited
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Feather
 | 
			
		||||
            name='heart'
 | 
			
		||||
            color={iconColorAction(status.favourited)}
 | 
			
		||||
            size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
          />
 | 
			
		||||
        </Pressable>
 | 
			
		||||
 | 
			
		||||
        <Pressable
 | 
			
		||||
          style={styles.action}
 | 
			
		||||
          onPress={() =>
 | 
			
		||||
            mutateAction({
 | 
			
		||||
              id: status.id,
 | 
			
		||||
              type: 'bookmark',
 | 
			
		||||
              stateKey: 'bookmarked',
 | 
			
		||||
              prevState: status.bookmarked
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Feather
 | 
			
		||||
            name='bookmark'
 | 
			
		||||
            color={iconColorAction(status.bookmarked)}
 | 
			
		||||
            size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
          />
 | 
			
		||||
        </Pressable>
 | 
			
		||||
 | 
			
		||||
        <Pressable style={styles.action} onPress={() => setModalVisible(true)}>
 | 
			
		||||
          <Feather
 | 
			
		||||
            name='share-2'
 | 
			
		||||
            color={iconColor}
 | 
			
		||||
            size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
          />
 | 
			
		||||
        </Pressable>
 | 
			
		||||
      </View>
 | 
			
		||||
 | 
			
		||||
      <Modal
 | 
			
		||||
        animationType='fade'
 | 
			
		||||
        presentationStyle='overFullScreen'
 | 
			
		||||
        transparent
 | 
			
		||||
        visible={modalVisible}
 | 
			
		||||
      >
 | 
			
		||||
        <Pressable
 | 
			
		||||
          style={styles.modalBackground}
 | 
			
		||||
          onPress={() => setModalVisible(false)}
 | 
			
		||||
        >
 | 
			
		||||
          <View style={styles.modalSheet}>
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() =>
 | 
			
		||||
                ActionSheetIOS.showShareActionSheetWithOptions(
 | 
			
		||||
                  {
 | 
			
		||||
                    url: status.uri,
 | 
			
		||||
                    excludedActivityTypes: [
 | 
			
		||||
                      'com.apple.UIKit.activity.Mail',
 | 
			
		||||
                      'com.apple.UIKit.activity.Print',
 | 
			
		||||
                      'com.apple.UIKit.activity.SaveToCameraRoll',
 | 
			
		||||
                      'com.apple.UIKit.activity.OpenInIBooks'
 | 
			
		||||
                    ]
 | 
			
		||||
                  },
 | 
			
		||||
                  () => {},
 | 
			
		||||
                  () => {
 | 
			
		||||
                    setModalVisible(false)
 | 
			
		||||
                    Toast.show({
 | 
			
		||||
                      type: 'success',
 | 
			
		||||
                      position: 'bottom',
 | 
			
		||||
                      text1: '分享成功',
 | 
			
		||||
                      visibilityTime: 2000,
 | 
			
		||||
                      autoHide: true,
 | 
			
		||||
                      bottomOffset: 65
 | 
			
		||||
                    })
 | 
			
		||||
                  }
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
              <Text>分享</Text>
 | 
			
		||||
            </Pressable>
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() => {
 | 
			
		||||
                Clipboard.setString(status.uri)
 | 
			
		||||
                setModalVisible(false)
 | 
			
		||||
                Toast.show({
 | 
			
		||||
                  type: 'success',
 | 
			
		||||
                  position: 'bottom',
 | 
			
		||||
                  text1: '链接复制成功',
 | 
			
		||||
                  visibilityTime: 2000,
 | 
			
		||||
                  autoHide: true,
 | 
			
		||||
                  bottomOffset: 65
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text>复制链接</Text>
 | 
			
		||||
            </Pressable>
 | 
			
		||||
            {status.account.id === localAccountId && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: status.id,
 | 
			
		||||
                    type: 'delete',
 | 
			
		||||
                    stateKey: 'id'
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>删除</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
            <Text>(删除并重发)</Text>
 | 
			
		||||
            <Pressable
 | 
			
		||||
              onPress={() => {
 | 
			
		||||
                setModalVisible(false)
 | 
			
		||||
                mutateAction({
 | 
			
		||||
                  id: status.id,
 | 
			
		||||
                  type: 'mute',
 | 
			
		||||
                  stateKey: 'muted',
 | 
			
		||||
                  prevState: status.muted
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Text>{status.muted ? '取消静音' : '静音'}</Text>
 | 
			
		||||
            </Pressable>
 | 
			
		||||
            {status.account.id === localAccountId && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: status.id,
 | 
			
		||||
                    type: 'pin',
 | 
			
		||||
                    stateKey: 'pinned',
 | 
			
		||||
                    prevState: status.pinned
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>{status.pinned ? '取消置顶' : '置顶'}</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
            <Text>静音用户,屏蔽用户,屏蔽域名,举报用户</Text>
 | 
			
		||||
          </View>
 | 
			
		||||
        </Pressable>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  actions: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    marginTop: constants.SPACING_M
 | 
			
		||||
  },
 | 
			
		||||
  action: {
 | 
			
		||||
    width: '20%',
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'center'
 | 
			
		||||
  },
 | 
			
		||||
  modalBackground: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
    backgroundColor: 'rgba(0, 0, 0, 0.75)',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    alignItems: 'flex-end'
 | 
			
		||||
  },
 | 
			
		||||
  modalSheet: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '50%',
 | 
			
		||||
    backgroundColor: 'white',
 | 
			
		||||
    flex: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default ActionsStatus
 | 
			
		||||
							
								
								
									
										87
									
								
								src/components/Timelines/Timeline/Shared/Attachment.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/components/Timelines/Timeline/Shared/Attachment.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Text, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
import AttachmentImage from './Attachment/AttachmentImage'
 | 
			
		||||
import AttachmentVideo from './Attachment/AttachmentVideo'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  media_attachments: Mastodon.Attachment[]
 | 
			
		||||
  sensitive: boolean
 | 
			
		||||
  width: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Attachment: React.FC<Props> = ({
 | 
			
		||||
  media_attachments,
 | 
			
		||||
  sensitive,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  let attachment
 | 
			
		||||
  let attachmentHeight
 | 
			
		||||
  // if (width) {}
 | 
			
		||||
  switch (media_attachments[0].type) {
 | 
			
		||||
    case 'unknown':
 | 
			
		||||
      attachment = <Text>文件不支持</Text>
 | 
			
		||||
      attachmentHeight = 25
 | 
			
		||||
      break
 | 
			
		||||
    case 'image':
 | 
			
		||||
      attachment = (
 | 
			
		||||
        <AttachmentImage
 | 
			
		||||
          media_attachments={media_attachments}
 | 
			
		||||
          sensitive={sensitive}
 | 
			
		||||
          width={width}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      attachmentHeight = width / 2
 | 
			
		||||
      break
 | 
			
		||||
    case 'gifv':
 | 
			
		||||
      attachment = (
 | 
			
		||||
        <AttachmentVideo
 | 
			
		||||
          media_attachments={media_attachments}
 | 
			
		||||
          sensitive={sensitive}
 | 
			
		||||
          width={width}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
      attachmentHeight =
 | 
			
		||||
        (width / media_attachments[0].meta.original.width) *
 | 
			
		||||
        media_attachments[0].meta.original.height
 | 
			
		||||
      break
 | 
			
		||||
    // Support multiple video
 | 
			
		||||
    // Supoort when video meta is empty
 | 
			
		||||
    // case 'video':
 | 
			
		||||
    //   attachment = (
 | 
			
		||||
    //     <AttachmentVideo
 | 
			
		||||
    //       media_attachments={media_attachments}
 | 
			
		||||
    //       sensitive={sensitive}
 | 
			
		||||
    //       width={width}
 | 
			
		||||
    //     />
 | 
			
		||||
    //   )
 | 
			
		||||
    //   attachmentHeight =
 | 
			
		||||
    //     (width / media_attachments[0].meta.original.width) *
 | 
			
		||||
    //     media_attachments[0].meta.original.height
 | 
			
		||||
    //   break
 | 
			
		||||
    // case 'audio':
 | 
			
		||||
    //   attachment = (
 | 
			
		||||
    //     <AttachmentAudio
 | 
			
		||||
    //       media_attachments={media_attachments}
 | 
			
		||||
    //       sensitive={sensitive}
 | 
			
		||||
    //       width={width}
 | 
			
		||||
    //     />
 | 
			
		||||
    //   )
 | 
			
		||||
    //   break
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View
 | 
			
		||||
      style={{
 | 
			
		||||
        width: width + 8,
 | 
			
		||||
        height: attachmentHeight,
 | 
			
		||||
        marginTop: 4,
 | 
			
		||||
        marginLeft: -4
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {attachment}
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Attachment
 | 
			
		||||
@@ -0,0 +1,106 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react'
 | 
			
		||||
import { Button, Image, Modal, StyleSheet, Pressable, View } from 'react-native'
 | 
			
		||||
import ImageViewer from 'react-native-image-zoom-viewer'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  media_attachments: Mastodon.Attachment[]
 | 
			
		||||
  sensitive: boolean
 | 
			
		||||
  width: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AttachmentImage: React.FC<Props> = ({
 | 
			
		||||
  media_attachments,
 | 
			
		||||
  sensitive,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  const [mediaSensitive, setMediaSensitive] = useState(sensitive)
 | 
			
		||||
  const [imageModalVisible, setImageModalVisible] = useState(false)
 | 
			
		||||
  const [imageModalIndex, setImageModalIndex] = useState(0)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (sensitive && mediaSensitive === false) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        setMediaSensitive(true)
 | 
			
		||||
      }, 10000)
 | 
			
		||||
    }
 | 
			
		||||
  }, [mediaSensitive])
 | 
			
		||||
 | 
			
		||||
  let images: { url: string; width: number; height: number }[] = []
 | 
			
		||||
  const imagesNode = media_attachments.map((m, i) => {
 | 
			
		||||
    images.push({
 | 
			
		||||
      url: m.url,
 | 
			
		||||
      width: m.meta.original.width,
 | 
			
		||||
      height: m.meta.original.height
 | 
			
		||||
    })
 | 
			
		||||
    return (
 | 
			
		||||
      <Pressable
 | 
			
		||||
        key={i}
 | 
			
		||||
        style={{ flexGrow: 1, height: width / 2, margin: 4 }}
 | 
			
		||||
        onPress={() => {
 | 
			
		||||
          setImageModalIndex(i)
 | 
			
		||||
          setImageModalVisible(true)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Image
 | 
			
		||||
          source={{ uri: m.preview_url }}
 | 
			
		||||
          style={styles.image}
 | 
			
		||||
          blurRadius={mediaSensitive ? width / 5 : 0}
 | 
			
		||||
        />
 | 
			
		||||
      </Pressable>
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <View style={styles.media}>
 | 
			
		||||
        {imagesNode}
 | 
			
		||||
        {mediaSensitive && (
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              position: 'absolute',
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              height: '100%'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              title='Press me'
 | 
			
		||||
              onPress={() => {
 | 
			
		||||
                setMediaSensitive(false)
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
      </View>
 | 
			
		||||
      <Modal
 | 
			
		||||
        visible={imageModalVisible}
 | 
			
		||||
        transparent={true}
 | 
			
		||||
        animationType='fade'
 | 
			
		||||
      >
 | 
			
		||||
        <ImageViewer
 | 
			
		||||
          imageUrls={images}
 | 
			
		||||
          index={imageModalIndex}
 | 
			
		||||
          onSwipeDown={() => setImageModalVisible(false)}
 | 
			
		||||
          enableSwipeDown={true}
 | 
			
		||||
          swipeDownThreshold={100}
 | 
			
		||||
          useNativeDriver={true}
 | 
			
		||||
        />
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  media: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    flexWrap: 'wrap',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    alignItems: 'stretch',
 | 
			
		||||
    alignContent: 'stretch'
 | 
			
		||||
  },
 | 
			
		||||
  image: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default AttachmentImage
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
import React, { useRef, useState } from 'react'
 | 
			
		||||
import { Pressable, View } from 'react-native'
 | 
			
		||||
import { Video } from 'expo-av'
 | 
			
		||||
import { Feather } from '@expo/vector-icons'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  media_attachments: Mastodon.Attachment[]
 | 
			
		||||
  sensitive: boolean
 | 
			
		||||
  width: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AttachmentVideo: React.FC<Props> = ({
 | 
			
		||||
  media_attachments,
 | 
			
		||||
  sensitive,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  const videoPlayer = useRef()
 | 
			
		||||
  const [mediaSensitive, setMediaSensitive] = useState(sensitive)
 | 
			
		||||
  const [videoPlay, setVideoPlay] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const video = media_attachments[0]
 | 
			
		||||
  const videoWidth = width
 | 
			
		||||
  const videoHeight =
 | 
			
		||||
    (width / video.meta.original.width) * video.meta.original.height
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View
 | 
			
		||||
      style={{
 | 
			
		||||
        width: videoWidth,
 | 
			
		||||
        height: videoHeight
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Video
 | 
			
		||||
        ref={videoPlayer}
 | 
			
		||||
        source={{ uri: video.remote_url || video.url }}
 | 
			
		||||
        style={{
 | 
			
		||||
          width: videoWidth,
 | 
			
		||||
          height: videoHeight
 | 
			
		||||
        }}
 | 
			
		||||
        resizeMode='cover'
 | 
			
		||||
        usePoster
 | 
			
		||||
        posterSourceThe={{ uri: video.preview_url }}
 | 
			
		||||
        useNativeControls
 | 
			
		||||
        shouldPlay={videoPlay}
 | 
			
		||||
      />
 | 
			
		||||
      {!videoPlay && (
 | 
			
		||||
        <Pressable
 | 
			
		||||
          onPress={() => {
 | 
			
		||||
            setMediaSensitive(false)
 | 
			
		||||
            videoPlayer.current.presentFullscreenPlayer()
 | 
			
		||||
            setVideoPlay(true)
 | 
			
		||||
          }}
 | 
			
		||||
          style={{
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: 0,
 | 
			
		||||
            left: 0,
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            height: '100%',
 | 
			
		||||
            flexDirection: 'row',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            justifyContent: 'center',
 | 
			
		||||
            backgroundColor: 'rgba(0, 0, 0, 0.25)'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Feather name='play' size={36} color='black' />
 | 
			
		||||
        </Pressable>
 | 
			
		||||
      )}
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default AttachmentVideo
 | 
			
		||||
							
								
								
									
										42
									
								
								src/components/Timelines/Timeline/Shared/Avatar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/components/Timelines/Timeline/Shared/Avatar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Image, Pressable, StyleSheet } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  uri: string
 | 
			
		||||
  id: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Avatar: React.FC<Props> = ({ uri, id }) => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
  // Need to fix go back root
 | 
			
		||||
  return (
 | 
			
		||||
    <Pressable
 | 
			
		||||
      style={styles.avatar}
 | 
			
		||||
      onPress={() => {
 | 
			
		||||
        navigation.navigate('Screen-Shared-Account', {
 | 
			
		||||
          id: id
 | 
			
		||||
        })
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Image source={{ uri: uri }} style={styles.image} />
 | 
			
		||||
    </Pressable>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  avatar: {
 | 
			
		||||
    width: constants.AVATAR_S,
 | 
			
		||||
    height: constants.AVATAR_S,
 | 
			
		||||
    marginRight: constants.SPACING_S
 | 
			
		||||
  },
 | 
			
		||||
  image: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
    borderRadius: 8
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Avatar
 | 
			
		||||
							
								
								
									
										59
									
								
								src/components/Timelines/Timeline/Shared/Card.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/components/Timelines/Timeline/Shared/Card.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  card: Mastodon.Card
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Card: React.FC<Props> = ({ card }) => {
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
  return (
 | 
			
		||||
    card && (
 | 
			
		||||
      <Pressable
 | 
			
		||||
        style={styles.card}
 | 
			
		||||
        onPress={() => {
 | 
			
		||||
          navigation.navigate('Webview', {
 | 
			
		||||
            uri: card.url
 | 
			
		||||
          })
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {card.image && (
 | 
			
		||||
          <View style={styles.left}>
 | 
			
		||||
            <Image source={{ uri: card.image }} style={styles.image} />
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
        <View style={styles.right}>
 | 
			
		||||
          <Text numberOfLines={1}>{card.title}</Text>
 | 
			
		||||
          {card.description ? (
 | 
			
		||||
            <Text numberOfLines={2}>{card.description}</Text>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )}
 | 
			
		||||
          <Text numberOfLines={1}>{card.url}</Text>
 | 
			
		||||
        </View>
 | 
			
		||||
      </Pressable>
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  card: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    height: 70,
 | 
			
		||||
    marginTop: 12
 | 
			
		||||
  },
 | 
			
		||||
  left: {
 | 
			
		||||
    width: 70
 | 
			
		||||
  },
 | 
			
		||||
  image: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%'
 | 
			
		||||
  },
 | 
			
		||||
  right: {
 | 
			
		||||
    flex: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Card
 | 
			
		||||
							
								
								
									
										61
									
								
								src/components/Timelines/Timeline/Shared/Content.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/components/Timelines/Timeline/Shared/Content.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import { Text } from 'react-native'
 | 
			
		||||
import Collapsible from 'react-native-collapsible'
 | 
			
		||||
 | 
			
		||||
import ParseContent from 'src/components/ParseContent'
 | 
			
		||||
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  content: string
 | 
			
		||||
  emojis: Mastodon.Emoji[]
 | 
			
		||||
  mentions: Mastodon.Mention[]
 | 
			
		||||
  spoiler_text?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Content: React.FC<Props> = ({
 | 
			
		||||
  content,
 | 
			
		||||
  emojis,
 | 
			
		||||
  mentions,
 | 
			
		||||
  spoiler_text
 | 
			
		||||
}) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const [spoilerCollapsed, setSpoilerCollapsed] = useState(true)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {content &&
 | 
			
		||||
        (spoiler_text ? (
 | 
			
		||||
          <>
 | 
			
		||||
            <Text>
 | 
			
		||||
              {spoiler_text}{' '}
 | 
			
		||||
              <Text
 | 
			
		||||
                onPress={() => setSpoilerCollapsed(!spoilerCollapsed)}
 | 
			
		||||
                style={{ color: theme.link }}
 | 
			
		||||
              >
 | 
			
		||||
                {spoilerCollapsed ? '点击展开' : '点击收起'}
 | 
			
		||||
              </Text>
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Collapsible collapsed={spoilerCollapsed}>
 | 
			
		||||
              <ParseContent
 | 
			
		||||
                content={content}
 | 
			
		||||
                size={constants.FONT_SIZE_M}
 | 
			
		||||
                emojis={emojis}
 | 
			
		||||
                mentions={mentions}
 | 
			
		||||
              />
 | 
			
		||||
            </Collapsible>
 | 
			
		||||
          </>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <ParseContent
 | 
			
		||||
            content={content}
 | 
			
		||||
            size={constants.FONT_SIZE_M}
 | 
			
		||||
            emojis={emojis}
 | 
			
		||||
            mentions={mentions}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Content
 | 
			
		||||
							
								
								
									
										68
									
								
								src/components/Timelines/Timeline/Shared/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/components/Timelines/Timeline/Shared/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Image, StyleSheet, Text } from 'react-native'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
 | 
			
		||||
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  content: string
 | 
			
		||||
  emojis: Mastodon.Emoji[]
 | 
			
		||||
  size: number
 | 
			
		||||
  fontBold?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Emojis: React.FC<Props> = ({
 | 
			
		||||
  content,
 | 
			
		||||
  emojis,
 | 
			
		||||
  size,
 | 
			
		||||
  fontBold = false
 | 
			
		||||
}) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
  const styles = StyleSheet.create({
 | 
			
		||||
    text: {
 | 
			
		||||
      fontSize: size,
 | 
			
		||||
      lineHeight: size + 2,
 | 
			
		||||
      color: theme.primary,
 | 
			
		||||
      ...(fontBold && { fontWeight: 'bold' })
 | 
			
		||||
    },
 | 
			
		||||
    image: {
 | 
			
		||||
      width: size,
 | 
			
		||||
      height: size
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  const hasEmojis = content.match(regexEmoji)
 | 
			
		||||
 | 
			
		||||
  return hasEmojis ? (
 | 
			
		||||
    <>
 | 
			
		||||
      {content.split(regexEmoji).map((str, i) => {
 | 
			
		||||
        if (str.match(regexEmoji)) {
 | 
			
		||||
          const emojiShortcode = str.split(regexEmoji)[1]
 | 
			
		||||
          const emojiIndex = emojis.findIndex(emoji => {
 | 
			
		||||
            return emojiShortcode === `:${emoji.shortcode}:`
 | 
			
		||||
          })
 | 
			
		||||
          return emojiIndex === -1 ? (
 | 
			
		||||
            <Text key={i} style={styles.text}>
 | 
			
		||||
              {emojiShortcode}
 | 
			
		||||
            </Text>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Image
 | 
			
		||||
              key={i}
 | 
			
		||||
              source={{ uri: emojis[emojiIndex].url }}
 | 
			
		||||
              style={styles.image}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          return (
 | 
			
		||||
            <Text key={i} style={styles.text}>
 | 
			
		||||
              {str}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <Text style={styles.text}>{content}</Text>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Emojis
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
import relativeTime from 'src/utils/relativeTime'
 | 
			
		||||
import Emojis from './Emojis'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  account: Mastodon.Account
 | 
			
		||||
  created_at?: Mastodon.Status['created_at']
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HeaderConversation: React.FC<Props> = ({ account, created_at }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <View>
 | 
			
		||||
      <View style={styles.nameAndDate}>
 | 
			
		||||
        <View style={styles.name}>
 | 
			
		||||
          {account.emojis ? (
 | 
			
		||||
            <Emojis
 | 
			
		||||
              content={account.display_name || account.username}
 | 
			
		||||
              emojis={account.emojis}
 | 
			
		||||
              size={14}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text numberOfLines={1}>
 | 
			
		||||
              {account.display_name || account.username}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        </View>
 | 
			
		||||
        {created_at && (
 | 
			
		||||
          <View>
 | 
			
		||||
            <Text style={styles.created_at}>{relativeTime(created_at)}</Text>
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
      </View>
 | 
			
		||||
      <Text style={styles.account} numberOfLines={1}>
 | 
			
		||||
        @{account.acct}
 | 
			
		||||
      </Text>
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  nameAndDate: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'space-between'
 | 
			
		||||
  },
 | 
			
		||||
  name: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    marginRight: 8,
 | 
			
		||||
    fontWeight: '900'
 | 
			
		||||
  },
 | 
			
		||||
  created_at: {
 | 
			
		||||
    fontSize: 12,
 | 
			
		||||
    lineHeight: 12,
 | 
			
		||||
    marginTop: 8,
 | 
			
		||||
    marginBottom: 8,
 | 
			
		||||
    marginRight: 8
 | 
			
		||||
  },
 | 
			
		||||
  account: {
 | 
			
		||||
    lineHeight: 14,
 | 
			
		||||
    flexShrink: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default HeaderConversation
 | 
			
		||||
							
								
								
									
										370
									
								
								src/components/Timelines/Timeline/Shared/HeaderDefault.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								src/components/Timelines/Timeline/Shared/HeaderDefault.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,370 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react'
 | 
			
		||||
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
import { useNavigation } from '@react-navigation/native'
 | 
			
		||||
import { Feather } from '@expo/vector-icons'
 | 
			
		||||
import Toast from 'react-native-toast-message'
 | 
			
		||||
import { useMutation, useQueryCache } from 'react-query'
 | 
			
		||||
 | 
			
		||||
import Emojis from './Emojis'
 | 
			
		||||
import relativeTime from 'src/utils/relativeTime'
 | 
			
		||||
import client from 'src/api/client'
 | 
			
		||||
import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
 | 
			
		||||
import { store } from 'src/store'
 | 
			
		||||
import { useTheme } from 'src/utils/styles/ThemeManager'
 | 
			
		||||
import constants from 'src/utils/styles/constants'
 | 
			
		||||
 | 
			
		||||
const fireMutation = async ({
 | 
			
		||||
  id,
 | 
			
		||||
  type,
 | 
			
		||||
  stateKey
 | 
			
		||||
}: {
 | 
			
		||||
  id: string
 | 
			
		||||
  type: 'mute' | 'block' | 'domain_blocks' | 'reports'
 | 
			
		||||
  stateKey?: 'muting' | 'blocking'
 | 
			
		||||
}) => {
 | 
			
		||||
  let res
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'mute':
 | 
			
		||||
    case 'block':
 | 
			
		||||
      res = await client({
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        instance: 'local',
 | 
			
		||||
        endpoint: `accounts/${id}/${type}`
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (res.body[stateKey] === true) {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '功能成功',
 | 
			
		||||
          visibilityTime: 2000,
 | 
			
		||||
          autoHide: true,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.resolve()
 | 
			
		||||
      } else {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '请重试',
 | 
			
		||||
          autoHide: false,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.reject()
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
    case 'domain_blocks':
 | 
			
		||||
      res = await client({
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        instance: 'local',
 | 
			
		||||
        endpoint: `domain_blocks`,
 | 
			
		||||
        query: {
 | 
			
		||||
          domain: id || ''
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (!res.body.error) {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'success',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '隐藏域名成功',
 | 
			
		||||
          visibilityTime: 2000,
 | 
			
		||||
          autoHide: true,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.resolve()
 | 
			
		||||
      } else {
 | 
			
		||||
        Toast.show({
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          position: 'bottom',
 | 
			
		||||
          text1: '隐藏域名失败,请重试',
 | 
			
		||||
          autoHide: false,
 | 
			
		||||
          bottomOffset: 65
 | 
			
		||||
        })
 | 
			
		||||
        return Promise.reject()
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
    // case 'reports':
 | 
			
		||||
    //   res = await client({
 | 
			
		||||
    //     method: 'post',
 | 
			
		||||
    //     instance: 'local',
 | 
			
		||||
    //     endpoint: `reports`,
 | 
			
		||||
    //     query: {
 | 
			
		||||
    //       domain: id || ''
 | 
			
		||||
    //     }
 | 
			
		||||
    //   })
 | 
			
		||||
 | 
			
		||||
    //   if (!res.body.error) {
 | 
			
		||||
    //     Toast.show({
 | 
			
		||||
    //       type: 'success',
 | 
			
		||||
    //       position: 'bottom',
 | 
			
		||||
    //       text1: '隐藏域名成功',
 | 
			
		||||
    //       visibilityTime: 2000,
 | 
			
		||||
    //       autoHide: true,
 | 
			
		||||
    //       bottomOffset: 65
 | 
			
		||||
    //     })
 | 
			
		||||
    //     return Promise.resolve()
 | 
			
		||||
    //   } else {
 | 
			
		||||
    //     Toast.show({
 | 
			
		||||
    //       type: 'error',
 | 
			
		||||
    //       position: 'bottom',
 | 
			
		||||
    //       text1: '隐藏域名失败,请重试',
 | 
			
		||||
    //       autoHide: false,
 | 
			
		||||
    //       bottomOffset: 65
 | 
			
		||||
    //     })
 | 
			
		||||
    //     return Promise.reject()
 | 
			
		||||
    //   }
 | 
			
		||||
    //   break
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  queryKey: App.QueryKey
 | 
			
		||||
  accountId: string
 | 
			
		||||
  domain: string
 | 
			
		||||
  name: string
 | 
			
		||||
  emojis?: Mastodon.Emoji[]
 | 
			
		||||
  account: string
 | 
			
		||||
  created_at: string
 | 
			
		||||
  application?: Mastodon.Application
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const HeaderDefault: React.FC<Props> = ({
 | 
			
		||||
  queryKey,
 | 
			
		||||
  accountId,
 | 
			
		||||
  domain,
 | 
			
		||||
  name,
 | 
			
		||||
  emojis,
 | 
			
		||||
  account,
 | 
			
		||||
  created_at,
 | 
			
		||||
  application
 | 
			
		||||
}) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const navigation = useNavigation()
 | 
			
		||||
  const localAccountId = getLocalAccountId(store.getState())
 | 
			
		||||
  const localDomain = getLocalUrl(store.getState())
 | 
			
		||||
  const [since, setSince] = useState(relativeTime(created_at))
 | 
			
		||||
  const [modalVisible, setModalVisible] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const queryCache = useQueryCache()
 | 
			
		||||
  const [mutateAction] = useMutation(fireMutation, {
 | 
			
		||||
    onMutate: () => {
 | 
			
		||||
      queryCache.cancelQueries(queryKey)
 | 
			
		||||
      const prevData = queryCache.getQueryData(queryKey)
 | 
			
		||||
      return prevData
 | 
			
		||||
    },
 | 
			
		||||
    onSuccess: (newData, params) => {
 | 
			
		||||
      if (params.type === 'domain_blocks') {
 | 
			
		||||
        console.log('clearing cache')
 | 
			
		||||
        queryCache.invalidateQueries(['Following', { page: 'Following' }])
 | 
			
		||||
      }
 | 
			
		||||
      // queryCache.setQueryData(queryKey, (oldData: any) => {
 | 
			
		||||
      //   oldData &&
 | 
			
		||||
      //     oldData.map((paging: any) => {
 | 
			
		||||
      //       paging.toots.map(
 | 
			
		||||
      //         (status: Mastodon.Status | Mastodon.Notification, i: number) => {
 | 
			
		||||
      //           if (status.id === newData.id) {
 | 
			
		||||
      //             paging.toots[i] = newData
 | 
			
		||||
      //           }
 | 
			
		||||
      //         }
 | 
			
		||||
      //       )
 | 
			
		||||
      //     })
 | 
			
		||||
      //   return oldData
 | 
			
		||||
      // })
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    },
 | 
			
		||||
    onError: (err, variables, prevData) => {
 | 
			
		||||
      queryCache.setQueryData(queryKey, prevData)
 | 
			
		||||
    },
 | 
			
		||||
    onSettled: () => {
 | 
			
		||||
      queryCache.invalidateQueries(queryKey)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // causing full re-render
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setSince(relativeTime(created_at))
 | 
			
		||||
    }, 1000)
 | 
			
		||||
  }, [since])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <View>
 | 
			
		||||
      <View style={styles.nameAndAction}>
 | 
			
		||||
        <View style={styles.name}>
 | 
			
		||||
          {emojis ? (
 | 
			
		||||
            <Emojis
 | 
			
		||||
              content={name}
 | 
			
		||||
              emojis={emojis}
 | 
			
		||||
              size={constants.FONT_SIZE_M}
 | 
			
		||||
              fontBold={true}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text numberOfLines={1} style={{ color: theme.primary }}>
 | 
			
		||||
              {name}
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
          <Text
 | 
			
		||||
            style={[styles.account, { color: theme.secondary }]}
 | 
			
		||||
            numberOfLines={1}
 | 
			
		||||
          >
 | 
			
		||||
            @{account}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </View>
 | 
			
		||||
        {accountId !== localAccountId && domain !== localDomain && (
 | 
			
		||||
          <Pressable
 | 
			
		||||
            style={styles.action}
 | 
			
		||||
            onPress={() => setModalVisible(true)}
 | 
			
		||||
          >
 | 
			
		||||
            <Feather
 | 
			
		||||
              name='more-horizontal'
 | 
			
		||||
              color={theme.secondary}
 | 
			
		||||
              size={constants.FONT_SIZE_M + 2}
 | 
			
		||||
            />
 | 
			
		||||
          </Pressable>
 | 
			
		||||
        )}
 | 
			
		||||
      </View>
 | 
			
		||||
      <View style={styles.meta}>
 | 
			
		||||
        <View>
 | 
			
		||||
          <Text style={[styles.created_at, { color: theme.secondary }]}>
 | 
			
		||||
            {since}
 | 
			
		||||
          </Text>
 | 
			
		||||
        </View>
 | 
			
		||||
        {application && application.name !== 'Web' && (
 | 
			
		||||
          <View>
 | 
			
		||||
            <Text
 | 
			
		||||
              onPress={() => {
 | 
			
		||||
                navigation.navigate('Webview', {
 | 
			
		||||
                  uri: application.website
 | 
			
		||||
                })
 | 
			
		||||
              }}
 | 
			
		||||
              style={[styles.application, { color: theme.secondary }]}
 | 
			
		||||
            >
 | 
			
		||||
              发自于 - {application.name}
 | 
			
		||||
            </Text>
 | 
			
		||||
          </View>
 | 
			
		||||
        )}
 | 
			
		||||
      </View>
 | 
			
		||||
 | 
			
		||||
      <Modal
 | 
			
		||||
        animationType='fade'
 | 
			
		||||
        presentationStyle='overFullScreen'
 | 
			
		||||
        transparent
 | 
			
		||||
        visible={modalVisible}
 | 
			
		||||
      >
 | 
			
		||||
        <Pressable
 | 
			
		||||
          style={styles.modalBackground}
 | 
			
		||||
          onPress={() => setModalVisible(false)}
 | 
			
		||||
        >
 | 
			
		||||
          <View style={styles.modalSheet}>
 | 
			
		||||
            {accountId !== localAccountId && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: accountId,
 | 
			
		||||
                    type: 'mute',
 | 
			
		||||
                    stateKey: 'muting'
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>静音用户</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
            {accountId !== localAccountId && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: accountId,
 | 
			
		||||
                    type: 'block',
 | 
			
		||||
                    stateKey: 'blocking'
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>屏蔽用户</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
            {domain !== localDomain && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: domain,
 | 
			
		||||
                    type: 'domain_blocks'
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>屏蔽域名</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
            {accountId !== localAccountId && (
 | 
			
		||||
              <Pressable
 | 
			
		||||
                onPress={() => {
 | 
			
		||||
                  setModalVisible(false)
 | 
			
		||||
                  mutateAction({
 | 
			
		||||
                    id: accountId,
 | 
			
		||||
                    type: 'reports'
 | 
			
		||||
                  })
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Text>举报用户</Text>
 | 
			
		||||
              </Pressable>
 | 
			
		||||
            )}
 | 
			
		||||
          </View>
 | 
			
		||||
        </Pressable>
 | 
			
		||||
      </Modal>
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  nameAndAction: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'space-between'
 | 
			
		||||
  },
 | 
			
		||||
  name: {
 | 
			
		||||
    flexBasis: '80%',
 | 
			
		||||
    flexDirection: 'row'
 | 
			
		||||
  },
 | 
			
		||||
  action: {
 | 
			
		||||
    flexBasis: '20%',
 | 
			
		||||
    alignItems: 'center'
 | 
			
		||||
  },
 | 
			
		||||
  account: {
 | 
			
		||||
    flexShrink: 1,
 | 
			
		||||
    marginLeft: constants.SPACING_XS,
 | 
			
		||||
    lineHeight: constants.FONT_SIZE_M + 2
 | 
			
		||||
  },
 | 
			
		||||
  meta: {
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    marginTop: constants.SPACING_XS,
 | 
			
		||||
    marginBottom: constants.SPACING_S
 | 
			
		||||
  },
 | 
			
		||||
  created_at: {
 | 
			
		||||
    fontSize: constants.FONT_SIZE_S
 | 
			
		||||
  },
 | 
			
		||||
  application: {
 | 
			
		||||
    fontSize: constants.FONT_SIZE_S,
 | 
			
		||||
    marginLeft: constants.SPACING_S
 | 
			
		||||
  },
 | 
			
		||||
  modalBackground: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
    backgroundColor: 'rgba(0, 0, 0, 0.75)',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    alignItems: 'flex-end'
 | 
			
		||||
  },
 | 
			
		||||
  modalSheet: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '50%',
 | 
			
		||||
    backgroundColor: 'white',
 | 
			
		||||
    flex: 1
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default HeaderDefault
 | 
			
		||||
							
								
								
									
										52
									
								
								src/components/Timelines/Timeline/Shared/Poll.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/Timelines/Timeline/Shared/Poll.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { StyleSheet, Text, View } from 'react-native'
 | 
			
		||||
 | 
			
		||||
import Emojis from './Emojis'
 | 
			
		||||
 | 
			
		||||
export interface Props {
 | 
			
		||||
  poll: Mastodon.Poll
 | 
			
		||||
}
 | 
			
		||||
// When haven't voted, result should not be shown but intead let people vote
 | 
			
		||||
const Poll: React.FC<Props> = ({ poll }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <View>
 | 
			
		||||
      {poll.options.map((option, index) => (
 | 
			
		||||
        <View key={index}>
 | 
			
		||||
          <View style={{ flexDirection: 'row' }}>
 | 
			
		||||
            <Text>
 | 
			
		||||
              {Math.round((option.votes_count / poll.votes_count) * 100)}%
 | 
			
		||||
            </Text>
 | 
			
		||||
            <Emojis
 | 
			
		||||
              content={option.title}
 | 
			
		||||
              emojis={poll.emojis}
 | 
			
		||||
              size={14}
 | 
			
		||||
            />
 | 
			
		||||
          </View>
 | 
			
		||||
          <View
 | 
			
		||||
            style={{
 | 
			
		||||
              width: `${Math.round(
 | 
			
		||||
                (option.votes_count / poll.votes_count) * 100
 | 
			
		||||
              )}%`,
 | 
			
		||||
              height: 5,
 | 
			
		||||
              backgroundColor: 'blue'
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </View>
 | 
			
		||||
      ))}
 | 
			
		||||
    </View>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  avatar: {
 | 
			
		||||
    width: 50,
 | 
			
		||||
    height: 50,
 | 
			
		||||
    marginRight: 8
 | 
			
		||||
  },
 | 
			
		||||
  image: {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default Poll
 | 
			
		||||
		Reference in New Issue
	
	Block a user